Files
tech-log-frontend/docs/superpowers/plans/2026-08-02-promotion-security-review-fixes.md
T

38 KiB
Raw Blame History

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.

  • Add failing tests for invalid tar, archive/tree mismatch, contradictory archived subordinate FAIL, exact archived policy bytes, and checkout-independent execution.

  • Run focused tests and record the expected RED diagnostics in the durable task report.

  • Archive the exact policy/verifier inputs required for independent release, supply-chain, dependency, license, vulnerability, and secret-scan checks.

  • Re-run producer checks against the extracted archive and require their result to agree with the assessment and member identities.

  • 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.

  • Add failing tests for provider FAIL/absence, arbitrary provider hash, swapped roles, shared-field mismatch, archive/report digest mismatch, and identical role keys.

  • Run focused tests and record RED.

  • Implement strict exact-five parsing/cross-record validation and expose a downstream CLI command.

  • Reject equal DER-SPKI fingerprints and equal role key identity before evaluation/finalization.

  • 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.

  • Add failing stubborn-descendant/short-timeout and sequence-clock expiry tests.

  • Run focused tests and record RED.

  • Extract the process runner, wait for close after timeout, and preserve the timeout diagnostic.

  • Issue provider timestamps immediately before execution, validate with a fresh clock after capture, and reject crossing expiry.

  • 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.

  • Add failing tests for leaf replacement during writes, final visibility mismatch, partial-failure cleanup, GITHUB_OUTPUT open/write failure, and expiry during staging.

  • Run focused tests and record RED.

  • Open the created leaf with O_DIRECTORY|O_NOFOLLOW, write through /proc/self/fd/<leafFd>, pin dev/ino, require visible identity equality, and propagate identity through cleanup.

  • Force directory/file modes with fchmod(0700/0400) independent of a restrictive owner-preserving umask.

  • Extract CLI dependencies; on any post-finalization output failure call direct cleanup before rethrowing.

  • 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.

  • Add failing contract/generator tests for the job condition, upload without always(), missing cleanup outputs, and nested install scripts lacking --ignore-scripts.

  • Run focused tests and record RED.

  • Add --ignore-scripts to verify:lockfile and recursively reject each reachable install invocation without it.

  • Extend the typed job condition and render the explicit cancellation-resistant exact-needs predicate.

  • 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)

  • Replace plaintext candidate fixtures with a real tar and canonical captured-archive/exact-five validation.

  • 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.

  • 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.

  • Add RED tests for unlink/recreate and chmod/mutation after a file write.
  • Implement bounded descriptor-relative capture and exact-five seal validation.
  • 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.

  • Add a RED signed other-run replay test.
  • Implement external expected-context comparison in library and CLI.
  • 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.

  • Add a RED pre-open replacement test.
  • Compare created and opened metadata and reject replacement.
  • 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.

  • Add the eight required global-option/alias RED cases plus malformed fail-closed cases.
  • Implement tokenization and manager-specific invocation classification.
  • 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.

  • Add RED forged-empty-SARIF and attestation-mismatch tests.
  • Derive one captured-archive scan context and bind it through supervisor, signed schema, finalizer records, and exact validation.
  • 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.

  • Add RED generator/contract assertions for no promotion job if and retained cleanup always().
  • Regenerate workflow and snapshot after the typed condition change.
  • Correct operator/security wording and run workflow/CI checks to GREEN.

Task D7: Verification

  • Run focused suites after each GREEN, then affected/full unit tests, all TypeScript targets, ESLint, check:ci, generated-byte check, and both diff checks.
  • Append exact PASS totals and sandbox-blocked commands to the ignored durable report.
  • 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.

  • Add RED tables for single-ampersand segmentation, false/override/malformed suppression, global-option run/implicit dependencies, yarn/corepack reachability, and builtin/script-name collisions.
  • Replace the regex traversal and separate install scan with one parser result.
  • 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.

  • Add a RED evaluator mismatch test.
  • Implement exact equality before vulnerability PASS assignment.
  • 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.

  • Add RED happy-path and required-env negative coverage using real finalizer output where sandbox execution permits.
  • Make only the minimal CLI/library changes needed for GREEN.
  • Separate child-process sandbox blockers from library assertions.

