# Promotion Security Review Fixes Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make the captured candidate archive and a strict exact-five bundle validator the only promotion authority while closing evidence, lifecycle, staging, output, freshness, and workflow gaps found by security review. **Architecture:** Candidate identity and local verification are derived exclusively from the inode-captured tar stream. The finalizer revalidates archived subordinate evidence, provider signatures, freshness, role-separated trust, and then creates and validates an exact-five bundle before returning descriptor- and inode-bound cleanup metadata. Provider execution, CLI output publication, and workflow gating expose small injectable boundaries so failure and cleanup behavior can be tested directly. **Tech Stack:** Node.js 24, TypeScript, Zod, Vitest, GNU tar, bubblewrap, Gitea Actions workflow generation. ## Global Constraints - Work sequentially on the current clean `develop` HEAD and produce one review-fix commit. - Every production change follows focused RED, observed expected failure, minimal GREEN, and regression verification. - Candidate verification performs no checkout reads; test subprocesses from outside the checkout with contradictory canaries. - Preserve real GNU tar and bubblewrap coverage; do not claim native uploader or atomic `renameat2`/`unlinkat` semantics. - Gitea 1.26.4 and act_runner 1.0.0 exact-five upload/download/cancel behavior remains an explicitly documented external smoke boundary. --- ### Task 1: Canonical Captured Archive and Archived Local Authority **Files:** - Modify: `scripts/lib/release-candidate.ts` - Modify: `scripts/lib/local-release-evidence.ts` - Modify: `scripts/lib/ci-candidate-archive.ts` - Delete: `scripts/lib/promotion-verifier.ts` - Delete: `scripts/verify-provider-evidence.ts` - Delete: `scripts/verify-supply-chain-promotion.ts` - Modify: `tests/unit/security-followup.test.ts` - Modify: `tests/unit/supply-chain.test.ts` - Modify: `tests/integration/security-followup-archive.test.ts` **Interfaces:** - Consumes: captured tar bytes plus expected SHA-256. - Produces: `withVerifiedCapturedCandidate()` callback data derived only from the extracted, exact-member, digest-verified tar; `verifyArchivedLocalEvidence()` independently recomputes all feasible archived checks. - [x] Add failing tests for invalid tar, archive/tree mismatch, contradictory archived subordinate FAIL, exact archived policy bytes, and checkout-independent execution. - [x] Run focused tests and record the expected RED diagnostics in the durable task report. - [x] Archive the exact policy/verifier inputs required for independent release, supply-chain, dependency, license, vulnerability, and secret-scan checks. - [x] Re-run producer checks against the extracted archive and require their result to agree with the assessment and member identities. - [x] Remove the obsolete standalone PASS issuers and route all fixture checking through real captured tar/finalizer validation. - [ ] Run focused archive, supply-chain, and integration tests to GREEN. ### Task 2: Exact-Five Validator and Role-Separated Trust **Files:** - Create: `scripts/lib/exact-promotion-bundle.ts` - Create: `scripts/verify-exact-promotion-bundle.ts` - Modify: `scripts/lib/provider-evidence.ts` - Modify: `scripts/lib/promotion-stager.ts` - Modify: `scripts/contracts/promotion-artifacts.ts` - Modify: `tests/unit/security-followup.test.ts` - Modify: `tests/unit/ci-artifact-contract.test.ts` **Interfaces:** - Consumes: exactly five captured byte buffers and two trusted Ed25519 identities. - Produces: `verifyExactPromotionBundle()` that requires literal verifier identity/version, provider and subordinate PASS states, exact hashes, and equal run/source/candidate/provider/trust fields. - [x] Add failing tests for provider FAIL/absence, arbitrary provider hash, swapped roles, shared-field mismatch, archive/report digest mismatch, and identical role keys. - [x] Run focused tests and record RED. - [x] Implement strict exact-five parsing/cross-record validation and expose a downstream CLI command. - [x] Reject equal DER-SPKI fingerprints and equal role key identity before evaluation/finalization. - [x] Invoke exact-five validation inside the finalizer before publication; the full real-build fixture rerun remains sandbox-blocked below. ### Task 3: Provider Lifecycle and Freshness **Files:** - Modify: `scripts/lib/provider-supervisor.ts` - Create: `scripts/lib/provider-process-runner.ts` - Modify: `scripts/run-and-validate-provider.ts` - Modify: `tests/unit/security-followup.test.ts` - Modify: `tests/unit/ci-artifact-contract.test.ts` **Interfaces:** - Consumes: a spawned bubblewrap child, injected timeout/clock, captured report. - Produces: a runner that SIGKILLs on timeout but rejects only after `close`, and supervision that samples freshness after provider/report capture. - [x] Add failing stubborn-descendant/short-timeout and sequence-clock expiry tests. - [x] Run focused tests and record RED. - [x] Extract the process runner, wait for close after timeout, and preserve the timeout diagnostic. - [x] Issue provider timestamps immediately before execution, validate with a fresh clock after capture, and reject crossing expiry. - [x] Run focused lifecycle tests, including real stubborn descendants, to GREEN; the shared real-build/bubblewrap fixture remains sandbox-blocked below. ### Task 4: Inode-Pinned Staging and Output-Failure Cleanup **Files:** - Modify: `scripts/lib/promotion-stager.ts` - Create: `scripts/lib/stage-verified-promotion-cli.ts` - Modify: `scripts/stage-verified-promotion.ts` - Modify: `scripts/cleanup-verified-promotion.ts` - Modify: `tests/unit/ci-artifact-contract.test.ts` **Interfaces:** - Produces: `FinalizedPromotion.stagingIdentity` and a testable CLI function whose append failure invokes cleanup from the in-memory result. - [x] Add failing tests for leaf replacement during writes, final visibility mismatch, partial-failure cleanup, GITHUB_OUTPUT open/write failure, and expiry during staging. - [x] Run focused tests and record RED. - [x] Open the created leaf with `O_DIRECTORY|O_NOFOLLOW`, write through `/proc/self/fd/`, pin dev/ino, require visible identity equality, and propagate identity through cleanup. - [x] Force directory/file modes with `fchmod(0700/0400)` independent of a restrictive owner-preserving umask. - [x] Extract CLI dependencies; on any post-finalization output failure call direct cleanup before rethrowing. - [x] Revalidate evidence freshness before sealing and immediately before publication; isolated lifecycle/mode/output tests are GREEN and the shared real-build fixture remains sandbox-blocked below. ### Task 5: Workflow and Install Policy **Files:** - Modify: `package.json` - Modify: `scripts/check-ci-contract.ts` - Modify: `scripts/contracts/ci-gates.ts` - Modify: `scripts/generate-ci-workflow.ts` - Modify: `config/ci/gates.json` - Modify: `.gitea/workflows/quality-gates.yml` - Modify: `tests/unit/ci-workflow-generation.test.ts` - Modify: `tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap` **Interfaces:** - Produces: promotion job `if: ${{ always() && needs.immutable_build.result == 'success' && needs.vulnerability_provider.result == 'success' && needs.provenance_provider.result == 'success' }}` and install-bearing script graph enforcement. - [x] Add failing contract/generator tests for the job condition, upload without `always()`, missing cleanup outputs, and nested install scripts lacking `--ignore-scripts`. - [x] Run focused tests and record RED. - [x] Add `--ignore-scripts` to `verify:lockfile` and recursively reject each reachable install invocation without it. - [x] Extend the typed job condition and render the explicit cancellation-resistant exact-needs predicate. - [x] Regenerate workflow/snapshot and run workflow contract/byte tests to GREEN. ### Task 6: Fixtures, Documentation, Full Verification, and Commit **Files:** - Modify: `scripts/check-supply-chain-provider-fixtures.ts` - Modify: `docs/security/supply-chain.md` - Modify: `docs/operations/ci-quality-gates.md` - Modify: `.superpowers/sdd/2026-08-01-release-evidence-remediation/task-4-report.md` (ignored durable report) - [x] Replace plaintext candidate fixtures with a real tar and canonical captured-archive/exact-five validation. - [x] Rewrite operator docs around the sole captured-archive/exact-five authority and retain the Gitea/runner external-smoke residual. - [ ] Run focused fixtures, archive integration, workflow snapshot/bytes, full unit, types, lint, `check:ci`, and diff checks; escalate only a sandbox-caused EPERM. - [x] Append all RED/GREEN and verification evidence/constraints to the durable report. - [ ] Invoke verification-before-completion, review the complete diff, commit once, and report commit/range/status. --- ## Review-Fix Wave D: Sealed Bytes, Replay Context, Scan Trust, and Cancellation **Constraint:** Work only in the existing uncommitted tree. Do not write `.git`, stage, or commit. Each task follows a focused RED→GREEN cycle and records sandbox `EPERM` separately from product failures. ### Task D1: Seal the actual staged inode bytes **Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `tests/unit/ci-artifact-contract.test.ts` **Interface:** The staging writer captures each canonical file through the already-open leaf FD using `O_NOFOLLOW`; it requires a regular single-link inode, mode `0400`, stable dev/ino/size, and the declared SHA-256. `verifyExactPromotionBundle` receives only these captured staged buffers immediately before return. - [x] Add RED tests for unlink/recreate and chmod/mutation after a file write. - [x] Implement bounded descriptor-relative capture and exact-five seal validation. - [x] Run focused staging tests to GREEN. ### Task D2: Bind downstream verification to external expected identity **Files:** `scripts/lib/exact-promotion-bundle.ts`, `scripts/verify-exact-promotion-bundle.ts`, `tests/unit/ci-artifact-contract.test.ts` **Interface:** `verifyExactPromotionBundle` requires `expected.run.id`, `expected.run.attempt`, `expected.sourceRevision`, and `expected.archiveSha256`; optional bundle/dist/lock/source-set digests are compared when supplied. The CLI obtains these values from dedicated environment variables and never derives them from the bundle. - [x] Add a RED signed other-run replay test. - [x] Implement external expected-context comparison in library and CLI. - [x] Run exact-bundle tests to GREEN where the sandbox permits. ### Task D3: Pin mkdir-to-open identity **Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, `docs/security/supply-chain.md`, `docs/operations/ci-quality-gates.md` **Interface:** A post-mkdir/pre-open test hook can replace the leaf. The implementation compares mkdir-returned pathname metadata with the `O_DIRECTORY|O_NOFOLLOW` handle `fstat` before any write; it never uses pathname chmod. - [x] Add a RED pre-open replacement test. - [x] Compare created and opened metadata and reject replacement. - [x] Document the residual portable Node same-UID pre-lstat/native-privilege boundary. ### Task D4: Conservatively parse install invocations **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts` **Interface:** A bounded shell/token parser recognizes `pnpm install|i`, `npm install|ci|i`, and `yarn install` after supported manager-global options with split or `=` values. Any reachable package-manager invocation that cannot be classified is rejected. - [x] Add the eight required global-option/alias RED cases plus malformed fail-closed cases. - [x] Implement tokenization and manager-specific invocation classification. - [x] Run install-policy tests to GREEN. ### Task D5: Signed secret-scan attestation **Files:** `scripts/lib/provider-evidence.ts`, `scripts/lib/provider-supervisor.ts`, `scripts/lib/provider-upload-validator.ts`, `scripts/lib/promotion-stager.ts`, `scripts/lib/exact-promotion-bundle.ts`, relevant unit/integration tests and docs. **Interface:** Vulnerability evidence v2 contains a strict `secretScanAttestation` with `status: PASS`, local-assessment, source-set, policy, SARIF, and scan-input digests. The supervisor derives the expected tuple from captured archive members, exports it to the provider, and upload/final verification requires exact equality under the Ed25519 signature. - [x] Add RED forged-empty-SARIF and attestation-mismatch tests. - [x] Derive one captured-archive scan context and bind it through supervisor, signed schema, finalizer records, and exact validation. - [x] Run provider/security tests to GREEN where the sandbox permits. ### Task D6: Cancellation-safe workflow and exact upload paths **Files:** `scripts/contracts/ci-gates.ts`, `scripts/generate-ci-workflow.ts`, `config/ci/gates.json`, generated workflow/snapshot, workflow tests, and operations/security docs. **Interface:** Promotion uses a typed dependency-success/no-job-if variant, so cancellation cannot be overridden by job-level `always()`. Step cleanup retains bare `always()` for ordinary failures. Upload documentation names the five canonical paths under `staging_root` and states cancellation cleanup remains a runner/native smoke boundary. - [x] Add RED generator/contract assertions for no promotion job `if` and retained cleanup `always()`. - [x] Regenerate workflow and snapshot after the typed condition change. - [x] Correct operator/security wording and run workflow/CI checks to GREEN. ### Task D7: Verification - [x] Run focused suites after each GREEN, then affected/full unit tests, all TypeScript targets, ESLint, `check:ci`, generated-byte check, and both diff checks. - [x] Append exact PASS totals and sandbox-blocked commands to the ignored durable report. - [x] Report modified files and remaining native/Gitea/unsandboxed verification boundaries; do not attempt git staging or commit. ## Wave E: Unified parser and downstream boundary review ### Task E1: One tokenized manager parser **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts` **Interface:** One parse result reports manager invocations, package-script dependencies, unsupported controls, and effective lifecycle suppression. Both graph traversal and install policy consume it. Single `&`, unknown manager grammar, and malformed options fail closed. The last valid `--ignore-scripts` assignment controls the effective value; false, contradictory, valueless, and malformed assignments are unsafe. Lifecycle-capable mutation builtins are never implicit repository scripts and require effective suppression. - [x] Add RED tables for single-ampersand segmentation, false/override/malformed suppression, global-option run/implicit dependencies, yarn/corepack reachability, and builtin/script-name collisions. - [x] Replace the regex traversal and separate install scan with one parser result. - [x] Run the parser-focused and full workflow-generation suites. ### Task E2: Evaluator-owned secret-scan equality **Files:** `scripts/lib/provider-evidence.ts`, `tests/unit/security-followup.test.ts`, finalizer tests. **Interface:** `evaluatePromotionEvidence` itself compares the parsed vulnerability report's signed `secretScanAttestation` with `expected.secretScanAttestation`. A mismatch makes vulnerability and overall promotion status `FAIL_UNVERIFIED`, including the production finalizer path. - [x] Add a RED evaluator mismatch test. - [x] Implement exact equality before vulnerability PASS assignment. - [x] Run security/provider-focused tests. ### Task E3: Downstream CLI exact-five contract **Files:** `tests/unit/ci-artifact-contract.test.ts`, `tests/unit/security-followup.test.ts`, `scripts/verify-exact-promotion-bundle.ts` if required. **Interface:** A real finalizer-produced canonical exact-five directory passes the downstream CLI when all required external expected values and trust keys are supplied. Every required expected variable missing or mismatched exits non-zero. Optional digests remain exact when present. - [x] Add RED happy-path and required-env negative coverage using real finalizer output where sandbox execution permits. - [x] Make only the minimal CLI/library changes needed for GREEN. - [x] Separate child-process sandbox blockers from library assertions. ### Task E4: Verification - [x] Run focused parser/security/CLI suites, TypeScript, ESLint, `check:ci`, and `git diff --check`. - [x] Run full unit if feasible and report nested-process `EPERM` separately. - [x] Do not stage or commit. ## Wave F: Manager parser boundary hardening **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Add behavior tests before production changes and keep unsupported manager grammar fail-closed. ### Task F1: Workspace dispatch and authoritative script lookup **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts` **Interface:** The parser receives the authoritative root `scripts` record. Explicit `run` resolves a root script; pnpm/yarn implicit dispatch resolves only a known root script. Yarn `workspace` and `workspaces` dispatch are unsupported because the root graph does not load workspace package scripts. Builtin aliases are canonicalized before root-script lookup. - [x] Add RED policy and graph tables for the three Yarn workspace dispatchers, pnpm `ln`, and unknown manager subcommands. - [x] Remove workspace dispatchers from safe builtins, pass known root scripts into the parser, and canonicalize `pnpm ln` to lifecycle `link` before implicit lookup. - [x] Run the focused dependency/lifecycle cases to GREEN. ### Task F2: Shell comments and lifecycle option state **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts` **Interface:** Unquoted `#` in a manager-bearing command is unsupported control syntax; quoted `#` remains ordinary token content. Lifecycle options are parsed in order into a canonical suppression state covering `--ignore-scripts`, `--no-ignore-scripts`, and `--config.ignore-scripts`; conflicting, malformed, unknown, or ineffective states fail closed. Other option-like lifecycle arguments require an explicit manager allowlist. - [x] Add RED comment, negative suppression, supported positive, and unknown lifecycle-option tables. - [x] Implement comment-aware tokenization and one ordered lifecycle argument parser. - [x] Preserve the checked-in `--frozen-lockfile --ignore-scripts` path and run focused tests to GREEN. ### Task F3: Verification - [x] Validate every checked-in package script through graph/install consumers without false positives. - [x] Run the full workflow-generation file and related security tests. - [x] Run all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; report nested-process `EPERM` separately. - [x] Update the durable report; do not stage or commit. ## Wave G: Verified cleanup and complete gate/parser preflight **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Every production change follows a focused failing behavior test. ### Task G1: Verified-FD-only failure cleanup **Files:** `scripts/lib/promotion-stager.ts`, `tests/unit/security-followup.test.ts`, security/operations documentation. **Interface:** `openedIdentityVerified` becomes true only after the opened directory descriptor matches the post-`mkdir` device/inode. Failure cleanup may unlink canonical files or `rmdir` only through that verified descriptor and a still-matching visible identity. A mismatched opened descriptor and any visible replacement are close-only; a moved original directory remains for fixture/operator cleanup because portable Node cannot safely recover it. - [x] Change the pre-open replacement regression to require both the replacement canary and displaced original directory to survive the failure. - [x] Run the focused test to RED against parent-directory identity scanning. - [x] Remove unverified inode discovery/recovery and gate descriptor cleanup on explicit identity verification. - [x] Run staging race and cleanup tests to GREEN and document the native residual. ### Task G2: Contract-wide lifecycle preflight **Files:** `scripts/contracts/ci-gates.ts`, `scripts/run-ci-gate.ts`, optional focused runner helper, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** `loadCiGateContract` runs `validateInstallScriptPolicy` over every unique contract command script after script existence and graph checks. The runner enters its execution callback only after this loader succeeds, enabling a no-execute regression without relying on a nested child process. - [x] Add RED loader tables for contradictory npm suppression, pnpm config false, and `pnpm ln`, plus a production runner-boundary no-execute spy. - [x] Enforce contract-command install policy and route runner execution through the preflight boundary. - [x] Run loader/runner preflight tests to GREEN. ### Task G3: Foreign manifest scope by command class **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** Manager-global options that change cwd, manifest or workspace scope are recorded during parsing. Explicit and implicit package-script dispatch with any such option is unsupported under the root-only graph. Lifecycle commands remain classifiable and are accepted only when their own ordered suppression/option grammar is safe. - [x] Add RED policy+graph tables for pnpm filter/dir/`-C`, npm workspace/prefix, and Yarn cwd dispatch. - [x] Add positive externally scoped lifecycle cases with verified suppression. - [x] Track scope options and reject only package-script dispatch; run focused tests to GREEN. ### Task G4: Argument-sensitive builtin grammar and verification **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`, durable report. **Interface:** Broad command-name-only safe builtins are replaced by exact per-manager read-only invocations. Init/explore/Yarn npm namespaces are unsupported. Audit is accepted only as an exact bare read-only command; `fix` and all unknown arguments are rejected. - [x] Add RED policy+graph coverage for npm init/explore/audit-fix and Yarn npm publish, plus a bare-audit positive. - [x] Replace permissive builtin lookup with exact argument grammar. - [x] Audit every current package script for graph/policy false positives. - [x] Run staging/parser/no-execute/workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; record sandbox `EPERM` separately and do not stage or commit. ## Wave H: npm post-script scope-option boundary **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Reproduce every reviewer command in a failing test before changing the parser. ### Task H1: Explicit and implicit npm dispatch arguments **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** After an explicit `npm run`/`run-script` dependency or an implicit npm lifecycle script, manager options before the first literal `--` are parsed using an exact harmless allowlist. Workspace/prefix selectors (`--workspace`, `-w`, `--workspaces`, `--prefix`, including supported attached/equal forms) and unknown manager options fail closed. Tokens after the first literal `--` are script arguments and cannot change the authoritative manifest scope. - [x] Add RED policy-and-graph coverage for all seven reviewer inputs, short/equal forms, unknown pre-delimiter options, the literal `--` boundary, and ordinary current-tree dispatch. - [x] Implement one npm post-script argument parser shared by explicit and implicit dispatch. - [x] Run focused parser tests to GREEN. ### Task H2: Contract loader and runner boundary **Files:** `tests/unit/ci-workflow-generation.test.ts`, contract preflight only if the RED test exposes a separate integration defect. **Interface:** Every reviewer input is rejected by contract loading while the referenced root scripts exist and are otherwise safe. `withCiGatePreflight` must not enter its callback for any rejected command. - [x] Add a table-driven loader/no-callback regression for the same seven reviewer inputs. - [x] Run focused preflight tests to GREEN. ### Task H3: Verification - [x] Re-audit current package scripts through graph and policy consumers. - [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`. - [x] Record results in the durable report and do not stage or commit. **Verification evidence:** The focused npm parser/preflight selection passed 31/31. The workflow file passed 208/210; its two remaining tests reached the known nested-spawn sandbox boundary and reported `EPERM`. Security, supply-chain, and local-promotion tests passed 64/64. All six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. Auditing the checked-in package found zero policy failures across 109 scripts and zero graph failures across 108 entries (excluding the intentionally direct runner entry `ci:gate`). A broader artifact-contract run passed 102 assertions and blocked 23 fixture cases at the same nested `git ls-files` `EPERM` boundary. No `.git` write was performed. Local pnpm 11.17 execution showed post-script `--filter`/`--dir` tokens arriving in the root script's argv, and official Yarn run documentation defines all parameters after the script name as script arguments. Those pre-existing negative expectations were therefore corrected to positive regressions; only npm receives the new post-script manager-option grammar. ## Wave I: npm hook closure and environment scope **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Add focused behavior tests and observe RED before each production change. ### Task I1: npm pre/main/post dependency closure **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** Explicit npm `run`/`run-script` and implicit `start`/`stop`/`restart`/`test` return existing root-manifest lifecycle hooks in `pre`, main, `post` order. Hooks are omitted only when ordered manager/tail suppression is unambiguously effective before the first literal `--`; bare, false, negative, contradictory, malformed, or post-delimiter suppression keeps hook traversal active or fails closed. - [x] Add RED policy/graph tables for nested, test, and restart pre/post hooks. - [x] Add RED suppression positives and false/negative/contradictory/delimiter negatives. - [x] Make npm tail parsing update the invocation suppression state and expand dependencies. - [x] Run hook/parser tests to GREEN. ### Task I2: Tokenized npm scope environment **Files:** `scripts/lib/package-script-graph.ts`, `scripts/contracts/ci-gates.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** Case-insensitive assignments to `npm_config_workspace`, `npm_config_workspaces`, or `npm_config_prefix` fail closed when their shell segment executes npm. Direct assignment, `env`, `/usr/bin/env`, and an exported assignment inherited by a later npm segment are covered without raw-substring false positives for quoted text. `withCiGatePreflight` also rejects the same sensitive keys inherited through `process.env` before entering its callback. - [x] Add RED policy/graph coverage for all reviewer assignment forms and quoted/current-tree positives. - [x] Add RED loader/no-callback coverage for command assignments and inherited process environment. - [x] Implement token/segment assignment state and the preflight environment boundary. - [x] Run environment/parser/preflight tests to GREEN. ### Task I3: Verification - [x] Audit every current script through graph and policy consumers. - [x] Run focused parser/preflight, workflow/security, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`. - [x] Update durable operations/security documentation and record sandbox-only nested spawn failures separately; do not stage or commit. **Verification evidence:** Hook closure began RED 9/9 and GREEN 9/9; ordered suppression began with 6 expected failures and finished GREEN 22/22; wrapper/export/inherited environment coverage began with 7 expected failures and finished GREEN 28/28. A final all-command-class environment RED 3/3 closed scoped lifecycle and builtin invocations. The combined Wave I focused selection passed 59/59. The complete workflow file passed 269/271; its only two failures were the existing nested child-spawn `EPERM` fixtures. Security, supply-chain, and local-promotion tests passed 64/64. All six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The checked-in tree had zero policy failures across 109 scripts, zero graph failures across 108 entries after excluding the intentional direct runner entry `ci:gate`, and no sensitive inherited npm scope environment. No `.git` write was performed. ## Wave J: coherent npm environment state and hook semantics **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Add each reviewer form as a failing regression before changing the parser. ### Task J1: Stateful shell npm-scope environment analysis **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** Shell segments carry a conservative npm-scope environment state to later reachable npm invocations. The parser recognizes any static path whose basename is `env`, optionally behind `command`, and rejects case-insensitive scope assignments in direct or env-wrapper contexts. Static assignment/export and `set -a` transitions are modeled across segments. Dynamic assignment names and environment mutations that cannot be modeled accurately (`set +a`, `unset`, `export -n`, `eval`, dot/source) make later npm dispatch unsupported. Quoted harmless text and non-scope static assignments remain accepted; analysis uses token and segment structure rather than raw substring matching. - [x] Add RED policy/graph tables for every reviewer state transition, env path, command wrapper, dynamic assignment name, and unsupported mutation. - [x] Implement a shared tokenized shell-environment state machine and immediate npm invocation environment inspection. - [x] Add harmless quoted/static positive regressions and run focused tests GREEN. ### Task J2: Actual npm lifecycle-hook suppression semantics **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** Existing pre/main/post hooks are traversed for `run`, `run-script`, `start`, `stop`, `restart`, and `test`. Bare `--ignore-scripts` means true and omits hooks just like explicit true. False, negative, contradictory, malformed, and post-delimiter forms retain hook traversal or fail closed according to the existing ordered grammar. - [x] Add hook safety/order coverage for run-script, start, and stop. - [x] Move bare suppression forms to positive regressions and retain all false, negative, contradictory, and delimiter negatives. - [x] Remove the explicitly-valued distinction and run focused tests GREEN. ### Task J3: Contract boundary and verification - [x] Run every environment reviewer command through policy, graph, contract loading, and `withCiGatePreflight`, asserting the callback is never entered. - [x] Retain the inherited process-environment regression and audit the current package tree for policy/graph false positives. - [x] Run workflow/security suites, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; record sandbox-only failures and do not stage or commit. **Verification evidence:** The initial Wave J selection produced 38 expected failures across loader/policy/graph environment cases and bare hook suppression, then passed 84/84 after implementation. A separate unsupported dynamic env-wrapper expansion regression went RED 2/2 and GREEN 2/2; the final combined selection passed 86/86. The full workflow file passed 327/329, with only the existing two nested child-spawn `EPERM` fixtures failing at the sandbox boundary. Security, supply-chain, local-promotion, and promotion-readiness tests passed 71/71. All six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The checked-in tree had zero policy failures across 109 scripts, zero graph failures across 108 entries after excluding the intentional direct runner entry `ci:gate`, and no sensitive inherited npm scope environment. No `.git` write was performed. ## Wave K: common shell-prefix grammar **Constraint:** Continue in the existing uncommitted tree. Do not write `.git`, stage, or commit. Every wrapper/prefix reviewer command must be RED in all four public enforcement paths before production changes. ### Task K1: Shared prefix parser and state-builtin targeting **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** A single token-based prefix helper consumes leading static assignments, then exact `command`/`exec` wrapper chains and their supported separator syntax. It reports the effective command token/index, whether parsing is uncertain, and the leading assignments. Both immediate npm env inspection and persistent `export`/`set` state updates use this result. `command --` is accepted; unknown `command` options and unmodeled `exec` options before npm/env fail closed. Static paths retain basename-`env` behavior. - [x] Add common policy/graph RED cases for `exec env`, `exec /bin/env`, `command exec env`, `command -- env`, assignment-prefixed `export`, and assignment-prefixed `set -a`. - [x] Reuse the same reviewer table through `loadCiGateContract` and `withCiGatePreflight`, asserting rejection and no callback entry. - [x] Implement the shared prefix parser, route immediate env inspection and state-builtin updates through it, and run the reviewer selection GREEN. - [x] Preserve split assignment/export ordering, dynamic LHS, quoted text, harmless `MESSAGE=...`, and supported command-wrapper positives. ### Task K2: Hook selection and final verification **Files:** `tests/unit/ci-workflow-generation.test.ts`, durable report. - [x] Ensure the final focused selection explicitly includes the bare npm hook suppression table as well as prefix/environment policy and runner tests. - [x] Run the full workflow file and security/supply/local-promotion suites; classify only the known nested-spawn sandbox failures separately. - [x] Audit all current scripts through policy and graph, then run all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; do not stage or commit. **Verification evidence:** The nine shared shell-prefix reviewer commands began RED in both enforcement tables, producing 18 expected failures across loader/runner and policy/graph, then passed 18/18 after the common parser was connected. The prefix negatives plus harmless positives passed 39/39. The final focused selection explicitly combined prefix cases, dynamic environment cases, effective/bare npm hook suppression, and harmless positives and passed 106/106. The complete workflow file passed 353/355; its only two failures were the known nested child-spawn `EPERM` fixtures. Security, supply-chain, local-promotion, and promotion-readiness tests passed 71/71. The current package tree had zero policy failures across 109 scripts, zero graph failures across 108 entries after excluding `ci:gate`, and no sensitive inherited npm scope environment. All six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. The shared workspace was preserved and no `.git` write was performed. ## Wave L: structural manager-prefix gap rejection **Constraint:** Continue in the shared uncommitted tree. Do not write `.git`, stage, or commit. Generalize the existing parser; do not add wrapper names to an allowlist. ### Task L1: Reject unmodeled tokens before package managers **Files:** `scripts/lib/package-script-graph.ts`, `tests/unit/ci-workflow-generation.test.ts`. **Interface:** The common shell-prefix result identifies the first effective command after modeled assignments and `command`/`exec` wrappers. When manager scanning later finds a package manager, every token between that effective command position and the manager position must belong to a grammar explicitly consumed by immediate env or corepack parsing. Otherwise the invocation is unsupported. This structural rule covers `nice`, absolute-path `nice`, `nohup`, and future unknown wrappers without naming them. - [x] Add policy/graph RED coverage for the four env-wrapper reviewer commands and direct unknown-wrapper manager commands (`nice npm`, `time pnpm`). - [x] Reuse the env-wrapper reviewer commands through contract loading and `withCiGatePreflight`, asserting the callback remains false. - [x] Implement one structural gap check in manager parsing and run RED cases GREEN without adding wrapper names. - [x] Retain modeled assignment, `command`/`exec`/env/corepack, current-tree, quoted echo, and harmless assignment positives. ### Task L2: Verification - [x] Run a focused selection containing structural negatives and all modeled prefix/environment positives. - [x] Run the workflow and security/supply/local-promotion suites, current-tree policy/graph/environment audit, all TypeScript targets, ESLint, `check:ci`, and `git diff --check`; record sandbox-only failures and do not stage or commit. **Verification evidence:** The six unmodeled-prefix reviewer commands began RED in both enforcement tables, producing 12 expected loader/runner and policy/graph failures, then passed 12/12 after one structural prefix-gap check was added. The unmodeled negatives plus harmless/modeled positives passed 36/36. The final focused Wave H–L environment/prefix and effective/bare hook selection passed 121/121. The full workflow file passed 368/370, with only the two known nested child-spawn `EPERM` fixtures failing at the sandbox boundary. Security, supply-chain, local-promotion, and promotion-readiness tests passed 71/71. The current package tree had zero policy failures across 109 scripts, zero graph failures across 108 entries after excluding `ci:gate`, and no sensitive inherited npm scope environment. All six TypeScript targets, ESLint, `check:ci`, and `git diff --check` passed. No wrapper-name allowlist was added, the workspace was preserved, and no `.git` write was performed.