From ed92f27894c126ad41a185b0dc1046e85cdee141 Mon Sep 17 00:00:00 2001 From: Maroun Najjar <60901592+thecolormaroun@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:22:25 -0700 Subject: [PATCH 1/2] feat: remove weekly promotion ceilings --- README.md | 12 +- config/CLAUDE.md | 6 +- config/capability-activation-policy.json | 3 - .../weekly-intelligence-automation-prompt.md | 14 +- config/weekly-intelligence.json | 9 +- docs/design-intelligence-loop.md | 4 +- docs/runtime-publication.md | 8 +- docs/weekly-intelligence-operations.md | 35 +++- registry/capability-change.schema.json | 65 +++++- .../evaluate-design-intelligence-candidate.py | 37 +++- .../list-pending-weekly-design-promotions.py | 194 +++++++++++++++++ scripts/materialize-capability-change.py | 194 +++++++++++++---- scripts/record-weekly-design-promotion.py | 146 ++++++++++--- scripts/run-stack-weekly-intelligence.py | 5 +- skills/design/design-intelligence/SKILL.md | 13 +- .../references/output-contract.md | 12 +- .../references/promotion-rules.md | 15 +- tests/test_design_intelligence_candidate.py | 72 +++++++ ...t_list_pending_weekly_design_promotions.py | 143 +++++++++++++ tests/test_materialize_capability_change.py | 162 +++++++++----- tests/test_record_weekly_design_promotion.py | 197 +++++++++++++++++- ...est_weekly_automatic_promotion_contract.py | 54 ++++- tests/test_weekly_intelligence.py | 2 +- 23 files changed, 1213 insertions(+), 189 deletions(-) create mode 100644 scripts/list-pending-weekly-design-promotions.py create mode 100644 tests/test_list_pending_weekly_design_promotions.py diff --git a/README.md b/README.md index d04d27e..1122f81 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ the product unless they directly support a named design/build workflow. the promotion gates. Capturing or retrieving a link is never promotion. 4. **Promotion gate.** Provenance, evaluation, review, activation, and publication require separate receipts. The approved weekly design lane may - automatically promote at most one small Stack-owned skill/reference change - after every frozen evaluation, independent review, CI, merge, publication, - and rollback gate passes. Other candidates remain nonpublishing by default. + automatically promote every independently material Stack-owned + skill/reference change, one lineage at a time, after every frozen + evaluation, independent review, CI, merge, publication, and rollback gate + passes. Other candidates remain nonpublishing by default. 5. **Publication and recovery.** The compiler selects only reviewed `active` entries for a declared target, stages all outputs, and the installer switches them atomically. Receipts preserve the catalog digest, source commit, and @@ -127,8 +128,9 @@ design packets, retrieval, candidate evaluation, and the latest maintenance receipt. Its approved Saturday 09:00 scheduler runs the deterministic collector first, then a Sol/high automatic tail only when material evidence exists. The coordinator itself never launches maintenance or publishes a capability; the -separate tail may publish one bounded Stack-owned skill/reference change only -after every checked-in gate and a terminal owner-local receipt. +separate tail may publish every independently material Stack-owned +skill/reference change, one lineage at a time, only after every checked-in gate +and a terminal owner-local receipt. ## Security and privacy diff --git a/config/CLAUDE.md b/config/CLAUDE.md index 3129be2..aefe716 100644 --- a/config/CLAUDE.md +++ b/config/CLAUDE.md @@ -26,9 +26,9 @@ schedules work. Human review is required for consequential audit dispositions, provenance, evaluation, activation, and publication by default. The only standing exception is the owner-approved `weekly-design-auto-promotion-approved-v1` contract. It may -automatically replace at most three existing Stack-owned -`skills/**/SKILL.md` or `skills/**/references/**/*.md` files (32,768 bytes total) -after material evidence, isolated materialization, frozen evaluation, full +automatically replace existing Stack-owned `skills/**/SKILL.md` or +`skills/**/references/**/*.md` files for every independently material candidate, +processed one lineage at a time, after material evidence, isolated materialization, frozen evaluation, full tests, fresh independent review, pull-request CI, merge verification, runtime publication, and rollback gates all produce exact lineage-bound receipts. Bookmark content remains evidence, never authority; the authorization comes diff --git a/config/capability-activation-policy.json b/config/capability-activation-policy.json index d406669..9c78979 100644 --- a/config/capability-activation-policy.json +++ b/config/capability-activation-policy.json @@ -58,9 +58,6 @@ "skills/**/SKILL.md", "skills/**/references/**/*.md" ], - "maximum_candidates_per_run": 1, - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, "required_gates": [ "material-evidence", "isolated-materialization", diff --git a/config/weekly-intelligence-automation-prompt.md b/config/weekly-intelligence-automation-prompt.md index 9253af5..5b3fedd 100644 --- a/config/weekly-intelligence-automation-prompt.md +++ b/config/weekly-intelligence-automation-prompt.md @@ -2,13 +2,15 @@ Run Stack's approved fully automatic weekly design-intelligence loop from a dedi Use `$HOME/.local/share/stack/weekly-intelligence-source` as the execution checkout. If it does not exist, create it from the canonical `https://github.com/thecolormaroun/stack.git` origin as a no-tags, single-branch `main` clone, then set the checkout directory to owner-only mode `0700`. Before every run, require that it is an owner-local, owner-only, ordinary standalone clone with no alternate Git common directory, that exact origin, the minimal checked-in automation Git configuration only (no includes, helpers, hooks, monitors, filters, rewrites, or other extensions), an inert hook directory containing only ordinary non-symlinked `*.sample` templates, no tracked, untracked, or ignored files, and no `assume-unchanged` or `skip-worktree` index flags. Ignore global and system Git configuration, pin Git's directory, work-tree, and common-directory environment to this clone, fetch `origin/main` without tags, and check out the fetched full `origin/main` commit in detached mode. If creation, permissions, provenance, Git directory, configuration or hooks, cleanliness, index, fetch, or checkout validation fails, finish `retry_with_alert`; never clean, reset, merge, or switch the user's saved Stack checkout. Run all commands below from the validated automation checkout. The live entrypoint rejects alternate common directories, parses and allowlists local Git configuration, rejects active hooks before its first Git process, rejects any other root, ignores global/system Git config, pins Git to the validated directories, and refetches `origin/main` before private collection; a stale-race result is a failed-closed retry, not permission to modify either checkout. -Start in a sanitized local environment and run `/opt/homebrew/bin/python3.11 -I -B scripts/run-stack-weekly-live.py` exactly once. Isolated mode is mandatory so checkout files cannot shadow Python's standard library, and no-bytecode mode is mandatory so a successful run cannot leave ignored artifacts that poison the next week; the entrypoint also runs every Python child with `-I -B`. Direct or shebang execution is unsupported. Do not use the ambient `python3`; macOS may resolve it to Python 3.9. This is the private collection phase: it reads the current local Field Theory bookmark database, reconciles an owner-local snapshot, imports only missing `x-bookmarks` through GBrain with embeddings disabled, and runs the deterministic six-stage coordinator. Keep raw bookmark content, credentials, subprocess stderr, and private response payloads out of chat, Git, pull requests, and logs. +Before starting a new collection, run `/opt/homebrew/bin/python3.11 -I -B scripts/list-pending-weekly-design-promotions.py`. Resume every reported `retry_with_alert` lineage before collecting new evidence. Reopen only the named owner-local decision and its digest-bound candidate packet. If its base commit is stale, close that exact attempt as a failed stale-base candidate before regenerating the same evidence-bound change against current `origin/main`; never silently abandon the old digest. A prior `published` or `rejected_no_queue` receipt closes that exact candidate digest. + +Start in a sanitized local environment and run `/opt/homebrew/bin/python3.11 -I -B scripts/run-stack-weekly-live.py` exactly once after the pending retry inventory is empty. Isolated mode is mandatory so checkout files cannot shadow Python's standard library, and no-bytecode mode is mandatory so a successful run cannot leave ignored artifacts that poison the next week; the entrypoint also runs every Python child with `-I -B`. Direct or shebang execution is unsupported. Do not use the ambient `python3`; macOS may resolve it to Python 3.9. This is the private collection phase: it reads the current local Field Theory bookmark database, reconciles an owner-local snapshot, imports only missing `x-bookmarks` through GBrain with embeddings disabled, and runs the deterministic six-stage coordinator. Keep raw bookmark content, credentials, subprocess stderr, and private response payloads out of chat, Git, pull requests, and logs. Do not stop at `awaiting_approval`. That legacy coordinator state now means the collection packet is ready for this approved automatic tail. Use the exact `live_binding_receipt_relative_path`, `live_binding_receipt_digest`, `campaign_receipt_relative_path`, and `campaign_receipt_digest` returned by the live entrypoint; never select a receipt merely because it is newest. Resolve both relative paths below `$HOME/.local/state/stack/weekly-intelligence`. Follow that bound campaign receipt to its design-packet, retrieval, and candidate-evaluation artifacts. If there is no fresh material evidence, finish as `no_action`. A quiet week is healthy and must not manufacture a candidate. -When material evidence exists, use Stack's design-intelligence instructions to produce a high-quality critique that separates visible facts, interpretation, reusable principles, suitable contexts, anti-patterns, accessibility, motion, responsive behavior, implementation cues, uncertainty, and citations. Prefer concrete visual or interaction evidence over category labels and generic design advice. Generate at most one candidate per run. +When material evidence exists, use Stack's design-intelligence instructions to produce a high-quality critique that separates visible facts, interpretation, reusable principles, suitable contexts, anti-patterns, accessibility, motion, responsive behavior, implementation cues, uncertainty, and citations. Prefer concrete visual or interaction evidence over category labels and generic design advice. Generate a separate candidate for every independently material candidate supported by the bound campaign artifacts. Do not combine unrelated lessons into one candidate. -Before any model/reviewer wave, run `$HOME/Codex/scripts/codex-quota-preflight.sh` exactly once and obey its executor cap and allowed models. The material-evidence gate requires either two independent sources supporting the same reusable behavior, one source plus a repeated critique failure, or one source that fixes a documented hard evaluation failure. A candidate must target an existing Stack-owned `skills/**/SKILL.md` or `skills/**/references/**/*.md`, change no more than three files or 32,768 bytes, preserve provenance and license boundaries, and avoid vendor, imported, routing, command, runtime, credential, source-corpus, and private-overlay paths. +Process candidates sequentially. Before each model/reviewer wave, run `$HOME/Codex/scripts/codex-quota-preflight.sh` exactly once and obey its executor cap and allowed models. The material-evidence gate requires either two independent sources supporting the same reusable behavior, one source plus a repeated critique failure, or one source that fixes a documented hard evaluation failure. A candidate must target existing Stack-owned `skills/**/SKILL.md` or `skills/**/references/**/*.md`, preserve provenance and license boundaries, and avoid vendor, imported, routing, command, runtime, credential, source-corpus, and private-overlay paths. There is no per-run candidate-count, changed-file, or byte ceiling; scope each candidate to one coherent independently evaluated behavior. Store every proposed file body as `candidate-content/.utf8` beside the owner-only candidate packet and put that relative `content_file` in the edit. This digest-bound transport keeps the packet small without limiting the number or size of promoted files. Encode the selected materiality basis in the candidate packet's `rationale.materiality` object with exactly `basis`, `source_count`, `critique_failure_ids`, and `evaluation_failure_ids`. `source_count` is the number of distinct `source_identity` values carried by the selected evidence citations, not the number of evidence IDs. Use basis `two-independent-sources`, `source-plus-repeated-critique-failure`, or `source-fixes-hard-evaluation-failure`; bind critique failures only to a persisted design-packet `cluster_id` whose two or more cards all carry an anti-pattern/failure record, and bind evaluation failures only to a persisted candidate-evaluation `hard_gate_failures[].failure_id`. Leave the non-applicable failure list empty. If the bound campaign artifacts do not contain the required source or failure records, that basis is unavailable. Both the materializer and terminal recorder independently reject a candidate that does not satisfy one of these shapes. @@ -16,12 +18,12 @@ Extend `source_lineage` with the exact campaign run ID, campaign receipt digest, Materialize the smallest candidate in an isolated clean worktree based on current `origin/main`; never edit or clean the user's primary checkout. Create an owner-local authorization bound to the campaign run ID and campaign receipt digest with authorization contract `weekly-design-auto-promotion-approved-v1`, scope `isolated-owner-local-patch-only`, decision `approved`, and reviewer identity `weekly-design-automation`. Invoke `/opt/homebrew/bin/python3.11 -I -B scripts/materialize-capability-change.py --automatic-weekly-design --campaign-receipt --campaign-receipt-digest `; the ordinary materializer mode is not acceptable for this lane. This command must reopen the bound campaign artifacts and validate distinct source identities and every critique/evaluation failure ID before it writes any materialization output or permits PR creation. Bind the patch to its exact evidence, base commit, target files, and rollback digests. Run the `frozen-design-eval` gate against the reviewed harness at `$HOME/Codex`, requiring the candidate to beat `codex-current` on at least four fixed fixtures, pass the protected holdout and rotating canary, and have no structural, behavioral, visual, accessibility, privacy, citation, mobile-width, overflow, workflow, critical-data, or HTML hard failure. Then run the full Stack repository tests and obtain a fresh independent review whose final verdict is `ship`. Any implementation fix invalidates that verdict and requires another fresh review. -Only after every local gate passes, create or update one lineage-bound non-draft pull request from the isolated branch, wait for all required GitHub checks, and require a green pull-request-ci gate. Merge automatically only when the head SHA and candidate digest still match the reviewed inputs. Verify the merge on `origin/main`; a merely opened or auto-merge-queued pull request is not merge verification. +Only after every local gate passes for the current candidate, create or update one lineage-bound non-draft pull request from the isolated branch, wait for all required GitHub checks, and require a green pull-request-ci gate. Merge automatically only when the head SHA and candidate digest still match the reviewed inputs. Verify the merge on `origin/main`; a merely opened or auto-merge-queued pull request is not merge verification. Finish that candidate's publication and persist its terminal receipt, then refresh and verify `origin/main`, before starting the next candidate. After verified merge, publish from a fresh clean checkout of the merged `origin/main` using `/opt/homebrew/bin/python3.11 -I -B scripts/bootstrap-stack.py --install --expected-source-commit ` with the standard owner-local deployment, staging, and receipt roots, then run `/opt/homebrew/bin/python3.11 -I -B scripts/stack-doctor.py`. Publication must use the canonical `https://github.com/thecolormaroun/stack.git` origin, refresh its remote `main` immediately before canonical catalog compilation and again immediately before pointer movement, and require clean source `HEAD`, refreshed `origin/main`, both stage source/tree attestations, and the supplied merge commit to agree exactly. Compile from an installer-owned 0700 detached snapshot of that commit into a unique owner-private staging transaction and seal the compiled stages read-only. Run each configured verifier only against a disposable copy in a dedicated process group, terminate the entire group on success or timeout, then make a fresh sealed publication copy and revalidate it immediately before linking, after linking, and before receipt persistence. Verifier errors and timeouts must preserve the immutable failed transaction receipts; any failure after a switch must roll back every switched pointer. The public installer must not accept caller-supplied target files or stages. Promotion proof must use the exact immutable transaction pair beneath the configured `~/.local/state/stack/runtime-receipts` root; an equivalent pair elsewhere is invalid. Require both runtime discovery plus the installer's rollback receipt before reporting `published`. Never install from the candidate branch or a dirty checkout. -Weak candidates finish `no_action`. Candidates that fail materiality, evaluation, tests, review, or policy finish `rejected_no_queue`; retain a small owner-local audit receipt but do not leave a human approval backlog or open pull request. Operational failures such as unavailable GitHub, eval harness, runtime publisher, or reviewer finish `retry_with_alert`, preserve resumable evidence, and retry on the next scheduled run without weakening a gate. Before recording either failure outcome, close the lineage pull request and disable auto-merge; an open pull request is never a resumable terminal state. +Weak findings do not become candidates. If the campaign contains no independently material candidates, finish the campaign as `no_action`. Candidates that fail evaluation, tests, review, or policy finish individually as `rejected_no_queue`; retain a small owner-local audit receipt but do not leave a human approval backlog or open pull request. Operational failures such as unavailable GitHub, eval harness, runtime publisher, or reviewer finish that candidate as `retry_with_alert`, preserve resumable evidence, and retry it on the next scheduled run without weakening a gate. Before recording either failure outcome, close the lineage pull request and disable auto-merge; an open pull request is never a resumable terminal state. One failed candidate does not weaken another candidate's gates or erase a completed terminal receipt. -For every terminal outcome, write the exact decision contract as an owner-local mode-0600 JSON document below `$HOME/.local/state/stack/weekly-intelligence/promotion-decisions`, then run `/opt/homebrew/bin/python3.11 -I -B scripts/record-weekly-design-promotion.py --live-receipt --live-receipt-digest --campaign-receipt --decision --out-dir "$HOME/.local/state/stack/weekly-intelligence/promotion-receipts"`. The decision's `evidence` object must contain exactly these keys: `candidate_packet`, `materialization`, `evaluation`, `repository_tests`, `independent_review`, `pull_request_ci`, `merge_verification`, `runtime_publication`, and `rollback_receipt`. Each used value is `{ "path": , "digest": }`; unused values are `null`. The test receipt must bind the candidate digest and reviewed head to the exact full-suite command and passing count. The review receipt must bind the same candidate and head to a fresh independent `ship` verdict. GitHub CI and merge receipts must bind the candidate, pull request, head, checks, merge commit, and `origin/main`. Runtime and rollback evidence must be the immutable `transactions//install.json` and `rollback.json` pair emitted by the atomic installer, with the same transaction ID and directory; mutable `latest.json` and `rollback-state.json` aliases are status pointers only and never publication proof. Do not report a terminal disposition unless this fail-closed recorder accepts it. A `published` receipt requires every named gate and every real evidence artifact. The terminal receipt retains only evidence digests, never raw private content or absolute source paths. +For every terminal outcome, write the exact decision contract as an immutable owner-local mode-0600 JSON document named `----.json` below `$HOME/.local/state/stack/weekly-intelligence/promotion-decisions`; use the literal `no-candidate` in place of `` for `no_action`. Then run `/opt/homebrew/bin/python3.11 -I -B scripts/record-weekly-design-promotion.py --live-receipt --live-receipt-digest --campaign-receipt --decision --out-dir "$HOME/.local/state/stack/weekly-intelligence/promotion-receipts"`. Candidate receipts are append-only and keyed by campaign run ID, candidate digest, and exact receipt digest, so retries and later completion for the same candidate cannot collide; a campaign-level `no_action` receipt has no candidate digest. The decision's `evidence` object must contain exactly these keys: `candidate_packet`, `materialization`, `evaluation`, `repository_tests`, `independent_review`, `pull_request_ci`, `merge_verification`, `runtime_publication`, and `rollback_receipt`. Each used value is `{ "path": , "digest": }`; unused values are `null`. The test receipt must bind the candidate digest and reviewed head to the exact full-suite command and passing count. The review receipt must bind the same candidate and head to a fresh independent `ship` verdict. GitHub CI and merge receipts must bind the candidate, pull request, head, checks, merge commit, and `origin/main`. Runtime and rollback evidence must be the immutable `transactions//install.json` and `rollback.json` pair emitted by the atomic installer, with the same transaction ID and directory; mutable `latest.json` and `rollback-state.json` aliases are status pointers only and never publication proof. Do not report a terminal disposition unless this fail-closed recorder accepts it. A `published` receipt requires every named gate and every real evidence artifact. The terminal receipt retains only evidence digests, never raw private content or absolute source paths. Do not enable Direct X/OAuth, paid-provider fallback, source-contract expansion, automatic upstream pin changes, destructive cleanup, or publication outside this exact Stack-owned skill/reference contract. Report a concise safe summary containing the collection outcome, candidate disposition, pull request and merge state when applicable, runtime publication state, rollback receipt state, and newest owner-local terminal receipt path. diff --git a/config/weekly-intelligence.json b/config/weekly-intelligence.json index 0415394..6852971 100644 --- a/config/weekly-intelligence.json +++ b/config/weekly-intelligence.json @@ -3,17 +3,14 @@ "campaign_id": "stack-weekly-intelligence", "provider_egress": "deny", "analysis_budget": { - "unit": "model_calls", + "unit": "concurrent_model_contexts", "maximum": 3, "authorized": true, - "note": "The approved weekly automation may use up to three Codex model contexts for critique, candidate authoring, and independent review; paid external-provider fallback remains prohibited." + "note": "The approved weekly automation may use up to three concurrent Codex model contexts; candidates run sequentially and the current quota preflight controls each wave. Paid external-provider fallback remains prohibited." }, "automatic_promotion": { "state": "active", "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_candidates_per_run": 1, - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, "runtime_receipts_root": "~/.local/state/stack/runtime-receipts", "allowed_path_patterns": [ "skills/**/SKILL.md", @@ -84,7 +81,7 @@ "reasoning_effort": "high", "execution_environment": "local", "prompt_path": "config/weekly-intelligence-automation-prompt.md", - "prompt_digest": "e13e5234738dc7d93c17bc53e10a378d2becc013a029fbb769a91a247b9c8bd1", + "prompt_digest": "c1ceba6d3e156e3ef3ebe4cc3194b6d8429cbb85964ab9df94ec847d0a589cd2", "enabled": true, "approval_required": true, "state": "active" diff --git a/docs/design-intelligence-loop.md b/docs/design-intelligence-loop.md index e60169c..19c7c60 100644 --- a/docs/design-intelligence-loop.md +++ b/docs/design-intelligence-loop.md @@ -150,8 +150,8 @@ persisted and a current canonical maintenance receipt is linked. Existing Hermes collection/curation stays intake-only, and upstream maintenance remains separate. Authorization contract `weekly-design-auto-promotion-approved-v1` permits the scheduled Sol/high tail -to advance at most one existing Stack-owned skill/reference change without a -recurring human review. Weak evidence becomes `no_action`, failed gates become +to advance every independently material existing Stack-owned skill/reference +change sequentially without recurring human review. Weak evidence becomes `no_action`, failed gates become `rejected_no_queue`, and operational outages become `retry_with_alert`. After enablement, every eight-day window must contain a terminal non-duplicate campaign receipt or a visible alert with the diff --git a/docs/runtime-publication.md b/docs/runtime-publication.md index 7890f1d..f6813a0 100644 --- a/docs/runtime-publication.md +++ b/docs/runtime-publication.md @@ -104,9 +104,11 @@ lease, and receipts. Hermes owns a Monday intake-only collection/curation lane. The Stack weekly intelligence lane coordinates Saturday evidence and links the latest maintenance receipt but never invokes maintenance. Under `weekly-design-auto-promotion-approved-v1`, its separate automatic tail may -publish one evaluated Stack-owned skill/reference candidate after verified -merge. Collection, curation, evaluation, maintenance, and publication failures -remain distinct. +publish every independently material Stack-owned skill/reference candidate, +one lineage at a time, after each candidate's verified merge. The next +candidate starts only after the prior candidate has a terminal receipt and the +automation has refreshed `origin/main`. Collection, curation, evaluation, +maintenance, and publication failures remain distinct. Stack provides a reviewed wrapper and reports the Stack commit and policy digest. The approved Saturday 09:00 `America/Los_Angeles` contract becomes live diff --git a/docs/weekly-intelligence-operations.md b/docs/weekly-intelligence-operations.md index 006d528..e0f7510 100644 --- a/docs/weekly-intelligence-operations.md +++ b/docs/weekly-intelligence-operations.md @@ -66,8 +66,13 @@ rejected before even a Git subprocess can run; a concurrent upstream advance fails closed as a stale checkout for the next scheduled retry. The active task uses `gpt-5.6-sol` at high reasoning. Deterministic collection -still runs first. Model work occurs only when material evidence exists; at most -one candidate can reach evaluation in a run. +still runs first. Model work occurs only when material evidence exists. Every +independently material candidate may reach evaluation and is processed +sequentially with a fresh quota preflight for each model/reviewer wave. +Before new collection, the automation runs +`scripts/list-pending-weekly-design-promotions.py` and resolves every accepted +`retry_with_alert` receipt that lacks a later published or rejected receipt for +the same campaign and candidate digest. Missing decision evidence fails closed. ## Inputs and idempotency @@ -212,8 +217,9 @@ integration and approval gates. The tail works from a clean isolated branch based on current `origin/main`, never the user's primary checkout. It may change only existing -`skills/**/SKILL.md` or `skills/**/references/**/*.md`, with one candidate, -three files, and 32,768 bytes maximum per run. Material evidence, isolated +`skills/**/SKILL.md` or `skills/**/references/**/*.md`. There is no per-run +candidate-count, changed-file, or byte ceiling; each candidate must remain one +coherent independently evaluated behavior. Material evidence, isolated materialization, frozen design evaluation, full tests, a fresh independent `ship` review, green pull-request checks, and merge verification are all required before publication. @@ -222,9 +228,10 @@ The materialization command must use `--automatic-weekly-design`. Its authorization binds the exact coordinator run ID and receipt digest to `weekly-design-auto-promotion-approved-v1`. The materializer independently enforces replacement-only edits, existing Stack-owned skill/reference paths, -the three-file limit, the byte limit, checkout preservation, and the disposable -no-network clone. The terminal recorder repeats the path, digest, and limit -checks against the immutable base commit. +checkout preservation, and the disposable no-network clone. The terminal +recorder repeats the path and digest checks against the immutable base commit +and writes candidate-specific receipts so multiple outcomes from one campaign +cannot collide. Publication runs only from a fresh clean checkout of verified merged `origin/main` through @@ -250,7 +257,7 @@ rejection while a pull request remains open: --live-receipt /owner-local/weekly-state/live/live-receipts/run-id.json \ --live-receipt-digest returned-live-binding-sha256 \ --campaign-receipt /owner-local/weekly-state/receipts/run-id.json \ - --decision /owner-local/weekly-state/promotion-decisions/run-id.json \ + --decision /owner-local/weekly-state/promotion-decisions/run-id--candidate-digest--decision-digest.json \ --out-dir /owner-local/weekly-state/promotion-receipts ``` @@ -267,6 +274,16 @@ rollback-state shape. Runtime proof must use the installer's immutable files share one transaction ID and directory; mutable root-level aliases are operational pointers, not promotion evidence. The input files and output are mode `0600` below owner-only mode-`0700` directories. Terminal receipts retain only digests and -safe states, never raw bookmark content or absolute source paths. The accepted +safe states, never raw bookmark content or absolute source paths. Candidate +outcomes are stored as +`----.json`; a +campaign-level `no_action` with no candidate retains `.json`. +Decision documents use the same append-only identity with their exact decision +digest and use `no-candidate` for a `no_action` decision. This keeps sequential candidates independent and lets a later completion for +the same candidate coexist with an earlier retry. Candidate bodies live in +owner-only digest-addressed `candidate-content/*.utf8` files beside the small +candidate packet, so large changes stream through validation without creating +a promotion-size ceiling. +The accepted dispositions are `published`, `no_action`, `rejected_no_queue`, and `retry_with_alert`; none creates a human approval queue. diff --git a/registry/capability-change.schema.json b/registry/capability-change.schema.json index 24499ec..5dc9a18 100644 --- a/registry/capability-change.schema.json +++ b/registry/capability-change.schema.json @@ -9,6 +9,39 @@ "schema_version", "change_id", "state", "approval_state", "base_commit", "source_lineage", "target", "rationale", "rollback", "edits", "evaluation" ], + "allOf": [ + { + "if": { + "properties": { + "source_lineage": {"required": ["campaign_run_id"]} + } + }, + "then": { + "properties": { + "edits": { + "items": { + "required": ["content_file"], + "not": {"required": ["content"]} + } + } + } + }, + "else": { + "properties": { + "edits": { + "maxItems": 5, + "items": { + "required": ["content"], + "not": {"required": ["content_file"]}, + "properties": { + "content": {"maxLength": 65536} + } + } + } + } + } + } + ], "properties": { "schema_version": {"const": 1}, "change_id": {"$ref": "#/$defs/opaque"}, @@ -22,7 +55,6 @@ "edits": { "type": "array", "minItems": 1, - "maxItems": 5, "items": {"$ref": "#/$defs/edit"} }, "evaluation": {"$ref": "#/$defs/evaluation"} @@ -68,7 +100,24 @@ "card_ids": {"$ref": "#/$defs/opaque_list"}, "revision_ids": {"$ref": "#/$defs/opaque_list"}, "evidence_ids": {"$ref": "#/$defs/opaque_list"}, - "parent_digests": {"$ref": "#/$defs/digest_list"} + "parent_digests": {"$ref": "#/$defs/digest_list"}, + "campaign_run_id": {"type": "string", "minLength": 1, "maxLength": 96}, + "campaign_receipt_digest": {"$ref": "#/$defs/digest"}, + "design_packet_artifact_digest": {"$ref": "#/$defs/digest"}, + "retrieval_artifact_digest": {"$ref": "#/$defs/digest"}, + "candidate_evaluation_artifact_digest": {"$ref": "#/$defs/digest"} + }, + "dependentRequired": { + "campaign_run_id": [ + "campaign_receipt_digest", + "design_packet_artifact_digest", + "retrieval_artifact_digest", + "candidate_evaluation_artifact_digest" + ], + "campaign_receipt_digest": ["campaign_run_id"], + "design_packet_artifact_digest": ["campaign_run_id"], + "retrieval_artifact_digest": ["campaign_run_id"], + "candidate_evaluation_artifact_digest": ["campaign_run_id"] } }, "target": { @@ -138,7 +187,11 @@ "edit": { "type": "object", "additionalProperties": false, - "required": ["path", "role", "operation", "before_digest", "after_digest", "content"], + "required": ["path", "role", "operation", "before_digest", "after_digest"], + "oneOf": [ + {"required": ["content"], "not": {"required": ["content_file"]}}, + {"required": ["content_file"], "not": {"required": ["content"]}} + ], "properties": { "path": {"$ref": "#/$defs/repo_path"}, "role": {"enum": ["skill", "reference", "registry", "test", "documentation"]}, @@ -150,7 +203,11 @@ ] }, "after_digest": {"$ref": "#/$defs/digest"}, - "content": {"type": "string", "minLength": 1, "maxLength": 65536} + "content": {"type": "string", "minLength": 1}, + "content_file": { + "type": "string", + "pattern": "^candidate-content/[a-f0-9]{64}\\.utf8$" + } } }, "evaluation": { diff --git a/scripts/evaluate-design-intelligence-candidate.py b/scripts/evaluate-design-intelligence-candidate.py index c64820e..8b397c2 100644 --- a/scripts/evaluate-design-intelligence-candidate.py +++ b/scripts/evaluate-design-intelligence-candidate.py @@ -155,11 +155,24 @@ def _validate_packet(packet: Mapping[str, Any]) -> tuple[str, dict[str, Any]]: raise DesignEvaluationError("candidate packet rationale shape is unsupported") if packet["target"].get("provider") != "stack" or packet["target"].get("package") != "stack" or packet["target"].get("upstream_pin") is not None: raise DesignEvaluationError("candidate packet target is not Stack-owned") - if not isinstance(packet["edits"], list) or not 1 <= len(packet["edits"]) <= 5: + automatic_weekly = campaign_fields <= set(lineage) + if not isinstance(packet["edits"], list) or not packet["edits"]: raise DesignEvaluationError("candidate packet edits are unsupported") + if not automatic_weekly and len(packet["edits"]) > 5: + raise DesignEvaluationError("manual candidate packet exceeds the file limit") + manual_total = 0 for edit in packet["edits"]: - if not isinstance(edit, Mapping) or set(edit) != {"path", "role", "operation", "before_digest", "after_digest", "content"}: + common = {"path", "role", "operation", "before_digest", "after_digest"} + expected_edit = common | ({"content_file"} if automatic_weekly else {"content"}) + if not isinstance(edit, Mapping) or set(edit) != expected_edit: raise DesignEvaluationError("candidate packet edit shape is unsupported") + if not automatic_weekly: + content = edit.get("content") + if not isinstance(content, str) or not content or len(content) > 65536: + raise DesignEvaluationError("manual candidate packet content is unsupported") + manual_total += len(content.encode("utf-8")) + if manual_total > 131072: + raise DesignEvaluationError("manual candidate packet exceeds the byte limit") evaluation = packet.get("evaluation") if not isinstance(evaluation, Mapping) or evaluation.get("profile") != "design-learning-v1" or evaluation.get("harness_required") is not True: raise DesignEvaluationError("candidate packet is not bound to design-learning-v1") @@ -176,6 +189,17 @@ def _validate_materialization(receipt: Mapping[str, Any], packet: Mapping[str, A "authorization", "active_checkout", "isolation", "quarantine", "activation", "next_gate", } + lineage = packet.get("source_lineage") + campaign_fields = { + "campaign_run_id", + "campaign_receipt_digest", + "design_packet_artifact_digest", + "retrieval_artifact_digest", + "candidate_evaluation_artifact_digest", + } + automatic_weekly = isinstance(lineage, Mapping) and campaign_fields <= set(lineage) + if automatic_weekly: + expected.add("campaign_evidence") if set(receipt) != expected or receipt.get("schema_version") != 1 or receipt.get("status") != "prepared" or receipt.get("receipt_kind") != "capability-change-materialization": raise DesignEvaluationError("materialization receipt is incomplete or not prepared") bound = receipt.get("change_digest", receipt.get("candidate_packet_digest")) @@ -203,6 +227,15 @@ def _validate_materialization(receipt: Mapping[str, Any], packet: Mapping[str, A activation = receipt.get("activation") if not isinstance(activation, Mapping) or any(activation.get(key) is not False for key in ("active_pointer", "install", "publish", "draft_pr", "network_action")): raise DesignEvaluationError("materialization receipt authorizes activation") + if automatic_weekly: + campaign_evidence = receipt.get("campaign_evidence") + if ( + not isinstance(campaign_evidence, Mapping) + or set(campaign_evidence) != {"receipt_digest", "materiality_verified_before_materialization"} + or campaign_evidence.get("receipt_digest") != lineage.get("campaign_receipt_digest") + or campaign_evidence.get("materiality_verified_before_materialization") is not True + ): + raise DesignEvaluationError("automatic materialization receipt lacks campaign evidence") def _manifest(value: Any, expected_split: str, expected_digest: str) -> tuple[dict[str, Any], str]: diff --git a/scripts/list-pending-weekly-design-promotions.py b/scripts/list-pending-weekly-design-promotions.py new file mode 100644 index 0000000..4e75a4b --- /dev/null +++ b/scripts/list-pending-weekly-design-promotions.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""List unresolved weekly design promotion retries without exposing content. + +The promotion recorder is append-only. A candidate digest is pending when at +least one accepted receipt says ``retry_with_alert`` and no accepted receipt +for that same campaign and candidate says ``published`` or +``rejected_no_queue``. This command returns only opaque identifiers and +owner-local decision file names so the scheduler can resume before collecting +new evidence. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import Any + + +AUTHORIZATION_CONTRACT = "weekly-design-auto-promotion-approved-v1" +HEX64 = re.compile(r"^[a-f0-9]{64}$") +SAFE_RUN_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,95}$") +MAX_CONTROL_BYTES = 1024 * 1024 + + +class PendingPromotionError(ValueError): + """The retry inventory cannot be trusted.""" + + +def _owner_directory(path: Path, label: str) -> Path | None: + resolved = Path(os.path.abspath(str(path.expanduser()))) + if not resolved.exists(): + return None + if resolved.is_symlink(): + raise PendingPromotionError(f"{label} may not be a symlink") + details = resolved.stat() + if ( + not stat.S_ISDIR(details.st_mode) + or details.st_uid != os.getuid() + or stat.S_IMODE(details.st_mode) != 0o700 + ): + raise PendingPromotionError(f"{label} must be an owner-only 0700 directory") + return resolved + + +def _private_json(path: Path, label: str) -> tuple[dict[str, Any], str]: + if path.is_symlink(): + raise PendingPromotionError(f"{label} may not be a symlink") + try: + details = path.stat() + except OSError as error: + raise PendingPromotionError(f"{label} is unavailable") from error + if ( + not stat.S_ISREG(details.st_mode) + or details.st_uid != os.getuid() + or stat.S_IMODE(details.st_mode) != 0o600 + or details.st_size > MAX_CONTROL_BYTES + ): + raise PendingPromotionError(f"{label} must be an owner-only bounded control file") + try: + raw = path.read_bytes() + value = json.loads(raw.decode("utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise PendingPromotionError(f"{label} is not readable JSON") from error + if not isinstance(value, dict): + raise PendingPromotionError(f"{label} must be a JSON object") + return value, hashlib.sha256(raw).hexdigest() + + +def _receipt_identity( + document: Mapping[str, Any], + filename: str, + file_digest: str, +) -> tuple[str, str, str] | None: + if ( + document.get("schema_version") != 1 + or document.get("receipt_kind") != "weekly-design-automatic-promotion" + or document.get("authorization_contract") != AUTHORIZATION_CONTRACT + ): + raise PendingPromotionError("promotion receipt identity is invalid") + campaign = document.get("campaign") + candidate = document.get("candidate") + disposition = document.get("disposition") + if not isinstance(campaign, Mapping) or not isinstance(candidate, Mapping): + raise PendingPromotionError("promotion receipt campaign or candidate is invalid") + run_id = campaign.get("run_id") + if not isinstance(run_id, str) or SAFE_RUN_ID.fullmatch(run_id) is None: + raise PendingPromotionError("promotion receipt campaign identity is invalid") + if candidate.get("state") == "absent" and disposition == "no_action": + if filename != f"{run_id}.json": + raise PendingPromotionError("no_action receipt filename is invalid") + return None + candidate_digest = candidate.get("digest") + if ( + candidate.get("state") != "selected" + or not isinstance(candidate_digest, str) + or HEX64.fullmatch(candidate_digest) is None + or disposition not in {"published", "rejected_no_queue", "retry_with_alert"} + ): + raise PendingPromotionError("promotion receipt candidate identity is invalid") + expected_name = f"{run_id}--{candidate_digest}--{file_digest}.json" + legacy_name = f"{run_id}.json" + if filename not in {expected_name, legacy_name}: + raise PendingPromotionError("promotion receipt filename digest is invalid") + return run_id, candidate_digest, disposition + + +def pending_promotions(receipts_dir: Path, decisions_dir: Path) -> dict[str, Any]: + receipts = _owner_directory(receipts_dir, "promotion receipt directory") + decisions = _owner_directory(decisions_dir, "promotion decision directory") + if receipts is None: + return {"status": "ready", "pending": [], "count": 0} + + retries: set[tuple[str, str]] = set() + completed: set[tuple[str, str]] = set() + for path in sorted(receipts.glob("*.json")): + document, file_digest = _private_json(path, "promotion receipt") + identity = _receipt_identity(document, path.name, file_digest) + if identity is None: + continue + run_id, candidate_digest, disposition = identity + key = (run_id, candidate_digest) + if disposition == "retry_with_alert": + retries.add(key) + else: + completed.add(key) + + pending = sorted(retries - completed) + decision_names: dict[tuple[str, str], list[str]] = {key: [] for key in pending} + if decisions is not None: + for path in sorted(decisions.glob("*.json")): + document, decision_digest = _private_json(path, "promotion decision") + candidate = document.get("candidate") + if not ( + document.get("authorization_contract") == AUTHORIZATION_CONTRACT + and document.get("disposition") == "retry_with_alert" + and isinstance(candidate, Mapping) + and isinstance(candidate.get("digest"), str) + ): + continue + for key in pending: + run_id, candidate_digest = key + expected_name = f"{run_id}--{candidate_digest}--{decision_digest}.json" + legacy_name = f"{run_id}.json" + if candidate["digest"] == candidate_digest and path.name in { + expected_name, + legacy_name, + }: + decision_names[key].append(path.name) + + rows = [ + { + "campaign_run_id": run_id, + "candidate_digest": candidate_digest, + "decision_files": decision_names[(run_id, candidate_digest)], + } + for run_id, candidate_digest in pending + ] + missing = [ + {"campaign_run_id": row["campaign_run_id"], "candidate_digest": row["candidate_digest"]} + for row in rows + if not row["decision_files"] + ] + return { + "status": "blocked" if missing else "ready", + "pending": rows, + "count": len(rows), + "missing_decisions": missing, + } + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + state = Path("~/.local/state/stack/weekly-intelligence").expanduser() + parser.add_argument("--receipts-dir", type=Path, default=state / "promotion-receipts") + parser.add_argument("--decisions-dir", type=Path, default=state / "promotion-decisions") + args = parser.parse_args(argv) + try: + result = pending_promotions(args.receipts_dir, args.decisions_dir) + except PendingPromotionError as error: + print(json.dumps({"status": "blocked", "reason": str(error)}, sort_keys=True)) + return 2 + print(json.dumps(result, sort_keys=True)) + return 0 if result["status"] == "ready" else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/materialize-capability-change.py b/scripts/materialize-capability-change.py index 986a3d1..2fa43b2 100644 --- a/scripts/materialize-capability-change.py +++ b/scripts/materialize-capability-change.py @@ -15,6 +15,7 @@ from __future__ import annotations import argparse +import codecs import hashlib import importlib.util import json @@ -55,6 +56,8 @@ re.IGNORECASE, ) SHEBANG_RE = re.compile(r"^#!(?:\s*)/", re.MULTILINE) +CONTENT_FILE_RE = re.compile(r"^candidate-content/[a-f0-9]{64}\.utf8$") +CONTENT_SCAN_OVERLAP = 256 class MaterializationError(ValueError): @@ -100,6 +103,7 @@ def _validate_automatic_campaign( campaign_receipt: Path | None, campaign_receipt_digest: str | None, repository: Path, + packet_dir: Path | None, ) -> None: if campaign_receipt is None or not _is_digest(campaign_receipt_digest): raise MaterializationError("automatic weekly campaign receipt and digest are required") @@ -110,7 +114,15 @@ def _validate_automatic_campaign( campaign, actual_digest, resolved = contract._campaign(campaign_receipt) if actual_digest != campaign_receipt_digest: raise MaterializationError("automatic weekly campaign digest does not match receipt") - contract._candidate_packet(packet, campaign, actual_digest, resolved, repository=repository) + candidate_path = packet_dir / "candidate.json" if packet_dir is not None else None + contract._candidate_packet( + packet, + campaign, + actual_digest, + resolved, + candidate_path=candidate_path, + repository=repository, + ) except MaterializationError: raise except contract.PromotionReceiptError as error: @@ -185,6 +197,90 @@ def visit(current: Any, path: str) -> None: return sorted(set(findings)) +def _content_file(edit: Mapping[str, Any], packet_dir: Path | None) -> Path: + relative = edit.get("content_file") + if packet_dir is None or not isinstance(relative, str) or CONTENT_FILE_RE.fullmatch(relative) is None: + raise MaterializationError("candidate content_file is invalid") + root = packet_dir.expanduser().resolve(strict=True) + expected_parent = root / "candidate-content" + candidate = root / relative + for path in (root, expected_parent, candidate): + if path.is_symlink(): + raise MaterializationError("candidate content_file contains a symlink") + try: + root_details = root.stat() + parent = expected_parent.stat() + details = candidate.stat() + except OSError as error: + raise MaterializationError("candidate content_file is unavailable") from error + if ( + not stat.S_ISDIR(root_details.st_mode) + or root_details.st_uid != os.getuid() + or stat.S_IMODE(root_details.st_mode) != 0o700 + or not stat.S_ISDIR(parent.st_mode) + or parent.st_uid != os.getuid() + or stat.S_IMODE(parent.st_mode) != 0o700 + or not stat.S_ISREG(details.st_mode) + or details.st_uid != os.getuid() + or stat.S_IMODE(details.st_mode) != 0o600 + ): + raise MaterializationError("candidate content_file permissions are unsafe") + return candidate + + +def _inspect_content(edit: Mapping[str, Any], packet_dir: Path | None) -> int: + """Validate candidate content in bounded memory and return its byte count.""" + + content = edit.get("content") + content_file = edit.get("content_file") + if (isinstance(content, str)) == (content_file is not None): + raise MaterializationError("candidate edit must declare exactly one content source") + if isinstance(content, str): + if not content: + raise MaterializationError("candidate edit content is empty") + encoded = content.encode("utf-8") + if SHEBANG_RE.search(content): + raise MaterializationError("executable candidate content is not allowed") + if privacy_scan(content): + raise MaterializationError("candidate privacy scan failed") + if digest_bytes(encoded) != edit.get("after_digest"): + raise MaterializationError("after_digest does not match declared content") + return len(encoded) + + path = _content_file(edit, packet_dir) + expected_name = f"candidate-content/{edit.get('after_digest')}.utf8" + if content_file != expected_name: + raise MaterializationError("candidate content_file is not digest-addressed") + digest = hashlib.sha256() + decoder = codecs.getincrementaldecoder("utf-8")() + overlap = "" + size = 0 + try: + with path.open("rb") as handle: + while chunk := handle.read(65536): + size += len(chunk) + digest.update(chunk) + text = decoder.decode(chunk) + scan = overlap + text + if SHEBANG_RE.search(scan): + raise MaterializationError("executable candidate content is not allowed") + if PRIVATE_FIELD_RE.search(scan): + raise MaterializationError("candidate privacy scan failed") + overlap = scan[-CONTENT_SCAN_OVERLAP:] + tail = overlap + decoder.decode(b"", final=True) + except UnicodeError as error: + raise MaterializationError("candidate content_file is not UTF-8") from error + except OSError as error: + raise MaterializationError("candidate content_file is unreadable") from error + if not size: + raise MaterializationError("candidate edit content is empty") + if SHEBANG_RE.search(tail) or PRIVATE_FIELD_RE.search(tail): + raise MaterializationError("candidate content_file safety scan failed") + if digest.hexdigest() != edit.get("after_digest"): + raise MaterializationError("after_digest does not match declared content_file") + return size + + def safe_relative(value: Any) -> PurePosixPath: if not isinstance(value, str) or not value or "\\" in value or "\x00" in value: raise MaterializationError("change path is unsafe") @@ -365,7 +461,8 @@ def _validate_packet( policy: Mapping[str, Any], *, automatic_weekly: bool = False, -) -> tuple[PurePosixPath, PurePosixPath, list[dict[str, Any]]]: + packet_dir: Path | None = None, +) -> None: expected_top_level = { "schema_version", "change_id", "state", "approval_state", "base_commit", "source_lineage", "target", "rationale", "rollback", "edits", "evaluation", @@ -469,25 +566,29 @@ def _validate_packet( if automatic_weekly: if not isinstance(automatic, Mapping) or automatic.get("state") != "active" or automatic.get("authorization_contract") != "weekly-design-auto-promotion-approved-v1": raise MaterializationError("automatic weekly design policy is unavailable") - maximum = automatic.get("maximum_changed_files") else: maximum = materialization.get("maximum_changed_files", 5) - if not isinstance(maximum, int) or maximum < 1: - raise MaterializationError("materialization policy is invalid") edits = packet.get("edits") - if not isinstance(edits, list) or not 1 <= len(edits) <= min(5, maximum): - raise MaterializationError("candidate must declare between one and five edits") + if automatic_weekly: + if not isinstance(edits, list) or not edits: + raise MaterializationError("automatic weekly candidate must declare at least one edit") + else: + if not isinstance(maximum, int) or maximum < 1: + raise MaterializationError("materialization policy is invalid") + if not isinstance(edits, list) or not 1 <= len(edits) <= min(5, maximum): + raise MaterializationError("candidate must declare between one and five edits") allowed_roles = materialization.get("allowed_roles", list(ROLE_TO_CHANGE_KIND)) if not isinstance(allowed_roles, list) or not set(allowed_roles) <= set(ROLE_TO_CHANGE_KIND): raise MaterializationError("materialization role policy is invalid") paths: set[str] = set() total_bytes = 0 - validated: list[dict[str, Any]] = [] + primary_found = False for edit in edits: if not isinstance(edit, Mapping): raise MaterializationError("candidate edit is not an object") - required = {"path", "role", "operation", "before_digest", "after_digest", "content"} - if set(edit) != required: + common = {"path", "role", "operation", "before_digest", "after_digest"} + expected_edit = common | ({"content_file"} if automatic_weekly else {"content"}) + if set(edit) != expected_edit: raise MaterializationError("candidate edit has unsupported or missing fields") relative = safe_relative(edit.get("path")) path_text = relative.as_posix() @@ -520,13 +621,9 @@ def _validate_packet( raise MaterializationError("candidate deletion or rename is not allowed") if automatic_weekly and (operation != "replace" or not _automatic_weekly_path(relative)): raise MaterializationError("automatic weekly edit path is outside the approved contract") - content = edit.get("content") - if not isinstance(content, str) or not content: - raise MaterializationError("candidate edit content is empty") - encoded = content.encode("utf-8") - total_bytes += len(encoded) - if SHEBANG_RE.search(content): - raise MaterializationError("executable candidate content is not allowed") + content_bytes = _inspect_content(edit, packet_dir) + if not automatic_weekly: + total_bytes += content_bytes current_digest = _path_digest(repository, relative) current_mode = _path_mode(repository, relative) before = edit.get("before_digest") @@ -536,27 +633,24 @@ def _validate_packet( else: if not _is_digest(before) or current_digest != before: raise MaterializationError("before_digest does not match the active base") - if not _is_digest(edit.get("after_digest")) or digest_bytes(encoded) != edit.get("after_digest"): - raise MaterializationError("after_digest does not match declared content") + if not _is_digest(edit.get("after_digest")): + raise MaterializationError("after_digest is invalid") if current_mode is not None and current_mode & 0o111: raise MaterializationError("executable candidate path is not allowed") rollback_digest = rollback["path_digests"].get(path_text) if rollback_digest != before: raise MaterializationError("rollback path digest does not match edit") - validated.append({"path": relative, "role": role, "operation": operation, "before_digest": before, "after_digest": edit["after_digest"], "content": content}) + primary_found = primary_found or role == primary_role if set(rollback["path_digests"]) != paths: raise MaterializationError("rollback path set does not match declared edits") - if not any(row["role"] == next(role for role, kind in ROLE_TO_CHANGE_KIND.items() if kind == change_kind) for row in validated): + if not primary_found: raise MaterializationError("candidate is missing its primary capability edit") - policy_total = automatic.get("maximum_total_bytes") if automatic_weekly and isinstance(automatic, Mapping) else materialization.get("maximum_total_bytes", 131072) - if not isinstance(policy_total, int) or total_bytes > policy_total: - raise MaterializationError("candidate exceeds materialization byte limit") - findings = privacy_scan(packet) - if findings: - raise MaterializationError("candidate privacy scan failed") + if not automatic_weekly: + policy_total = materialization.get("maximum_total_bytes", 131072) + if not isinstance(policy_total, int) or total_bytes > policy_total: + raise MaterializationError("candidate exceeds materialization byte limit") if overlap.get("status") == "human_review_required": raise MaterializationError("candidate overlap analysis requires human review") - return capability_path, capability_root, validated def _validate_authorization( @@ -680,7 +774,7 @@ def _clone_exact(repository: Path, base_commit: str, destination: Path) -> Path: return clone -def _apply_edits(checkout: Path, edits: list[dict[str, Any]]) -> None: +def _apply_edits(checkout: Path, edits: list[dict[str, Any]], packet_dir: Path | None) -> None: for edit in edits: relative = edit["path"] _assert_no_symlink_components(checkout, relative) @@ -692,11 +786,30 @@ def _apply_edits(checkout: Path, edits: list[dict[str, Any]]) -> None: raise MaterializationError("isolated before_digest changed") if edit["operation"] == "create" and destination.exists(): raise MaterializationError("isolated create destination already exists") - destination.write_bytes(edit["content"].encode("utf-8")) + digest = hashlib.sha256() + try: + with destination.open("wb") as output: + if "content" in edit: + chunks = (edit["content"].encode("utf-8"),) + for chunk in chunks: + output.write(chunk) + digest.update(chunk) + else: + source = _content_file(edit, packet_dir) + with source.open("rb") as input_file: + while chunk := input_file.read(65536): + output.write(chunk) + digest.update(chunk) + except OSError as error: + raise MaterializationError("unable to write candidate content") from error + if digest.hexdigest() != edit["after_digest"]: + raise MaterializationError("candidate content changed during materialization") os.chmod(destination, 0o600) if destination.stat().st_mode & 0o111: raise MaterializationError("isolated candidate path became executable") - _run_git(checkout, ["add", "--", *[edit["path"].as_posix() for edit in edits]]) + paths = [edit["path"].as_posix() for edit in edits] + for offset in range(0, len(paths), 128): + _run_git(checkout, ["add", "--", *paths[offset:offset + 128]]) def _render_review_markdown(packet: Mapping[str, Any], receipt: Mapping[str, Any]) -> str: @@ -736,8 +849,11 @@ def materialize_change( ) -> dict[str, Any]: """Materialize ``packet`` and return its deterministic receipt.""" + packet_dir: Path | None = None if isinstance(packet, (Path, str)): - packet = load_object(Path(packet), "candidate packet") + packet_path = Path(packet).expanduser().resolve(strict=True) + packet_dir = packet_path.parent + packet = load_object(packet_path, "candidate packet") if isinstance(authorization, (Path, str)): authorization = load_object(Path(authorization), "materialization authorization") if output_dir is None: @@ -755,7 +871,13 @@ def materialize_change( if privacy_scan(packet): raise MaterializationError("candidate privacy scan failed") _validate_authorization(authorization, packet, automatic_weekly=automatic_weekly) - _validate_packet(packet, active, policy_doc, automatic_weekly=automatic_weekly) + _validate_packet( + packet, + active, + policy_doc, + automatic_weekly=automatic_weekly, + packet_dir=packet_dir, + ) if automatic_weekly: _validate_automatic_campaign( packet, @@ -763,6 +885,7 @@ def materialize_change( campaign_receipt, campaign_receipt_digest, active, + packet_dir, ) output = _owner_output_dir(Path(output_dir), active) before_status = _status_digest(active) @@ -775,7 +898,7 @@ def materialize_change( checkout = _clone_exact(active, packet["base_commit"], temporary_root) _apply_edits(checkout, [ {**row, "path": safe_relative(row["path"])} for row in edit_rows - ]) + ], packet_dir) changed = _run_git(checkout, ["diff", "--cached", "--name-only", "--no-ext-diff"]).splitlines() expected_paths = sorted(row["path"] for row in edit_rows) if sorted(changed) != expected_paths: @@ -886,11 +1009,10 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--campaign-receipt-digest", help="SHA-256 of --campaign-receipt; required with --automatic-weekly-design") args = parser.parse_args(argv) try: - packet = load_object(args.packet, "candidate packet") authorization = load_object(args.authorization, "materialization authorization") policy = _load_policy(args.policy) receipt = materialize_change( - packet, + args.packet, authorization, repository=args.repository, output_dir=args.out, diff --git a/scripts/record-weekly-design-promotion.py b/scripts/record-weekly-design-promotion.py index bad397e..8febca3 100644 --- a/scripts/record-weekly-design-promotion.py +++ b/scripts/record-weekly-design-promotion.py @@ -43,6 +43,7 @@ def _configured_runtime_receipts_root() -> Path: COMMIT = re.compile(r"^[a-f0-9]{40}$") SAFE_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,95}$") OPAQUE = re.compile(r"^[a-z][a-z0-9-]{1,32}:[a-f0-9]{16,64}$") +CONTENT_FILE_RE = re.compile(r"^candidate-content/[a-f0-9]{64}\.utf8$") AUTHORIZATION_CONTRACT = "weekly-design-auto-promotion-approved-v1" REQUIRED_GATES = ( "material-evidence", @@ -112,7 +113,12 @@ def _reject_symlink_ancestors(path: Path, *, allow_missing_leaf: bool = False) - raise PromotionReceiptError("owner-local path must not use a symlink") -def _private_file(path: Path, label: str) -> tuple[Path, bytes]: +def _private_file( + path: Path, + label: str, + *, + maximum_bytes: int | None = MAX_PRIVATE_JSON_BYTES, +) -> tuple[Path, bytes]: lexical = Path(os.path.abspath(str(Path(path).expanduser()))) _reject_symlink_ancestors(lexical) resolved = lexical.resolve(strict=False) @@ -131,7 +137,7 @@ def _private_file(path: Path, label: str) -> tuple[Path, bytes]: raise PromotionReceiptError(f"{label} must be an owner-only regular file with mode 0600") if not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.getuid() or stat.S_IMODE(parent.st_mode) != 0o700: raise PromotionReceiptError(f"{label} parent must be an owner-only directory with mode 0700") - if details.st_size > MAX_PRIVATE_JSON_BYTES: + if maximum_bytes is not None and details.st_size > maximum_bytes: raise PromotionReceiptError(f"{label} exceeds the private JSON size limit") flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) descriptor = -1 @@ -142,18 +148,18 @@ def _private_file(path: Path, label: str) -> tuple[Path, bytes]: not stat.S_ISREG(opened.st_mode) or opened.st_uid != os.getuid() or stat.S_IMODE(opened.st_mode) != 0o600 - or opened.st_size > MAX_PRIVATE_JSON_BYTES + or (maximum_bytes is not None and opened.st_size > maximum_bytes) ): raise PromotionReceiptError(f"{label} changed during validation") with os.fdopen(descriptor, "rb") as handle: descriptor = -1 - content = handle.read(MAX_PRIVATE_JSON_BYTES + 1) + content = handle.read() if maximum_bytes is None else handle.read(maximum_bytes + 1) except OSError as error: raise PromotionReceiptError(f"{label} is unavailable") from error finally: if descriptor >= 0: os.close(descriptor) - if len(content) > MAX_PRIVATE_JSON_BYTES: + if maximum_bytes is not None and len(content) > maximum_bytes: raise PromotionReceiptError(f"{label} exceeds the private JSON size limit") return resolved, content @@ -235,12 +241,17 @@ def _live_binding( return value, exact_digest -def _decision(value: Mapping[str, Any] | Path | str) -> dict[str, Any]: +def _decision( + value: Mapping[str, Any] | Path | str, +) -> tuple[dict[str, Any], Path | None, str | None]: if isinstance(value, (Path, str)): - _resolved, raw = _private_file(Path(value), "promotion decision") + resolved, raw = _private_file(Path(value), "promotion decision") document = _object_from_bytes(raw, "promotion decision") + file_digest = _digest_bytes(raw) elif isinstance(value, Mapping): document = dict(value) + resolved = None + file_digest = None else: raise PromotionReceiptError("promotion decision must be an object") expected = { @@ -262,7 +273,7 @@ def _decision(value: Mapping[str, Any] | Path | str) -> dict[str, Any]: raise PromotionReceiptError("promotion disposition is invalid") if not isinstance(document.get("reason_code"), str) or SAFE_ID.fullmatch(document["reason_code"]) is None: raise PromotionReceiptError("promotion reason code is invalid") - return document + return document, resolved, file_digest def _evidence_documents(value: Any) -> tuple[ @@ -290,7 +301,11 @@ def _evidence_documents(value: Any) -> tuple[ path = descriptor.get("path") if not isinstance(path, str) or not path: raise PromotionReceiptError(f"{name} evidence path is invalid") - resolved, raw = _private_file(Path(path), f"{name} evidence") + resolved, raw = _private_file( + Path(path), + f"{name} evidence", + maximum_bytes=None if name == "candidate_packet" else MAX_PRIVATE_JSON_BYTES, + ) if _digest_bytes(raw) != expected_digest: raise PromotionReceiptError(f"{name} evidence digest does not match the file") documents[name] = _object_from_bytes(raw, f"{name} evidence") @@ -315,6 +330,62 @@ def _automatic_path(value: Any) -> bool: return False +def _candidate_content_size(edit: Mapping[str, Any], candidate_path: Path | None) -> int: + """Validate one inline or digest-addressed body without buffering a file.""" + + content_file = edit.get("content_file") + if not isinstance(content_file, str) or CONTENT_FILE_RE.fullmatch(content_file) is None: + raise PromotionReceiptError("candidate packet content_file is invalid") + if content_file != f"candidate-content/{edit.get('after_digest')}.utf8": + raise PromotionReceiptError("candidate packet content_file is not digest-addressed") + if candidate_path is None: + raise PromotionReceiptError("candidate packet path is required for content_file") + root = candidate_path.parent.resolve(strict=True) + source = root / content_file + expected_parent = root / "candidate-content" + _reject_symlink_ancestors(source) + try: + parent = expected_parent.stat() + details = source.stat() + except OSError as error: + raise PromotionReceiptError("candidate packet content_file is unavailable") from error + if ( + not stat.S_ISDIR(parent.st_mode) + or parent.st_uid != os.getuid() + or stat.S_IMODE(parent.st_mode) != 0o700 + or not stat.S_ISREG(details.st_mode) + or details.st_uid != os.getuid() + or stat.S_IMODE(details.st_mode) != 0o600 + ): + raise PromotionReceiptError("candidate packet content_file permissions are unsafe") + + digest = hashlib.sha256() + size = 0 + descriptor = -1 + try: + descriptor = os.open(source, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + opened = os.fstat(descriptor) + if ( + not stat.S_ISREG(opened.st_mode) + or opened.st_uid != os.getuid() + or stat.S_IMODE(opened.st_mode) != 0o600 + ): + raise PromotionReceiptError("candidate packet content_file changed during validation") + with os.fdopen(descriptor, "rb") as handle: + descriptor = -1 + while chunk := handle.read(65536): + size += len(chunk) + digest.update(chunk) + except OSError as error: + raise PromotionReceiptError("candidate packet content_file is unreadable") from error + finally: + if descriptor >= 0: + os.close(descriptor) + if not size or digest.hexdigest() != edit.get("after_digest"): + raise PromotionReceiptError("candidate packet content_file digest is invalid") + return size + + def _git_base_file(base_commit: str, path: str, repository: Path = ROOT) -> bytes: try: result = subprocess.run( @@ -370,8 +441,9 @@ def _candidate_packet( campaign: Mapping[str, Any], campaign_digest: str, campaign_path: Path, + candidate_path: Path | None = None, repository: Path = ROOT, -) -> tuple[str, int, int, list[dict[str, Any]]]: +) -> tuple[str, int, int]: if document.get("schema_version") != 1 or not isinstance(document.get("base_commit"), str) or COMMIT.fullmatch(document["base_commit"]) is None: raise PromotionReceiptError("candidate packet identity is invalid") target = document.get("target") @@ -504,31 +576,24 @@ def _candidate_packet( if not material: raise PromotionReceiptError("candidate packet does not satisfy an approved materiality basis") edits = document.get("edits") - if not isinstance(edits, list) or not 1 <= len(edits) <= 3: - raise PromotionReceiptError("candidate packet exceeds the automatic file limit") + if not isinstance(edits, list) or not edits: + raise PromotionReceiptError("candidate packet must declare at least one edit") total = 0 - safe_edits: list[dict[str, Any]] = [] seen: set[str] = set() for edit in edits: - if not isinstance(edit, Mapping) or set(edit) != {"path", "role", "operation", "before_digest", "after_digest", "content"}: + common = {"path", "role", "operation", "before_digest", "after_digest"} + if not isinstance(edit, Mapping) or set(edit) != common | {"content_file"}: raise PromotionReceiptError("candidate packet edit shape is invalid") path = edit.get("path") role = edit.get("role") if not _automatic_path(path) or role not in {"skill", "reference"} or edit.get("operation") != "replace" or path in seen: raise PromotionReceiptError("candidate packet edit is outside the automatic contract") seen.add(path) - content = edit.get("content") - if not isinstance(content, str) or not content: - raise PromotionReceiptError("candidate packet edit content is invalid") - encoded = content.encode("utf-8") - total += len(encoded) - if total > 32768 or _digest_bytes(encoded) != edit.get("after_digest"): - raise PromotionReceiptError("candidate packet byte or digest limit is invalid") + total += _candidate_content_size(edit, candidate_path) base = _git_base_file(document["base_commit"], path, repository) if _digest_bytes(base) != edit.get("before_digest"): raise PromotionReceiptError("candidate packet before digest does not match its base commit") - safe_edits.append({key: edit[key] for key in ("path", "role", "operation", "before_digest", "after_digest")}) - return _digest_json(document), len(edits), total, safe_edits + return _digest_json(document), len(edits), total def _materialization(document: Mapping[str, Any], packet: Mapping[str, Any], packet_digest: str, campaign: Mapping[str, Any], campaign_digest: str) -> None: @@ -537,6 +602,17 @@ def _materialization(document: Mapping[str, Any], packet: Mapping[str, Any], pac authorization = document.get("authorization") if not isinstance(authorization, Mapping) or authorization.get("authorization_contract") != AUTHORIZATION_CONTRACT or authorization.get("campaign_run_id") != campaign.get("run_id") or authorization.get("campaign_receipt_digest") != campaign_digest: raise PromotionReceiptError("materialization evidence is not bound to the campaign authorization") + campaign_evidence = document.get("campaign_evidence") + if ( + not isinstance(campaign_evidence, Mapping) + or set(campaign_evidence) != { + "receipt_digest", + "materiality_verified_before_materialization", + } + or campaign_evidence.get("receipt_digest") != campaign_digest + or campaign_evidence.get("materiality_verified_before_materialization") is not True + ): + raise PromotionReceiptError("materialization evidence lacks verified campaign materiality") packet_paths = [row.get("path") for row in packet.get("edits", []) if isinstance(row, Mapping)] receipt_paths = [row.get("path") for row in document.get("edits", []) if isinstance(row, Mapping)] if isinstance(document.get("edits"), list) else [] active = document.get("active_checkout") @@ -647,9 +723,9 @@ def _validate_candidate(value: Any) -> str: elif state == "selected": if not isinstance(value.get("digest"), str) or HEX64.fullmatch(value["digest"]) is None: raise PromotionReceiptError("selected candidate digest is invalid") - if not isinstance(value.get("changed_files"), int) or isinstance(value.get("changed_files"), bool) or not 1 <= value["changed_files"] <= 3: + if not isinstance(value.get("changed_files"), int) or isinstance(value.get("changed_files"), bool) or value["changed_files"] < 1: raise PromotionReceiptError("selected candidate file count is invalid") - if not isinstance(value.get("total_bytes"), int) or isinstance(value.get("total_bytes"), bool) or not 1 <= value["total_bytes"] <= 32768: + if not isinstance(value.get("total_bytes"), int) or isinstance(value.get("total_bytes"), bool) or value["total_bytes"] < 1: raise PromotionReceiptError("selected candidate byte count is invalid") elif state not in CANDIDATE_STATES: raise PromotionReceiptError("candidate state is invalid") @@ -742,11 +818,12 @@ def _validate_outcome( packet = evidence.get("candidate_packet") if packet is None: raise PromotionReceiptError("selected candidate requires a bound candidate packet") - packet_digest, changed_files, total_bytes, _safe_edits = _candidate_packet( + packet_digest, changed_files, total_bytes = _candidate_packet( packet, campaign, campaign_digest, campaign_path, + evidence_paths["candidate_packet"] or Path(), ) if document["candidate"] != { "state": "selected", @@ -885,7 +962,13 @@ def record( campaign, campaign_digest, ) - document = _decision(decision) + document, decision_path, decision_digest = _decision(decision) + if decision_path is not None: + candidate_digest = document.get("candidate", {}).get("digest") + candidate_component = candidate_digest if candidate_digest is not None else "no-candidate" + expected_name = f"{campaign['run_id']}--{candidate_component}--{decision_digest}.json" + if decision_path.name != expected_name: + raise PromotionReceiptError("promotion decision filename digest is invalid") evidence, evidence_digests, evidence_paths = _evidence_documents(document["evidence"]) _validate_outcome( document, @@ -915,7 +998,14 @@ def record( "evidence": evidence_digests, "receipt_persisted": True, } - destination = _output_directory(Path(out_dir)) / f"{campaign['run_id']}.json" + candidate_digest = document["candidate"]["digest"] + receipt_digest = _digest_json(receipt) + receipt_name = ( + f"{campaign['run_id']}--{candidate_digest}--{receipt_digest}.json" + if candidate_digest is not None + else f"{campaign['run_id']}.json" + ) + destination = _output_directory(Path(out_dir)) / receipt_name _persist(destination, receipt) return receipt diff --git a/scripts/run-stack-weekly-intelligence.py b/scripts/run-stack-weekly-intelligence.py index 74a6dbc..3a2ae7d 100644 --- a/scripts/run-stack-weekly-intelligence.py +++ b/scripts/run-stack-weekly-intelligence.py @@ -372,15 +372,12 @@ def load_config(path: str | Path = DEFAULT_CONFIG) -> dict[str, Any]: if value.get("provider_egress") != "deny": raise WeeklyIntelligenceError("provider_egress_not_denied") budget = value.get("analysis_budget") - if not isinstance(budget, dict) or set(budget) != {"unit", "maximum", "authorized", "note"} or budget.get("unit") != "model_calls" or budget.get("maximum") != 3 or budget.get("authorized") is not True or not isinstance(budget.get("note"), str) or not budget["note"]: + if not isinstance(budget, dict) or set(budget) != {"unit", "maximum", "authorized", "note"} or budget.get("unit") != "concurrent_model_contexts" or budget.get("maximum") != 3 or budget.get("authorized") is not True or not isinstance(budget.get("note"), str) or not budget["note"]: raise WeeklyIntelligenceError("analysis_budget_invalid") promotion = value.get("automatic_promotion") expected_promotion = { "state": "active", "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_candidates_per_run": 1, - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, "runtime_receipts_root": "~/.local/state/stack/runtime-receipts", "allowed_path_patterns": [ "skills/**/SKILL.md", diff --git a/skills/design/design-intelligence/SKILL.md b/skills/design/design-intelligence/SKILL.md index 406d4fe..ae18220 100644 --- a/skills/design/design-intelligence/SKILL.md +++ b/skills/design/design-intelligence/SKILL.md @@ -1,6 +1,6 @@ --- name: design-intelligence -description: "Run the weekly design intelligence loop: scan curated sources, Arc bookmarks, Field Theory/X bookmarks, and GBrain deltas; synthesize a design digest; and automatically promote one evaluated Stack-owned skill/reference improvement when every publication gate passes." +description: "Run the weekly design intelligence loop: scan curated sources, Arc bookmarks, Field Theory/X bookmarks, and GBrain deltas; synthesize a design digest; and automatically promote every independently evaluated Stack-owned skill/reference improvement when every publication gate passes." --- # Design Intelligence Loop @@ -9,7 +9,7 @@ Use this skill for the rebuilt weekly design digest and taste-compounding workfl The loop has two jobs: - Find high-signal design inspiration from curated sources and Maroun's saved links. -- Convert repeated lessons into evaluated Studio/CDO skill updates and publish at most one automatically when every scoped gate passes. +- Convert repeated lessons into evaluated Studio/CDO skill updates and publish every independently material candidate sequentially when every scoped gate passes. ## Operating Rule @@ -17,8 +17,9 @@ Run source intake read-only. Do not mutate Arc, Field Theory, X/Twitter, GBrain Promotion is a separate automatic tail under authorization contract `weekly-design-auto-promotion-approved-v1`. It may touch only existing -Stack-owned skill/reference Markdown, at most one candidate and three files per -run. It still requires isolated materialization, frozen evaluation, full tests, +Stack-owned skill/reference Markdown. It has no per-run candidate-count, +changed-file, or byte ceiling, and processes one independent candidate lineage +at a time. It still requires isolated materialization, frozen evaluation, full tests, a fresh independent `ship` review, green pull-request checks, verified merge, atomic runtime publication, discovery, and rollback evidence. @@ -62,6 +63,10 @@ Every run must produce: - A weekly design digest with Output A, Output B, and Output C. - A promotion packet with proposed Studio/CDO changes, evidence links, gate results, and a `no_action`, `rejected_no_queue`, `retry_with_alert`, or published disposition. +Automatic candidate JSON keeps proposed bodies in sibling owner-only, +digest-addressed `candidate-content/*.utf8` files. This transport permits every +independently material candidate and does not impose a file-count or byte cap. + The digest may suggest Zettelkasten notes but must not write them. Skill and reference changes advance only through the separate automatic promotion tail; the digest itself never edits a runtime or checkout. diff --git a/skills/design/design-intelligence/references/output-contract.md b/skills/design/design-intelligence/references/output-contract.md index 021417f..65b0e4e 100644 --- a/skills/design/design-intelligence/references/output-contract.md +++ b/skills/design/design-intelligence/references/output-contract.md @@ -77,9 +77,10 @@ For each note: ## Output C - Studio Skill Update Candidate -Select at most one material skill/reference update. A quiet week should select -none. The digest never applies it; the separate approved automatic tail owns -evaluation, pull-request merge, and publication. +Select every independently material skill/reference update. Keep unrelated +behaviors in separate candidates and process them sequentially. A quiet week +should select none. The digest never applies them; the separate approved +automatic tail owns evaluation, pull-request merge, and publication. For each update: - target file. @@ -88,6 +89,11 @@ For each update: - duplicate/idempotency check. - eval required before promotion. +For an automatic candidate, write each complete proposed file body to an +owner-only digest-addressed `candidate-content/.utf8` file beside +the candidate JSON and reference it with `content_file`. This streams large +changes through validation without imposing a changed-file or byte ceiling. + ## Backfill Packet For the outage catch-up, chunk by week and include: diff --git a/skills/design/design-intelligence/references/promotion-rules.md b/skills/design/design-intelligence/references/promotion-rules.md index 73ac673..6a7204b 100644 --- a/skills/design/design-intelligence/references/promotion-rules.md +++ b/skills/design/design-intelligence/references/promotion-rules.md @@ -20,10 +20,10 @@ Source intake must not mutate: - critique logs. Source collection is read-only. Promotion runs separately under exact contract -`weekly-design-auto-promotion-approved-v1`, is limited to one candidate and -three Stack-owned skill/reference Markdown files per run, and uses an isolated -branch plus pull request. The active checkout is never the materialization -surface. +`weekly-design-auto-promotion-approved-v1`, processes every independently +material candidate sequentially, and uses an isolated branch plus pull request +for each candidate. It has no per-run candidate-count, changed-file, or byte +ceiling. The active checkout is never the materialization surface. ## Candidate Thresholds @@ -82,8 +82,9 @@ If the candidate is useful but the eval is not run, label it `retry_with_alert`, ## Runtime Promotion The approved automatic tail may create or update one lineage-bound pull -request, merge it after all required checks pass, and publish the merged Stack -revision to Claude and Codex. It must require, in order: +request at a time, merge it after all required checks pass, and publish every +independently material candidate to Claude and Codex. It has no candidate, +changed-file, or byte ceiling. Each candidate must require, in order: 1. `material-evidence` and exact source citations. 2. a live-binding receipt that fixes the exact canonical campaign path and @@ -91,7 +92,7 @@ revision to Claude and Codex. It must require, in order: 3. candidate lineage whose packet, card, revision, evidence, and change IDs are present in the bound design-packet and retrieval artifacts, plus exact design-packet, retrieval, and candidate-evaluation artifact digests. -4. isolated automatic-weekly materialization with path/size limits and rollback digests. +4. isolated automatic-weekly materialization with exact path and rollback digests. 5. `frozen-design-eval` with the baseline, protected holdout, and rotating canary. 6. the full Stack repository test suite. 7. a fresh independent review with verdict `ship`. diff --git a/tests/test_design_intelligence_candidate.py b/tests/test_design_intelligence_candidate.py index 85a5d54..62d10c6 100644 --- a/tests/test_design_intelligence_candidate.py +++ b/tests/test_design_intelligence_candidate.py @@ -138,6 +138,78 @@ def test_missing_or_incomplete_harness_is_blocked(self) -> None: self.assertEqual("blocked-eval", unsafe["status"]) self.assertIn("unsafe_eval_root", unsafe["reason_codes"]) + def test_manual_packet_limits_remain_fail_closed(self) -> None: + too_many = json.loads(json.dumps(self.packet)) + too_many["edits"] = [dict(too_many["edits"][0], path=f"skills/design/fixture/references/{index}.md") for index in range(6)] + with self.assertRaisesRegex(EVALUATOR.DesignEvaluationError, "file limit"): + EVALUATOR._validate_packet(too_many) + + oversized_edit = json.loads(json.dumps(self.packet)) + oversized_edit["edits"][0]["content"] = "x" * 65537 + with self.assertRaisesRegex(EVALUATOR.DesignEvaluationError, "content"): + EVALUATOR._validate_packet(oversized_edit) + + oversized_total = json.loads(json.dumps(self.packet)) + oversized_total["edits"] = [ + dict(oversized_total["edits"][0], path=f"skills/design/fixture/references/{index}.md", content="x" * 50000) + for index in range(3) + ] + with self.assertRaisesRegex(EVALUATOR.DesignEvaluationError, "byte limit"): + EVALUATOR._validate_packet(oversized_total) + + external_manual = json.loads(json.dumps(self.packet)) + external_manual["edits"][0].pop("content") + external_manual["edits"][0]["content_file"] = "candidate-content/" + "a" * 64 + ".utf8" + with self.assertRaisesRegex(EVALUATOR.DesignEvaluationError, "shape"): + EVALUATOR._validate_packet(external_manual) + + def test_automatic_packet_uses_external_content_without_file_limit(self) -> None: + automatic = json.loads(json.dumps(self.packet)) + automatic["source_lineage"].update({ + "campaign_run_id": "weekly-fixture", + "campaign_receipt_digest": "2" * 64, + "design_packet_artifact_digest": "3" * 64, + "retrieval_artifact_digest": "4" * 64, + "candidate_evaluation_artifact_digest": "5" * 64, + }) + prototype = automatic["edits"][0] + prototype.pop("content") + prototype["content_file"] = f"candidate-content/{prototype['after_digest']}.utf8" + automatic["edits"] = [ + dict(prototype, path=f"skills/design/fixture/references/{index}.md") + for index in range(6) + ] + + digest, _evaluation = EVALUATOR._validate_packet(automatic) + + self.assertEqual(EVALUATOR.digest_json(automatic), digest) + + def test_automatic_materialization_campaign_evidence_reaches_evaluator(self) -> None: + automatic = json.loads(json.dumps(self.packet)) + automatic["source_lineage"].update({ + "campaign_run_id": "weekly-fixture", + "campaign_receipt_digest": "2" * 64, + "design_packet_artifact_digest": "3" * 64, + "retrieval_artifact_digest": "4" * 64, + "candidate_evaluation_artifact_digest": "5" * 64, + }) + edit = automatic["edits"][0] + edit.pop("content") + edit["content_file"] = f"candidate-content/{edit['after_digest']}.utf8" + materialization = json.loads(json.dumps(self.materialization)) + materialization["change_digest"] = EVALUATOR.digest_json(automatic) + materialization["authorization"]["change_digest"] = materialization["change_digest"] + materialization["campaign_evidence"] = { + "receipt_digest": automatic["source_lineage"]["campaign_receipt_digest"], + "materiality_verified_before_materialization": True, + } + + EVALUATOR._validate_materialization( + materialization, + automatic, + EVALUATOR.digest_json(automatic), + ) + def test_four_development_wins_do_not_override_holdout_regression_or_hard_gate(self) -> None: results = self.results() holdout = json.loads(results["holdout"].read_text()) diff --git a/tests/test_list_pending_weekly_design_promotions.py b/tests/test_list_pending_weekly_design_promotions.py new file mode 100644 index 0000000..ff1ede9 --- /dev/null +++ b/tests/test_list_pending_weekly_design_promotions.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "list_pending_weekly_design_promotions", + ROOT / "scripts" / "list-pending-weekly-design-promotions.py", +) +assert SPEC and SPEC.loader +PENDING = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PENDING) + + +class ListPendingWeeklyDesignPromotionsTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.receipts = self.root / "receipts" + self.decisions = self.root / "decisions" + self.receipts.mkdir(mode=0o700) + self.decisions.mkdir(mode=0o700) + self.run_id = "weekly-fixture" + self.candidate_digest = "a" * 64 + + def tearDown(self) -> None: + self.temporary.cleanup() + + def write(self, directory: Path, name: str, document: dict) -> str: + path = directory / name + raw = json.dumps(document).encode("utf-8") + path.write_bytes(raw) + path.chmod(0o600) + return PENDING.hashlib.sha256(raw).hexdigest() + + def write_decision(self, *, run_id: str | None = None, legacy: bool = False) -> str: + document = self.decision() + raw = json.dumps(document).encode("utf-8") + digest = PENDING.hashlib.sha256(raw).hexdigest() + campaign = run_id or self.run_id + name = ( + f"{campaign}.json" + if legacy + else f"{campaign}--{self.candidate_digest}--{digest}.json" + ) + self.write(self.decisions, name, document) + return name + + def write_receipt(self, disposition: str) -> str: + document = self.receipt(disposition) + raw = json.dumps(document).encode("utf-8") + digest = PENDING.hashlib.sha256(raw).hexdigest() + name = f"{self.run_id}--{self.candidate_digest}--{digest}.json" + self.write(self.receipts, name, document) + return name + + def receipt(self, disposition: str) -> dict: + return { + "schema_version": 1, + "receipt_kind": "weekly-design-automatic-promotion", + "authorization_contract": PENDING.AUTHORIZATION_CONTRACT, + "disposition": disposition, + "campaign": {"run_id": self.run_id}, + "candidate": {"state": "selected", "digest": self.candidate_digest}, + } + + def decision(self) -> dict: + return { + "authorization_contract": PENDING.AUTHORIZATION_CONTRACT, + "disposition": "retry_with_alert", + "candidate": {"state": "selected", "digest": self.candidate_digest}, + } + + def test_retry_is_pending_with_only_safe_decision_filename(self) -> None: + self.write_receipt("retry_with_alert") + decision_name = self.write_decision() + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("ready", result["status"]) + self.assertEqual(1, result["count"]) + self.assertEqual([decision_name], result["pending"][0]["decision_files"]) + self.assertNotIn(str(self.root), json.dumps(result)) + + def test_later_terminal_receipt_closes_the_same_candidate_digest(self) -> None: + self.write_receipt("retry_with_alert") + self.write_receipt("published") + self.write_decision() + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("ready", result["status"]) + self.assertEqual([], result["pending"]) + + def test_missing_retry_decision_fails_closed(self) -> None: + self.write_receipt("retry_with_alert") + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("blocked", result["status"]) + self.assertEqual(1, len(result["missing_decisions"])) + + def test_wrong_campaign_decision_cannot_satisfy_pending_retry(self) -> None: + self.write_receipt("retry_with_alert") + self.write_decision(run_id="weekly-other") + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("blocked", result["status"]) + self.assertEqual([], result["pending"][0]["decision_files"]) + + def test_legacy_selected_receipt_and_decision_remain_resumable(self) -> None: + self.write(self.receipts, f"{self.run_id}.json", self.receipt("retry_with_alert")) + decision_name = self.write_decision(legacy=True) + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("ready", result["status"]) + self.assertEqual(1, result["count"]) + self.assertEqual([decision_name], result["pending"][0]["decision_files"]) + + def test_legacy_terminal_selected_receipt_is_closed(self) -> None: + self.write(self.receipts, f"{self.run_id}.json", self.receipt("published")) + + result = PENDING.pending_promotions(self.receipts, self.decisions) + + self.assertEqual("ready", result["status"]) + self.assertEqual([], result["pending"]) + + def test_wrong_receipt_filename_fails_closed(self) -> None: + self.write(self.receipts, "retry.json", self.receipt("retry_with_alert")) + + with self.assertRaisesRegex(PENDING.PendingPromotionError, "filename digest"): + PENDING.pending_promotions(self.receipts, self.decisions) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_materialize_capability_change.py b/tests/test_materialize_capability_change.py index b614581..b98b69c 100644 --- a/tests/test_materialize_capability_change.py +++ b/tests/test_materialize_capability_change.py @@ -39,6 +39,13 @@ def setUp(self) -> None: target_root = self.repo / "skills" / "design" / "fixture" target_root.mkdir(parents=True) (target_root / "SKILL.md").write_text("---\nname: fixture\n---\n# Fixture\n", encoding="utf-8") + references = target_root / "references" + references.mkdir() + for index in range(1, 7): + (references / f"rule-{index}.md").write_text( + f"# Rule {index}\n\nExisting guidance.\n", + encoding="utf-8", + ) (target_root / "capability.json").write_text(json.dumps({"canonical_name": "fixture", "ownership": {"provider": "stack", "package": "stack"}}), encoding="utf-8") (self.repo / "registry").mkdir() (self.repo / "registry" / "capabilities.json").write_text(json.dumps({ @@ -145,6 +152,34 @@ def automatic_authorization(self, packet: dict, **overrides: object) -> dict: value.update(overrides) return value + @staticmethod + def automatic_policy() -> dict: + return { + "materialization": {"allowed_roles": ["skill", "reference", "registry", "test", "documentation"]}, + "automatic_weekly_design_promotion": { + "state": "active", + "authorization_contract": "weekly-design-auto-promotion-approved-v1", + }, + } + + def packet_bundle(self, packet: dict, name: str) -> Path: + bundle = self.root / name + bundle.mkdir(mode=0o700) + content_dir = bundle / "candidate-content" + content_dir.mkdir(mode=0o700) + for edit in packet["edits"]: + if "content" not in edit: + continue + content = edit.pop("content").encode() + blob = content_dir / f"{edit['after_digest']}.utf8" + blob.write_bytes(content) + blob.chmod(0o600) + edit["content_file"] = f"candidate-content/{blob.name}" + packet_path = bundle / "candidate.json" + packet_path.write_text(MATERIALIZER.canonical_json(packet)) + packet_path.chmod(0o600) + return packet_path + def automatic_campaign(self, packet: dict) -> tuple[Path, str]: state = self.root / "campaign-state" state.mkdir(mode=0o700) @@ -318,20 +353,13 @@ def test_authorization_is_exactly_bound_and_symlink_parent_is_rejected(self) -> def test_automatic_weekly_mode_accepts_only_existing_skill_or_reference_replacements(self) -> None: packet = self.packet() campaign, campaign_digest = self.automatic_campaign(packet) + packet_path = self.packet_bundle(packet, "automatic-packet") receipt = MATERIALIZER.materialize_change( - packet, + packet_path, self.automatic_authorization(packet), repository=self.repo, output_dir=self.root / "automatic", - policy={ - "materialization": {"allowed_roles": ["skill", "reference", "registry", "test", "documentation"]}, - "automatic_weekly_design_promotion": { - "state": "active", - "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, - }, - }, + policy=self.automatic_policy(), automatic_weekly=True, campaign_receipt=campaign, campaign_receipt_digest=campaign_digest, @@ -343,44 +371,90 @@ def test_automatic_weekly_mode_accepts_only_existing_skill_or_reference_replacem support.update({"path": "tests/fixture.md", "role": "test", "operation": "create", "before_digest": None}) support["after_digest"] = MATERIALIZER.digest_bytes(support["content"].encode()) rejected = self.packet(edits=[support]) + self.automatic_authorization(rejected) + rejected_path = self.packet_bundle(rejected, "rejected-packet") + rejected_authorization = self.automatic_authorization(rejected) with self.assertRaisesRegex(MATERIALIZER.MaterializationError, "automatic weekly"): MATERIALIZER.materialize_change( - rejected, - self.automatic_authorization(rejected), + rejected_path, + rejected_authorization, repository=self.repo, output_dir=self.root / "rejected", - policy={ - "materialization": {"allowed_roles": ["skill", "reference", "registry", "test", "documentation"]}, - "automatic_weekly_design_promotion": { - "state": "active", - "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, - }, - }, + policy=self.automatic_policy(), automatic_weekly=True, ) + def test_automatic_weekly_mode_has_no_candidate_file_or_byte_cap(self) -> None: + references = self.repo / "skills/design/fixture/references" + for index in range(1, 141): + (references / f"rule-{index}.md").write_text(f"# Rule {index}\n") + subprocess.run(["git", "-C", str(self.repo), "add", "skills/design/fixture/references"], check=True) + subprocess.run( + ["git", "-C", str(self.repo), "commit", "-m", "fixture: add uncapped references"], + check=True, + stdout=subprocess.DEVNULL, + ) + self.base = subprocess.check_output( + ["git", "-C", str(self.repo), "rev-parse", "HEAD"], + text=True, + ).strip() + + edits = [] + bundle = self.root / "uncapped-bundle" + bundle.mkdir(mode=0o700) + content_dir = bundle / "candidate-content" + content_dir.mkdir(mode=0o700) + for index in range(1, 141): + relative = f"skills/design/fixture/references/rule-{index}.md" + before = (self.repo / relative).read_bytes() + content = before.decode() + "\n" + (f"Expanded rule {index}. " * 350) + "\n" + after_digest = MATERIALIZER.digest_bytes(content.encode()) + blob = content_dir / f"{after_digest}.utf8" + blob.write_bytes(content.encode()) + blob.chmod(0o600) + edits.append({ + "path": relative, + "role": "reference", + "operation": "replace", + "before_digest": MATERIALIZER.digest_bytes(before), + "after_digest": after_digest, + "content_file": f"candidate-content/{blob.name}", + }) + self.assertGreater(len(edits), 128) + self.assertGreater(sum(path.stat().st_size for path in content_dir.iterdir()), 32768) + packet = self.packet(change_kind="reference-update", edits=edits) + campaign, campaign_digest = self.automatic_campaign(packet) + packet_path = bundle / "candidate.json" + packet_path.write_text(MATERIALIZER.canonical_json(packet)) + packet_path.chmod(0o600) + + receipt = MATERIALIZER.materialize_change( + packet_path, + self.automatic_authorization(packet), + repository=self.repo, + output_dir=self.root / "uncapped-automatic", + policy=self.automatic_policy(), + automatic_weekly=True, + campaign_receipt=campaign, + campaign_receipt_digest=campaign_digest, + ) + + self.assertEqual("prepared", receipt["status"]) + self.assertEqual(len(edits), len(receipt["edits"])) + def test_automatic_weekly_forged_failure_id_is_rejected_before_materialization(self) -> None: packet = self.packet() campaign, campaign_digest = self.automatic_campaign(packet) packet["rationale"]["materiality"]["critique_failure_ids"] = ["failure:" + "f" * 16] + packet_path = self.packet_bundle(packet, "forged-packet") output = self.root / "forged" with self.assertRaisesRegex(MATERIALIZER.MaterializationError, "material failure IDs"): MATERIALIZER.materialize_change( - packet, + packet_path, self.automatic_authorization(packet), repository=self.repo, output_dir=output, - policy={ - "materialization": {"allowed_roles": ["skill", "reference", "registry", "test", "documentation"]}, - "automatic_weekly_design_promotion": { - "state": "active", - "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, - }, - }, + policy=self.automatic_policy(), automatic_weekly=True, campaign_receipt=campaign, campaign_receipt_digest=campaign_digest, @@ -390,42 +464,26 @@ def test_automatic_weekly_forged_failure_id_is_rejected_before_materialization(s def test_automatic_campaign_cannot_bypass_preflight_by_omitting_mode_flag(self) -> None: packet = self.packet() self.automatic_campaign(packet) + packet_path = self.packet_bundle(packet, "implicit-packet") output = self.root / "implicit-automatic" with self.assertRaisesRegex(MATERIALIZER.MaterializationError, "campaign receipt and digest"): MATERIALIZER.materialize_change( - packet, + packet_path, self.automatic_authorization(packet), repository=self.repo, output_dir=output, - policy={ - "materialization": {"allowed_roles": ["skill", "reference"]}, - "automatic_weekly_design_promotion": { - "state": "active", - "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, - }, - }, + policy=self.automatic_policy(), ) self.assertFalse(output.exists()) def test_cli_cannot_bypass_automatic_preflight_by_omitting_mode_flag(self) -> None: packet = self.packet() self.automatic_campaign(packet) + packet_path = self.packet_bundle(packet, "cli-automatic-packet") authorization = self.automatic_authorization(packet) - policy = { - "materialization": {"allowed_roles": ["skill", "reference"]}, - "automatic_weekly_design_promotion": { - "state": "active", - "authorization_contract": "weekly-design-auto-promotion-approved-v1", - "maximum_changed_files": 3, - "maximum_total_bytes": 32768, - }, - } - packet_path = self.root / "automatic-packet.json" + policy = self.automatic_policy() authorization_path = self.root / "automatic-authorization.json" policy_path = self.root / "automatic-policy.json" - packet_path.write_text(json.dumps(packet), encoding="utf-8") authorization_path.write_text(json.dumps(authorization), encoding="utf-8") policy_path.write_text(json.dumps(policy), encoding="utf-8") output = self.root / "cli-implicit-automatic" diff --git a/tests/test_record_weekly_design_promotion.py b/tests/test_record_weekly_design_promotion.py index 07ef1cc..6c01e1d 100644 --- a/tests/test_record_weekly_design_promotion.py +++ b/tests/test_record_weekly_design_promotion.py @@ -182,11 +182,26 @@ def record(self, decision: dict, out: Path) -> dict: out, ) + def packet_total_bytes(self) -> int: + total = 0 + for edit in self.packet["edits"]: + if "content" in edit: + total += len(edit["content"].encode()) + else: + total += (self.evidence_root / edit["content_file"]).stat().st_size + return total + def _candidate_packet(self) -> dict: base = subprocess.check_output(["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True).strip() path = "skills/design/design-intelligence/SKILL.md" before = subprocess.check_output(["git", "-C", str(ROOT), "show", f"{base}:{path}"]) content = before.decode("utf-8") + "\n\n" + after_digest = hashlib.sha256(content.encode()).hexdigest() + content_dir = self.evidence_root / "candidate-content" + content_dir.mkdir(mode=0o700, exist_ok=True) + blob = content_dir / f"{after_digest}.utf8" + blob.write_bytes(content.encode()) + blob.chmod(0o600) campaign = json.loads(self.campaign.read_text()) stage_digests = {stage["id"]: stage["output_digest"] for stage in campaign["stages"]} return { @@ -234,8 +249,8 @@ def _candidate_packet(self) -> dict: "role": "skill", "operation": "replace", "before_digest": hashlib.sha256(before).hexdigest(), - "after_digest": hashlib.sha256(content.encode()).hexdigest(), - "content": content, + "after_digest": after_digest, + "content_file": f"candidate-content/{blob.name}", }], "evaluation": { "profile": "design-learning-v1", @@ -280,6 +295,23 @@ def write_path(self, path: Path, value: dict) -> tuple[Path, str]: path.chmod(0o600) return path, hashlib.sha256(raw).hexdigest() + def write_decision_path(self, decision: dict, *, valid_name: bool = True) -> Path: + directory = self.root / "promotion-decisions" + directory.mkdir(mode=0o700, exist_ok=True) + raw = RECORDER._canonical(decision) + digest = hashlib.sha256(raw).hexdigest() + campaign_run_id = json.loads(self.campaign.read_text())["run_id"] + candidate_digest = decision["candidate"]["digest"] or "no-candidate" + name = ( + f"{campaign_run_id}--{candidate_digest}--{digest}.json" + if valid_name + else "wrong-decision-name.json" + ) + path = directory / name + path.write_bytes(raw) + path.chmod(0o600) + return path + @staticmethod def gates(status: str = "passed") -> dict[str, str]: return {gate: status for gate in RECORDER.REQUIRED_GATES} @@ -305,6 +337,10 @@ def published_decision(self) -> dict: "campaign_run_id": json.loads(self.campaign.read_text())["run_id"], "campaign_receipt_digest": self.campaign_digest, }, + "campaign_evidence": { + "receipt_digest": self.campaign_digest, + "materiality_verified_before_materialization": True, + }, "active_checkout": {"head_before": self.packet["base_commit"], "head_after": self.packet["base_commit"], "status_digest_before": "c" * 64, "status_digest_after": "c" * 64, "unchanged": True}, "isolation": {"disposable_checkout": True, "network": "not_used", "temporary_checkout_cleaned": True}, } @@ -375,7 +411,12 @@ def published_decision(self) -> dict: "authorization_contract": RECORDER.AUTHORIZATION_CONTRACT, "disposition": "published", "reason_code": "published", - "candidate": {"state": "selected", "digest": self.packet_digest, "changed_files": 1, "total_bytes": len(self.packet["edits"][0]["content"].encode())}, + "candidate": { + "state": "selected", + "digest": self.packet_digest, + "changed_files": len(self.packet["edits"]), + "total_bytes": self.packet_total_bytes(), + }, "gates": self.gates(), "pull_request": {"state": "merged", "number": 42, "head_sha": self.head_sha, "merge_commit": self.merge_commit}, "runtime": {"state": "published", "targets": ["claude", "codex"], "install_receipt_digest": artifacts["runtime_publication"][1], "rollback_receipt_digest": artifacts["rollback_receipt"][1]}, @@ -387,7 +428,12 @@ def inactive_decision(self, disposition: str, candidate: bool) -> dict: if candidate: packet_path, packet_file_digest = self.write("candidate", self.packet) evidence["candidate_packet"] = {"path": str(packet_path), "digest": packet_file_digest} - candidate_summary = {"state": "selected", "digest": self.packet_digest, "changed_files": 1, "total_bytes": len(self.packet["edits"][0]["content"].encode())} + candidate_summary = { + "state": "selected", + "digest": self.packet_digest, + "changed_files": len(self.packet["edits"]), + "total_bytes": self.packet_total_bytes(), + } else: candidate_summary = {"state": "absent", "digest": None, "changed_files": 0, "total_bytes": 0} return { @@ -406,7 +452,10 @@ def test_real_coordinator_receipt_and_every_evidence_file_are_required_for_publi receipt = self.record(self.published_decision(), self.output) self.assertEqual("published", receipt["disposition"]) self.assertEqual(set(RECORDER.EVIDENCE_KEYS), set(receipt["evidence"])) - path = self.output / f"{receipt['campaign']['run_id']}.json" + path = self.output / ( + f"{receipt['campaign']['run_id']}--{receipt['candidate']['digest']}--" + f"{RECORDER._digest_json(receipt)}.json" + ) self.assertNotIn(str(self.root), path.read_text()) self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode)) @@ -445,6 +494,28 @@ def test_fabricated_or_mismatched_evidence_fails_closed(self) -> None: with self.assertRaisesRegex(RECORDER.PromotionReceiptError, "does not match"): self.record(decision, self.root / "bad-digest") + missing_materiality = self.published_decision() + materialization_path = Path(missing_materiality["evidence"]["materialization"]["path"]) + materialization = json.loads(materialization_path.read_text()) + materialization.pop("campaign_evidence") + materialization_path.write_bytes(RECORDER._canonical(materialization)) + missing_materiality["evidence"]["materialization"]["digest"] = hashlib.sha256( + materialization_path.read_bytes() + ).hexdigest() + with self.assertRaisesRegex(RECORDER.PromotionReceiptError, "verified campaign materiality"): + self.record(missing_materiality, self.root / "missing-campaign-materiality") + + mismatched_materiality = self.published_decision() + materialization_path = Path(mismatched_materiality["evidence"]["materialization"]["path"]) + materialization = json.loads(materialization_path.read_text()) + materialization["campaign_evidence"]["receipt_digest"] = "0" * 64 + materialization_path.write_bytes(RECORDER._canonical(materialization)) + mismatched_materiality["evidence"]["materialization"]["digest"] = hashlib.sha256( + materialization_path.read_bytes() + ).hexdigest() + with self.assertRaisesRegex(RECORDER.PromotionReceiptError, "verified campaign materiality"): + self.record(mismatched_materiality, self.root / "mismatched-campaign-materiality") + decision = self.published_decision() review_path = Path(decision["evidence"]["independent_review"]["path"]) review = json.loads(review_path.read_text()) @@ -644,6 +715,122 @@ def test_no_action_has_no_candidate_queue_or_evidence(self) -> None: receipt = self.record(self.inactive_decision("no_action", candidate=False), self.output) self.assertEqual("no_action", receipt["disposition"]) self.assertTrue(all(value is None for value in receipt["evidence"].values())) + self.assertTrue((self.output / f"{receipt['campaign']['run_id']}.json").is_file()) + + def test_selected_candidate_has_no_file_or_byte_cap(self) -> None: + paths = [ + "skills/design/design-intelligence/references/output-contract.md", + "skills/design/design-intelligence/references/promotion-rules.md", + "skills/design/design-intelligence/references/card-contract.md", + "skills/design/design-intelligence/references/source-adapters.md", + ] + edits = [] + base = self.packet["base_commit"] + for index, path in enumerate(paths): + before = subprocess.check_output(["git", "-C", str(ROOT), "show", f"{base}:{path}"]) + repetitions = 40000 if index == 0 else 350 + content = before.decode("utf-8") + "\n" + (f"Expanded candidate guidance {index}. " * repetitions) + "\n" + edits.append({ + "path": path, + "role": "reference", + "operation": "replace", + "before_digest": hashlib.sha256(before).hexdigest(), + "after_digest": hashlib.sha256(content.encode()).hexdigest(), + "content": content, + }) + self.assertGreater(len(edits), 3) + self.assertGreater(sum(len(edit["content"].encode()) for edit in edits), 32768) + content_dir = self.evidence_root / "candidate-content" + content_dir.mkdir(mode=0o700, exist_ok=True) + for edit in edits: + content = edit.pop("content").encode() + blob = content_dir / f"{edit['after_digest']}.utf8" + blob.write_bytes(content) + blob.chmod(0o600) + edit["content_file"] = f"candidate-content/{blob.name}" + self.packet["edits"] = edits + self.packet["rationale"]["change_kind"] = "reference-update" + self.packet["rollback"]["path_digests"] = {edit["path"]: edit["before_digest"] for edit in edits} + self.packet_digest = RECORDER._digest_json(self.packet) + + decision = self.published_decision() + candidate_path = Path(decision["evidence"]["candidate_packet"]["path"]) + self.assertLess(candidate_path.stat().st_size, RECORDER.MAX_PRIVATE_JSON_BYTES) + receipt = self.record(decision, self.output) + + self.assertEqual(len(edits), receipt["candidate"]["changed_files"]) + self.assertGreater(receipt["candidate"]["total_bytes"], 32768) + + def test_multiple_selected_candidate_receipts_do_not_collide(self) -> None: + first = self.record(self.published_decision(), self.output) + first_path = self.output / ( + f"{first['campaign']['run_id']}--{first['candidate']['digest']}--" + f"{RECORDER._digest_json(first)}.json" + ) + + edit = self.packet["edits"][0] + current = (self.evidence_root / edit["content_file"]).read_bytes() + revised = current + b"\nA second independently evaluated refinement.\n" + edit["after_digest"] = hashlib.sha256(revised).hexdigest() + revised_blob = self.evidence_root / "candidate-content" / f"{edit['after_digest']}.utf8" + revised_blob.write_bytes(revised) + revised_blob.chmod(0o600) + edit["content_file"] = f"candidate-content/{revised_blob.name}" + self.packet_digest = RECORDER._digest_json(self.packet) + second = self.record(self.published_decision(), self.output) + second_path = self.output / ( + f"{second['campaign']['run_id']}--{second['candidate']['digest']}--" + f"{RECORDER._digest_json(second)}.json" + ) + + self.assertNotEqual(first_path, second_path) + self.assertTrue(first_path.is_file()) + self.assertTrue(second_path.is_file()) + + def test_retry_then_publication_for_same_candidate_is_append_only(self) -> None: + retry = self.inactive_decision("retry_with_alert", candidate=True) + retry["gates"]["material-evidence"] = "passed" + retry["gates"]["isolated-materialization"] = "unavailable" + retry_receipt = self.record(retry, self.output) + + published_receipt = self.record(self.published_decision(), self.output) + retry_path = self.output / ( + f"{retry_receipt['campaign']['run_id']}--{retry_receipt['candidate']['digest']}--" + f"{RECORDER._digest_json(retry_receipt)}.json" + ) + published_path = self.output / ( + f"{published_receipt['campaign']['run_id']}--{published_receipt['candidate']['digest']}--" + f"{RECORDER._digest_json(published_receipt)}.json" + ) + + self.assertNotEqual(retry_path, published_path) + self.assertEqual("retry_with_alert", json.loads(retry_path.read_text())["disposition"]) + self.assertEqual("published", json.loads(published_path.read_text())["disposition"]) + + def test_decision_file_requires_campaign_candidate_and_exact_digest_name(self) -> None: + decision = self.inactive_decision("retry_with_alert", candidate=True) + decision["gates"]["material-evidence"] = "passed" + decision["gates"]["isolated-materialization"] = "unavailable" + valid = self.write_decision_path(decision) + + receipt = RECORDER.record( + self.live_receipt, + self.live_receipt_digest, + self.campaign, + valid, + self.output, + ) + + self.assertEqual("retry_with_alert", receipt["disposition"]) + invalid = self.write_decision_path(decision, valid_name=False) + with self.assertRaisesRegex(RECORDER.PromotionReceiptError, "filename digest"): + RECORDER.record( + self.live_receipt, + self.live_receipt_digest, + self.campaign, + invalid, + self.root / "invalid-decision-name", + ) def test_rejected_and_retry_outcomes_cannot_leave_open_pull_requests(self) -> None: rejected = self.inactive_decision("rejected_no_queue", candidate=True) diff --git a/tests/test_weekly_automatic_promotion_contract.py b/tests/test_weekly_automatic_promotion_contract.py index 728f3a4..d5832b3 100644 --- a/tests/test_weekly_automatic_promotion_contract.py +++ b/tests/test_weekly_automatic_promotion_contract.py @@ -58,12 +58,13 @@ def test_activation_policy_grants_only_the_weekly_design_contract(self) -> None: self.assertFalse(policy["materialization"]["runtime_publication_authority"]) self.assertFalse(policy["publication"]["candidate_may_publish"]) - def test_strong_model_and_bounded_automatic_promotion_are_active(self) -> None: + def test_strong_model_and_uncapped_automatic_promotion_are_active(self) -> None: config = WEEKLY.load_config() self.assertEqual("gpt-5.6-sol", config["scheduler"]["model"]) self.assertEqual("high", config["scheduler"]["reasoning_effort"]) self.assertTrue(config["analysis_budget"]["authorized"]) + self.assertEqual("concurrent_model_contexts", config["analysis_budget"]["unit"]) self.assertEqual(3, config["analysis_budget"]["maximum"]) promotion = config["automatic_promotion"] @@ -72,9 +73,9 @@ def test_strong_model_and_bounded_automatic_promotion_are_active(self) -> None: "weekly-design-auto-promotion-approved-v1", promotion["authorization_contract"], ) - self.assertEqual(1, promotion["maximum_candidates_per_run"]) - self.assertEqual(3, promotion["maximum_changed_files"]) - self.assertEqual(32768, promotion["maximum_total_bytes"]) + self.assertNotIn("maximum_candidates_per_run", promotion) + self.assertNotIn("maximum_changed_files", promotion) + self.assertNotIn("maximum_total_bytes", promotion) self.assertEqual( ["skills/**/SKILL.md", "skills/**/references/**/*.md"], promotion["allowed_path_patterns"], @@ -113,7 +114,8 @@ def test_automation_prompt_owns_the_full_automatic_tail(self) -> None: required_phrases = ( "Do not stop at `awaiting_approval`", - "at most one candidate", + "every independently material candidate", + "Process candidates sequentially", "material-evidence", "frozen-design-eval", "fresh independent review", @@ -128,6 +130,46 @@ def test_automation_prompt_owns_the_full_automatic_tail(self) -> None: for phrase in required_phrases: with self.subTest(phrase=phrase): self.assertIn(phrase, prompt) + self.assertLess( + prompt.index("scripts/list-pending-weekly-design-promotions.py"), + prompt.index("scripts/run-stack-weekly-live.py"), + ) + self.assertIn( + "persist its terminal receipt, then refresh and verify `origin/main`, before starting the next candidate", + prompt, + ) + self.assertIn("candidate-content/.utf8", prompt) + self.assertIn("Candidate receipts are append-only", prompt) + + def test_candidate_schema_has_no_automatic_file_or_content_ceiling(self) -> None: + schema = json.loads( + (ROOT / "registry" / "capability-change.schema.json").read_text() + ) + edits = schema["properties"]["edits"] + edit = schema["$defs"]["edit"] + + self.assertNotIn("maxItems", edits) + self.assertNotIn("maxLength", edit["properties"]["content"]) + self.assertIn("content_file", edit["properties"]) + self.assertIn("oneOf", edit) + manual_contract = schema["allOf"][0]["else"]["properties"]["edits"] + automatic_contract = schema["allOf"][0]["then"]["properties"]["edits"] + self.assertEqual(5, manual_contract["maxItems"]) + self.assertEqual(65536, manual_contract["items"]["properties"]["content"]["maxLength"]) + self.assertIn("content_file", automatic_contract["items"]["required"]) + lineage = schema["$defs"]["source_lineage"] + campaign_fields = { + "campaign_run_id", + "campaign_receipt_digest", + "design_packet_artifact_digest", + "retrieval_artifact_digest", + "candidate_evaluation_artifact_digest", + } + self.assertTrue(campaign_fields <= set(lineage["properties"])) + self.assertEqual( + campaign_fields - {"campaign_run_id"}, + set(lineage["dependentRequired"]["campaign_run_id"]), + ) def test_coordinator_prepares_the_automatic_tail_without_manual_queue(self) -> None: with tempfile.TemporaryDirectory() as temporary: @@ -187,7 +229,7 @@ def test_automatic_promotion_policy_drift_fails_closed(self) -> None: config = json.loads((ROOT / "config" / "weekly-intelligence.json").read_text()) with tempfile.TemporaryDirectory() as temporary: path = Path(temporary) / "weekly.json" - config["automatic_promotion"]["maximum_changed_files"] = 99 + config["automatic_promotion"]["maximum_changed_files"] = 3 path.write_text(json.dumps(config), encoding="utf-8") with self.assertRaisesRegex( WEEKLY.WeeklyIntelligenceError, diff --git a/tests/test_weekly_intelligence.py b/tests/test_weekly_intelligence.py index 9dd9376..01026c9 100644 --- a/tests/test_weekly_intelligence.py +++ b/tests/test_weekly_intelligence.py @@ -359,7 +359,7 @@ def test_report_checkpoint_fingerprint_includes_automation_contract(self) -> Non maintenance=maintenance, ) changed = json.loads(json.dumps(config)) - changed["automatic_promotion"]["maximum_total_bytes"] -= 1 + changed["automatic_promotion"]["weak_candidate_outcome"] = "changed" drifted = WEEKLY.stage_input_fingerprints( config=changed, input_digests=input_digests, From 42e018ab460a25c8cc3430308765ecf7a826ec0a Mon Sep 17 00:00:00 2001 From: Maroun Najjar <60901592+thecolormaroun@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:33:51 -0700 Subject: [PATCH 2/2] Address PR review feedback (#39) Require retry receipts to identify a selected candidate and cover the guard with a regression test. --- scripts/record-weekly-design-promotion.py | 2 ++ tests/test_record_weekly_design_promotion.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/scripts/record-weekly-design-promotion.py b/scripts/record-weekly-design-promotion.py index 8febca3..d6090f4 100644 --- a/scripts/record-weekly-design-promotion.py +++ b/scripts/record-weekly-design-promotion.py @@ -884,6 +884,8 @@ def _validate_outcome( if pull_request not in {"not_created", "closed"} or runtime != "not_run": raise PromotionReceiptError("rejected candidate cannot leave a review queue or runtime change") elif disposition == "retry_with_alert": + if candidate != "selected": + raise PromotionReceiptError("retry_with_alert requires a selected candidate") if "unavailable" not in gates.values(): raise PromotionReceiptError("retry_with_alert requires an unavailable gate") if pull_request not in {"not_created", "closed"} or runtime != "not_run": diff --git a/tests/test_record_weekly_design_promotion.py b/tests/test_record_weekly_design_promotion.py index 6c01e1d..9d2fddc 100644 --- a/tests/test_record_weekly_design_promotion.py +++ b/tests/test_record_weekly_design_promotion.py @@ -717,6 +717,16 @@ def test_no_action_has_no_candidate_queue_or_evidence(self) -> None: self.assertTrue(all(value is None for value in receipt["evidence"].values())) self.assertTrue((self.output / f"{receipt['campaign']['run_id']}.json").is_file()) + def test_retry_requires_a_selected_candidate(self) -> None: + retry = self.inactive_decision("retry_with_alert", candidate=False) + retry["gates"]["isolated-materialization"] = "unavailable" + + with self.assertRaisesRegex( + RECORDER.PromotionReceiptError, + "retry_with_alert requires a selected candidate", + ): + self.record(retry, self.output) + def test_selected_candidate_has_no_file_or_byte_cap(self) -> None: paths = [ "skills/design/design-intelligence/references/output-contract.md",