Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bc7111a
ci(actions): isolate pull request concurrency
seonghobae Sep 4, 2026
a8d6ba9
test(ci): align image concurrency contract
seonghobae Sep 4, 2026
765757b
fix(actions): centralize hourly development admission
seonghobae Sep 4, 2026
cb04d4f
test(actions): align central development dispatch contract
seonghobae Sep 4, 2026
068ea2f
fix(actions): dispatch centralized product development
seonghobae Sep 4, 2026
f0f9f61
test(actions): reject elapsed model-run termination
seonghobae Sep 4, 2026
b23435f
fix(actions): separate admin timeout from model execution
seonghobae Sep 4, 2026
9e46758
test(actions): drop obsolete model timeout budget assertion
seonghobae Sep 4, 2026
552abc4
test(actions): distinguish admin and model termination
seonghobae Sep 4, 2026
258d0b5
test(actions): remove obsolete elapsed-time budget helper
seonghobae Sep 4, 2026
1cd5cfa
docs(actions): distinguish admin and model termination
seonghobae Sep 4, 2026
decfcfb
test(actions): keep buyer-gap development work-conserving
seonghobae Sep 5, 2026
5e84885
fix(actions): keep buyer-gap development work-conserving
seonghobae Sep 5, 2026
9032aea
fix(actions): isolate concurrent buyer-gap proposal paths
seonghobae Sep 5, 2026
024a94a
docs(actions): document work-conserving path isolation
seonghobae Sep 5, 2026
da66ec6
docs(doctoring): record work-conserving isolation decision
seonghobae Sep 5, 2026
7d850f6
test(actions): require work-conserving path isolation
seonghobae Sep 5, 2026
5d97fa8
fix(tests): align commercial dispatch with work-conserving admission
seonghobae Sep 5, 2026
b91b1c4
fix(tests): track path-isolated publisher revalidation
seonghobae Sep 5, 2026
a545af0
test(actions): make spawn spy test callback async
seonghobae Sep 5, 2026
d23a055
test(actions): align readiness fixtures with current authority
seonghobae Sep 5, 2026
2de27d2
merge: restack #550 on protected reviewer truth
seonghobae Sep 6, 2026
158d818
merge(context-fabric): restack concurrency owner on protected context…
seonghobae Sep 6, 2026
f6ec87a
fix(reviewer): retain self-cycle exclusion
seonghobae Sep 6, 2026
fec1025
chore(hourly): restack run-independence lane on protected main
seonghobae Sep 6, 2026
12f8e3d
merge: converge workflow-concurrency lane onto protected #552 truth
seonghobae Sep 6, 2026
ad0f512
merge: restack workflow concurrency after #527 trust integration
seonghobae Sep 6, 2026
3ed5bd9
merge(main): restack workflow concurrency after protected trust integ…
seonghobae Sep 6, 2026
aeb9c46
merge(main): non-force restack workflow concurrency after #526
seonghobae Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
- main

concurrency:
group: noema-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
verify:
Expand Down
116 changes: 89 additions & 27 deletions .github/workflows/hourly-product-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ on:
required: false
default: false
type: boolean
schedule:
- cron: "47 * * * *"

concurrency:
group: hourly-orchestrator-product-development-${{ github.repository }}
cancel-in-progress: false
Expand All @@ -22,9 +19,6 @@ env:
DEFAULT_BRANCH: main
OPENCODE_VERSION: "1.17.13"
OPENCODE_SHA256: 157afa289d1a8d9372de0ce19ac726119b937a1f6b201808d46f06e4e59bb348
# One gateway-backed session plus setup/diagnostic reserve fits in 55 minutes.
OPENCODE_RUN_TIMEOUT_SECONDS: "2700"
OPENCODE_KILL_GRACE_SECONDS: "30"
MAX_CHANGED_FILES: "40"
MAX_DIFF_BYTES: "500000"
MAX_PR_TITLE_BYTES: "120"
Expand All @@ -51,7 +45,7 @@ jobs:
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}
steps:
- name: Enforce zero-open-PR single-flight gate
- name: Validate work-conserving single-flight admission
id: gate
shell: bash
env:
Expand Down Expand Up @@ -80,13 +74,9 @@ jobs:
fi

if [ "$(jq 'length' <<<"$open_prs")" -gt 0 ]; then
{
echo "dispatch=false"
echo "reason=open_pull_request"
} >>"$GITHUB_OUTPUT"
echo "An open pull request exists; exact-head PR governance owns this hour." \
echo "open_pull_request_count=at_least_one" >>"$GITHUB_OUTPUT"
echo "Open pull-request lanes remain; a new proposal is allowed only if publication proves path isolation from every live PR." \
>>"$GITHUB_STEP_SUMMARY"
exit 0
fi