Task E4: Verification

  • Run focused parser/security/CLI suites, TypeScript, ESLint, check:ci, and git diff --check.
  • Run full unit if feasible and report nested-process EPERM separately.
  • 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.

  • Add RED policy and graph tables for the three Yarn workspace dispatchers, pnpm ln, and unknown manager subcommands.
  • Remove workspace dispatchers from safe builtins, pass known root scripts into the parser, and canonicalize pnpm ln to lifecycle link before implicit lookup.
  • 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.

  • Add RED comment, negative suppression, supported positive, and unknown lifecycle-option tables.
  • Implement comment-aware tokenization and one ordered lifecycle argument parser.
  • Preserve the checked-in --frozen-lockfile --ignore-scripts path and run focused tests to GREEN.

Task F3: Verification

  • Validate every checked-in package script through graph/install consumers without false positives.
  • Run the full workflow-generation file and related security tests.
  • Run all TypeScript targets, ESLint, check:ci, and git diff --check; report nested-process EPERM separately.
  • 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.

  • Change the pre-open replacement regression to require both the replacement canary and displaced original directory to survive the failure.
  • Run the focused test to RED against parent-directory identity scanning.
  • Remove unverified inode discovery/recovery and gate descriptor cleanup on explicit identity verification.
  • 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.

  • Add RED loader tables for contradictory npm suppression, pnpm config false, and pnpm ln, plus a production runner-boundary no-execute spy.
  • Enforce contract-command install policy and route runner execution through the preflight boundary.
  • 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.

  • Add RED policy+graph tables for pnpm filter/dir/-C, npm workspace/prefix, and Yarn cwd dispatch.
  • Add positive externally scoped lifecycle cases with verified suppression.
  • 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.

  • Add RED policy+graph coverage for npm init/explore/audit-fix and Yarn npm publish, plus a bare-audit positive.
  • Replace permissive builtin lookup with exact argument grammar.
  • Audit every current package script for graph/policy false positives.
  • 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.

  • 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.
  • Implement one npm post-script argument parser shared by explicit and implicit dispatch.
  • 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.

  • Add a table-driven loader/no-callback regression for the same seven reviewer inputs.
  • Run focused preflight tests to GREEN.

Task H3: Verification

  • Re-audit current package scripts through graph and policy consumers.
  • Run workflow/security suites, all TypeScript targets, ESLint, check:ci, and git diff --check.
  • 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.

  • Add RED policy/graph tables for nested, test, and restart pre/post hooks.
  • Add RED suppression positives and false/negative/contradictory/delimiter negatives.
  • Make npm tail parsing update the invocation suppression state and expand dependencies.
  • 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.

  • Add RED policy/graph coverage for all reviewer assignment forms and quoted/current-tree positives.
  • Add RED loader/no-callback coverage for command assignments and inherited process environment.
  • Implement token/segment assignment state and the preflight environment boundary.
  • Run environment/parser/preflight tests to GREEN.

Task I3: Verification

  • Audit every current script through graph and policy consumers.
  • Run focused parser/preflight, workflow/security, all TypeScript targets, ESLint, check:ci, and git diff --check.
  • 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.

  • Add RED policy/graph tables for every reviewer state transition, env path, command wrapper, dynamic assignment name, and unsupported mutation.
  • Implement a shared tokenized shell-environment state machine and immediate npm invocation environment inspection.
  • 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.

  • Add hook safety/order coverage for run-script, start, and stop.
  • Move bare suppression forms to positive regressions and retain all false, negative, contradictory, and delimiter negatives.
  • Remove the explicitly-valued distinction and run focused tests GREEN.

Task J3: Contract boundary and verification

  • Run every environment reviewer command through policy, graph, contract loading, and withCiGatePreflight, asserting the callback is never entered.
  • Retain the inherited process-environment regression and audit the current package tree for policy/graph false positives.
  • 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.

  • 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.
  • Reuse the same reviewer table through loadCiGateContract and withCiGatePreflight, asserting rejection and no callback entry.
  • Implement the shared prefix parser, route immediate env inspection and state-builtin updates through it, and run the reviewer selection GREEN.
  • 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.

  • Ensure the final focused selection explicitly includes the bare npm hook suppression table as well as prefix/environment policy and runner tests.
  • Run the full workflow file and security/supply/local-promotion suites; classify only the known nested-spawn sandbox failures separately.
  • 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.

  • Add policy/graph RED coverage for the four env-wrapper reviewer commands and direct unknown-wrapper manager commands (nice npm, time pnpm).
  • Reuse the env-wrapper reviewer commands through contract loading and withCiGatePreflight, asserting the callback remains false.
  • Implement one structural gap check in manager parsing and run RED cases GREEN without adding wrapper names.
  • Retain modeled assignment, command/exec/env/corepack, current-tree, quoted echo, and harmless assignment positives.

Task L2: Verification

  • Run a focused selection containing structural negatives and all modeled prefix/environment positives.
  • 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 HL 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.