if { [ "$ORCHESTRATOR_KEY_CONFIGURED" != "true" ] \
Expand Down Expand Up @@ -141,6 +131,12 @@ jobs:
supportability, or operations gap that can be completed as exactly one bounded
pull request. Do not create another repository.

Existing open pull requests are independent governance lanes, not a global stop.
Select an unrelated buyer gap from current protected main. A trusted publisher will
fail closed if any proposed changed path overlaps any live open pull request or if
protected main advances. Do not intentionally duplicate or replace work already owned
by an active pull-request lane.

Keep Noema independently deployable and preserve its modular MSA role with
ContextualWisdomLab/.github, naruon, contextual-orchestrator, and other CWL
services. Keep interfaces explicit and replaceable. Route every Noema LLM
Expand Down Expand Up @@ -206,7 +202,7 @@ jobs:
run: |
set -euo pipefail
{
echo "Dry run: the zero-open-PR gate permits one bounded OpenCode proposal."
echo "Dry run: work-conserving admission permits one bounded OpenCode proposal; publication still requires current-base and open-PR path isolation."
echo
cat "$RUNNER_TEMP/noema-agent-prompt.md"
} >>"$GITHUB_STEP_SUMMARY"
Expand Down Expand Up @@ -280,8 +276,7 @@ jobs:
run: |
set -euo pipefail
prompt="$(cat "$RUNNER_TEMP/noema-agent-prompt.md")"
if timeout --kill-after="${OPENCODE_KILL_GRACE_SECONDS}s" "${OPENCODE_RUN_TIMEOUT_SECONDS}s" \
env -u GH_TOKEN -u GITHUB_TOKEN \
if env -u GH_TOKEN -u GITHUB_TOKEN \
-u REPOSITORY_TOKEN \
-u ACTIONS_ID_TOKEN_REQUEST_TOKEN \
-u ACTIONS_ID_TOKEN_REQUEST_URL \
Expand Down Expand Up @@ -712,7 +707,7 @@ jobs:
permission-metadata: read
permission-pull-requests: write

- name: Revalidate queue and default-branch head
- name: Revalidate open-PR path isolation and default-branch head
shell: bash
env:
GH_TOKEN: ${{ steps.maintainer_app.outputs.token }}
Expand All @@ -725,20 +720,81 @@ jobs:
exit 1
fi

if ! open_prs="$(
gh pr list \
--repo "$GITHUB_REPOSITORY" \
--state open \
--limit 1 \
--json number,url
proposal_paths="$RUNNER_TEMP/proposal-paths.b64"
git diff --cached --name-only -z | node -e '
const chunks = [];
process.stdin.on("data", (chunk) => chunks.push(chunk));
process.stdin.on("end", () => {
const names = Buffer.concat(chunks).toString("utf8").split("\0").filter(Boolean);
for (const name of names) {
process.stdout.write(Buffer.from(name, "utf8").toString("base64") + "\n");
}
});
' >"$proposal_paths"
LC_ALL=C sort -u -o "$proposal_paths" "$proposal_paths"

isolation_check="$RUNNER_TEMP/verify-open-pr-path-isolation.sh"
cat >"$isolation_check" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
exclude_pr="${1:-}"
proposal_paths="$RUNNER_TEMP/proposal-paths.b64"
reserved_paths="$RUNNER_TEMP/open-pr-paths.b64"
overlap_paths="$RUNNER_TEMP/open-pr-overlap.b64"
: >"$reserved_paths"

if ! open_pr_numbers="$(
gh api --paginate \
"repos/${GITHUB_REPOSITORY}/pulls?state=open&per_page=100" \
--jq '.[].number'
)"; then
echo "::error::pull_request_inventory_unavailable_after_generation"
exit 1
fi
if [ "$(jq 'length' <<<"$open_prs")" -gt 0 ]; then
echo "::error::open_pull_request_after_generation"

while IFS= read -r pull_number; do
[ -n "$pull_number" ] || continue
if ! [[ "$pull_number" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::pull_request_inventory_invalid_after_generation"
exit 1
fi
if [ -n "$exclude_pr" ] && [ "$pull_number" = "$exclude_pr" ]; then
continue
fi
if ! expected_files="$(
gh api "repos/${GITHUB_REPOSITORY}/pulls/${pull_number}" --jq '.changed_files'
)"; then
echo "::error::pull_request_file_inventory_unavailable_after_generation"
exit 1
fi
if ! [[ "$expected_files" =~ ^[0-9]+$ ]] || [ "$expected_files" -gt 3000 ]; then
echo "::error::pull_request_file_inventory_unbounded_after_generation"
exit 1
fi
before_count="$(wc -l <"$reserved_paths" | tr -d '[:space:]')"
if ! gh api --paginate \
"repos/${GITHUB_REPOSITORY}/pulls/${pull_number}/files?per_page=100" \
--jq '.[].filename | @base64' >>"$reserved_paths"; then
echo "::error::pull_request_file_inventory_unavailable_after_generation"
exit 1
fi
after_count="$(wc -l <"$reserved_paths" | tr -d '[:space:]')"
if [ $((after_count - before_count)) -ne "$expected_files" ]; then
echo "::error::pull_request_file_inventory_incomplete_after_generation"
exit 1
fi
done <<<"$open_pr_numbers"

LC_ALL=C sort -u -o "$reserved_paths" "$reserved_paths"
comm -12 "$proposal_paths" "$reserved_paths" >"$overlap_paths"
if [ -s "$overlap_paths" ]; then
echo "::error::open_pull_request_after_generation_path_overlap"
exit 1
fi
SCRIPT
chmod 0500 "$isolation_check"

"$isolation_check"

if ! live_base="$(
gh api \
Expand Down Expand Up @@ -894,13 +950,19 @@ jobs:
echo "::error::created_pull_request_queue_inventory_unavailable"
false
fi
if [ "$open_pr_numbers" != "$pr_number" ]; then
created_pr_occurrences="$(grep -Fxc -- "$pr_number" <<<"$open_pr_numbers" || true)"
if [ "$created_pr_occurrences" -ne 1 ]; then
echo "::error::created_pull_request_queue_conflict"
false
fi

if ! "$RUNNER_TEMP/verify-open-pr-path-isolation.sh" "$pr_number"; then
echo "::error::created_pull_request_queue_conflict_path_overlap"
false
fi

trap - ERR
{
echo "Opened bounded pull request: $pr_url"
echo "Opened bounded path-isolated pull request: $pr_url"
echo "hourly-commercial-readiness owns review, repair, exact-head revalidation, and merge."
} >>"$GITHUB_STEP_SUMMARY"
4 changes: 2 additions & 2 deletions .github/workflows/patch-validator-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ on:
workflow_dispatch:

concurrency:
group: noema-patch-validator-image-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.

permissions:
contents: read
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/reviewer-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ on:
- main

concurrency:
group: noema-reviewer-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read
Expand Down
20 changes: 16 additions & 4 deletions docs/doctoring/hourly-product-development-prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ This doctoring note uses APA 7 reference form. It separates source-supported fac

## Problem statement

The scheduled development path has two independent credential prerequisites:
The centrally dispatched development path has two independent credential prerequisites:

1. `NOEMA_LLM_API_URL` and `NOEMA_LLM_API_KEY` permit the read-only OpenCode proposal job to reach the `contextual-orchestrator` gateway.
2. `NOEMA_MAINTAINER_APP_CLIENT_ID` and `NOEMA_MAINTAINER_APP_PRIVATE_KEY` permit the later non-executing publisher to create one repository-scoped branch and pull request.

Checking only the inference token can spend model compute on a proposal that the workflow is structurally unable to publish. That is a deterministic configuration failure rather than a model-quality failure and should be rejected before checkout or inference.

A separate scheduling problem exists when independent review lanes are waiting on Checks or external capacity. Treating the mere existence of any open pull request as a repository-wide stop converts one blocked lane into a global development stall. Noema therefore distinguishes lane-level governance from new buyer-gap development. A healthy commercial-readiness pass may dispatch one product-development run while other pull requests remain open, but publication must prove that the proposal is based on the unchanged protected head and does not reuse any changed path owned by another live pull request.

## Source-supported controls

GitHub documents that a workflow reads a secret only when the workflow explicitly includes it, and recommends granting credentials the minimum possible permissions. GitHub further recommends GitHub Apps as fine-grained, short-lived, non-user-bound credentials when repository automation needs permissions beyond read-only access. These facts support separating the gateway inference token from the repository publication credential and preserving read-only job-level `GITHUB_TOKEN` permissions. This is a least privilege control: model execution never receives publication authority, and publication receives only the repository-scoped permissions required to create one branch and pull request.

GitHub's pull-request REST API exposes the current pull request, its `changed_files` count, and a paginated list of changed files. Noema uses those source-of-truth surfaces to reject a proposal when it cannot enumerate a competing PR completely or when an exact changed path overlaps. This is a repository-specific conflict-reduction control, not a proof of semantic independence: separate files can still participate in one invariant.

NIST SP 800-218 Version 1.1 recommends integrating secure-development requirements and verification into the software life cycle. NIST SP 800-218A augments that framework with practices specific to generative AI and foundation-model systems. The December 2025 SP 800-218 Revision 1 initial public draft describes updated secure and reliable development practices, but remains a draft; Noema therefore records it as a current informative source while retaining the final Version 1.1 and final AI community profile as the normative published references.

## Noema-specific decision
Expand All @@ -30,6 +34,8 @@ Before OpenCode starts, the proposal gate evaluates only presence booleans:

The workflow does not reveal values, import the private key, mint an App token, or call a model during this gate. Missing publication configuration returns the stable reason `maintainer_app_unavailable` and stops before checkout, dependency installation, OpenCode download, or gateway inference. Missing gateway configuration returns `orchestrator_gateway_unavailable`.

The gate also verifies that the open-PR inventory itself can be read. An existing PR is not a failure reason. If another PR is present, the workflow records that a governed lane exists and continues only under the later publication rule: all proposal changed paths must be disjoint from all currently open PR changed paths. The publisher reads the complete open-PR inventory twice around remote creation, validates each PR's reported `changed_files` count against the paginated file list, rejects inventories beyond GitHub's supported 3,000-file PR listing bound, and compares base64-encoded path identities so embedded whitespace cannot turn a path into a line-oriented false match. A current open PR may therefore coexist with a newly created proposal only when the exact path sets remain disjoint.

The App token is still minted only in the third, non-executing publication job. Presence checking does not prove that the key is valid, that the App remains installed, or that permissions are sufficient; those live failures continue to fail closed when `actions/create-github-app-token` runs. This preserves the late-token trust boundary while preventing known-impossible sessions.

Manual `dry_run` deliberately bypasses credential-presence requirements because it performs no checkout, model call, artifact publication, branch push, or pull-request creation. It remains an operator inspection path rather than evidence that a live proposal can be published.
Expand All @@ -45,14 +51,18 @@ Executable tests must prove that:
- both Maintainer App presence booleans are evaluated in the pre-inference gate;
- either missing value produces `dispatch=false` and `reason=maintainer_app_unavailable`;
- missing gateway URL or key produces `orchestrator_gateway_unavailable`;
- the gate appears before task preparation, checkout, and OpenCode execution;
- unreadable open-PR inventory fails closed while the existence of a readable open PR does not globally suppress a healthy development pass;
- a proposal whose exact path intersects any other open PR fails closed before remote creation;
- after PR creation, path isolation is re-evaluated with the newly created PR excluded, so a raced overlapping PR causes cleanup rather than acceptance;
- incomplete or unbounded competing-PR file inventory fails closed;
- protected `main` must still equal the proposal base before publication;
- `dry_run=true` remains available without production credentials;
- the dedicated gateway token and reviewer App identity remain separate; and
- operations and doctoring documents describe the same failure reason and credential names.
- operations and doctoring documents describe the same failure reasons and credential names.

## Residual risk

Presence booleans can become stale between the initial gate and publication, and they cannot validate App installation scope or private-key correctness. Exact publication remains protected by fresh token minting, queue and base-head revalidation, repository-scoped permissions, and ordinary pull-request governance. The new gate reduces deterministic cost waste; it is not a substitute for live App readiness evidence under issue #29.
Presence booleans can become stale between the initial gate and publication, and they cannot validate App installation scope or private-key correctness. Exact publication remains protected by fresh token minting, base-head revalidation, repository-scoped permissions, path-isolation checks before and after remote PR creation, and ordinary pull-request governance. GitHub does not expose an atomic transaction combining "no path overlap", base-head compare-and-swap, branch creation, and PR creation, so a narrow race remains after the final read. Different files can also violate one shared invariant without a literal path collision. These residual risks are why path isolation is only an admission control: it does not replace semantic review, required exact-head Checks, branch protection, or successor restacking. The gate reduces deterministic cost waste and global queue stalls; it is not a substitute for live App readiness evidence under issue #29.

## APA 7 references

Expand All @@ -62,6 +72,8 @@ GitHub. (2026). *Secrets*. GitHub Docs. Retrieved August 5, 2026, from https://d

GitHub. (2026). *Making authenticated API requests with a GitHub App in a GitHub Actions workflow*. GitHub Docs. Retrieved August 5, 2026, from https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow

GitHub. (2026). *REST API endpoints for pull requests*. GitHub Docs. Retrieved September 5, 2026, from https://docs.github.com/en/rest/pulls/pulls

Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure software development framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218

Booth, H., Ogata, M., Kent, K., Souppaya, M., & Dodson, D. (2025). *Secure software development framework (SSDF) version 1.2: Recommendations for mitigating the risk of software vulnerabilities* (Initial Public Draft NIST Special Publication 800-218, Revision 1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218r1.ipd
Loading