Compare commits

..
Author SHA1 Message Date
coder-mos2 e910a45ab3 fix(rm-02): narrow inventory drift guarantee
ci/woodpecker/pr/ci Pipeline was successful
2026-08-01 15:09:57 -05:00
coder-mos2 c5b0d510d7 wip(rm-02): close all success output paths 2026-08-01 14:45:11 -05:00
coder-mos2 9b7005d59b wip(rm-02): round-4 remediation held at RM-60 boundary 2026-08-01 14:39:36 -05:00
f10-coder fbb6191298 fix(quality): anchor quantified registry populations
ci/woodpecker/pr/ci Pipeline was successful
2026-08-01 12:39:49 -05:00
f10-coder 32b490a712 fix(quality): close registry silent-defeat paths
ci/woodpecker/pr/ci Pipeline was successful
2026-08-01 11:28:28 -05:00
f10-coder 83d2ecb224 chore(quality): advance registry activation seam
ci/woodpecker/pr/ci Pipeline was successful
2026-08-01 10:06:09 -05:00
coder-mos1andf10-coder 9e1a7a44b7 fix(quality): preserve binding diagnostics 2026-08-01 09:58:47 -05:00
coder-mos1andf10-coder 8b1b873056 fix(quality): prove criterion binding semantics 2026-08-01 09:58:47 -05:00
coder-mos1andf10-coder d119635265 fix(test): preserve sandbox refusal provenance 2026-08-01 09:58:47 -05:00
coder-mos1andf10-coder abaed0c103 test(ci): classify Bubblewrap EPERM exactly 2026-08-01 09:58:47 -05:00
f10-coder 04cc031774 fix(quality): record current-tree trust boundary 2026-08-01 09:58:47 -05:00
f10-coder e89599758b fix(ci): unshallow gate replay history 2026-08-01 09:58:46 -05:00
f10-coder 0b4aa4751a feat(quality): add anti-inert gate registry 2026-08-01 09:58:46 -05:00
coder-mos1andmos-dt-0 f4fd5967fc RM-61: prove ci-postgres teardown discrimination (#1033)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: coder-mos1 <[email protected]>
2026-08-01 14:54:04 +00:00
mos-dt-0andMos f65e9ea656 docs(remediation): mission-state snapshot at the RM-01 seam (#1028)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: mos-dt-0 <[email protected]>
2026-08-01 01:08:11 +00:00
mos-dt-0andMos f58b3699a6 RM-01: reproducible checkout — the pre-push gate fails on code, not environment (#1027)
ci/woodpecker/push/publish Pipeline failed
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: mos-dt-0 <[email protected]>
2026-08-01 00:50:12 +00:00
55 changed files with 6787 additions and 2240 deletions
+1
View File
@@ -8,6 +8,7 @@ coverage
.env.local
*.tsbuildinfo
.pnpm-store
__pycache__/
docs/reports/
# Step-CA dev password — real file is gitignored; commit only the .example
+1 -1
View File
@@ -1 +1 @@
pnpm typecheck && pnpm lint && pnpm format:check
pnpm preflight && pnpm typecheck && pnpm lint && pnpm format:check
+4 -4
View File
@@ -1,5 +1,5 @@
@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/
# Pin the pnpm store to the same path the ci-base image warms (Dockerfile.ci),
# so the pipeline `pnpm install --prefer-offline` consumes the baked store
# instead of repopulating a fresh one.
store-dir=/root/.local/share/pnpm/store
# HOME resolves to /root in the ci-base image, preserving its warmed-store path.
# Non-root checkouts use their own HOME. Override without editing this file via
# NPM_CONFIG_STORE_DIR (pnpm's environment form of the store-dir setting).
store-dir=${HOME}/.local/share/pnpm/store
+13 -3
View File
@@ -68,15 +68,25 @@ steps:
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh
- bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
# Anti-inert-gate registry. Deliberately unconditional: no path filter and no
# step-level `when`, because a gate can be disabled by changes outside its own path.
gate-verify:
image: *node_image
commands:
- *enable_pnpm
- pnpm gate:verify
depends_on:
- install
- sanitization
- upgrade-guard
typecheck:
image: *node_image
commands:
- *enable_pnpm
- pnpm typecheck
depends_on:
- install
- sanitization
- upgrade-guard
- gate-verify
# lint, format, and test are independent — run in parallel after typecheck
lint:
+14
View File
@@ -201,8 +201,21 @@ git clone [email protected]:mosaicstack/stack.git
cd stack
# Install dependencies. The local tier uses in-process PGlite; leave DATABASE_URL unset.
# The pnpm store defaults to $HOME/.local/share/pnpm/store. Override it without
# editing the checkout with NPM_CONFIG_STORE_DIR=$HOME/another-store if needed.
pnpm install
# Verify dependencies and generated state before running source-quality gates.
# Missing dependencies exit 42; stale/foreign apps/web/.next state exits 43.
# The web build certifies its exact standalone symlink manifest; added, removed,
# retargeted, or manifest-only-tampered generated links also exit 43. This detects
# accidental, independent, stale, and foreign-residue mutation—the class exposed by
# a five-month-stale .next that produced 19 phantom TS2307 errors.
# It does NOT defend against a same-UID actor that can rewrite both manifest and
# marker consistently (CWE-345). RM-59 tracks the required executor/spine-side
# trust anchor outside worktree authority.
pnpm preflight
# Optional local queue service only. This does not start PostgreSQL.
docker compose up -d valkey
@@ -230,6 +243,7 @@ Gateway start command until KBN-101-02 makes that state fail closed.
### Quality Gates
```bash
pnpm preflight # Checkout/dependency/generated-state validation
pnpm typecheck # TypeScript type checking (all packages)
pnpm lint # ESLint (all packages)
pnpm test # Vitest (all packages)
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.2",
"private": true,
"scripts": {
"build": "next build",
"build": "node ../../scripts/build-web.mjs",
"dev": "next dev",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
+3
View File
@@ -0,0 +1,3 @@
# Administrator Guide
- [Gate registry operations](quality-gate-registry.md)
+35
View File
@@ -0,0 +1,35 @@
# Gate Registry Operations
## Routine verification
Run `pnpm gate:verify` from a dependency-installed checkout. Exit zero means registry observations matched their declared `actual` values; it does **not** assert required-behavior conformance while deltas remain. Open `DEFECT` records are checked descriptions of current behavior with tracked owners, never successful gate outcomes.
Investigate any of these immediately:
- `GATE VERIFY FAILED` — registry structure, observed behavior, provenance, claim binding, source/deployment identity, or negative-control detection changed. The verifier aggregates independent phase failures, so repair the responsible stable-ID diagnostics as well as any accompanying stale-fixture error; do not treat the generic error as a substitute.
- `unregistered gate` — an executable appeared under a declared gate root without a registry entry.
- `no negative control` — a gate has no must-fail case.
- `DEPLOYED IDENTITY UNAVAILABLE` — the runner cannot reach the installed enforcing copy. The pinned observation is checked, but live equality is not asserted.
- `HISTORY_PROVENANCE_FORBIDDEN` — a history assertion path was reintroduced at the repository layer; remove it and keep RM-60 as the tracked external-boundary owner.
## Updating a gate
1. Add or change the criterion, its exact criterion-side `caseRefs`, and matching case-side `criterionIds`. Preserve recursive closed-schema validation for every nested object; new fields require explicit key and type handling.
2. Observe the must-fail case fail for its own stated reason; moving the binding to any undeclared case must fail verification.
3. Declare an exact inerting mutation and observe the verifier detect it.
4. If required and actual behavior differ, add a tracked remediation owner and justification.
5. If meaning changed, append provenance; never replace the original silently.
6. For an installed counterpart, verify live byte identity and update the observed digest only from measured evidence.
7. Run focused verifier tests, `pnpm gate:verify`, and the repository baseline gates.
Do not add an ownerless exception or describe an open delta as pass/green/OK.
## CI behavior
Woodpecker runs `gate-verify` on every pull request and protected-main push without path filtering. This is deliberate: changes outside gate files can make a gate inert. The step needs no local history preparation because RM-02 asserts no history-provenance property.
**DOES:** PR CI executes current-tree verification unprivileged and fail-closed. It compares the manifest and verifier inventory separately with `gates/required-gates.baseline.json`, and consumed case evidence carries a subject checked against its gate definition. **Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.** The registered shrink-both, overclaim, and per-gate evidence-subject controls must remain red for their stated reasons.
**DOES NOT:** No local git state in the PR checkout is trustworthy as a history anchor because PR-controlled lifecycle code executes before the gate. The verifier has no history-verification path, and its closed current-tree observation renderer has no history/ancestry/lineage success class. Do not add a local ref, config, remote URL, source constant, or author-positioned path as a replacement anchor.
`scripts/gate-history-exclusion-control.mjs` enforces the output incapacity by testing alternate success wording and production renderer wiring; its registered fixture proves adding a prohibited success class goes red. RM-60 is the tracked owner of the provider-controlled/protected pre-execution boundary. If a bootstrap override is ever proposed before RM-60, it requires a separate explicit, loud, audited, retiring, negative-controlled design; RM-02 contains no override.
+3
View File
@@ -0,0 +1,3 @@
# Developer Guide
- [Gate registry and negative controls](quality-gate-registry.md)
@@ -0,0 +1,43 @@
# Gate Registry and Negative Controls
`gates/gates.manifest.json` is the machine-readable registry for the initial RM-02 gate slice. Run:
```bash
pnpm gate:verify
```
## Registered slice
The registry covers root typecheck, lint, and format checks; RM-01 checkout preflight; the Mosaic CI queue guard; and root Husky pre-commit/pre-push hooks. It does not imply repository-wide coverage. Framework scripts, package-local build/test scripts, templates, and deployment/release scripts remain assigned to RM-54.
Every gate declares exact invocations, observed and required outcomes, criterion bindings, and a single exact inerting mutation. Every case result carries an evidence-side subject that is compared with the gate definition when the result is consumed; the population control mutates that evidence-side subject independently for every required gate. Every must-fail case requires a non-empty reason diagnostic. The verifier rejects a stale, ambiguous, crashing, or ineffective mutation. Fixture and mutation writes reject path traversal and final-component symlinks. Every nested manifest object used by outcomes, mutations, fixtures, deployments, defects, compatibility, provenance, coverage, and merge assertions has closed keys and strict field types; a misspelling cannot silently turn a required comparison into an absent optional field. Independent validation phases collect labeled failures instead of letting one thrown fixture, claim, discovery, deployment, mutation, or compatibility error mask already-known stable-ID diagnostics. This proves detection of the **declared** inerting mutation, not every possible semantic weakening.
## Required versus actual
A case may record different `required` and `actual` outcomes only with a tracked owner. The verifier checks current reality against `actual`, prints each difference as `DEFECT (owner: ...)`, and fails when behavior changes without a matching registry update. A defect is never described as passing, green, or OK.
The queue guard currently has RM-03-owned deltas. In particular, its stdin/heredoc classifier does not consume piped status JSON, so terminal-success, no-status, and terminal-failure payloads become `unknown`; unknown and malformed states exit zero; push purpose defaults to `main`. RM-02 records these observations and does not edit the guard.
## Criteria, prose, and compatibility
Each criterion declares exact `caseRefs`; the verifier compares those semantic declarations bidirectionally with case-side `criterionIds` and requires at least one must-fail case. Moving a criterion ID to an unrelated case therefore fails as both a missing declared exercising case and an undeclared binding. Registered meta-negative controls misbind a criterion, remove meaning provenance, and misbind a prose claim, and each must make structure verification red for its stated reason.
Designated governing prose uses `GATE-CLAIM:<id>` markers. Each claim also names the exact must-fail `caseRef` that exercises its criterion; unknown, positive-only, unrelated, unbound, or registered-but-missing claims fail. Orchestrator-owned claims from `TASKS.md` are bound through `docs/remediation/GATE-CLAIMS.md`, which records source headings and anchored text without changing task tracking. Marker completeness still requires RM-54 review because arbitrary English claims cannot be inferred safely.
Compatibility checks detect direct contradictions in declared finite constructions. The verifier combines referenced case fixtures and environments in one isolated tree, rejects conflicting fixture/environment values, executes the construction's exact invocation, and checks its exact outcome. They do not prove semantic consistency of arbitrary natural language.
Restatements preserve original text, current text, reason, finding/task, and date.
## Source and deployed identity
A gate with an external installed counterpart declares it explicitly. When the installed queue guard is reachable, its bytes must equal repository source and an internal drift control is observed red. In CI the operator-home installation may be outside the container; the verifier checks the pinned observed source digest, reports `DEPLOYED IDENTITY UNAVAILABLE (owner: RM-04)`, and does not infer live equality.
## Current-tree and history-provenance boundary
**DOES:** Every PR evaluates the current checkout's registered gates and declared inerting mutations directly, unprivileged and fail-closed. The seven-gate population, verifier inventory, and `gates/required-gates.baseline.json` are compared inside the checkout. Evidence-side subjects are consumed and compared with gate definitions for every gate. **Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.**
**DOES NOT:** This repository layer establishes history provenance at all. `pnpm install` executes PR-controlled lifecycle code before `gate:verify`, so no local ref, git config, remote URL, constant, or author-positioned path in the checkout can anchor a history claim. An observation saying “unverifiable” while returning zero would be a green wearing a disclaimer, so the claim and history verifier path are removed.
The production output path uses a closed current-tree observation renderer; history, ancestry, and provider-lineage success are not representable observation classes. `scripts/gate-history-exclusion-control.mjs` exercises alternate success wording and production renderer wiring. Its registered must-fail case turns red if a prohibited success class is added or renderer consumption is bypassed. RM-60 owns the provider-controlled/protected pre-execution boundary needed before history provenance can be asserted. No bootstrap override exists in RM-02.
Universal checks first prove populations non-empty. The production profile reads the same-checkout baseline before comparing the verifier inventory and manifest separately, while `gateRefs` on the D-38/D-40 criteria must exactly span every registered gate. Population controls mutate evidence-side subjects and type-strict comparison inputs one gate at a time and require rejection across the complete inventory. A registered prose-claim control fails when the same-checkout mechanism is described as an adversarial protection.
+57
View File
@@ -14,6 +14,63 @@
---
## RM-02 Gate Registry and Negative-Control Verifier (#1029)
### Problem and objective
Existing deterministic gates can return success without enforcing their stated property. RM-02 introduces a machine-readable registry and an unconditional CI verifier that distinguishes required behavior from observed behavior, proves every registered negative control can detect its own failure reason, and makes source/deployment drift visible.
### Scope
**In scope:** root typecheck, lint, and format gates; RM-01 checkout preflight; the Mosaic CI queue guard; root Husky pre-commit and pre-push hooks; criterion bindings; modeled compatibility; meaning-change provenance; security/integrity prose claim markers; source-versus-deployed identity; unprivileged current-tree PR verification; independent required-inventory comparison; evidence-side subject consumption; and enforced structural exclusion of history provenance until RM-60 provides protected external authority.
**Out of scope:** fixing the queue guard (RM-03); exhaustive registration of every repository executable (RM-54); semantic proof that arbitrary English criteria are mutually satisfiable (RM-54/RM-55); history provenance before RM-60's provider-controlled/protected execution boundary; and a same-authority trust anchor for repository-authored evidence (RM-25/RM-59).
### Normative requirements
1. `RM02-REQ-01`: The JSON registry SHALL give every gate and criterion a stable ID and SHALL declare exact invocation, input classes, cases, exact observed and required exit codes, reason diagnostics, and criterion bindings.
2. `RM02-REQ-02`: Every gate SHALL have at least one observed-red must-fail case. The verifier SHALL reject missing, stale, ambiguous, or ineffective declared inert mutations and SHALL name an externally inerted gate.
3. `RM02-REQ-03`: Every acceptance criterion SHALL declare exact criterion-side `caseRefs` that match case-side `criterionIds` bidirectionally and include a case that can fail for that criterion's stated reason. Moving a binding to an unrelated case SHALL fail verification. Security/integrity prose claims in the designated governing documents SHALL carry bound `GATE-CLAIM:<id>` markers and declare the exact must-fail case exercising the claim criterion.
4. `RM02-REQ-04`: Declared finite compatibility scenarios SHALL execute together and direct modeled contradictions SHALL fail. This does not claim semantic consistency of arbitrary English.
5. `RM02-REQ-05`: Restated criteria SHALL retain original text, current text, reason, finding/task, and dated meaning-change history.
6. `RM02-REQ-06`: An observed behavior differing from required behavior SHALL be reported as `DEFECT` with a tracked owner; an ownerless delta SHALL fail verification. Such a gate SHALL never be described as passing, green, or OK.
7. `RM02-REQ-07`: Executables under declared gate roots SHALL fail with `unregistered gate` when absent from the registry. The initial coverage boundary SHALL explicitly list exclusions and bind the broader inventory to RM-54.
8. `RM02-REQ-08`: Every gate with a deployed counterpart SHALL register source/deployed byte identity and a must-fail drift control. Gates without a deployed counterpart SHALL say so explicitly.
9. `RM02-REQ-09`: CI SHALL run `pnpm gate:verify` on every pull request without path filtering and on protected-main pushes.
10. `RM02-REQ-10` (restated): PR CI SHALL perform unprivileged, fail-closed current-tree verification only. The production observation renderer SHALL be a closed current-tree-only output type with no representable history-provenance success state. A registered must-fail control SHALL turn red if history/ancestry/lineage success is added to that renderer or if production output bypasses the renderer. RM-60 owns the provider-controlled/protected pre-execution boundary required to establish history provenance.
11. `RM02-REQ-11` (`D-46`): The required seven-gate inventory SHALL be read from a same-checkout baseline and compared separately with both the verifier inventory and manifest. Shrinking the verifier inventory and manifest together while leaving the baseline intact SHALL fail for the removed gate. **Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.**
12. `RM02-REQ-12` (`D-38`): Every consumed case result SHALL carry an evidence-side subject independently declared from the gate definition. The verifier SHALL compare that evidence subject with the gate definition when consuming the result. A population control SHALL mutate the evidence-side subject independently for every required gate and observe rejection for every gate.
13. `RM02-REQ-13` (`D-40`): For every gate listed in the required inventory baseline, each discriminator and comparison input SHALL have a recursively closed, type-strict schema. Unknown, misspelled, wrong-type, or present-but-empty nested assertion fields SHALL fail rather than disabling an assertion.
14. `RM02-REQ-14` (`D-46`): No universally quantified registry check SHALL run until its population is proven non-empty and compared with the same-checkout baseline. The required seven-gate inventory, criteria, prose claims, and compatibility scenarios SHALL reject empty populations before reporting that all registered cases ran.
#### RM02-REQ-10 meaning-change provenance
- **Original:** “assert that every merged commit passed every required gate, evaluated AGAINST THAT COMMIT'S OWN TREE — not against current main.”
- **Restatement:** The repository verifier performs current-tree verification and is structurally incapable of asserting history provenance. RM-60 supplies the provider-controlled/protected pre-execution boundary before any repository-controlled lifecycle code executes.
- **Reason:** `pnpm install` executes PR-controlled lifecycle code before `gate:verify`; therefore no local ref, git config, remote URL, constant, or author-positioned path in the checkout is a trustworthy history anchor. A local disclaimer would be a green wearing a note, not a property. The history-provenance claim is removed rather than weakened.
### Acceptance criteria
1. `RM02-AC-01`: A healthy current tree returns zero while prominently reporting the queue guard's required-versus-actual `DEFECT (owner: RM-03)` delta.
2. `RM02-AC-02`: Externally mutate any registered gate at its declared inerting point so its failure path succeeds; verification returns nonzero and names that gate. The verifier's internal meta-control is observed red before its healthy result is trusted.
3. `RM02-AC-03`: An executable added under a declared gate root without an entry returns nonzero and includes `unregistered gate`.
4. `RM02-AC-04`: A gate with zero must-fail cases returns nonzero and includes `no negative control`.
5. `RM02-AC-05`: Unbound or semantically misbound criteria, prose claims bound to unrelated cases, unbound governing prose markers, ownerless behavior deltas, stale mutations, source/deployed drift, and modeled compatibility conflicts each return nonzero with the responsible stable ID. A simultaneous stale fixture or independent phase error SHALL NOT mask responsible stable-ID diagnostics. Registered meta-negative controls move a criterion binding, remove meaning provenance, and redirect a prose claim to an unrelated case; each is observed red for its stated reason.
6. `RM02-AC-06`: CI configuration invokes the verifier unconditionally on every pull request.
7. `RM02-AC-07`: PR output states adjacent `DOES`/`DOES NOT` boundaries: the repository layer verifies current-tree registered cases, same-checkout inventory drift, and evidence-side subject consumption; it does not establish history provenance at all. **Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.**
8. `RM02-AC-08`: Registered must-fail controls reject an emptied registry; shrinking the verifier inventory and manifest together; overstating the same-checkout inventory boundary; adding history/ancestry/lineage success to the closed observation renderer or bypassing renderer consumption; a misspelled nested outcome field; a wrong outcome field type; and a present-but-empty outcome pattern.
9. `RM02-AC-09`: Population controls iterate every gate listed in the baseline and prove consumed evidence-subject mismatch and wrong-type comparison input are rejected for each gate. `RM02-EVIDENCE-SUBJECT-BINDING`, `RM02-TYPE-STRICT-SCHEMA`, `RM02-HISTORY-PROVENANCE-EXCLUDED`, and `RM02-NONEMPTY-ANCHORED-QUANTIFICATION` are bidirectionally bound to their must-fail controls.
### Risks, dependencies, and verification boundary
- **DOES:** The repository verifier proves declared current-tree controls, modeled scenarios, bidirectional criterion/case relationships, source/deployed equality at execution time, a same-checkout seven-gate baseline comparison, and evidence-side subject consumption.
- **DOES NOT:** This layer establishes no history provenance. PR-controlled lifecycle code executes before the gate, so no local git state in the checkout is trustworthy as a history anchor. RM-60 owns the provider-controlled/protected pre-execution boundary. The production verifier has no history verifier path, and its closed current-tree observation renderer cannot represent a history-provenance success state.
- **Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.** A registered claim control fails if the checked artifacts overstate this boundary.
- The verifier does **not** infer arbitrary-English semantics or defend against an actor able to rewrite the gate, registry, verifier, controls, and baseline consistently.
- `ASSUMPTION:` RM-54 is the owner for expanding registration and prose-marker coverage beyond this approved seven-gate slice; rationale: the remediation task graph already assigns the fleet-wide inert-gate audit there.
---
## Problem Statement
Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and Next.js frontend. It handles chat, projects, tasks, and LLM routing but lacks orchestration depth, agent coordination, shared memory, and remote access. The Mosaic framework (`~/.config/mosaic`) provides agent guides, shell-based orchestration tools, and quality rails — but these are loose scripts, not an integrated platform. The `@mosaicstack/*` packages in mosaic-mono-v0 began consolidating these into TypeScript packages (brain, queue, coord, cli, prdy, quality-rails) but have no UI, no auth, and no agent runtime integration.
+6
View File
@@ -1,5 +1,11 @@
# Documentation Sitemap
## Gate verification
- [Developer gate registry guide](DEVELOPER-GUIDE/quality-gate-registry.md) — manifest schema, negative controls, evidence-side subjects, and the enforced RM-60 history-provenance exclusion. Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.
- [Gate registry operations](ADMIN-GUIDE/quality-gate-registry.md) — routine verification, failure interpretation, registry updates, and unconditional CI behavior.
- [RM-02 governing claim index](remediation/GATE-CLAIMS.md) — marker bindings for orchestrator-owned remediation claims without modifying task tracking.
## Compaction refresh lease broker
- [Internal broker protocol](architecture/lease-broker-protocol.md) — kernel identity, ancestry and generation invariants, framed requests, responses, and persisted cycle bindings.
@@ -0,0 +1,95 @@
# RM-02 Gate Registry Implementation Plan
> **For Pi:** Use test-driven development and execute each task RED → GREEN → refactor.
**Goal:** Build a machine-readable seven-gate registry and an unconditional CI verifier that detects inert gates, binds criteria to observed negative controls, records defects honestly, and verifies the current PR tree unprivileged and fail-closed.
**Architecture:** A dependency-free Node CLI reads `gates/gates.manifest.json` and the same-checkout `gates/required-gates.baseline.json`, validates closed schemas and references, then runs typed cases in isolated main-disk fixtures. Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary. Gate-specific fixture setup remains declarative; exact invocations, evidence-side subjects, and exact observed/required exits stay in JSON. The production verifier is structurally incapable of asserting history provenance; RM-60 owns the provider-controlled/protected pre-execution boundary.
**Tech Stack:** Node.js ESM, `node:test`, JSON, shell gates, pnpm, Woodpecker CI.
---
### Task 1: Meta-negative-control kernel
**Files:**
- Create: `scripts/gate-verify.test.mjs`
- Create: `scripts/gate-verify.mjs`
1. Write a black-box fixture whose gate failure branch has already been changed to success.
2. Run `node --test scripts/gate-verify.test.mjs`; require failure because the absent verifier does not name the inert gate.
3. Implement manifest loading, exact process execution, and named mismatch reporting only.
4. Re-run and require the external inert mutation to produce verifier nonzero while the test passes.
5. Add an internally applied declared mutation and require a healthy fixture to report its negative control armed.
### Task 2: Registry structural clauses
**Files:**
- Modify: `scripts/gate-verify.test.mjs`
- Modify: `scripts/gate-verify.mjs`
Add failing tests, one behavior at a time, for: `unregistered gate`; `no negative control`; unbound criterion; unbound `GATE-CLAIM`; ownerless required/actual delta; stale/ambiguous/ineffective mutation; direct modeled conflict; missing meaning-change provenance. Implement the minimum validator after each observed RED.
### Task 3: Gate fixtures and seven-gate manifest
**Files:**
- Create: `gates/gates.manifest.json`
- Create: `gates/fixtures/quality-case.mjs`
- Create: `gates/fixtures/preflight-case.mjs`
- Create: `gates/fixtures/queue-case.sh`
- Create: `gates/fixtures/hook-case.sh`
- Modify: `scripts/gate-verify.test.mjs`
Register typecheck, lint, format, RM-01 preflight, queue guard, pre-commit, and pre-push. For each, first run its known-bad case against an intentionally inert fixture and observe verifier RED; then restore the real gate behavior and require its exact observed exit/reason. Record queue defects as required/actual deltas owned by RM-03, never as pass/green/OK.
### Task 4: Source-versus-deployed identity
**Files:**
- Modify: `gates/gates.manifest.json`
- Modify: `scripts/gate-verify.test.mjs`
- Modify: `scripts/gate-verify.mjs`
Write and observe a failing test with a byte-mutated deployed counterpart. Implement byte equality and internal drift mutation controls. Declare `none` explicitly for gates without deployed counterparts.
### Task 5: Prose claims and compatibility constructions
**Files:**
- Modify: `docs/remediation/MISSION.md`
- Modify: `docs/remediation/TASKS.md` only if coordinator authorization overrides the worker prohibition; otherwise place markers in an RM-02 claim index that references immutable source anchors.
- Modify: `gates/gates.manifest.json`
- Modify: verifier tests/implementation.
Enumerate current security/integrity claims, bind each marker/id to a negative case, and reject unbound markers. Execute finite compatibility scenarios and clearly document that arbitrary English consistency is outside the model.
### Task 6: Current-tree boundary and enforced history-provenance exclusion
**Files:**
- Create: `gates/required-gates.baseline.json`
- Create: `scripts/gate-history-exclusion-control.mjs`
- Create: `scripts/gate-inventory-shrink-control.mjs`
- Modify: `scripts/gate-verify.mjs`
- Modify: `gates/gates.manifest.json`
Verify current-tree behavior only. Remove the anchor-dependent history verifier and provider-history consumption because PR-controlled lifecycle code executes before the gate and makes every local git anchor untrustworthy. Use a closed current-tree observation renderer and register a must-fail control that turns red if history/ancestry/lineage success becomes representable or production output bypasses renderer consumption. Compare the verifier inventory and manifest separately against the same-checkout baseline, register must-fail attacks that shrink either paired representation, and register an overclaim control. Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary. State both directions: current-tree controls, inventory shape, and evidence subjects are enforced here; history provenance is not established until RM-60 provides a provider-controlled/protected pre-execution boundary.
### Task 7: CI and documentation
**Files:**
- Modify: `package.json`
- Modify: `.woodpecker/ci.yml`
- Create/update: `docs/DEVELOPER-GUIDE/quality-gate-registry.md`
- Create/update: `docs/ADMIN-GUIDE/quality-gate-registry.md`
- Modify: `docs/SITEMAP.md`
Add `gate:verify`; run it in an unconditional PR/main CI step. Document invocation, defect semantics, coverage/exclusions, marker syntax, replay/provider boundaries, and source/deployed identity.
### Task 8: Verification and delivery
Run focused tests, `pnpm gate:verify`, checkout tests, typecheck, lint, format, and applicable integration tests. Run Codex code and security review; remediate and re-review. Verify authorship, commit, execute queue guard before push, push, create PR via Mosaic wrapper, and send exact-head review request to rev-974 through the coordinator. Do not merge without coordinator authorization.
-58
View File
@@ -1,58 +0,0 @@
# RM-02 / PR #1030 — PRE-REGISTERED RE-REVIEW: the merge-base anchor round
**Subject head (exact):** `fbb61912981abb250d289ec7aafd4316db6fdb11`
**Supersedes:** the second NO-GO at `32b490a7` (D-45/D-46/D-47). That verdict is VOID.
**Registered by:** `mos-remediation` **before any reviewer read the diff.** **Reviewer:** `rev-974`.
## What the orchestrator already reproduced (do not re-spend the review here)
- Derived boundary `f4fd5967` **equals** an independently computed `git merge-base HEAD origin/main`.
- Emptying `criteria`/`gates`/`proseClaims`/`compatibilityScenarios` now fails with
_"population must be non-empty and anchored before evaluation"_.
- The both-directions bootstrap statement is present in `gate-history.mjs` with the Builds 12 residual.
## A — attack the ANCHOR (highest value)
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A1** | `targetRef` is the literal `refs/remotes/origin/main`. **Who controls that ref in the environment where the gate runs?** Can a PR author influence it — a `remote.origin.url` rewrite, a crafted `refs/remotes/origin/main` in their own clone, a push to a fork's `main`? If the gate trusts a locally-writable ref, the anchor moved from the manifest into git config. |
| **A2** | **Absent/stale target:** if `refs/remotes/origin/main` is missing, stale, or the fetch is shallow, does it fail CLOSED (line 57 suggests it does) or silently derive a narrower range? |
| **A3** | **Delayed introduction — the round-2 attack, re-run.** Commit a gate change BEFORE adding the manifest. Does it now stay in range and fail the own-tree read? |
| **A4** | **Branch from an old main:** branch from a point far behind `origin/main`, so merge-base is old. Does the range widen correctly (safe) or include unrelated main commits that cannot carry manifests (a new false-red)? Over-inclusion is the natural failure mode of this fix. |
| **A5** | Is the **bootstrap residual** recorded as a **tracked dependency on Builds 12**, not prose? (D-19's third mandatory move.) |
## P — the non-empty/anchored precondition
| id | check |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **P1** | Does the precondition run **before** every quantified check, on **every** path — or only the ones the author enumerated? Find a quantified check that still runs before its precondition. |
| **P2** | **Seven required gate IDs anchored to canonical sources** — can that anchor list itself be emptied, shrunk, or pointed elsewhere? An anchor list the author edits is D-45 again, one level in. |
| **P3** | The author reports finding and REMOVING a **production CLI fixture-profile bypass** in their own pre-commit review. **Verify it is gone from the shipped CLI path**, that the remaining relaxation is test-support only and non-executable in production, and that the CLI rejection is tested. This was self-disclosed — confirm it independently. |
## Q — clauses quantified over the population (D-47)
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Q1** | `gateRefs` **exactly spans** every registered gate. Can a gate be added WITHOUT a corresponding ref — i.e. does adding a gate to the population escape the clause? |
| **Q2** | Deleting `gateRefs`, or shrinking gates and refs **together**, must fail. Shrink-together is the D-46 shape; verify it is a genuine control and not a regression guard mislabelled. |
| **Q3** | Do the population controls **iterate all seven gates** and mutate evidence-subject and comparison-type **per gate**, or only a representative one? |
| **Q4** | Honest labelling: author claims delayed-introduction, empty-populations, fixture-profile bypass and removable gateRefs as **genuine red-first**, and shrink-together/exact-span as **regression guards**. Verify each claim against pre-fix code (D-8). |
## C — head, CI, integrity
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **C1** | Head `fbb61912` on git and provider; `Closes #1029` present. |
| **C2** | Exact-head CI terminal-green, full `-f json`, counts stated incl. `clone`; machine-checked with `verify-terminal-green.py --expect-commit fbb61912981abb250d289ec7aafd4316db6fdb11`. |
| **C3** | Nothing weakened: `32b490a7``fbb61912` removes/relaxes no existing case, test, or assertion. |
## Reviewer instruction
Verdict bound to `fbb61912981abb250d289ec7aafd4316db6fdb11`, evidence enumerated, posted durably under
your own identity. If a check is unrunnable, **say so**.
**A1 and P2 are the ones I most want answered** — both ask whether the fix moved the author's control
point again rather than removing it, which is now the mission's named first-class principle and has
recurred three times. **Attack outside this set.**
**No one dispatching this review may state a conclusion on an open check (D-39).**
-67
View File
@@ -1,67 +0,0 @@
# RM-02 / PR #1030 — PRE-REGISTERED RE-REVIEW: the four-blocker hardening pass
**Subject head (exact):** `32b490a712a5e8e77f13c40c60099b51feb38994`
**Supersedes:** the NO-GO at `83d2ecb2` (D-44, four silent-defeat paths). That verdict is VOID.
**Registered by:** `mos-remediation` **before any reviewer read the diff.** **Reviewer:** `rev-974`.
## Framing — attack the FIX, not the original bug
Every round on this mission, **the remediation introduced the next hole**: the commit-binding fix
shipped a type confusion; the type-strict fix was clean but the registry around it had four defeats.
So the highest-value checks here are **not** "was each blocker fixed" — the orchestrator already
reproduced three of the four attacks as dead — but **"is the NEW mechanism itself defeatable?"**
Blocker 1's fix replaced an author-settable field with a **derivation**. A derivation has inputs. **The
question is who controls them.**
## H — the new derivation (highest value)
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **H1** | `deriveHistoryBoundary` = parent of the first first-parent commit introducing `gates/gates.manifest.json`. **Are its inputs author-controlled?** Can a PR author influence the derived value by adding, moving, renaming, deleting-and-recreating, or symlinking that path — or by adding a SECOND manifest earlier in history? If the derivation is gameable, blocker 1 is not fixed, only relocated. |
| **H2** | **First-parent traversal:** what happens on a merge commit, an octopus merge, a squash, a rebase that reorders, or a branch where the introducing commit is not on the first-parent chain? Does the boundary silently move, or fail closed? |
| **H3** | **Shallow/partial clone:** the branch previously needed an unshallow fix (`e8959975` "unshallow gate replay history"). If history is shallow, does the derivation fail CLOSED or silently derive a wrong/empty boundary? |
| **H4** | **Path deletion:** if `gates/gates.manifest.json` is absent at HEAD, or was deleted and re-added, what is derived? Fail closed, or a boundary that excludes the deletion window? |
| **H5** | Are the **three registered seam controls (HEAD, HEAD^, introduction) genuinely RED-FIRST** — do they fail against the PRE-fix code for their own stated reason? A control that was always green is a regression guard, not a must-fail control (D-8). |
## S — the recursive schema closure
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **S1** | **Is closure COMPLETE or enumerated?** The author lists the objects they closed. Find one they did **not** — any nested object anywhere in the manifest — and inject an unknown key there. If it is accepted, the fix covers instances, not the class. |
| **S2** | **Wrong-type, not just unknown-key:** `outputPattern` as a number/array/object/null rather than misspelled. Does the closed schema type-check values, or only key names? |
| **S3** | **Empty/degenerate values:** `outputPattern: ""` — does an empty regex match everything and silently neuter the assertion the same way a typo did? The author claims an empty-pattern control; verify it is bound. |
| **S4** | Confirm the registered typo/wrong-type/empty-pattern controls are **genuine red-first** against pre-fix code, and that anything labelled a regression guard is honestly labelled as one. |
## E — provider evidence (blocker 4)
| id | check |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **E1** | Global validation runs **before** per-commit filtering **on every path**, including the HEAD-only path the author calls out. Is there any code path that reaches per-commit assessment without it? |
| **E2** | **Duplicate identity by another key:** the fix makes pipeline NUMBER globally unique. Can two records still collide on a different identity dimension — same commit + different number, same URL, same step-id — and certify the wrong subject? |
| **E3** | "Exactly one gate-verify step per record" — what if a record has zero, or two with different outcomes? Fail closed? |
## C — clauses, integrity, CI
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **C1** | The three new criteria (`RM02-EVIDENCE-SUBJECT-BINDING`, `RM02-TYPE-STRICT-SCHEMA`, `RM02-HISTORY-BOUNDARY`) are **checked criteria with bidirectionally bound cases**, not prose. Orchestrator observed caseRefs 1 / 3 / 3 — verify the bindings actually run and can fail. |
| **C2** | **B1/B2 satisfied in substance:** does `RM02-EVIDENCE-SUBJECT-BINDING` express D-38 generally ("does this gate bind its evidence to its subject?") or only the one pipeline-number instance? Same for D-40 vs the one typo instance. **A clause that only covers its originating instance is not a clause.** |
| **C3** | **Nothing weakened:** diff `83d2ecb2``32b490a7` removes/relaxes no existing case, test, or assertion. |
| **C4** | Exact-head CI terminal-green, full `-f json`, counts stated **including `clone`**; machine-checked with `verify-terminal-green.py --expect-commit 32b490a712a5e8e77f13c40c60099b51feb38994`. |
| **C5** | `Closes #1029` still present (D-38c). |
## Reviewer instruction
Verdict bound to `32b490a712a5e8e77f13c40c60099b51feb38994`, evidence enumerated, posted durably under
your own identity. If a check is unrunnable, **say so** — never substitute a passing variant.
**H1 and S1 are the two I most want answered**, because they ask whether each fix closed a **class** or
an **instance**. Attack outside this set: four of the last five blockers on this PR came from mutating
something nobody had registered.
**Environment note:** `pnpm gate:verify` fails on `checkout-preflight` with outcome 42 (bubblewrap
class) in at least one worktree. If you hit it, report it as an environment limitation — do not treat it
as a finding, and do not let it mask one.
**No one dispatching this review may state a conclusion on an open check (D-39).**
-59
View File
@@ -1,59 +0,0 @@
# RM-02 / PR #1030 — PRE-REGISTERED RE-REVIEW: honest narrowing + (d)-strict
**Subject head (exact):** `e910a45ab3578a9c77ad3eeaa52b31b72611f6c6`
**Supersedes:** the third NO-GO at `fbb61912` (D-45/D-46/D-47) and the D-52 stop. Those are VOID.
**Registered by:** `mos-remediation` before any reviewer read the diff. **Reviewer:** `rev-974`.
## Already reproduced by the orchestrator (do not re-spend here)
- The required narrowing sentence is **verbatim** in `gates/required-gates.baseline.json` and in
`scripts/gate-inventory-claim-control.mjs`: _"Detects accidental and incompetent inventory drift
within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier
consistently — that guarantee requires RM-60's external boundary."_
- **The overclaim control FIRES**: clean ⇒ exit 0; injecting "protected, independently anchored" into
the baseline purpose ⇒ **exit 84**, `INVENTORY_CLAIM_OVERSTATED`.
- CI #2201 at the exact head, 10/10 incl. `clone`; machine check bound, `exempted_steps 0`, no anomalies.
## N — the narrowing (D-52's ruling is the wording)
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **N1** | ★ Is the narrowed claim **honest EVERYWHERE**, or only where the control looks? Find any place — docs, comments, observations, manifest prose, CLI output — that still implies the baseline is protected or independently anchored. **The control only checks the files it checks.** |
| **N2** | ★ Can the **overclaim control itself** be defeated? It is PR-controlled like everything else. Weaken/skip/reword it so an overstatement passes. If it can be trivially neutered, D-52 is re-armed one level down — the exact recursion this PR keeps producing. |
| **N3** | Does the control detect **semantic** overclaim or only the literal strings it greps? An artifact could assert protection in different words and pass. |
| **N4** | Is the narrowed claim **TRUE**? Verify the baseline genuinely catches accidental/inconsistent drift — not just that it says so. A narrowed claim that is still overstated is D-48 again. |
## H — (d)-strict history removal
| id | check |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **H1** | Is the verifier **structurally incapable** of reporting history-provenance-verified, or merely not currently doing so? The manifest says "structurally incapable" — test the claim. |
| **H2** | ★ **TRAP 1 re-check:** nothing emits the unverifiable state as an OBSERVATION (which exits 0). Confirm no path yields exit 0 while implying provenance was considered. |
| **H3** | ★ **TRAP 2 re-check:** the exclusion is **enforced**, not merely declared in `coverageBoundary.excluded`. Re-enable history provenance without a trustworthy anchor — does a registered case go RED? |
| **H4** | The residual is stated **correctly** — no local git state is trustworthy because PR code executes before the gate — **NOT** the D-48 wording ("compromised main"). |
## B3 / R — evidence-subject and renderer
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **B1** | Subject is on the **EVIDENCE side** and compared **at consumption**, not `gate.evidenceSubject === gate.id` metadata-vs-itself. Mutate the evidence-side subject **per gate**. |
| **B2** | Can evidence still be moved or misbound while every gate satisfies its check? |
| **R1** | Renderer now covers the **final success stdout/stderr** paths. |
## C — integrity + CI
| id | check |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **C1** | Head `e910a45a` on git + provider; `Closes #1029` present. |
| **C2** | Exact-head CI terminal-green, `-f json`, counts incl. `clone`; machine-checked with `--expect-commit e910a45ab3578a9c77ad3eeaa52b31b72611f6c6`. |
| **C3** | Nothing weakened `fbb61912``e910a45a`. Red-first labels honest (D-8). |
| **C4** | **NO fourth local anchor was invented.** Blocker 1 and blocker 2's adversarial guarantee both track RM-60. |
## Reviewer instruction
Verdict bound to `e910a45ab3578a9c77ad3eeaa52b31b72611f6c6`, evidence enumerated, posted durably.
Unrunnable ⇒ **say so**. **N1 and N2 are the sharp ones** — this PR's history is that every fix
relocates the defect one level down, and N2 asks whether the control that enforces honesty is itself
honest. **Attack outside the set: six for six so far.**
**No one dispatching this review may state a conclusion on an open check (D-39).**
-71
View File
@@ -1,71 +0,0 @@
# RM-02 / PR #1030 — PRE-REGISTERED ACCEPTANCE CHECKS (post-rebase, keystone)
**Subject head (exact):** `83d2ecb2243f1b0987ef2bc14de47f444285a684`
**Rebased onto:** `main` @ `f4fd5967fc5d4cbc72d680b199d88224aa855131` (post-RM-61)
**Registered by:** `mos-remediation` **BEFORE the reviewer read the diff.** **Reviewer:** `rev-974`
(author ≠ reviewer; branch commits are `f10-coder`'s and `coder-mos1`'s).
**Prior reviews 63 / 65 are SUPERSEDED; this head carries NO live review of any kind.**
RM-02 is the **keystone**: it is the anti-inert-gate registry every downstream gate is measured against.
A defect that survives here propagates into every gate this mission ships.
## ★ CRUX — the one non-mechanical change, made by the author, that turned a red green
The rebase was clean. One further commit was **not** purely mechanical and the author disclosed it
unprompted (`83d2ecb2` — advance registry activation seam):
gates/gates.manifest.json
- "activationCommit": "f65e9ea656ec466e12640bf6ab5d46fe07ff160c"
+ "activationCommit": "f4fd5967fc5d4cbc72d680b199d88224aa855131"
Stated rationale: after the rebase, `pnpm gate:verify` correctly failed because the newly-merged
pre-registry RM-61 commit was treated as _prospective_ and required a manifest that could not exist.
`activationCommit` sets the lower bound of `activationCommit..HEAD` — **the set of commits required to
carry own-tree registry provenance** (`gate-history.mjs:314``listProspectiveCommits`). Advancing it
**shrinks that set**.
**Determine, do not assume — and I have deliberately formed and stated no verdict (D-39):**
| id | check |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A1** | **Is the advance JUSTIFIED?** Can a pre-registry commit on `main` (e.g. `f4fd5967`) satisfy the provenance requirement at all, or is requiring it impossible-by-construction? If impossible, is advancing the seam the _correct_ remedy, or does a correct remedy exist that does not move an author-controlled field? |
| **A2** | **Is the advance MINIMAL?** Was it advanced exactly to the new parent baseline, or beyond it? Does any commit that _should_ remain covered fall outside the new range? |
| **A3** | **Are all 7 branch commits still PROSPECTIVE under the new seam?** Enumerate them and show each is still required to carry provenance. The author claims "own-tree provenance over every registry-era branch commit" — verify the claim, do not accept it. |
| **A4** | ★★ **IS THE SEAM ITSELF CONSTRAINED?** The only validation found is `merge-base --is-ancestor activationCommit head` (`gate-history.mjs:288`). **A commit is its own ancestor.** Determine what `activationCommit = HEAD` does: is the prospective range empty, does the per-commit loop iterate zero times, and does the history check therefore PASS VACUOUSLY? If so, the anti-inert-gate registry contains an author-settable field that can inert its own history check. **Run it. Report the observed exit and whether any failure is raised.** |
| **A5** | **If A4 shows vacuity is reachable, is there a registered MUST-FAIL negative control for it?** RM-02's own founding rule is that a gate with no proven failure path manufactures evidence. Does the registry hold a case that goes RED when the seam is over-advanced? If not, that is a hole in the registry's coverage of itself. |
| **A6** | **Self-verification shape (charter / D-19 / D-39b):** the field was advanced by the change's own author to make the author's gate pass. Independent of whether the value is correct, determine whether the _mechanism_ permits an audited party to relax its own audit, and whether that needs a constraint, an owner, or an escalation. |
## Registry substance — the clauses this PR must now carry
| id | check |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **B1** | **D-38 clause present and enforced:** the registry asks of every gate _"does this gate BIND its evidence to the subject under review?"_ Ruled in-scope for THIS PR by Mos. Verify it exists as a checked clause, not prose. |
| **B2** | **D-40 clause present and enforced:** _"discriminator and comparison inputs must be TYPE-STRICT."_ Also ruled in-scope for THIS PR. `== 0` matching `False`/`0.0` is the banked instance; the clause is the general form. |
| **B3** | **D-42's clause is CORRECTLY ABSENT** — provider-side negative control tracks separately (Mos: landing it now would give the registry a clause with nothing to satisfy it, going red on its own clause). Confirm its absence is deliberate and recorded, not forgotten. |
| **B4** | **The four founding clauses still hold** (`MISSION.md`): every check proven **right** (red for its own stated reason), the set **covers**, no two criteria **conflict**, and criterion evolution **retains original text + restatement + reason**. |
| **B5** | **Nothing was weakened, skipped, or deleted by the rebase.** Diff `f9746b23``83d2ecb2` and confirm no registered case, test, or assertion was removed or relaxed to make the rebase land. |
| **B6** | **`coverageBoundary.excluded[]` is honest in BOTH directions** (charter principle 4): what it does NOT cover is stated beside what it DOES, and the gap is tracked (`trackedBy: RM-54`) rather than implied-fixed. |
| **B7** | **The RM-02 execution boundary** (`gate-history.mjs`, DOES / DOES NOT, owner RM-60, xref RM-59) states its limits in both directions and names a tracked owner — not a documented gap with no owner. |
## Head + CI (re-run these; do not carry them forward from the author's report)
| id | check |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **C1** | Head `83d2ecb2…` on **both** git and provider; `Closes #1029` present in the body (delivery-issue rule, D-38c). |
| **C2** | Exact-head CI terminal-green by **full `-f json` scan**, counts stated **including `clone`**. Note the count changed 9 → **10** (this PR adds `gate-verify`) — confirm the new step is real, not a miscount. |
| **C3** | **Machine-check it, do not assert it:** `verify-terminal-green.py --expect-commit 83d2ecb2243f1b0987ef2bc14de47f444285a684`. Report exit, `exempted_steps`, `commit == expected_commit`. |
| **C4** | `exempted_steps=0` is expected here — `ci-postgres` succeeded, so the #1000 exemption was **not needed**. Confirm the exemption is present-but-unused rather than silently inapplicable. |
## Reviewer instruction
Verdict bound to `83d2ecb2243f1b0987ef2bc14de47f444285a684`, evidence enumerated per check, posted
durably under your own identity. If a check is unrunnable, **say so** — never substitute a variant that
passes. Scan CI from `-f json`, never default text (D-33); state your counts.
**A4 is the check I most want an answer to and the one I have least confidence about.** Attack outside
this set as well: every blocker found on this mission so far came from mutating something nobody had
registered a check for.
**No one dispatching this review may state a conclusion on an open check (D-39).** Observations may be
relayed to you; verdicts may not. The ruling is yours.
-42
View File
@@ -1,42 +0,0 @@
# RM-03 / PR #1032 — PRE-REGISTERED RE-REVIEW (post-freshening)
**Subject head (exact):** `868b9b871a5118762aa5b8aafecd2f0592f7ab5c`
**Rebased onto:** `main` @ `f4fd5967` **Registered by:** `mos-remediation` before the reviewer read the
diff. **Reviewer:** `rev-974`.
**Why this exists:** review 66 APPROVED and the merge-gate GO were bound to `78ec47cd`; `main` drifted
under a long hold and the PR went `mergeable=false` (**D-50 — a GO has a shelf life**). Both verdicts
are VOID at the new head. Jason's merge approval is **standing** and executes once this re-gates.
## What the orchestrator already verified (do not re-spend here)
- `git range-diff 78ec47cd...868b9b87`: commits **25 are `=` (patch-equivalent)**; only commit 1 differs.
- That single difference is the `test:framework-shell` chain, resolved as a **UNION** — main's
`test-terminal-green-contract.sh` **and** RM-03's `tristate`, `github-checks`, `merge-queue-branch`,
`merge-head-pin`, plus the pre-existing `branch-absent`. **Nothing dropped from either side.**
- CI #2199 at the exact head: 9 children, 9 success incl. `clone`; machine check exit 0, bound, no anomalies.
- `mergeable` is now **true**.
## Checks
| id | check |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **R1** | ★ **THE GUARD CAN FAIL.** RM-03 exists because the queue guard is ZERO-INFORMATION (D-23) — it returns pass for every possible input. Prove the rebased head's guard **actually fails** on asserted non-readiness, not merely that its tests pass. This is the whole deliverable. |
| **R2** | ★ **NO QUEUE-GUARD SEMANTIC MOVED IN THE REBASE.** Confirm the range-diff equivalence independently. A queue-guard semantic resolved quietly re-breaks the thing this PR fixes. |
| **R3** | **Union is complete in BOTH directions:** no RM-03 suite lost to main's version, no main suite (esp. `test-terminal-green-contract.sh`) lost to RM-03's. Enumeration count reconciles. |
| **R4** | **Tri-state is real** (`verified` / `written-unverified` / `failed`) — P-WRAPPER-001. Verify an indeterminate result cannot present as a pass; that is the exact defect (`state=unknown exit 0`). |
| **R5** | The guard's **exit-asserting non-null-case** tests are genuine — each fails for its own stated reason against pre-fix code, not merely present (D-8). |
| **R6** | `test:framework-shell` is **runnable to completion in CI** (canonical env). Known: it exits 97 on some hosts via a Bash 5.2.15 `BASH_LINENO` assertion — if you hit that, report it as an environment limitation, do not treat it as a finding and do not let it mask one. |
| **R7** | Exact-head CI terminal-green, full `-f json`, counts stated incl. `clone`; machine-checked with `--expect-commit 868b9b871a5118762aa5b8aafecd2f0592f7ab5c`. |
| **R8** | `Closes #1019` present in the body (D-38c delivery-issue rule). |
| **R9** | Nothing weakened by the rebase: no test, case, or assertion removed or relaxed to make it land. |
## Reviewer instruction
Verdict bound to `868b9b871a5118762aa5b8aafecd2f0592f7ab5c`, evidence enumerated, posted durably under
your own identity. If a check is unrunnable, **say so**.
**R1 is the deliverable and R2 is the risk.** Attack outside this set. **Do not cite the queue guard's
own green as evidence of anything** — it remains inert until this very PR lands (D-23).
**No one dispatching this review may state a conclusion on an open check (D-39).**
@@ -1,40 +0,0 @@
# RM-61 / PR #1033 — PRE-REGISTERED RE-REVIEW ACCEPTANCE CHECKS
**Subject head (exact):** `033b2ffb46674b2c0bcc5197273c109b461f62d9`
**Supersedes:** review 67 / comment 20403 @ `e7b29219` (NO GO). That verdict is VOID — the head moved.
**Registered by:** `mos-remediation` (orchestrator). **Reviewer:** `rev-974` (author ≠ reviewer).
**Registered BEFORE the reviewer read the diff.** Any head move after this file is committed voids the
re-review and requires re-registration.
## Scope discipline
The prior review passed AC1AC8 and still found a blocker, because the registered set tested whether the
signature DISCRIMINATES and never tested whether the evidence was BOUND TO ITS SUBJECT (Finding 3 /
coverage-failure-mode-2, D-17 class). This set therefore carries the binding property as a first-class
check, and **RR7 explicitly invites the reviewer to attack outside the set** — a registered set is
protection against retrofitting only, never a ceiling on scrutiny.
## Checks
| id | check | verdict form |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **RR1** | The full 12-case contract harness passes at the exact head: `bash test-terminal-green-contract.sh` | ⇒0, and states 12 cases |
| **RR2** | ★CRUX — the ORIGINAL ATTACK IS DEAD. Take the real `#2188` record, mutate ONLY `commit` to an unrelated 40-hex value, verify against the true expected head | ⇒1, `exempted_steps == 0`, anomaly naming expected vs actual |
| **RR3** | Missing `--expect-commit` cannot be defaulted, inferred, or skipped | ⇒2 (not 0, not 1) |
| **RR4** | Malformed expected commit (short SHA, non-hex, empty) is rejected — no silent normalisation into a pass | ⇒2 |
| **RR5** | A record with the `commit` key ABSENT (not merely different) is rejected — fail-closed on missing, not just on mismatch | ⇒1, `exempted_steps == 0` |
| **RR6** | The genuine artifact still passes when correctly bound: real `#2188` + its true head | ⇒0, `exempted_steps == 1`, exactly one `WP-K8S-1000-CI-POSTGRES-TEARDOWN` |
| **RR7** | ★RED-FIRST, PROVED RETROACTIVELY. The four new cases must FAIL against the OLD verifier at `e7b29219` — otherwise they do not test what they claim (D-8 class). Run the new cases against the previous implementation | new cases ⇒≠0 under `e7b29219` |
| **RR8** | AC2 OF THE PRIOR SET DID NOT REGRESS: both REAL controls stay terminal red — `#2189` (`ci-postgres` exit 1) and `#2191` (exit 137, `test` exit 61) | both ⇒1, `exempted_steps == 0` |
| **RR9** | Still NO fetch / trigger / retry / re-roll / sleep / network of any kind in the verifier or harness. The coin flip must remain removed, not codified (D-21) | grep ⇒ no such call sites |
| **RR10** | The doc/baseline changes REQUIRE the current provider PR head to be passed — they must not merely mention it. Check `merge-gate.md`, `CI-CD-PIPELINES.md`, `woodpecker/README.md` state it as a requirement a gate operator cannot satisfy by omission | reviewer judgement, quote the lines |
| **RR11** | Exemption remains bound to #1000 and retires with it; signature conjunction unchanged and not widened by this fix | diff-scoped, ⇒ no widening |
| **RR12** | Case-sensitivity: an uppercase-hex record commit against a lowercase expected head must NOT silently pass by accident of comparison. State which way it resolves and whether it fails closed | state the observed behaviour |
## Reviewer instruction
Report the verdict **bound to `033b2ffb46674b2c0bcc5197273c109b461f62d9`** and state each check's
observed result, including counts read from `pipeline-status.sh -f json` (never default text — it omits
`clone`, D-33). If any check is unrunnable, **say so** — never substitute a passing variant. Attack
outside this set and report anything it finds; RR7 and RR12 exist because the last blocker was found
exactly that way.
-60
View File
@@ -1,60 +0,0 @@
# RM-61 / PR #1033 — PRE-REGISTERED ACCEPTANCE CHECKS: `exit_code` TYPE-STRICTNESS (D-40)
**Registered by:** `mos-remediation` (orchestrator) — **BEFORE the fix was pushed and before any
reviewer read a diff.** At registration time the fix existed only as an unpushed local commit
(`6e7a336d`) on coder-mos1's machine which **I have not read**. There is therefore no diff for these
checks to have been retrofitted to.
**Subject head:** TBD — binds to the NEW head once coder-mos1 pushes. The prior head
`033b2ffb46674b2c0bcc5197273c109b461f62d9` and its review 69 / pipeline #2193 go VOID on that push;
that cost was accepted deliberately by Mos's BLOCKING ruling on D-40.
**Ruling being enforced (Mos, 2026-08-01):** `exit_code` must be accepted ONLY as a real integer.
`false`, `0.0`, `"0"`, and `null` must all fail to satisfy the exemption. Wrong-ACCEPT is the
disqualifying direction; this hole sits inside the load-bearing discriminator.
## ⚠ Read this before writing the tests — RED-FIRST HERE IS NOT UNIFORM
Two of the four near-miss values **already block** at `033b2ffb`. Demanding "all four observed RED
first" would be demanding an impossible red for two of them, and the predictable response to an
impossible demand is a fudge — weakening something real to manufacture the red. So state it precisely:
| value | behaviour at `033b2ffb` (pre-fix) | what the new case is |
| ------- | --------------------------------- | ------------------------- |
| `false` | **WRONGLY EXEMPTS** (exit 0) | **genuine RED-FIRST** |
| `0.0` | **WRONGLY EXEMPTS** (exit 0) | **genuine RED-FIRST** |
| `"0"` | correctly blocks (exit 1) | **regression guard** only |
| `null` | correctly blocks (exit 1) | **regression guard** only |
Claiming red-first for `"0"` or `null` would be a false claim about your own evidence. Say which is
which. **Do not weaken anything to make a green case go red** (D-8).
## Checks
| id | check | verdict form |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| **TS1** | ★RED-FIRST. `exit_code: false` on the real #2188 record is **observed wrongly exempting at `033b2ffb`** (exit 0, `exempted_steps 1`), then blocks after the fix | pre-fix ⇒0/exempt 1 · post-fix ⇒1/exempt 0 |
| **TS2** | ★RED-FIRST. Same for `exit_code: 0.0` | pre-fix ⇒0/exempt 1 · post-fix ⇒1/exempt 0 |
| **TS3** | REGRESSION GUARD. `exit_code: "0"` blocked before AND after — state honestly that it was already green | both ⇒1, `exempted_steps 0` |
| **TS4** | REGRESSION GUARD. `exit_code: null` blocked before AND after | both ⇒1, `exempted_steps 0` |
| **TS5** | ★NOT OVER-TIGHTENED. The genuine artifact — real #2188, real integer `exit_code: 0`, correctly bound head — still passes | ⇒0, `exempted_steps 1`, exactly one `WP-K8S-1000-CI-POSTGRES-TEARDOWN` |
| **TS6** | The strictness sits on the value the EXEMPTION rests on, not on a cosmetic sibling. Show the guarded comparison is the one feeding `exemption_applies` | reviewer quotes the line |
| **TS7** | `bool` is excluded EXPLICITLY, not incidentally. A bare `isinstance(x, int)` still admits `True`/`False` — verify the `not isinstance(x, bool)` clause exists | ⇒ clause present; `true` also blocks |
| **TS8** | Negative control unaffected: a genuine failed step with a real integer non-zero exit still blocks | ⇒1, `exempted_steps 0` |
| **TS9** | NO REGRESSION ON THE PRIOR ROUND'S BINDING WORK: mutated record commit ⇒1; `--expect-commit` omitted ⇒2; record `commit` absent ⇒1 | ⇒1 / ⇒2 / ⇒1 |
| **TS10** | Signature conjunction NOT widened elsewhere by this fix — `POD_NOT_FOUND` / name / type / state untouched | diff-scoped ⇒ no widening |
| **TS11** | Still no fetch / trigger / retry / re-roll / sleep / network | grep ⇒ no call sites |
| **TS12** | Full harness passes at the new head; **state the case count** (12 previously, expected 16 — confirm the actual number rather than the expected one) | ⇒0, count stated |
## Reviewer instruction
Verdict bound to the NEW head, evidence enumerated per check, CI counts from `pipeline-status.sh -f json`
(never default text — it omits `clone`, D-33). If a check is unrunnable, **say so**; never substitute a
passing variant.
**Attack outside this set and report what you find.** Both blockers on this PR so far — the missing
commit binding, and this type confusion — were found outside the registered set, by mutating a field
nobody had registered a check for. That is now the expectation, not a bonus.
**Nobody dispatching this review may state a conclusion on an open check here (D-39).** If you are sent
an observation, it is an observation; the ruling is yours.
-65
View File
@@ -23,68 +23,3 @@ Merged PR #868 (`b79336a8`) shipped a file that FAILS `pnpm format:check` ⇒ th
### **D-4 / P-LIFECYCLE + hygiene — a dispatched agent silently IGNORED an in-message context reset.**
planner-sol was at 64.3%/372k; the brief asked it to reset first; it began work on dirty context anyway. Only an out-of-band `/new` driven by the orchestrator guaranteed clean state. Confirms the postmortem thesis: **instructions are not enforcement.** Reset must be a mechanical pre-dispatch step, not a request.
<!-- board-roll: 2 entries rolled from BOARD.md -->
### **D-7 / P-FLEET-001 — stale-GC-on-disk: shared 30G /tmp hit 100% ENOSPC, degrading two seats.**
~5.2G was session scratch dead 8-9 days (this session's own footprint: 88K). Same missing capability as orphaned-tmux-session GC, applied to disk — not a quota or discipline problem. Resolved manually by Mos (lead coordinator) after independent verification; `/tmp` now 79%. **The gap IS the finding:** the authority to reap exists, the deterministic reaper does not. Folded into RM-50 with explicit requirements (mechanical liveness, age threshold, dry-run, audit event per reap — never a heuristic sweep). Refusing to unilaterally delete another session's scratch was correct doctrine; the fix is a reaper, not braver agents.
### **D-6 / P-QUEUE-001 — the mandated queue guard returned PASS on an UNKNOWN state, live, today.**
Running the required `ci-queue-wait.sh --purpose push` before pushing produced `state=unknown ... exit 0` — the exact defect at `ci-queue-wait.sh:282-288` that PR #1023 is parked on. It also evaluated `branch=main` rather than the branch being pushed. The mission's own required pre-push gate passed me on an indeterminate result. Third independent live instance of the class.
<!-- board-roll: 1 entry rolled from BOARD.md -->
### **D-8 / P-CONFORMANCE-001 — a PRE-REGISTERED acceptance check that was not runnable as written.**
PR #1025 AC2's fixture `mkdir -p apps/*/venv/lib` creates a literal `apps/*/venv/lib` dir when the glob is unmatched — it did not test what it claimed. rev-974 ran it exactly as written, caught it, re-ran the intended assertion at an explicit path, and **disclosed** rather than silently substituting a working fixture and reporting PASS. **Pre-registration protects a check from being retrofitted to the implementation; it does not make the check correct.** An unverified gate appeared inside the mechanism built to catch unverified gates. Hard requirement on RM-02: the registry must self-verify that every registered case runs AND can fail — presence is not evidence.
<!-- board-roll: Decisions-log narrative rolled from BOARD.md 2026-08-01 (D-43: meet the
byte budget by ROLLING OUT, never by rewording what stays) -->
### Decisions log — full record in [`TASKS.md`](./TASKS.md)
All 44 findings (D-1…D-6 in `BOARD-LEDGER.md`, D-7…D-43 + D-38c in `TASKS.md`) and every ruling with its
rationale live there. **Not duplicated here** — a second copy is a second thing to go stale, which this
board had done three times in one night (gate list, capability registry, DECISION-1 status), and three
more times by the next rotation seam (RM-61 "building", "nothing implemented yet", DECISION-1/2/3
"must be ruled"). The rulings a fresh seat needs are items 47 above; they are **not** repeated here,
because that repetition is what went stale.
<!-- board-roll: Sequencing section rolled verbatim from BOARD.md 2026-08-01 (D-43: meet the
byte budget by ROLLING OUT, never by rewording what stays). Canonical: MISSION.md + TASKS.md §5 -->
### Sequencing — see [`MISSION.md`](./MISSION.md)
Builds 1-5, the cross-cutting retirements, and DECISION-1's corrected wire-in target are stated once in
the charter and `TASKS.md` §5. **Not repeated here** — the previous copy of DECISION-1's status on this
board is one of the six stale restatements below.
<!-- board-roll: capability/seat-identity bullets rolled verbatim from BOARD.md 2026-08-01
(D-43: roll out, never reword). Authoritative home: TASKS.md D-11 / D-11a / D-11b. -->
- Capability is **per-path** (D-11b → **superseded in part by D-13/D-15**): a token file is **necessary,
not sufficient**. Three layers — token file (raw-API), `tea` login (tea paths), **repository permission**
(writes). Before dispatch, assert `permissions.push == true` **as that seat**, not token existence and
not a 200 on a read. Mos owns provisioning; escalate missing pairs.
- Seat identity (D-11a): token identity AND `git config user.name`/`user.email` must BOTH be set and
agree. Exporting `MOSAIC_GIT_IDENTITY` alone does NOT fix commit authorship.
<!-- board-roll: D-26 gate-restatement rationale rolled verbatim from BOARD.md 2026-08-01 -->
### Why the board must not restate the delivery gates (D-26)
Restating the gate from memory is how the merge-gate step went missing from mission setup twice,
once inside the correction for it (**D-26**).
<!-- board-roll: Fleet seats rolled verbatim from BOARD.md 2026-08-01 (D-43: roll out, never
reword). NOTE: all these seats are UNMANAGED per D-41; `mosaic fleet ps` is live truth. -->
## Fleet seats
- mos-remediation — project orchestrator (Claude, /src/mosaic-stack, socket `mosaic-fleet`) — ACTIVE
- planner-opus — adversarial planner (robustness), Opus 5, socket `default` — DELIVERED, idle
- planner-sol — adversarial planner (pragmatic), gpt-5.6-sol, socket `default` — DELIVERED, idle
- rev-974 — mosaicstack reviewer identity (id 16, write:repository) — idle, on call
- Mos (mos-claude) — lead coordinator, socket `default` — relay path to Jason
+73 -63
View File
@@ -1,83 +1,93 @@
# mos-remediation — LIVE BOARD (keep < 8 KB)
**Phase:** EXECUTING — RM-03 at owner-merge; RM-61 **MERGED**; **RM-02 keystone is the front**.
**Updated:** 2026-08-01 — seam crossed; successor seat resumed, attested from the files, and is driving.
⚠ That was a **MANUAL pane respawn** (prior seat ~803k tokens): it validates the checkpoint+rehydration
**design**, NOT a lifecycle **mechanism** — P-LIFECYCLE rotation does not exist yet (**D-41 / RM-62**).
**Phase:** EXECUTING — P0 open. RM-01 MERGED; RM-02 (keystone gate registry) is next.
**Updated:** 2026-07-31 (mos-remediation orchestrator; seat active on `mosaic-fleet`).
## Head
- Charter + 15 decisions + 4-build plan: `MISSION.md`. Backlog + all findings: `TASKS.md`.
- Planning DONE (58 tasks, P0P5). **DECISION-1/2/3 all RULED by Mos 2026-07-31** (`TASKS.md` §5) —
nothing is waiting on a decision. D-2's availability _target_ is Jason-pending and non-blocking.
- **Executing, not planning.** RM-01 is MERGED; three lanes are live (see In-flight).
- Orchestrator seat `mos-remediation` LIVE, owns the mission, resumed across the rotation seam
2026-08-01 and re-attested to Mos from the files. Residency attestation: PASS.
- Mission charter + 15 decisions + 4-build plan: PERSISTED (`docs/remediation/MISSION.md`).
- HOLD lifted for this workstream (Jason 2026-07-31). Nothing implemented yet — planning first.
- Orchestrator seat `mos-remediation` is LIVE and owns the mission. Residency attestation: PASS.
- **TASK-0 DONE** — checkout repaired, all three gates green HONESTLY (no `--no-verify`), branch pushed.
- **TASK-1 DONE** — both planners delivered independently on clean context; reconciled into `TASKS.md`
(58 tasks across P0P5, 7 convergences, 7 adjudicated disagreements, 3 escalated decisions).
- **NEXT ACTION IS NOT MINE:** DECISION-1/2/3 (`TASKS.md` §5) must be ruled before P0 dispatch.
RM-01 is dispatchable immediately regardless — it depends on nothing and blocks everything.
## In-flight
| Task | Owner | State |
| ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| RM-01 checkout | — | **MERGED** `f58b3699` (#1027) |
| RM-03 queue guard | Jason (re-sync) | ✅ **MERGED** `58b971ab` (#1032), #1019 closed. ⚠ **NOT DELIVERED**: installed guard still the broken one (291 lines / 0 `ASSERTED_NOT_READY` vs main 482 / 5). Re-sync via `mosaic upgrade`, then **prove it blocks a KNOWN-RED pipeline****D-51** |
| RM-02 registry ★key | rev-974 | **RE-REVIEW @ `e910a45a`** — (d)-strict history REMOVED + blocker 2 NARROWED (D-52) + blocker 3 + renderer. Overclaim control **fires at exit 84**, verified by orchestrator. CI 2201 **10/10**. ACs @ `dde38717` |
| RM-61 CI exemption | — | ✅ **MERGED** `f4fd5967` (#1033). #1034 closed; **#1000 stays OPEN** (retirement trigger). Exemption is on `main` |
| RM-59 / RM-60 | Jason (infra) | tracked deps; RM-60 option **B** |
| #1023 queue attempt | Jason | SUPERSEDED-PENDING-JASON — live REQUEST_CHANGES, do **not** merge |
### For the incoming orchestrator — read this before acting
1. **Lane state lives in the In-flight table above — this item does NOT restate it.** It went stale
three times in one session by duplicating that table (D-26's class). Read the table. ⚠ **And
re-derive any board claim from the provider before load-bearing use (D-43)** — the board is
sole-written and has no independent verifier.
2. **`docs/remediation/TASKS.md` is authoritative**, not the newest voice in a chat. It holds 53 findings
(D-1…D-6 in `BOARD-LEDGER.md`, D-7…D-52 + D-38c in TASKS.md), every ruling with its rationale, and the
requirements each finding placed on RM-02/RM-34/RM-50/RM-55.
3. **`MISSION.md` carries the first-class principles** — read them there, they are not listed here.
Two added 2026-08-01: **the anchor must live outside the audited party's authority** (D-19/D-25/D-45,
third arrival) and **no universally-quantified check may pass over an empty set** (D-44/D-46).
4. **Seat identity:** `export MOSAIC_GIT_IDENTITY=<seat>` is stripped by a context reset (**D-34**) —
every dispatch/rehydration brief must re-export it, or the seat cannot use its credentials.
5. **Scan CI from `-f json`, never default text** — text mode omits `clone` (**D-33**). State counts.
6. **The queue guard is zero-information until RM-03 merges** (**D-23**) — never cite its green.
7. **The bounded CI re-roll used on RM-02 was a one-time stopgap, NOT policy.** A per-PR free re-roll is
D-21 normalisation. Do not repeat it; RM-61 is the fix.
## Delivery gates — REFERENCE, do not restate
Canonical: `~/.config/mosaic/fleet/roles.local/merge-gate.md` (verdict authority) +
`~/.config/mosaic/fleet/roles/validator.md`. Order and the freeze/zero-information rules: `MISSION.md`
and `KICKSTART.md`. **Read them there** (why: **D-26**, in `BOARD-LEDGER.md`).
| Task | Owner | State |
| ----------------------------------- | --------------- | ------------------------------------------------------------------------- |
| RM-01 reproducible checkout | — | **MERGED** `f58b3699` (PR #1027) — rev-974 APPROVE + CI #2172 8/8 green |
| RM-02 gate registry ★keystone | unassigned | **READY** — depends only on RM-01; not held by RM-03 |
| RM-03 queue guard (3 defects) | — | HOLD — #1023 SUPERSEDED-PENDING-JASON |
| RM-59 close D-19 residual risk | — | BLOCKED by RM-12/RM-21/RM-25 (spine + executor) — tracked edge, not prose |
| `remediation/state` snapshot → main | mos-remediation | opening at this mission seam |
## Fleet seats
Roster rolled verbatim to [`BOARD-LEDGER.md`](./BOARD-LEDGER.md); live truth is `mosaic fleet ps`.
- mos-remediation — project orchestrator (Claude, /src/mosaic-stack, socket `mosaic-fleet`) — ACTIVE
- planner-opus — adversarial planner (robustness), Opus 5, socket `default` — DELIVERED, idle
- planner-sol — adversarial planner (pragmatic), gpt-5.6-sol, socket `default` — DELIVERED, idle
- rev-974 — mosaicstack reviewer identity (id 16, write:repository) — idle, on call
- Mos (mos-claude) — lead coordinator, socket `default` — relay path to Jason
## Gate status
- Delivery gates active: author≠reviewer, diff-blind pre-registered checks, CI-green, merged-PR completion.
- Freeze: LIFTED for this workstream only.
- Git identity: orchestrator runs `MOSAIC_GIT_IDENTITY=mos-dt-0` INTERIM. Mos ruled gate-16 HOLDS (author≠reviewer is what
- Git identity: `MOSAIC_GIT_IDENTITY=mos-dt-0` INTERIM. Mos ruled gate-16 HOLDS (author≠reviewer is what
gate-16 protects; rev-974 reviews, mos-dt-0 never self-reviews). Dedicated identity TRACKED, Mos provisions.
- Capability + seat identity (**D-11b / D-11a**, incl. the false-NEGATIVE twin): authoritative in
`TASKS.md`. Short form — **assert the DIFFERENTIAL as that seat** (authenticated `push:true` vs
unauthenticated `push:false`); a single endpoint can be true for anyone or 403 for an unrelated
scope. Full text rolled to [`BOARD-LEDGER.md`](./BOARD-LEDGER.md).
-**LIVE HAZARD (D-37) — one shared `.git/config` re-identifies EVERY worktree at once.** Every seat,
including `rev-974`'s review worktree, currently authors as **`coder-mos1`**; `MOSAIC_GIT_IDENTITY`
does **not** override it. **STANDING ORDER: commit with explicit
`git -c user.name=<seat> -c user.email=<seat>@…`, and NOBODY rewrites the shared config mid-flight.**
Real fix authorised, Mos owns it, sequenced at a quiet seam. **#1024 implicated.** Detail: D-37.
- Standing worker-brief doctrine (mandatory in EVERY brief): re-export `MOSAIC_GIT_IDENTITY` (**D-34**);
commit early/WIP (**D-31**); don't weaken a RED test to pass; if a check is unrunnable SAY SO, never
substitute; `agent-send -f` never `-m`; artifacts off shared `/tmp`; scan CI from `-f json` (**D-33**);
**relay observations into an open review, NEVER your own conclusion on an open check (D-39)**; the
**author never adjudicates their own PR's blocker status** — surface evidence, prepare the fix, hold.
- Capability check (D-11b): before dispatching seat X to provider Y, verify
`~/.config/mosaic/secrets/gitea-tokens/gitea-<Y>-<X>.token` exists. Token-file set = authoritative
capability registry. Mos owns provisioning; escalate missing pairs to him.
- Seat identity (D-11a): token identity AND `git config user.name`/`user.email` must BOTH be set and
agree. Exporting `MOSAIC_GIT_IDENTITY` alone does NOT fix commit authorship.
- Standing worker-brief doctrine (accreted, mandatory in every brief): don't weaken a RED test to make
it pass; if a check is unrunnable as written SAY SO, never silently substitute; `agent-send -f` never
`-m`; heavy artifacts off shared `/tmp`.
- Remote control: native `/remote-control` NOT wired in this runtime. Path is **Mos-relay**
(Jason ↔ mos-claude via Discord ↔ mos-remediation via agent-send). Not a blocker.
## Decisions log — full record in [`TASKS.md`](./TASKS.md)
## Sequencing (from MISSION.md)
All 53 findings (D-1…D-6 in `BOARD-LEDGER.md`, D-7…D-52 + D-38c in `TASKS.md`) and every ruling with
its rationale live there. **Not duplicated here.** The history of _why_ this board must not restate —
six stale copies across two seams — is rolled verbatim into [`BOARD-LEDGER.md`](./BOARD-LEDGER.md).
1. Spine + choke-point service (MACP wiring @ mosaic_orchestrator.py::run_single_task) + PG/Redis
**CONTESTED — see DECISION-1.** Both planners independently reject this wire-in point: that
controller is `"enabled": false` and references a dispatcher that does not exist here. Charter text
left UNCHANGED pending Mos/Jason ruling; do not treat it as settled.
2. Rotation daemon (finish Mission Control Plane, reuse packages/coord)
3. Comms service (envelope→service→PG/Redis→adapters)
4. Hygiene + conformance harness
Cross-cutting retirements: flat-file tracking, 3 MACP islands, silent MOSAIC BYPASS.
## Dogfood evidence — live failure classes, not hypotheticals
> Newest first. Oldest entries roll to `BOARD-LEDGER.md` via `board-roll.sh` when this file
> exceeds its 8 KB cap. Keystone detail is duplicated in `TASKS.md` §1a, so rolling loses nothing.
<!-- BOARD-ROLL:START -->
### **D-8 / P-CONFORMANCE-001 — a PRE-REGISTERED acceptance check that was not runnable as written.**
PR #1025 AC2's fixture `mkdir -p apps/*/venv/lib` creates a literal `apps/*/venv/lib` dir when the glob is unmatched — it did not test what it claimed. rev-974 ran it exactly as written, caught it, re-ran the intended assertion at an explicit path, and **disclosed** rather than silently substituting a working fixture and reporting PASS. **Pre-registration protects a check from being retrofitted to the implementation; it does not make the check correct.** An unverified gate appeared inside the mechanism built to catch unverified gates. Hard requirement on RM-02: the registry must self-verify that every registered case runs AND can fail — presence is not evidence.
### **D-7 / P-FLEET-001 — stale-GC-on-disk: shared 30G /tmp hit 100% ENOSPC, degrading two seats.**
~5.2G was session scratch dead 8-9 days (this session's own footprint: 88K). Same missing capability as orphaned-tmux-session GC, applied to disk — not a quota or discipline problem. Resolved manually by Mos (lead coordinator) after independent verification; `/tmp` now 79%. **The gap IS the finding:** the authority to reap exists, the deterministic reaper does not. Folded into RM-50 with explicit requirements (mechanical liveness, age threshold, dry-run, audit event per reap — never a heuristic sweep). Refusing to unilaterally delete another session's scratch was correct doctrine; the fix is a reaper, not braver agents.
### **D-6 / P-QUEUE-001 — the mandated queue guard returned PASS on an UNKNOWN state, live, today.**
Running the required `ci-queue-wait.sh --purpose push` before pushing produced `state=unknown ... exit 0` — the exact defect at `ci-queue-wait.sh:282-288` that PR #1023 is parked on. It also evaluated `branch=main` rather than the branch being pushed. The mission's own required pre-push gate passed me on an indeterminate result. Third independent live instance of the class.
<!-- BOARD-ROLL:END -->
## Decisions log
- 2026-07-31 — Mission set up by Mos post-postmortem (15/15 decided). Dogfood posture active.
- 2026-07-31 — Mos: stale `.mosaic/orchestrator/mission.json` is RESIDUE of the disabled Python
orchestrator rail that this plan RETIRES. Do NOT invest in it; do NOT build on that rail. The 0/0
milestone banner is cosmetic. (Supersedes any plan to repair it.)
- 2026-07-31 — Mos: planners must be dispatched with GUARANTEED clean context, not requested-clean.
Prior default-socket planner sessions predate this mission; dirty context is the indicted hygiene.
- 2026-07-31 — mos-remediation: worker briefs forbid all git ops and restrict each worker to a single
named output file, so two planners can share one checkout without a branch race (M2-era incident doctrine).
+40
View File
@@ -0,0 +1,40 @@
# RM-02 Governing Claim Index
This index binds remediation claims that live in orchestrator-owned `TASKS.md` without modifying that file. The source heading and quoted text are the reviewable anchor; `gate:verify` enforces each marker-to-criterion binding. Marker completeness beyond this approved slice remains RM-54.
## Prose is an enforceable claim
<!-- GATE-CLAIM:PROSE-IS-A-CLAIM -->
- Source: `docs/remediation/TASKS.md`, heading `D-20 — the orchestrator's own documentation overclaimed, and a reviewer disproved it empirically`.
- Anchored text: “The defect was not in the code — it was in this file.”
## Generated-state verification scope
<!-- GATE-CLAIM:GENERATED-STATE-SCOPE -->
- Source: `docs/remediation/TASKS.md`, heading `D-19 — an integrity property that cannot exist at the layer it was specified`.
- Anchored text: “a verifier that cannot detect the attack is not a verifier.”
## Execution trust boundary
<!-- GATE-CLAIM:EXECUTION-TRUST-BOUNDARY -->
- Source: RM-02 ruling recorded under `docs/remediation/TASKS.md`, RM-02 clause 4, D-25.
- Anchored text: “SELF-VERIFICATION BY THE AUDITED PARTY IS NOT VERIFICATION.”
- Dependency: RM-60/#1031, cross-referenced with RM-59.
## Same-checkout inventory drift boundary
<!-- GATE-CLAIM:INVENTORY-DRIFT-BOUNDARY -->
- Source: RM-02 ruling after D-48/CWE-353 reproduced against the round-4 baseline.
- Boundary: Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.
- Negative control: `checkout-preflight/inventory-claim-overstatement` rewrites the boundary as protection and must go red.
## Criterion restatement provenance
<!-- GATE-CLAIM:CRITERION-RESTATEMENT -->
- Source: `docs/remediation/TASKS.md`, heading `D-18 — two pre-registered criteria were mutually unsatisfiable, discoverable only at implementation`.
- Anchored text: “Both criteria were pre-registered. They cannot both be satisfied.”
+2 -12
View File
@@ -25,18 +25,8 @@ mechanically until Build 3 (rotation) makes it automatic.
## Standing invariants (never violate)
- **North star:** deterministic-right-answer → code/gate; LLM only for judgment.
- **Delivery gates — REFERENCE the canonical files, never restate them:**
`~/.config/mosaic/fleet/roles.local/merge-gate.md` (verdict authority) and
`~/.config/mosaic/fleet/roles/validator.md` (validator role). Order:
independent review (author ≠ reviewer, `rev-974`; pre-registered diff-blind checks committed before the
diff is read) → remediation → **CI terminal-green at the exact head by full step scan**
**merge-gate verdict `GO`/`NO-GO`/`HOLD`**, commit-bound and **void the instant the head moves**, posted
durably with enumerated evidence under its own minted identity → **coordinator head-pinned merge**.
The queue guard runs but is **zero-information until RM-03 lands** (D-23) and must not be cited as evidence.
**After a `GO`, freeze pushes** — even a doc tweak voids the verdict. The coordinator assigns the gate seat.
- **Query for refutation, never for confirmation.** A subordinate asked to confirm a hypothesis will
agree — the bias is in the question, not the answerer, and agent seats are agreeable by construction.
State the hypothesis as yours, ask for the evidence that KILLS it, and reproduce when it matters.
- **Delivery gates:** author≠reviewer; PRE-REGISTERED diff-blind checks committed before reading the diff;
CI terminal-green; completion = merged PR + closed issue. rev-974 = the mosaicstack reviewer identity.
- **Dogfooding:** every fix validated against its live seed case (MISSION.md lists them).
- **Tracking → DB** (hard cutover); do NOT re-invest in flat-file tracking. jarvis-brain PDA is off-limits.
- **Git identity:** export `MOSAIC_GIT_IDENTITY=<your-seat>` so wrappers author correctly and survive respawn.
+10 -143
View File
@@ -12,6 +12,8 @@ gate/program; the LLM handles only genuine judgment.
### First-class principle — observe the property, not the exit code
<!-- GATE-CLAIM:OBSERVE-PROPERTY -->
> **No write is done until the requested PROPERTY is observed. A success exit code is not evidence.**
>
> **Success output is designed to be believed.** That is the whole reason the inert-gate class exists
@@ -30,6 +32,8 @@ gate/program; the LLM handles only genuine judgment.
### First-class principle — pre-registration prevents retrofitting, and nothing else
<!-- GATE-CLAIM:PREREGISTRATION-BOUNDARY -->
> **A pre-registered check set can fail in three distinct ways:**
>
> | mode | the set is… | found as |
@@ -56,6 +60,8 @@ gate/program; the LLM handles only genuine judgment.
### Corollary — never ship an integrity claim dressed as a property
<!-- GATE-CLAIM:ARTIFACT-INTEGRITY-BOUNDARY -->
> A verification artifact that can be forged by whoever it is meant to catch verifies nothing. If a
> manifest, marker, ledger, or receipt is writable by the same actor whose behaviour it certifies, it
> **certifies the attack.** Such an artifact must sit inside the integrity envelope it belongs to,
@@ -68,6 +74,8 @@ gate/program; the LLM handles only genuine judgment.
### First-class principle — when a property cannot exist at the layer it was specified
<!-- GATE-CLAIM:IMPOSSIBLE-LAYER-BOUNDARY -->
> Some required properties are **impossible at the layer that asked for them** — not hard, impossible.
> A local check cannot defend against an actor who can rewrite the check itself. When that happens,
> there are exactly three honest moves, and all three are mandatory:
@@ -91,99 +99,6 @@ gate/program; the LLM handles only genuine judgment.
> seat in the loop. Residual risk bound to **RM-59** (`depends_on: RM-12, RM-21, RM-25`), where the
> choke-point executor and spine verify from _outside_ the worktree's authority.
### First-class principle — query for refutation, never for confirmation
> **A subordinate asked to confirm a hypothesis will agree. Ask it to refute, with evidence.**
>
> The bias is induced by the **query**, not by the answerer's diligence. _"The DB flaked — please
> confirm"_ and _"confirm or refute this, with the log line that proves it"_ are different instruments,
> and they return different answers to the same question. The first harvests agreement; only the second
> can return **"you are wrong, and here is why."**
>
> This matters most with agent subordinates, which are **agreeable by construction**: fluent, eager to
> be useful, and structurally disinclined to tell the dispatcher their premise is false. A confirmation
> query aimed at one is close to a guaranteed yes — so the discipline cannot rest on the answerer being
> rigorous. **It has to be built into how the question is asked.**
>
> Promoted to the charter by Mos (2026-08-01). Origin: the orchestrator hypothesised that a coincident
> `ci-postgres` failure caused a CI test failure and asked the implementing seat to **confirm or refute**
> it. The seat **refuted it** with the log (`ci-postgres:5432 - accepting connections`, migrations
> completed) and identified the real cause. Reproduction on an identical head then settled it. Had the
> query been phrased for confirmation, the agreement would have been returned, **D-21 would have been
> re-classified on a false premise**, and a banked finding would have been silently corrupted.
>
> **Operationally:** state your hypothesis explicitly, mark it as yours, ask for _evidence that kills
> it_, and say what evidence would change your mind. A hypothesis you cannot describe how to falsify is
> not yet a hypothesis. Where the answer is consequential, **reproduce** rather than accept — two
> independent runs beat one confident report.
### First-class principle — the anchor must live outside the audited party's authority
> **You cannot fix "the author controls X" by deriving X from something the author ALSO controls.**
> Deriving merely **moves** the control point; it does not remove it.
>
> Promoted to the charter by Mos (2026-08-01) on the **THIRD INDEPENDENT ARRIVAL** of the same
> conclusion, each reached while trying to ship something else, each from a different direction:
>
> | arrival | the audited party controls… | found as |
> | ----------------- | ------------------------------------------------------------------- | -------- |
> | manifest | the tree that certifies its own generated state (same-UID, CWE-345) | **D-19** |
> | sandbox | the code that enters the sandbox, before the boundary exists | **D-25** |
> | **registry seam** | **WHEN the tracked path is introduced, hence the derived boundary** | **D-45** |
>
> Round 1 the value was an author-settable **field**, so it was **derived**. Round 2 the derivation
> depended on **when the author introduces the path**. Same authority, new costume. **Three
> impossibility-derivations of one conclusion is not a coincidence to note — it is the strongest
> architectural evidence this mission has produced**, and it is precisely what **forces Builds 12**
> rather than making them a preference.
>
> **Operationally:** anchor to a reference the audited party cannot move. A git **merge-base against
> `main`** is such a reference for a PR author (they own their branch; they do not own `main`).
> **State the bootstrap in BOTH directions (D-19):** that anchor is sound against an author who cannot
> rewrite `main` — the threat in scope — and **NOT** sound against an attacker who can. **That residual
> is what Builds 12 close, and it must be recorded as a tracked dependency, never implied.**
### First-class principle — no universally-quantified check may pass over an empty set
> **"All registered cases ran" is VACUOUSLY TRUE when there are no registered cases.**
> **Non-emptiness and anchoring are PRECONDITIONS asserted before the quantified check runs — not
> properties hoped for after it.**
>
> Promoted by Mos (2026-08-01) after the identical vacuity appeared **twice, at two different levels**:
> `activationCommit = HEAD` emptied the **commit range** (D-44), and an emptied manifest — `criteria`,
> `gates`, `proseClaims`, `compatibilityScenarios` all `[]` — emptied the **registry population**
> (D-46), each yielding **exit 0**. The first was fixed **as an instance**; the principle was never
> extracted, **so it returned one level up.**
>
> **Delete every gate and every criterion TOGETHER and no remaining reference complains — because every
> reference went with them.** A check that quantifies over a population must actually **range** over it,
> and that population must be **provably complete and non-empty**.
>
> **Corollary (same disease):** a clause written for the instance that produced it is not a clause.
> **Do not relabel the originating instances as the general clauses** — quantify over the population.
### First-class principle — redundant observation on evidence-bearing steps
> **Two observers of the same evidence, disagreeing, catch what neither catches alone.** Apply redundancy
> not only to judgement calls but to **evidence gathering itself** — the step everyone assumes is
> mechanical and therefore skips.
>
> Promoted by Mos (2026-08-01) from **D-33**. A seat scanned a pipeline with `-f json` and reported 9
> steps; the orchestrator scanned the same pipeline in the wrapper's default text mode and reported 8.
> **The default output omits `clone`.** Every "full step scan" that night had been 8-of-9 and was stated
> as complete in good faith. No verdict changed — but the _method_ was wrong, invisibly, and **only the
> disagreement between two counts surfaced it.**
>
> The reason it survived: **a summary that resembles an enumeration is more dangerous than one that
> obviously summarises.** A labelled list of named steps with states _looks_ like the artifact, so nobody
> checks it against the record. Compare D-24 — `mergeable` was a _true answer to a different question_;
> this was a _true answer to a smaller one_. Neither is a lie; both pass every sniff test.
>
> **Operationally:** where a step _produces evidence a decision rests on_, have it produced twice by
> different means, and treat **any divergence as a finding rather than as noise to reconcile**. Prefer the
> machine-readable record over the human-readable rendering — _read the artifact, not the summary_ — and
> state the counts observed so a divergence is detectable at all.
## Decision record (authoritative, immutable)
- **15/15 proposals decided: 13 accept, 2 modify (P-AUTHORITY-001, P-INBOX-001), 0 reject.**
@@ -202,31 +117,6 @@ gate/program; the LLM handles only genuine judgment.
| **4. Comms service** | AUTHORITY, INBOX (+ versioning roadmap) | Envelope (comms/v1) → sole-path service → PG/Redis → pluggable adapters (tmux→Matrix/Discord/Slack/Telegram). Version the protocol, not participants. |
| **+ Hygiene & proof** | FLEET, WORKFLOW, CONFORMANCE | One roster-owned socket/host + stale GC; allowlist auto-sync; the conformance harness that fault-injects the failure classes and proves builds 14 hold. |
## Why Builds 12 are necessary — two independent impossibility proofs
**The choke-point executor and PG spine are not a design preference. They are forced.** Twice during
the mission's own first deliveries, work stopped against a security property that **cannot exist** at
the layer that needed it — and both times the only resolution was an authority _outside_ the audited
party's control, which is precisely what Builds 12 provide.
| | the audited party controls… | so what fails | found as |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------- | -------- |
| **Artifact integrity** | the manifest that certifies its own generated tree (same-UID write ⇒ regenerate manifest _and_ marker consistently, CWE-345) | tamper-evidence | **D-19** |
| **Execution integrity** | the code that enters the sandbox (PR-controlled config executes _before_ the boundary exists) | isolation | **D-25** |
Both reduce to one sentence:
> **Self-verification by the audited party is not verification.**
And to one consequence: **the anchor must live outside the audited party's authority.** A local check
cannot defend against an actor who can rewrite the check; a repo cannot grant a capability to
PR-controlled config and simultaneously prevent that config from using it.
**An architecture forced by two independent impossibility proofs is stronger evidence than one argued
for.** Neither proof was sought — both arrived while trying to ship something else, from different
directions (a symlink manifest; a CI sandbox), at different layers. RM-59 and RM-60 are the two tracked
dependencies this creates, and they are the same dependency in different clothes.
## The finding that sets the cost
**Built-but-unwired disease.** `@mosaicstack/macp` is stranded (nothing calls it); `packages/coord` primitives
@@ -279,29 +169,6 @@ orphaned context loader, a fail-open bypass. **Work = wire + consolidate + retir
- **Project orchestrator** `mos-remediation` (this seat) owns the mission; coordinates under Mos (lead).
- **Adversarial task decomposition:** `planner-opus` (robustness) + `planner-sol` (pragmatic) each decompose
the plan independently; orchestrator reconciles into `TASKS.md`/DB tasks. Oppositional by design.
- **Delivery gates — REFERENCE, do not restate.** The authoritative definitions live at
[`~/.config/mosaic/fleet/roles.local/merge-gate.md`](file:///home/hermes/.config/mosaic/fleet/roles.local/merge-gate.md)
(verdict authority) and
[`~/.config/mosaic/fleet/roles/validator.md`](file:///home/hermes/.config/mosaic/fleet/roles/validator.md)
(validator/certificate role). **Read them; do not paraphrase them.** Restating an authoritative source
is lossy every time — see D-26, where a subset restated from memory dropped a security precondition.
**Gate order** (the sequence only; the definitions are in the files above):
1. Independent review, **author ≠ reviewer** (`rev-974` on mosaicstack), with PRE-REGISTERED diff-blind
acceptance checks committed before the diff is read
2. Remediation of findings
3. **CI terminal-green** at the exact full-40 head, by **full step scan**
4. **Merge-gate verdict — `GO` / `NO-GO` / `HOLD`** (class `merge-gate`; "Ultron" is an _instance name_,
display data, never an authority source). **Bound to a commit and VOID the instant the head moves.**
`HOLD` persists until replaced; a `NO-GO` answered by an empty commit must be re-issued as `NO-GO`.
Posted durably on the PR under the gate's own minted identity, **enumerating** its evidence — a bare
"GO — gates verified" is non-conforming.
5. **Coordinator merge**, head-pinned. The gate never merges; it holds `push=False` by design.
**The CI queue guard runs but is ZERO-INFORMATION until RM-03 lands** (D-23: it returns pass for every
possible input). It must not be cited as evidence by any gate, including the coordinator's own merge path.
**Assignment:** the coordinator assigns the merge-gate seat; the orchestrator does not. The orchestrator
owns getting a PR _gate-ready_.
- **Delivery gates (non-negotiable):** author≠reviewer, PRE-REGISTERED diff-blind acceptance checks committed
before reading the diff, CI terminal-green, completion = merged PR + closed issue. rev-974 = mosaicstack reviewer.
- **Compaction survival:** see `KICKSTART.md` in this dir — the resume procedure. Persist typed state, not transcript.
+12 -1540
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
# RM-61 — CI contract exemption for #1000 teardown artifact
**Tracking:** RM-61 / issue #1000
**Branch:** `fix/rm-61-ci-contract-exemption`
**Owner:** `coder-mos1`
## Objective
Determine, by red-first provider controls, whether the `ci-postgres` pod-not-found teardown signature discriminates from a real PostgreSQL failure. Only if it discriminates may a named, bounded CI-contract exemption be implemented. The exemption must retire when #1000 is fixed; fixing #1000 is the closure path.
## Pre-registered kill criterion
If an injected real `ci-postgres` failure also yields `pods "wp-svc-<ULID>-ci-postgres" not found` as the service's provider-visible failure, the signature does not discriminate. Option B is unsafe; stop exemption implementation and fall to Option A (#1000).
## Plan
1. Capture full `-f json` records for the 11 supplied observations and state counts.
2. Run one startup-failure control using the real pgvector/PostgreSQL image with an invalid `initdb` argument.
3. Run one post-readiness crash control using real PostgreSQL, `pg_isready`, and a deliberate postmaster kill while a DB-dependent probe is active.
4. Compare the raw `ci-postgres` service record independently of failures in dependent steps.
5. Investigate runner/time/head clustering only as a hypothesis; never encode incidental correlates or retries into policy.
6. If and only if the controls discriminate, implement and test the exact exemption, document its two-way boundary, and track retirement at #1000.
## Budget
No explicit token cap supplied. Working estimate: 20K30K tokens. Limit provider controls to the two pre-registered runs; no retries or re-roll policy.
## Initial evidence
Historical JSON saved locally under `.evidence/rm-61/` (not for commit). Supplied pipelines: 11 total. Child-step counts: five pipelines with 9 children and six with 10 children. Seven contain the `ci-postgres` pod-not-found failure (#2170, #2175, #2180, #2181, #2182, #2187, #2188); four do not (#2158, #2167, #2184, #2186). Every observed workflow reports `agent_id=44`, so the available JSON does not separate clean and artifact runs by runner. This refutes runner identity as a discriminator in the sampled record.
## Progress
- [x] Requirements and kill criterion recorded before control implementation.
- [x] Historical full-JSON records captured.
- [x] Startup-failure control observed terminal.
- [x] Post-readiness crash control observed terminal.
- [x] Discrimination verdict recorded: Option B may proceed.
- [x] Conditional exemption implementation.
## Tests / evidence
### Control 1 — real startup failure
- Commit: `3931b0e29eb834914f7b17e4db7e221481d436fa`
- Pipeline: #2189, exact commit match.
- Full JSON child scan: 9 total — 7 success, 2 failure, 0 skipped/pending/running.
- `ci-postgres`: `state=failure`, `exit_code=1`, `error=null`, with a five-second execution window.
- `test`: `state=failure`, `exit_code=1` after the readiness budget expired.
- Pipeline/workflow: terminal `failure`.
This control is red and its service record differs from #1000 (`exit_code=0` plus pod-not-found). It proves the startup-failure direction only. It does not settle the dangerous post-readiness crash/garbage-collection path.
### Control 2 — real post-readiness crash
- Commit: `25ac59715a94dd1b52ef42577472eb44ecc4b446`
- Pipeline: #2191, exact commit match.
- Full JSON child scan: 9 total — 7 success, 2 failure, 0 skipped/pending/running.
- Service log proves PostgreSQL reached `database system is ready to accept connections`, the test created the arm table, and the service then killed postmaster PID 7.
- Test log proves a successful `SELECT 1` followed by `Connection refused`; it exited the pre-registered control code 61.
- `ci-postgres`: `state=failure`, `exit_code=137`, `error=null`, with a 203-second execution window.
- `test`: `state=failure`, `exit_code=61`.
- Pipeline/workflow: terminal `failure`.
This is the dangerous post-readiness crash path. Its service record is not pod-not-found and therefore differs from #1000 independently of the dependent test failure.
### Discrimination verdict
Both real failures are provider-visible as process exits (`exit_code=1` startup; `exit_code=137` crash) with no pod-not-found error. The seven observed #1000 artifacts are provider reconciliation misses (`exit_code=0` plus the exact pod-not-found error). The declared kill criterion did not fire, so Option B may proceed with a matcher requiring the full conjunction. This evidence does **not** prove every future Kubernetes failure is distinguishable; it proves these two concrete real-failure classes remain blocking and bounds the exemption to the observed reconciliation shape.
### Unit red-first checkpoint
The nine-case contract harness was written before the verifier. First execution exited 1 because `verify-terminal-green.py` did not exist; no exemption implementation was live. Cases pre-register ordinary green, the exact artifact, both provider controls, near-miss signatures, an independent failure, and a skipped step.
### Control 2 setup attempt — invalid, excluded from evidence
- Commit: `9455cd6a2650b2b7e70f746c07933d96e5cb3d20`
- Pipeline: #2190, exact commit match.
- Full JSON child scan: 9 total — 7 success, 2 failure, 0 skipped/pending/running.
- Service log: `/bin/sh: 0: -c requires an argument`.
- Root cause: Woodpecker service `commands` did not become the third `sh -c` argument. PostgreSQL never started, so this run is **not** the post-readiness crash control and provides no discrimination evidence.
- Focused remediation: place the script directly in the third `entrypoint` element and supply `PGPASSWORD` for the marker query. This is a control-fixture correction, not a retry of #1000 and not evidence for either verdict.
## Implementation evidence
- `verify-terminal-green.py` consumes only the full JSON/API record; it performs no fetch, retry, or trigger.
- Exact #2188 record: exit 0, 10 children, 9 success + 1 named exemption.
- Historical set: #2158/#2167/#2184/#2186 pass with no exemption; #2170/#2175/#2182/#2187/#2188 pass with one named exemption; #2180/#2181 remain red because independent failures exist.
- Provider controls: #2189 and #2191 both exit 1 under the verifier; neither is exempted.
- Unit harness: initial 9/9 cases passed after the red-first checkpoint; review remediation expands this to 12 cases with expected-head match/missing/mismatch coverage.
- Test-membership guard: PASS, population 45; 26 enumerated, 19 signed exclusions; all 39 surface paths present.
- Python compile: PASS.
- `pnpm typecheck`: PASS, 45/45 tasks.
- `pnpm lint`: PASS, 25/25 tasks.
- `pnpm format:check`: PASS after moving local evidence outside the repository tree.
- `test:framework-shell`: RM-61 and all preceding suites passed, then the pre-existing wake assertion aborted with exit 97 because this host's Bash 5.2.15 reports `BASH_LINENO [3 5]` where that suite requires `[3 4]`. RM-61 does not modify the wake suite; the command is not fully runnable on this host as written and no substitute result is claimed.
## Independent review
- Review 67 / comment 20403 at exact head `e7b29219e11efd0a19395156ac0b154bec0c3a73`: **REQUEST CHANGES**.
- Blocker: the verifier echoed the pipeline commit but did not bind it to the current PR head; mutating only #2188's commit still returned terminal-green.
- Remediation: require `--expect-commit <full-40>`, add a pipeline anomaly on missing/mismatched record commits, emit expected and observed values, wire both CI documentation and the merge-gate baseline to pass provider PR head, and add match/missing/mismatch tests.
- This binding is not prohibited head-based clustering policy: it proves the evidence belongs to the commit under verdict. Runner/node/time/head correlation remains excluded from the teardown signature itself.
- Review 69 later approved the commit-binding remediation at exact head `033b2ffb46674b2c0bcc5197273c109b461f62d9`; pipeline #2193 was 9/9 success. Before merge-gate, an independent adjudicator found that Python treats JSON `false == 0`, allowing a non-integer exit value to match. The prior gate-ready state was withdrawn. The type-strict set distinguishes genuine red-first controls (`false`, `0.0`, which wrongly exempted) from regression guards (`true`, `"0"`, `null`, which already blocked). Remediation requires the decoded type to be exactly `int` and excludes `bool` explicitly.
## Documentation checklist
- [x] CI contract documented in the canonical framework CI/CD guide.
- [x] Operator command documented in the Woodpecker tool README.
- [x] Merge-gate baseline points to the deterministic verifier and named retirement.
- [x] Tracking and retirement cite issue #1000.
- [x] Both positive and negative guarantee boundaries are stated.
- [x] No API/auth/schema/user-facing navigation change; OpenAPI, user guide, and sitemap are not applicable.
## Risks
The controls establish discrimination for deterministic startup failure and an armed post-readiness postmaster crash on the current Woodpecker Kubernetes provider. They cannot prove that every future Kubernetes failure mode will preserve a non-zero exit before reconciliation. The exact matcher minimizes that residual risk, and issue #1000 remains the mandatory provider-seam closure and retirement trigger.
@@ -0,0 +1,107 @@
# RM-02 Gate Registry Scratchpad (#1029)
## Objective
Deliver the seven-gate registry and RED-first anti-inert verifier on `feat/rm-02-gate-registry`, preserving required-versus-actual defects without laundering them as success.
## Constraints and boundaries
- Do not modify `ci-queue-wait.sh`; RM-03 owns that fix.
- Do not modify `docs/remediation/TASKS.md`; workers cannot edit orchestrator tracking.
- Every declared behavior must be observed, not inferred from an exit code.
- Repository-local evidence does not establish same-authority tamper resistance; RM-25/RM-59 own the external trust anchor.
- Coverage is seven logical gates; broader inventory is RM-54.
- Budget assumption: no explicit token ceiling was supplied. Keep implementation dependency-free (stock Node), avoid repeated full monorepo installs, and keep generated test artifacts under the worktree/main disk.
## Plan
1. Update PRD and tracking references.
2. Write black-box meta-negative-control first and observe it fail for the missing verifier behavior.
3. Implement minimal manifest parser/case runner/mutation detector; observe meta-control succeed.
4. Add schema, coverage, provenance, prose marker, compatibility, discovery, deployment identity, and defect-delta tests RED-first.
5. Register seven gates with exact cases and run each case.
6. Add prospective per-commit replay and bounded provider-evidence reporting.
7. Wire unconditional Woodpecker CI and update developer/admin documentation.
8. Run situational and baseline verification, independent Codex review, remediate, commit, queue guard, push, PR, coordinator/reviewer handoff.
## Progress
- 2026-08-01: Design approved by `mos-remediation`; rulings A-E and source/deployed identity addition incorporated.
- 2026-08-01: Isolated worktree created from `origin/main` f65e9ea6; RM-01 f58b3699 verified as ancestor.
- 2026-08-01: Git author set to `f10-coder <[email protected]>`; provider issue #1029 created with `MOSAIC_GIT_IDENTITY=f10-coder`.
- 2026-08-01: Source/deployed queue-guard SHA-256 observed equal (`19cda2f...`); this observation is not yet an enforced property.
## Tests and RED-first evidence
- Meta-negative RED first: `node --test scripts/gate-verify.test.mjs` failed because the absent verifier did not name externally inerted `meta-fixture`.
- Structural RED: nine tests failed before implementation for internal mutation, unregistered executable, missing negative control, ownerless delta, unbound criterion/claim, modeled conflict, missing provenance, and deployment drift.
- Clause hardening RED: positive-only security criterion and missing registered claim marker both passed incorrectly before validation was added.
- CI wiring RED: package script and unconditional Woodpecker step tests both failed before wiring.
- History RED: history test failed with missing module before own-tree manifest selection/provider classification was implemented.
- `pnpm gate:verify`: exit 0; seven gates each reported `META-NEGATIVE-CONTROL ... observed red`; queue source/deployed drift control observed red; six queue behavior deltas printed as `DEFECT (owner: RM-03)`.
- Focused Node tests: 54/54 pass after third-round hardening (36 verifier/wiring plus 18 history/provider tests).
- `pnpm typecheck`: pass (45/45 Turbo tasks).
- `pnpm lint`: pass (25/25 Turbo tasks).
- `pnpm format:check`: pass.
- `pnpm test`: repository suites reached 45/46 Turbo tasks; all application/package tests shown passed, then the known host-specific wake assertion aborted exit 97 (`BASH_LINENO convention violated`, #973/D-16). No test was edited or bypassed. CI remains the authoritative full-suite environment.
## Self-surfaced defect
The queue guard's `get_state_from_status_json` runs `python3 - <<'PY'` while provider JSON is piped to the shell function. The heredoc owns stdin, so Python never reads provider JSON. Terminal success, no-status, terminal failure, and malformed payloads all classify as `unknown`; the associated fail-open outcomes are recorded under RM-03. No queue-guard source was modified.
## Independent review remediation
- Final pre-commit Codex code review found three blockers: a tautological deployment drift control, independently observed rather than combined compatibility cases, and unauthorized edits to orchestrator-owned `TASKS.md`.
- Deployment identity now uses one shared file comparator for both live equality and a temporary drifted deployed copy; a test makes that comparator inert and proves the meta-control fails.
- Compatibility scenarios now merge referenced fixtures/environments into one isolated construction and execute an exact scenario invocation; a test proves two independently valid fixtures coexist in the combined run.
- `TASKS.md` changes were reverted. `docs/remediation/GATE-CLAIMS.md` binds source headings and anchored text without editing orchestrator tracking.
- Codex security review found the Bubblewrap replay shared the runner PID namespace. Replay now unshares PID, IPC, and UTS namespaces, and an abuse-case test proves a sibling runner PID is invisible.
- Second review found empty reason diagnostics, final-symlink fixture writes, and lifecycle-script mutation of authoritative history files. Must-fail cases now require a reason pattern; writes use no-follow semantics; and replay snapshots every archived file before install and rejects any changed, deleted, or type/mode-shifted source before executing the verifier. Dedicated negative tests cover all three.
- Third code review found ambiguous duplicate provider steps and order-sensitive JSON outcome comparison. Provider evidence now requires exactly one `gate-verify` step in the authoritative rerun, and structural equality normalizes object keys. Both regressions have RED-first tests. Third security review reported no findings.
- Initial PR pipeline #2177 exposed Woodpecker's shallow boundary: the activation parent object was present but marked shallow, so `merge-base --is-ancestor` correctly refused to infer ancestry. The unconditional gate step now unshallows before ancestry/provenance checks; its wiring test was observed RED before the CI fix.
- Pipeline #2178 then proved the unprivileged Docker runner cannot establish Bubblewrap namespaces. A privileged experiment remained uncommitted and was rejected after Codex correctly rated it CRITICAL: PR-controlled code executes before an in-repository sandbox and could directly use the granted capability.
- `mos-remediation` and `rev-974` independently ruled Option C. RM02-REQ-10 now retains its original text, restatement, and reason: PR CI verifies only the current tree, unprivileged and fail-closed; isolated own-tree replay is deferred to RM-60/#1031's external pre-execution authority, cross-referenced with RM-59. Future protected post-merge replay is detection with quarantine/revert, never pre-merge prevention.
- RED-first boundary test proved the old path executed an inert intermediate verifier. The revised path states adjacent `DOES`/`DOES NOT` claims, validates historical manifest provenance without executing it, and infers no replay success. Direct sandbox tests remain hard-fail; unprivileged CI asserts terminal refusal instead of treating replay as success. Pipelines #2179/#2180/#2181 exposed two runner refusal forms: namespace denial as `spawnSync bwrap` with `error.code=EPERM`, and a test image without Bubblewrap as `error.code=ENOENT`. The replay diagnostic now preserves spawn errors. Parent-generated launcher/entry metadata distinguishes refusal before sandbox entry from child-controlled output; the detector recognizes exact `spawnSync bwrap` provenance for `EPERM`/`EACCES`/`ENOENT` and known namespace-refusal text only when the entry command provably did not run. Focused negative assertions reject unrelated `spawnSync git EPERM`, verifier output that merely says `bwrap ENOENT`, and exact namespace-denial impersonation without provenance or after sandbox entry.
- Exact-head independent review at `38f1b249` found one valid diagnostic-masking blocker: canonical verification knew four stable-ID rebinding failures but a thrown stale fixture replacement reached the outer catch first and emitted only the generic error. RED-first reproduction confirmed the canonical path omitted both responsible criterion IDs. Verification now collects labeled failures independently across claims, discovery, deployment, case execution/outcome checks, mutation, and compatibility, preserving structural stable-ID failures alongside the stale-fixture signal. The canonical regression test requires both missing-binding IDs and the generic fixture error.
- Exact-head independent review at `9b4d4beb` found two valid blockers. RED-first controls reproduced both: denial-looking child stderr was accepted as sandbox unavailability, and moving meaning/prose criterion IDs to an unrelated type-error case left `gate:verify` green. Bubblewrap execution now emits a parent-generated random entry marker and returns parent-owned launcher/entry metadata; unavailability requires Bubblewrap launcher provenance plus proof entry never ran, so exact denial impersonation from plain or entered-child results is rejected. Criterion objects now declare exact `caseRefs`, checked bidirectionally against case-side `criterionIds`; prose claims declare an exact must-fail `caseRef`. Registered must-fail cases move a criterion binding, remove meaning provenance, and redirect a prose claim, each producing its stable reason. The review freeze was deliberately lifted before remediation.
- Option C security review reported no findings. Code review rejected an initial unrelated typecheck binding for the new security criterion. It was replaced with a dedicated registered `privileged-pr-gate` case: the fixture injects a privilege key into the gate step, the wiring control rejects it for that exact reason, and `gate:verify` observes the boundary negative control. Follow-up hardening uses a closed exact gate-step construction, rejects privilege across the entire pipeline, rejects non-canonical/merged YAML keys, and pins the unrestricted PR/main trigger block; quoted/escaped/alias/merge/duplicate/filter bypass tests pass. Final Codex code review approved with no findings.
- Exact-head review at `83d2ecb2` found four silent-defeat paths. Genuine RED-first tests on the pre-fix code proved: author-controlled HEAD/parent/introduction seams produced no boundary failure; cross-commit duplicate pipeline number 7 returned terminal-success; and misspelling `outputPattern` as `outputPatern` in required/actual left canonical verification green. Fixes derive the seam from Git, validate provider identity globally before subject filtering, and recursively close/type-check nested schemas. New registry criteria `RM02-EVIDENCE-SUBJECT-BINDING`, `RM02-TYPE-STRICT-SCHEMA`, and `RM02-HISTORY-BOUNDARY` are bidirectionally bound to eight registered must-fail controls. The broad nested-object typo table and exact derived-boundary test are regression guards added after implementation, not claimed as RED-first. Pre-commit Codex review then found that global evidence failure was only observed—not failed—when HEAD was the sole prospective commit, and that non-object collection entries threw before normalization. Both were reproduced RED-first, then fixed by one collection validator used before iteration and by per-commit assessment. Follow-up review found collection validation still omitted exactly-one-gate-step cardinality for unrelated subjects; a focused test reproduced terminal-success RED-first, and collection validation now rejects that ambiguity globally. Security review then found present-but-empty outcome patterns were truthy-optional assertion bypasses; a reason-specific test reproduced that they lacked the required schema diagnostic, and present pattern fields now require non-whitespace content.
- Exact-head review at `32b490a7` found three population-level blockers. Genuine RED-first tests proved a gate change before author-delayed registry introduction fell outside the range, and empty criteria/gates/prose/scenario populations returned zero. History now anchors at the provider target merge-base; delayed introduction is a registered must-fail control. Production verification requires non-empty populations and a hardcoded seven-gate ID/source inventory before quantified checks. D-38/D-40 criteria now quantify over `gateRefs` exactly spanning every registered gate; each gate declares its evidence subject, and population controls mutate evidence subject and comparison type for every gate. The history record states both bootstrap directions: sound against a branch author unable to rewrite main, not sound against compromised/rewritten main, with residual owned by Builds 1-2. Pre-commit review rejected an initial production CLI `--fixture-profile` test relaxation as a vacuity bypass. That flag was removed; synthetic fixtures now use a non-executable test-support runner, while regression tests prove the shipped CLI rejects the flag and production population checks remain mandatory. Follow-up review then proved deleting `gateRefs` skipped population validation; a RED-first loop reproduced all three deletions, and the three general criterion IDs now require the field before exact-span validation.
## Documentation checklist
- PRD, developer guide, admin guide, governing claim index, sitemap, plan, and scratchpad updated.
- User/API documentation not applicable: no user workflow or API changed.
- Independent review documentation check pending rev-974 at the revised exact head.
- Canonical documentation remains in-repository; no external publication requested.
## Rebase onto RM-61
- Rebasing `f9746b23` onto main `f4fd5967` completed mechanically with no conflicts.
- The first post-rebase `pnpm gate:verify` correctly failed because the old activation seam `f65e9ea6` made the newly merged pre-registry RM-61 commit part of prospective history even though that commit predates the registry. An initial manifest update to `f4fd5967` had the right value but retained an author-controlled mechanism. Independent mutation proved HEAD, HEAD's parent, and the introduction commit could each make history coverage vacuous or partial. The manifest field is now forbidden; the verifier derives the boundary as the parent of the first first-parent registry-introduction commit, and registered must-fail controls reject all three unsafe candidates.
## Risks/blockers
- Current queue guard intentionally has required-versus-actual deltas owned by RM-03.
- Provider CI cannot report the currently executing pipeline as terminal success; current-commit evidence must be labeled pending and becomes historical current-tree evidence only after provider completion.
- Isolated per-commit execution requires RM-60/#1031. Until that external authority exists, no replay success is claimed. A future protected post-merge failure requires quarantine/revert.
- CI containers may not expose the operator-home deployed queue guard. In that layer the verifier checks the pinned observed digest and reports live identity unavailable under RM-04; it does not infer live equality.
## coder-mos2 remediation — fourth round
- Coordinator correction loaded: Blocker 2 requires a genuine shrink-both control against an independent inventory baseline; Blocker 3 requires evidence-side subjects consumed against gate definitions for every gate; anchor-dependent history provenance must be removed rather than relabelled because PR lifecycle code makes all local git state untrustworthy before verification.
- Boundary to preserve in both directions: this repository layer DOES verify current-tree registered cases, independently anchored inventory shape, evidence subject consumption, and an enforced absence of history-provenance claims. It DOES NOT establish history provenance at all. RM-60 owns the provider-controlled/protected pre-execution boundary required for that property; no local ref, config, URL, constant, or author-positioned path is treated as an anchor.
- TDD plan: add three genuine red-first controls before implementation: shrink verifier inventory plus manifest together; mutate evidence-side subject for every gate; and reject the currently enabled history verifier/import/report path. Existing controls retained as regression guards and labelled honestly.
- Identity observation before first commit: `git var GIT_AUTHOR_IDENT` returned `coder-mos2 <[email protected]>` using process-scoped author/committer variables; shared repository config was not modified.
- Genuine RED-first evidence: `node --test scripts/gate-remediation.test.mjs` produced 0/3 passing on frozen head `fbb61912`: source+manifest shrink exited zero, no evidence-side case subjects existed, and the production verifier still imported/invoked history verification. These were failures for the three stated blocker reasons, not regression guards.
- Sixth mutation found before review: after the first baseline implementation, shrinking the new baseline and manifest together while leaving the verifier inventory unchanged still exited zero. A new test observed that attack RED first. Inventory equality diagnostics are now bidirectional, and the registered shrink control attacks both source+manifest and baseline+manifest pairs.
- Regression guards retained and honestly labelled: manifest-only shrink, exact `gateRefs` span, production fixture-profile rejection, and broad nested schema checks already blocked before this round.
- Current focused evidence before independent review: remediation controls 4/4; verifier/wiring/remediation suite 39/39; canonical `pnpm gate:verify` exits zero while reporting six RM-03-owned `DEFECT` deltas and all seven gate meta-negative controls observed red.
- Independent Codex code review requested changes on two valid blockers. First, the evidence population control called the subject helper directly rather than traversing production result consumption. It now creates one lightweight executed fixture per required gate, invokes the real `verifyRegistry` path, and fails to observe rejection if the production consumer is removed; a regression mutation proves that coupling. Second, a lexical history blacklist overclaimed structural incapacity. Production output now passes through a closed current-tree observation renderer with no history/ancestry/lineage success class; the exclusion control tests three alternate success wordings plus exact production renderer wiring, and the registered must-fail fixture adds a prohibited class.
- Codex review test attempts were unrunnable in its read-only sandbox (`EROFS`/`EPERM`); the reviewer disclosed this rather than substituting a passing variant. Local writable-worktree tests remain the runnable evidence.
- Renderer remediation: every non-error production observation and final summary now routes through the closed current-tree output renderer. The exclusion control additionally requires one stdout sink, exactly two `GATE VERIFY FAILED` stderr sinks, no console sinks, and renderer use for both per-observation and final-summary paths. Regression attacks cover allowlisted history/ancestry/lineage wording, writer assertion removal, direct final-success stdout, and direct success stderr; focused suite 41/41, `pnpm gate:verify` green with six declared RM-03 deltas, and format check green.
- Coordinator ruling after CWE-353: retain the same-checkout inventory comparison but narrow its claim. Canonical current boundary: “Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.” This supersedes the earlier “independent/anchored” wording in the historical entries above; those entries remain as append-only evidence, not current claims.
- Genuine RED-first overclaim control: before claim remediation, `node scripts/gate-inventory-claim-control.mjs` exited 84 and named every artifact missing the narrowed boundary plus each current overclaim. The registered `inventory-claim-overstatement` case now rewrites the baseline purpose to a protection claim and must exit 84 with `INVENTORY_CLAIM_OVERSTATED`. Existing shrink-pair controls remain regression guards for the same-checkout drift property, not adversarial-integrity claims.
- Follow-up code review found a genuine propagation/control gap: `docs/PRD.md` still said “independent seven-gate baseline,” and the first forbidden regex did not match the intervening qualifier. The reviewer observed the control green against that overclaim. The PRD now says same-checkout, the detector rejects `independent … baseline` across bounded same-line qualifiers, the registered mutation uses the exact qualified wording, and a focused behavioral test requires exit 84 for it. Follow-up security review passed with risk `none` and explicitly accepted the narrowed RM-60 boundary.
@@ -0,0 +1,58 @@
# RM-01 — Reproducible checkout
- Task/ref: RM-01 (`docs/remediation/TASKS.md`, internal mission tracking)
- Objective: make checkout/install/typecheck hooks fail on code rather than environmental residue, for root CI and non-root seats.
- Scope: pnpm store configuration, transactional Husky installation, dependency/generated-state preflight, checkout regression tests, developer documentation.
- Constraints: isolated worktree; no skip-switch fixes; no writes under `/root` or `/tmp`; workers do not edit `docs/remediation/TASKS.md`; author does not review or merge.
- Acceptance: AC1AC8 from the orchestrator dispatch/addendum.
- Plan:
1. Add RED-first tests for missing dependencies, stale/foreign `.next`, and interrupted hook installation.
2. Implement environment-overridable HOME-based pnpm store defaults, deterministic preflight, and transactional hook installation.
3. Run focused tests, install/build/baseline gates, and explicit AC negative controls.
4. Obtain independent review, push after queue guard, open PR, and send evidence to `mos-remediation`.
- Budget: orchestrator estimate 6K/60K; no explicit hard token cap. Keep scope to RM-01 and avoid unrelated cleanup.
- Risks: 97%-full shared `/tmp`; native dependency install size; root-owned fixtures may require Docker for realistic verification.
## Progress / evidence
- Worktree created at `/home/hermes/agent-work/rm-01` from `origin/main` `06e0d403`.
- `/tmp` baseline: 28G used, 889M available (97%); worktree and planned store are on `/home`.
- Root causes confirmed from source: committed `.npmrc` pins `/root`; `prepare` invokes Husky directly; web typecheck includes generated `.next` types without validating ownership/freshness.
## Checkpoint evidence (c45e5e19)
- AC1 IN PROGRESS: non-root `pnpm install --frozen-lockfile --store-dir "$HOME/.local/share/pnpm/store"` exited 0; `pnpm exec turbo run typecheck --force` exited 0 (45/45 uncached). Clean CI-container run not performed.
- AC2 DONE: with `node_modules` absent, `pnpm preflight` exited 42 with `MOSAIC_PREFLIGHT_MISSING_DEPS` and `run pnpm install`; after install it exited 0.
- AC3 DONE: appending `export const x: number = "s"` to `packages/types/src/index.ts` made `pnpm -w typecheck` exit 2 with TS2322; reverting made it exit 0.
- AC4 IN PROGRESS: local `pnpm -w build` exited 0 and `git status --porcelain` showed no generated residue beyond the intended RM-01 source changes. Fresh-clone proof not performed.
- AC5 DONE: non-root install exited 0; `pnpm store path` resolved `/home/hermes/.local/share/pnpm/store/v10`; no `/root` write was attempted.
- AC6 IN PROGRESS: focused failure/rollback tests passed, but final review found a concurrent-install race. Two installers can both observe `.husky/_` absent; after one installs successfully, the losing install's catch path can quarantine the winner's active hooks and restore stale Git config (`scripts/install-hooks.mjs`, activation/catch transaction). A RED regression is committed after the checkpoint.
- AC7 DONE: install/store/worktree were on `/home`; full `pnpm -w build` exited 0; `/tmp` usage changed by 4096 bytes during the build (23,805,173,760 → 23,805,177,856 bytes), not materially.
- AC8 DONE for the implemented path: store resolves under `$HOME`; test/quarantine/build state resolves under the worktree; no implemented component requires a writable path outside `$HOME` or the worktree.
## Continuation evidence
- AC6 DONE: the committed race reproducer was observed RED (`node --test --test-name-pattern='a competing successful installer is not removed by the losing process' scripts/install-hooks.test.mjs`, exit 1/ENOENT), then passed after cleanup became ownership-safe. The losing installer never removes an active hook set or restores Git configuration it did not activate. `pnpm test:checkout` passes 21/21, exit 0, including the original race and a post-rename peer-replacement regression.
- Generated-state remediation: replaced mtime inference with a source/build-input fingerprint, written only after a serialized successful Next build with unchanged inputs. Failed/interrupted/overlapping builds leave no trusted marker. The fingerprint uses Next's own environment loader, covers resolved `NEXT_PUBLIC_*` values, inherited TypeScript configuration, lock/workspace inputs, and rejects symlink inputs.
- Baseline: `pnpm typecheck`, `pnpm lint`, and `pnpm format:check` each exit 0. Local `pnpm test` still exits 97 only at the pre-existing Bash `BASH_LINENO` convention guard (#973/#1003), after checkout tests and package tests pass; this is not reported as a green full-suite result.
- Automated review remediation: resolved findings for peer-hook ownership, stale/failed build markers, build-input changes, expanded environment inputs, inherited TypeScript config, symlink inputs, and overlapping build serialization. Independent PR review remains assigned to rev-974.
- AC1 DONE at `0f706119`: a clean clone created inside `git.mosaicstack.dev/mosaicstack/stack/ci-base:latest` ran the exact acceptance sequence `pnpm install --frozen-lockfile && pnpm -w typecheck`; exit 0 with 45/45 uncached typecheck tasks successful. An earlier bind-mounted clone attempt exited 1 because root in the container rejected the host-owned Git directory; that failed attempt is not counted as evidence.
- AC4 DONE at `0f706119`: in that same fresh clone and CI image, `pnpm -w build` completed 25/25 tasks and the immediately following `git status --porcelain` was empty; combined assertion exit 0.
- Push BLOCKED after the required queue guard: `git push origin fix/rm-01-reproducible-checkout` was rejected by Gitea with `User permission denied for writing` / `pre-receive hook declined`, despite `MOSAIC_GIT_IDENTITY=f10-coder` resolving username `f10-coder` from the provisioned `gitea-mosaicstack-f10-coder.token`.
## Review remediation — restated AC2
- Independent review correctly found that an added symlink under a successfully built `.next` tree passed preflight. The exact reviewer control, `ln -s /etc/hosts apps/web/.next/reviewer-symlink && pnpm preflight`, was observed passing before remediation.
- The original blanket symlink wording conflicts with AC4 because canonical Next `output: 'standalone'` emits legitimate pnpm dependency symlinks. The coordinator independently verified 42 such links and approved the operative restatement: `.next` itself must not be a symlink; descendant symlinks must exactly match the successful build's certified manifest.
- RED-first controls were observed failing together against the prior implementation (exit 1): `.next` root, added, removed, retargeted, tampered-manifest, and canonical-style certified-link cases. The build now publishes the manifest atomically before the existing source certification commit marker; that marker binds the manifest SHA-256. Missing/partial/modified manifests remain untrusted.
- GREEN evidence: the six-case symlink control passes; the exact reviewer-added link exits 43; removing it restores preflight exit 0. The added RED-first build-publication control also proves a symlinked `.next` cannot redirect certification writes outside the checkout. `pnpm test:checkout` passes 23 top-level tests / 29 including subtests. Canonical `pnpm --filter @mosaicstack/web build` and the following `pnpm preflight` both exit 0.
- Threat-model ruling: the manifest detects accidental, independent, stale, and foreign-residue mutation—the class exposed by the five-month-stale `.next` that produced 19 phantom TS2307 errors. It does not defend against a same-UID actor able to rewrite both manifest and marker consistently (CWE-345); no local worktree construction can without an external trust anchor. RM-59 tracks the residual: executor/spine-side attestation outside worktree authority, dependent on RM-12, RM-21, and RM-25.
- AC8 concrete proof at `df7530ae`: a clean clone ran in `ci-base:latest` with Docker `--read-only`; its only writable mounts were `/workspace` (the worktree) and `/home/ci` (`HOME`, with `NPM_CONFIG_STORE_DIR=/home/ci/store`). `pnpm install --frozen-lockfile && pnpm -w typecheck` exited 0 with 45/45 uncached tasks. This proves the implemented checkout path requires no writable location outside `$HOME` and the worktree. An initial fixture attempt failed only because Git required `/workspace` safe-directory setup; it is not counted as evidence.
## Handoff
1. Keep the newly committed RED tests red until implementing: (a) source-fingerprint marker support for valid incremental `.next` output, and (b) ownership-safe concurrent hook activation.
2. The latest automated review rejected oldest-generated-file mtime as a false positive for valid incremental Next output. Use a source-content fingerprint marker written only after successful `next build`; do not continue tuning mtimes.
3. For Husky, generation in an isolated temporary Git repo avoids mutating real `core.hooksPath` during staging. Preserve that design. Fix the losing concurrent process so it never removes a peer's completed hook set or restores stale config.
4. Codex review runs in a read-only sandbox, so its attempts to run the fixture-writing Node tests report opaque test-file failures. The same tests run normally in the worktree.
5. Full `pnpm test` is not green on this host: it exits 97 at the pre-existing Bash `BASH_LINENO` convention guard (#1003), after the changed checkout tests and package tests pass. Do not weaken that gate.
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"purpose": "Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.",
"gates": [
{
"id": "quality-typecheck",
"source": "package.json"
},
{
"id": "quality-lint",
"source": "package.json"
},
{
"id": "quality-format",
"source": "package.json"
},
{
"id": "checkout-preflight",
"source": "scripts/preflight.mjs"
},
{
"id": "ci-queue-wait",
"source": "packages/mosaic/framework/tools/git/ci-queue-wait.sh"
},
{
"id": "hook-pre-commit",
"source": ".husky/pre-commit"
},
{
"id": "hook-pre-push",
"source": ".husky/pre-push"
}
]
}
+7 -3
View File
@@ -6,11 +6,15 @@
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"gate:verify": "node scripts/gate-verify.mjs",
"preflight": "node scripts/preflight.mjs",
"clean:generated": "node scripts/clean-generated.mjs",
"typecheck": "pnpm preflight && turbo run typecheck",
"test:checkout": "node --test scripts/*.test.mjs",
"test": "pnpm test:checkout && turbo run test",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
"prepare": "husky"
"prepare": "node scripts/install-hooks.mjs"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -13,7 +13,14 @@ It is a **gate** role: the one and only merge path.
2. **Use the wrapped scripts as the ONLY merge path** — the merge-gate merges
**exclusively** by calling **`pr-merge.sh`** (the merge action, which carries the
authoritative forbidden-path guard) and **`pr-ci-wait.sh`** (to wait for green
CI before merging). These two scripts are the _only_ sanctioned merge path.
CI before merging). Before issuing a verdict, scan the full JSON/API child-step
record (including `clone`) with **`verify-terminal-green.py --expect-commit
<current-provider-PR-head>`** and record the equal expected/observed full-40
commits, exact step count, anomalies, and named exemptions. Missing or mismatched
commit binding is a hard refusal. The verifier's sole interim
exemption is `WP-K8S-1000-CI-POSTGRES-TEARDOWN`; it is signature-scoped, tracked
by #1000, and retires when #1000 is fixed. These scripts are the _only_
sanctioned merge path.
3. **Never call the raw API** — the merge-gate **does NOT** call `tea`, the raw
Gitea/forge HTTP API, or any other merge mechanism directly. Only `pr-merge.sh`
and `pr-ci-wait.sh`.
@@ -868,6 +868,38 @@ steps:
7. **Test on a short-lived non-main branch first** — open a PR and verify quality gates before merging to `main`
8. **Verify images appear** in Gitea Packages tab after successful pipeline
## Terminal-Green Full-Step Contract
A successful pipeline summary is not sufficient: verification MUST consume the full JSON/API child-step record, including `clone`.
```bash
PR_HEAD=<full-40-hex-provider-head>
~/.config/mosaic/tools/woodpecker/pipeline-status.sh \
-r mosaicstack/stack -n <pipeline-number> -f json \
| ~/.config/mosaic/tools/woodpecker/verify-terminal-green.py \
--expect-commit "$PR_HEAD" -
```
`PR_HEAD` MUST come from the current provider PR metadata and MUST be the full 40-hex head, not a local branch guess. The verifier fails if the argument is missing, malformed, absent from the pipeline record, or differs from that record.
The verifier reports the expected and observed commits, total step count, state counts, anomalies, and any applied exemption. Exit `0` means the record satisfies the contract; exit `1` means the commit binding or at least one pipeline, workflow, or child-step state blocks terminal-green; exit `2` means the invocation or JSON input could not be verified.
### Named interim exemption: `WP-K8S-1000-CI-POSTGRES-TEARDOWN`
Only this exact conjunction is exempted:
- pipeline and workflow state are `success`;
- exactly one non-success child exists;
- its name is `ci-postgres` and type is `service`;
- its state is `failure`, exit code is the JSON integer `0` (not boolean, float, string, or null); and
- its error exactly matches `pods "wp-svc-<ULID>-ci-postgres" not found`.
Every near miss remains blocking, including non-zero service exits, startup failures, post-readiness crashes, connection errors, image-pull errors, skipped steps, another failed child, malformed pod names, duplicate matches, or a non-success pipeline/workflow.
**Boundary in both directions:** this exemption recognizes the observed Woodpecker Kubernetes reconciliation miss after an otherwise-successful run. It does not prove that every future PostgreSQL or Kubernetes failure is distinguishable. It does prove, through provider controls, that a deterministic startup failure (`exit_code=1`) and an armed post-readiness postmaster crash (`exit_code=137`, dependent probe `Connection refused`) do not match and remain red.
**Tracking and retirement:** [mosaicstack/stack#1000](https://git.mosaicstack.dev/mosaicstack/stack/issues/1000) owns the provider-seam fix. This exemption MUST be removed when #1000 is fixed. It is not authority to retry or re-trigger a pipeline, and no per-PR re-roll is part of the contract.
## Post-Merge CI Monitoring (Hard Rule)
For source-code delivery, completion is not allowed at "PR opened" stage.
@@ -26,12 +26,13 @@ A Woodpecker API token is required. To configure:
## Scripts
| Script | Purpose |
| --------------------- | -------------------------------------------- |
| `pipeline-list.sh` | List recent pipelines for a repo |
| `pipeline-status.sh` | Get status of a specific or latest pipeline |
| `pipeline-trigger.sh` | Trigger a new pipeline build |
| `ci-wait.sh` | Block until pipeline(s) reach terminal state |
| Script | Purpose |
| -------------------------- | -------------------------------------------------------------- |
| `pipeline-list.sh` | List recent pipelines for a repo |
| `pipeline-status.sh` | Get status of a specific or latest pipeline |
| `pipeline-trigger.sh` | Trigger a new pipeline build |
| `ci-wait.sh` | Block until pipeline(s) reach terminal state |
| `verify-terminal-green.py` | Verify every JSON/API child step under the bounded CI contract |
## Common Options
@@ -59,4 +60,9 @@ A Woodpecker API token is required. To configure:
# Block until one or more pipelines finish (event-driven CI wait)
~/.config/mosaic/tools/woodpecker/ci-wait.sh -r usc/uconnect -n 3917 -n 3918
# Verify the full JSON child-step record; do not use the text summary for this gate
PR_HEAD=<full-40-hex-provider-head>
~/.config/mosaic/tools/woodpecker/pipeline-status.sh -r mosaicstack/stack -n 2188 -f json \
| ~/.config/mosaic/tools/woodpecker/verify-terminal-green.py --expect-commit "$PR_HEAD" -
```
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Red-first contract harness for RM-61 / #1000.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERIFIER="$SCRIPT_DIR/verify-terminal-green.py"
EXPECTED_COMMIT=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
write_fixture() {
local file="$1" pipeline_status="$2" postgres_state="$3" postgres_exit="$4" postgres_error="$5" test_state="$6"
python3 - "$file" "$pipeline_status" "$postgres_state" "$postgres_exit" "$postgres_error" "$test_state" <<'PY'
import json, sys
path, pipeline_status, pg_state, pg_exit, pg_error, test_state = sys.argv[1:]
steps = [
{"name": "clone", "type": "clone", "state": "success", "exit_code": 0, "error": None},
{"name": "ci-postgres", "type": "service", "state": pg_state, "exit_code": int(pg_exit), "error": pg_error or None},
{"name": "test", "type": "commands", "state": test_state, "exit_code": 0 if test_state == "success" else 1, "error": None},
]
json.dump({
"number": 9999,
"status": pipeline_status,
"commit": "a" * 40,
"workflows": [{"name": "ci", "state": pipeline_status, "children": steps}],
}, open(path, "w"))
PY
}
expect_exit() {
local expected_exit="$1" label="$2" file="$3" expected_commit="${4:-$EXPECTED_COMMIT}"
set +e
output=$(python3 "$VERIFIER" --expect-commit "$expected_commit" "$file" 2>&1)
actual=$?
set -e
if [[ "$actual" -ne "$expected_exit" ]]; then
printf 'FAIL %s: expected exit %s, got %s\n%s\n' "$label" "$expected_exit" "$actual" "$output" >&2
exit 1
fi
printf 'PASS %s\n' "$label"
printf '%s' "$output"
}
# Ordinary terminal green.
write_fixture "$TMP/green.json" success success 0 '' success
out=$(expect_exit 0 green "$TMP/green.json")
grep -q '"total_steps": 3' <<<"$out"
grep -q '"exempted_steps": 0' <<<"$out"
# Exact, named #1000 teardown artifact: the only permitted non-success child.
artifact='pods "wp-svc-01kyxzjhdf6w81swsnbfzh85z9-ci-postgres" not found'
write_fixture "$TMP/artifact.json" success failure 0 "$artifact" success
out=$(expect_exit 0 exact-artifact "$TMP/artifact.json")
grep -q '"exemption_id": "WP-K8S-1000-CI-POSTGRES-TEARDOWN"' <<<"$out"
grep -q '"exempted_steps": 1' <<<"$out"
# Negative controls: both real PostgreSQL failures must remain red.
write_fixture "$TMP/startup.json" failure failure 1 '' failure
expect_exit 1 startup-failure "$TMP/startup.json" >/dev/null
write_fixture "$TMP/crash.json" failure failure 137 '' failure
expect_exit 1 post-readiness-crash "$TMP/crash.json" >/dev/null
# The exemption is signature-scoped, not step-scoped.
write_fixture "$TMP/wrong-error.json" success failure 0 'connection refused' success
expect_exit 1 other-postgres-error "$TMP/wrong-error.json" >/dev/null
write_fixture "$TMP/wrong-pod.json" success failure 0 'pods "other-ci-postgres" not found' success
expect_exit 1 wrong-pod-signature "$TMP/wrong-pod.json" >/dev/null
write_fixture "$TMP/nonzero-artifact.json" success failure 137 "$artifact" success
expect_exit 1 nonzero-with-artifact-text "$TMP/nonzero-artifact.json" >/dev/null
# JSON booleans and non-integer zero look equal to 0 in Python but are not exit codes.
python3 - "$TMP/artifact.json" "$TMP" <<'PY'
import json, os, sys
record = json.load(open(sys.argv[1]))
for label, value in (("false", False), ("true", True), ("float", 0.0), ("string", "0"), ("null", None)):
changed = json.loads(json.dumps(record))
changed["workflows"][0]["children"][1]["exit_code"] = value
json.dump(changed, open(os.path.join(sys.argv[2], f"exit-{label}.json"), "w"))
PY
for label in false true float string null; do
expect_exit 1 "non-integer-exit-$label" "$TMP/exit-$label.json" >/dev/null
done
# Exact artifact cannot mask any independent failure or non-success pipeline.
write_fixture "$TMP/artifact-plus-failure.json" failure failure 0 "$artifact" failure
expect_exit 1 artifact-plus-real-failure "$TMP/artifact-plus-failure.json" >/dev/null
write_fixture "$TMP/skipped.json" success success 0 '' skipped
expect_exit 1 skipped-step "$TMP/skipped.json" >/dev/null
# The scanned pipeline must be bound to an explicit, full PR-head commit.
set +e
missing_output=$(python3 "$VERIFIER" "$TMP/artifact.json" 2>&1)
missing_rc=$?
set -e
if [[ "$missing_rc" -ne 2 ]] || ! grep -q -- '--expect-commit' <<<"$missing_output"; then
printf 'FAIL missing-expected-commit: expected usage exit 2\n%s\n' "$missing_output" >&2
exit 1
fi
expect_exit 1 mismatched-expected-commit "$TMP/artifact.json" bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb >/dev/null
python3 - "$TMP/artifact.json" "$TMP/missing-record-commit.json" <<'PY'
import json, sys
record = json.load(open(sys.argv[1]))
record.pop("commit")
json.dump(record, open(sys.argv[2], "w"))
PY
expect_exit 1 missing-record-commit "$TMP/missing-record-commit.json" >/dev/null
printf 'terminal-green contract harness: PASS (17 cases)\n'
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""Verify Mosaic's full-step Woodpecker terminal-green contract.
RM-61 permits one named, signature-scoped exception for issue #1000. The
exception retires when #1000 is fixed; all other non-success states block.
This program consumes the JSON/API record emitted by pipeline-status.sh -f json.
It does not fetch, retry, or re-trigger pipelines.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import Counter
from pathlib import Path
from typing import Any
EXEMPTION_ID = "WP-K8S-1000-CI-POSTGRES-TEARDOWN"
EXEMPTION_ISSUE = "https://git.mosaicstack.dev/mosaicstack/stack/issues/1000"
POD_NOT_FOUND = re.compile(
r'^pods "wp-svc-[0-9a-hjkmnp-tv-z]{26}-ci-postgres" not found$'
)
def fail_usage(message: str) -> int:
print(f"terminal-green contract input error: {message}", file=sys.stderr)
return 2
def load_record(argument: str | None) -> dict[str, Any]:
if argument in (None, "-"):
value = json.load(sys.stdin)
else:
with Path(argument).open(encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise ValueError("pipeline record must be a JSON object")
return value
def is_issue_1000_artifact(step: dict[str, Any]) -> bool:
error = step.get("error")
exit_code = step.get("exit_code")
return (
step.get("name") == "ci-postgres"
and step.get("type") == "service"
and step.get("state") == "failure"
and type(exit_code) is int
and not isinstance(exit_code, bool)
and exit_code == 0
and isinstance(error, str)
and POD_NOT_FOUND.fullmatch(error) is not None
)
def verify(record: dict[str, Any], expected_commit: str) -> tuple[int, dict[str, Any]]:
anomalies: list[dict[str, Any]] = []
candidates: list[dict[str, Any]] = []
steps: list[dict[str, Any]] = []
pipeline_status = record.get("status")
actual_commit = record.get("commit")
if actual_commit != expected_commit:
anomalies.append(
{
"scope": "pipeline",
"name": str(record.get("number", "unknown")),
"state": pipeline_status,
"reason": "pipeline commit does not equal the expected PR head",
"expected_commit": expected_commit,
"actual_commit": actual_commit,
}
)
if pipeline_status != "success":
anomalies.append(
{
"scope": "pipeline",
"name": str(record.get("number", "unknown")),
"state": pipeline_status,
"reason": "pipeline status is not success",
}
)
workflows = record.get("workflows")
if not isinstance(workflows, list) or not workflows:
anomalies.append(
{
"scope": "pipeline",
"name": str(record.get("number", "unknown")),
"state": pipeline_status,
"reason": "workflows are missing or empty",
}
)
workflows = []
for workflow_index, workflow in enumerate(workflows):
if not isinstance(workflow, dict):
anomalies.append(
{
"scope": "workflow",
"name": str(workflow_index),
"state": None,
"reason": "workflow is not an object",
}
)
continue
workflow_name = str(workflow.get("name", workflow_index))
if workflow.get("state") != "success":
anomalies.append(
{
"scope": "workflow",
"name": workflow_name,
"state": workflow.get("state"),
"reason": "workflow state is not success",
}
)
children = workflow.get("children")
if not isinstance(children, list) or not children:
anomalies.append(
{
"scope": "workflow",
"name": workflow_name,
"state": workflow.get("state"),
"reason": "child-step list is missing or empty",
}
)
continue
for child_index, child in enumerate(children):
if not isinstance(child, dict):
anomalies.append(
{
"scope": "step",
"name": f"{workflow_name}[{child_index}]",
"state": None,
"reason": "step is not an object",
}
)
continue
steps.append(child)
if child.get("state") == "success":
continue
if is_issue_1000_artifact(child):
candidates.append(child)
continue
anomalies.append(
{
"scope": "step",
"name": child.get("name"),
"type": child.get("type"),
"state": child.get("state"),
"exit_code": child.get("exit_code"),
"error": child.get("error"),
"reason": "non-success step does not match the #1000 teardown signature",
}
)
if len(candidates) > 1:
anomalies.append(
{
"scope": "exemption",
"name": EXEMPTION_ID,
"state": "invalid",
"reason": "the #1000 exemption may apply to exactly one step",
}
)
exemption_applies = len(candidates) == 1 and not anomalies
state_counts = Counter(str(step.get("state", "missing")) for step in steps)
result: dict[str, Any] = {
"schema_version": "mosaic-terminal-green/v1",
"verdict": "terminal-green" if not anomalies else "not-terminal-green",
"pipeline_number": record.get("number"),
"commit": actual_commit,
"expected_commit": expected_commit,
"pipeline_status": pipeline_status,
"total_steps": len(steps),
"state_counts": dict(sorted(state_counts.items())),
"exempted_steps": 1 if exemption_applies else 0,
"anomalies": anomalies,
}
if exemption_applies:
candidate = candidates[0]
result["exemptions"] = [
{
"exemption_id": EXEMPTION_ID,
"step": candidate.get("name"),
"signature": candidate.get("error"),
"tracking_issue": EXEMPTION_ISSUE,
"retires_when": "issue #1000 is fixed",
}
]
else:
result["exemptions"] = []
return (0 if not anomalies else 1), result
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="verify the full Woodpecker terminal-green child-step contract"
)
parser.add_argument(
"--expect-commit",
required=True,
metavar="FULL_SHA",
help="full 40-hex PR-head commit that the pipeline record must match",
)
parser.add_argument("record", nargs="?", default="-", help="pipeline JSON file or -")
arguments = parser.parse_args()
if re.fullmatch(r"[0-9a-fA-F]{40}", arguments.expect_commit) is None:
parser.error("--expect-commit must be a full 40-hex commit")
arguments.expect_commit = arguments.expect_commit.lower()
return arguments
def main() -> int:
arguments = parse_arguments()
try:
record = load_record(arguments.record)
except (OSError, ValueError, json.JSONDecodeError) as error:
return fail_usage(str(error))
code, result = verify(record, arguments.expect_commit)
print(json.dumps(result, indent=2, sort_keys=True))
return code
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { generatedSymlinkManifest, sourceFingerprint } from './preflight.mjs';
const scriptRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
function run(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) resolve();
else
reject(
new Error(signal ? `next build terminated by ${signal}` : `next build exited ${code}`),
);
});
});
}
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function requireRealDirectory(target, { allowMissing = false } = {}) {
try {
const stats = await lstat(target);
if (!stats.isDirectory() || stats.isSymbolicLink()) {
throw new Error(`${target} must be a real directory, not a symbolic link.`);
}
} catch (error) {
if (allowMissing && error.code === 'ENOENT') return;
throw error;
}
}
async function acquireBuildLock(root) {
const workRoot = path.join(root, '.mosaic-test-work');
const lock = path.join(workRoot, 'web-build.lock');
const nonce = randomUUID();
const owner = JSON.stringify({ pid: process.pid, nonce });
const deadline = Date.now() + 120_000;
await mkdir(workRoot, { recursive: true });
while (Date.now() < deadline) {
try {
await mkdir(lock);
await writeFile(path.join(lock, 'owner.json'), owner, { mode: 0o600 });
return async () => {
const current = await readFile(path.join(lock, 'owner.json'), 'utf8');
if (current !== owner) throw new Error('Web build lock ownership changed before release.');
const released = `${lock}.released-${nonce}`;
await rename(lock, released);
await rm(released, { recursive: true, force: true });
};
} catch (error) {
if (error.code !== 'EEXIST') throw error;
let lockOwner;
try {
lockOwner = JSON.parse(await readFile(path.join(lock, 'owner.json'), 'utf8'));
} catch (ownerError) {
if (ownerError.code === 'ENOENT') {
await delay(25);
continue;
}
throw new Error(`Web build lock is unreadable at ${lock}.`, { cause: ownerError });
}
try {
process.kill(lockOwner.pid, 0);
} catch (processError) {
if (processError.code !== 'ESRCH') throw processError;
const stale = `${lock}.stale-${nonce}`;
try {
await rename(lock, stale);
await rm(stale, { recursive: true, force: true });
} catch (renameError) {
if (renameError.code !== 'ENOENT') throw renameError;
}
continue;
}
await delay(25);
}
}
throw new Error(`Timed out waiting for the web build lock at ${lock}.`);
}
export async function buildWeb({
root = scriptRoot,
fingerprint = sourceFingerprint,
runBuild = async (webDir) =>
run(path.join(webDir, 'node_modules', '.bin', 'next'), ['build'], {
cwd: webDir,
stdio: 'inherit',
}),
} = {}) {
const releaseLock = await acquireBuildLock(root);
try {
const webDir = path.join(root, 'apps', 'web');
const nextDir = path.join(webDir, '.next');
const certificationMarker = path.join(nextDir, '.mosaic-source-hash');
const symlinkManifest = path.join(nextDir, '.mosaic-symlink-manifest');
const certificationTemporary = `${certificationMarker}.${randomUUID()}.tmp`;
const manifestTemporary = `${symlinkManifest}.${randomUUID()}.tmp`;
const before = await fingerprint(root);
await requireRealDirectory(nextDir, { allowMissing: true });
await Promise.all([
rm(certificationMarker, { force: true }),
rm(symlinkManifest, { force: true }),
]);
await runBuild(webDir);
await requireRealDirectory(nextDir);
const after = await fingerprint(root);
if (after !== before) {
throw new Error(
'Web build inputs changed during next build; generated output was not certified.',
);
}
const manifestContents = await generatedSymlinkManifest(nextDir);
const certificationContents = `${JSON.stringify({
version: 1,
sourceFingerprint: before,
symlinkManifestHash: createHash('sha256').update(manifestContents).digest('hex'),
})}\n`;
await Promise.all([
writeFile(certificationTemporary, certificationContents, { mode: 0o600 }),
writeFile(manifestTemporary, manifestContents, { mode: 0o600 }),
]);
// The certification marker is the commit point. Publishing the manifest first
// leaves interrupted builds untrusted because the marker remains absent.
await rename(manifestTemporary, symlinkManifest);
await rename(certificationTemporary, certificationMarker);
} finally {
await releaseLock();
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await buildWeb();
}
+149
View File
@@ -0,0 +1,149 @@
import assert from 'node:assert/strict';
import { access, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { buildWeb } from './build-web.mjs';
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `build-web-${process.pid}`);
async function fixture(name) {
const root = path.join(fixtureRoot, name);
await mkdir(path.join(root, 'apps', 'web', '.next'), { recursive: true });
return root;
}
async function exists(target) {
try {
await access(target);
return true;
} catch {
return false;
}
}
test.after(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
test('a successful web build atomically publishes its source and symlink certification', async () => {
const root = await fixture('success');
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
await buildWeb({ root, fingerprint: async () => 'certified', runBuild: async () => {} });
assert.deepEqual(JSON.parse(await readFile(marker, 'utf8')), {
version: 1,
sourceFingerprint: 'certified',
symlinkManifestHash: '8a5a375cea6a55d24bd5f875856da63feba33adbefb15a92a0007719b84bcf11',
});
assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
});
test('a failed web build leaves no certification marker', async () => {
const root = await fixture('failure');
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
await writeFile(marker, 'stale\n');
await writeFile(manifest, 'stale\n');
await assert.rejects(
buildWeb({
root,
fingerprint: async () => 'before',
runBuild: async () => {
throw new Error('build failed');
},
}),
/build failed/,
);
assert.equal(await exists(marker), false);
assert.equal(await exists(manifest), false);
});
test('overlapping web builds are serialized while the marker remains absent', async () => {
const root = await fixture('overlap');
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
await writeFile(marker, 'stale\n');
await writeFile(manifest, 'stale\n');
let releaseFirst;
let secondEntered = false;
const firstEntered = new Promise((resolve) => {
releaseFirst = resolve;
});
let markFirstEntered;
const firstStarted = new Promise((resolve) => {
markFirstEntered = resolve;
});
const first = buildWeb({
root,
fingerprint: async () => 'certified',
runBuild: async () => {
markFirstEntered();
await firstEntered;
},
});
await firstStarted;
const second = buildWeb({
root,
fingerprint: async () => 'certified',
runBuild: async () => {
secondEntered = true;
},
});
await new Promise((resolve) => setTimeout(resolve, 75));
assert.equal(secondEntered, false);
assert.equal(await exists(marker), false);
assert.equal(await exists(manifest), false);
releaseFirst();
await Promise.all([first, second]);
assert.equal(secondEntered, true);
assert.equal(JSON.parse(await readFile(marker, 'utf8')).sourceFingerprint, 'certified');
assert.equal(await readFile(manifest, 'utf8'), '{"version":1,"links":[]}\n');
});
test('a build that replaces .next with a symbolic link cannot publish outside the checkout', async () => {
const root = await fixture('symbolic-next');
const nextDir = path.join(root, 'apps', 'web', '.next');
const outside = path.join(root, 'outside-generated');
await mkdir(outside);
await assert.rejects(
buildWeb({
root,
fingerprint: async () => 'certified',
runBuild: async () => {
await rm(nextDir, { recursive: true });
await symlink(outside, nextDir);
},
}),
/must be a real directory/,
);
assert.equal(await exists(path.join(outside, '.mosaic-source-hash')), false);
assert.equal(await exists(path.join(outside, '.mosaic-symlink-manifest')), false);
});
test('inputs changed during a web build are not certified', async () => {
const root = await fixture('changed-inputs');
const marker = path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash');
const manifest = path.join(root, 'apps', 'web', '.next', '.mosaic-symlink-manifest');
const fingerprints = ['before', 'after'];
await assert.rejects(
buildWeb({
root,
fingerprint: async () => fingerprints.shift(),
runBuild: async () => {},
}),
/inputs changed during next build/,
);
assert.equal(await exists(marker), false);
assert.equal(await exists(manifest), false);
});
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
import { access, mkdir, rename, rm } from 'node:fs/promises';
import path from 'node:path';
const root = process.cwd();
const generated = path.join(root, 'apps', 'web', '.next');
const quarantineRoot = path.join(root, '.mosaic-test-work', 'generated-quarantine');
try {
await access(generated);
} catch (error) {
if (error.code === 'ENOENT') process.exit(0);
throw error;
}
await mkdir(quarantineRoot, { recursive: true });
const quarantine = path.join(quarantineRoot, `web-next-${Date.now()}-${process.pid}`);
try {
await rename(generated, quarantine);
} catch (error) {
console.error(
`MOSAIC_GENERATED_CLEAN_FAILED: could not quarantine apps/web/.next. Fix: sudo rm -rf '${generated}', then rerun pnpm preflight`,
);
throw error;
}
try {
await rm(quarantine, { recursive: true, force: true });
} catch {
console.warn(
`Generated state was deactivated but could not be deleted; quarantined at ${quarantine}`,
);
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const root = process.cwd();
const manifest = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
manifest.gateRoots = ['.mosaic-empty-gate-root'];
manifest.criteria = [];
manifest.gates = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-empty-population-'));
try {
const manifestPath = path.join(directory, 'manifest.json');
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
structureOnly: true,
fixtureProfile: false,
});
const required = ['criteria', 'gates', 'proseClaims', 'compatibilityScenarios'];
const missing = required.filter(
(population) =>
!result.failures.some((failure) =>
failure.includes(`${population} population must be non-empty and anchored`),
),
);
if (missing.length > 0) {
process.stdout.write(`empty populations were not rejected: ${missing.join(', ')}\n`);
process.exitCode = 0;
} else {
process.stderr.write(
'empty universally quantified registry populations rejected before evaluation\n',
);
process.exitCode = 1;
}
} finally {
await rm(directory, { recursive: true, force: true });
}
@@ -0,0 +1,48 @@
#!/usr/bin/env node
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { assertCurrentTreeObservation } from './gate-verify.mjs';
const prohibitedReports = [
'HISTORY PROVENANCE VERIFIED local-ref',
'COMMIT ANCESTRY VERIFIED origin/main',
'PROVIDER LINEAGE SUCCESS pipeline-7',
];
const accepted = prohibitedReports.filter((report) => {
try {
assertCurrentTreeObservation(report);
return true;
} catch {
return false;
}
});
const verifier = await readFile(path.join(process.cwd(), 'scripts', 'gate-verify.mjs'), 'utf8');
const stdoutWrites = [...verifier.matchAll(/process\.stdout\.write\s*\(/g)].length;
const stderrWrites = [...verifier.matchAll(/process\.stderr\.write\s*\(/g)].length;
const gatedErrorWrites = [...verifier.matchAll(/process\.stderr\.write\(`GATE VERIFY FAILED:/g)]
.length;
const currentTreeWriterCalls = [...verifier.matchAll(/writeCurrentTreeOutput\s*\(/g)].length;
const productionRendererWired =
stdoutWrites === 1 &&
stderrWrites === 2 &&
gatedErrorWrites === 2 &&
currentTreeWriterCalls === 3 &&
/function writeCurrentTreeOutput\(output\) \{\s*assertCurrentTreeObservation\(output\);\s*process\.stdout\.write/s.test(
verifier,
) &&
/for \(const observation of observations\) \{\s*writeCurrentTreeOutput\(observation\);\s*\}/s.test(
verifier,
) &&
/writeCurrentTreeOutput\(\s*`REGISTRY SUMMARY open behavior deltas:/s.test(verifier) &&
!/\bconsole\.(?:log|info|debug|error|warn)\s*\(/.test(verifier);
if (accepted.length > 0 || !productionRendererWired) {
process.stderr.write(
`HISTORY_PROVENANCE_FORBIDDEN: closed current-tree observation renderer rejected=${prohibitedReports.length - accepted.length}/${prohibitedReports.length} production-wired=${productionRendererWired}\n`,
);
process.exit(79);
}
process.stdout.write('history provenance reporting capability is absent; owner RM-60\n');
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
import { readFile } from 'node:fs/promises';
import path from 'node:path';
const boundary =
"Detects accidental and incompetent inventory drift within a checkout; does NOT survive an adversary who rewrites baseline, manifest, and verifier consistently — that guarantee requires RM-60's external boundary.";
const artifacts = [
'gates/required-gates.baseline.json',
'gates/gates.manifest.json',
'docs/PRD.md',
'docs/ADMIN-GUIDE/quality-gate-registry.md',
'docs/DEVELOPER-GUIDE/quality-gate-registry.md',
'docs/remediation/GATE-CLAIMS.md',
'docs/plans/2026-08-01-rm-02-gate-registry.md',
'docs/SITEMAP.md',
];
const forbidden = [
/\bindependent\b[^\n.]{0,80}\bbaseline\b/i,
/independently baselined/i,
/independently anchored inventory/i,
/protected (?:inventory )?(?:anchor|baseline)/i,
/inventory (?:anchor|anchored)/i,
];
const failures = [];
for (const relativePath of artifacts) {
const contents = await readFile(path.join(process.cwd(), relativePath), 'utf8');
const claims =
relativePath === 'gates/gates.manifest.json'
? JSON.parse(contents)
.criteria.map((criterion) => criterion.currentText)
.join('\n')
: contents;
if (!claims.includes(boundary)) failures.push(`${relativePath}: narrowed boundary is missing`);
const overclaim = forbidden.find((pattern) => pattern.test(claims));
if (overclaim) failures.push(`${relativePath}: inventory protection is overstated`);
}
if (failures.length > 0) {
for (const failure of failures) {
process.stderr.write(`INVENTORY_CLAIM_OVERSTATED: ${failure}\n`);
}
process.exit(84);
}
process.stdout.write('inventory drift boundary is stated in both directions; owner RM-60\n');
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const root = process.cwd();
const removedGateId = 'hook-pre-push';
const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n";
function shrinkManifest(manifest) {
const removedGate = manifest.gates.find((gate) => gate.id === removedGateId);
assert.ok(removedGate);
const removedCaseRefs = new Set(
removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`),
);
const removedCriterionIds = new Set(
manifest.criteria
.filter(
(criterion) =>
criterion.caseRefs.length > 0 &&
criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)),
)
.map((criterion) => criterion.id),
);
manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId);
manifest.criteria = manifest.criteria
.filter((criterion) => !removedCriterionIds.has(criterion.id))
.map((criterion) => ({
...criterion,
caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
...(criterion.gateRefs
? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) }
: {}),
}));
manifest.proseClaims = manifest.proseClaims.filter(
(claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef),
);
manifest.compatibilityScenarios = manifest.compatibilityScenarios
.map((scenario) => ({
...scenario,
caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
}))
.filter((scenario) => scenario.caseRefs.length > 0);
for (const gate of manifest.gates) {
for (const gateCase of gate.cases) {
gateCase.criterionIds = gateCase.criterionIds.filter(
(criterionId) => !removedCriterionIds.has(criterionId),
);
}
}
}
async function attack(mode) {
const fixture = await mkdtemp(path.join(os.tmpdir(), `gate-inventory-${mode}-`));
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
await mkdir(path.join(fixture, 'gates'), { recursive: true });
const source = await readFile(path.join(root, 'scripts', 'gate-verify.mjs'), 'utf8');
if (source.split(inventoryEntry).length - 1 !== 1) {
throw new Error('source inventory fixture drifted');
}
await writeFile(
path.join(fixture, 'scripts', 'gate-verify.mjs'),
mode === 'source-manifest' ? source.replace(inventoryEntry, '') : source,
);
const baseline = JSON.parse(
await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'),
);
if (mode === 'baseline-manifest') {
baseline.gates = baseline.gates.filter((gate) => gate.id !== removedGateId);
}
await writeFile(
path.join(fixture, 'gates', 'required-gates.baseline.json'),
`${JSON.stringify(baseline)}\n`,
);
const manifest = JSON.parse(
await readFile(path.join(root, 'gates', 'gates.manifest.json'), 'utf8'),
);
shrinkManifest(manifest);
await writeFile(
path.join(fixture, 'gates', 'gates.manifest.json'),
`${JSON.stringify(manifest)}\n`,
);
const result = spawnSync(
process.execPath,
[
path.join(fixture, 'scripts', 'gate-verify.mjs'),
'--root',
fixture,
'--manifest',
'gates/gates.manifest.json',
'--structure-only',
],
{ cwd: fixture, encoding: 'utf8' },
);
const combined = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
return (
result.status !== 0 &&
new RegExp(`(?:baseline|verifier inventory).*${removedGateId}`, 'i').test(combined)
);
} finally {
await rm(fixture, { recursive: true, force: true });
}
}
const sourceManifestRejected = await attack('source-manifest');
const baselineManifestRejected = await attack('baseline-manifest');
if (sourceManifestRejected && baselineManifestRejected) {
process.stderr.write(
'INVENTORY_SHRINK_REJECTED: source+manifest and baseline+manifest shrink attacks detected\n',
);
process.exit(83);
}
process.stdout.write(
`inventory shrink attack escaped: source-manifest=${sourceManifestRejected} baseline-manifest=${baselineManifestRejected}\n`,
);
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env node
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { verifyRegistry } from './gate-verify.mjs';
const mode = process.argv[2];
const root = process.cwd();
const source = JSON.parse(await readFile(path.join(root, 'gates/gates.manifest.json'), 'utf8'));
const expectedGateIds = source.gates.map((gate) => gate.id);
if (expectedGateIds.length === 0) {
process.stderr.write('gate population control requires a non-empty anchored inventory\n');
process.exit(2);
}
async function rejectedForEveryGate(mutate, diagnostic) {
for (const gateId of expectedGateIds) {
const manifest = structuredClone(source);
const gate = manifest.gates.find((candidate) => candidate.id === gateId);
mutate(gate);
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-population-control-'));
const manifestPath = path.join(directory, 'manifest.json');
try {
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root,
manifest: manifestPath,
structureOnly: true,
fixtureProfile: false,
});
if (!result.failures.some((failure) => diagnostic(failure, gateId))) return false;
} finally {
await rm(directory, { recursive: true, force: true });
}
}
return true;
}
let rejected;
if (mode === 'evidence-subject') {
rejected = true;
for (const gateId of expectedGateIds) {
const directory = await mkdtemp(path.join(os.tmpdir(), 'gate-evidence-consumption-'));
try {
await mkdir(path.join(directory, 'gates'), { recursive: true });
const probe = path.join(directory, 'gates', 'probe.sh');
await writeFile(probe, '#!/bin/sh\necho EVIDENCE_PROBE >&2\nexit 7\n');
await chmod(probe, 0o755);
const manifest = {
schemaVersion: 1,
gateRoots: ['gates'],
governingClaimFiles: [],
coverageBoundary: { included: ['evidence fixture'], excluded: [], trackedBy: 'RM-02' },
criteria: [
{
id: 'EVIDENCE-CONSUMPTION',
originalText: 'Consumed evidence stays bound to its gate.',
currentText: 'Consumed evidence stays bound to its gate.',
claimType: 'integrity',
source: 'gate-population-control',
meaningChanges: [],
caseRefs: [`${gateId}/probe`],
},
],
proseClaims: [],
compatibilityScenarios: [],
gates: [
{
id: gateId,
source: 'gates/probe.sh',
invocation: ['gates/probe.sh'],
deployment: { kind: 'none', reason: 'population fixture' },
inertMutation: {
file: 'gates/probe.sh',
find: 'exit 7',
replace: 'exit 0',
caseId: 'probe',
expected: { exitCode: 0 },
},
cases: [
{
id: 'probe',
criterionIds: ['EVIDENCE-CONSUMPTION'],
mustFail: true,
required: { exitCode: 7 },
actual: { exitCode: 7 },
evidence: { subject: 'different-gate-subject' },
reasonPattern: 'EVIDENCE_PROBE',
},
],
},
],
};
const manifestPath = path.join(directory, 'gates', 'gates.manifest.json');
await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`);
const result = await verifyRegistry({
root: directory,
manifest: manifestPath,
structureOnly: false,
fixtureProfile: true,
});
if (
!result.failures.some(
(failure) =>
failure.includes(`gate ${gateId}: consumed evidence subject`) &&
failure.includes('does not match gate definition'),
)
) {
rejected = false;
break;
}
} finally {
await rm(directory, { recursive: true, force: true });
}
}
} else if (mode === 'type-strict') {
rejected = await rejectedForEveryGate(
(gate) => {
gate.cases[0].actual.exitCode = '0';
},
(failure, gateId) =>
failure.includes(
`${gateId}/${source.gates.find((gate) => gate.id === gateId).cases[0].id}.actual.exitCode`,
) && failure.includes('expected an integer'),
);
} else {
process.stderr.write(`unknown gate population control ${String(mode)}\n`);
process.exit(2);
}
if (!rejected) {
process.stdout.write(`${mode} population control did not reject every registered gate\n`);
process.exit(0);
}
process.stderr.write(`${mode} population control rejected every registered gate\n`);
process.exit(1);
+283
View File
@@ -0,0 +1,283 @@
import assert from 'node:assert/strict';
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
const root = process.cwd();
const verifierPath = path.join(root, 'scripts', 'gate-verify.mjs');
const manifestPath = path.join(root, 'gates', 'gates.manifest.json');
const requiredGateId = 'hook-pre-push';
function output(result) {
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
}
function shrinkManifest(manifest, removedGateId) {
const removedGate = manifest.gates.find((gate) => gate.id === removedGateId);
assert.ok(removedGate, `fixture gate ${removedGateId} must exist`);
const removedCaseRefs = new Set(
removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`),
);
const removedCriterionIds = new Set(
manifest.criteria
.filter(
(criterion) =>
criterion.caseRefs.length > 0 &&
criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)),
)
.map((criterion) => criterion.id),
);
manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId);
manifest.criteria = manifest.criteria
.filter((criterion) => !removedCriterionIds.has(criterion.id))
.map((criterion) => ({
...criterion,
caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
...(criterion.gateRefs
? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) }
: {}),
}));
manifest.proseClaims = manifest.proseClaims.filter(
(claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef),
);
manifest.compatibilityScenarios = manifest.compatibilityScenarios
.map((scenario) => ({
...scenario,
caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
}))
.filter((scenario) => scenario.caseRefs.length > 0);
for (const gate of manifest.gates) {
for (const gateCase of gate.cases) {
gateCase.criterionIds = gateCase.criterionIds.filter(
(criterionId) => !removedCriterionIds.has(criterionId),
);
}
}
}
test('shrinking the verifier inventory and manifest together is rejected by a same-checkout baseline', async () => {
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-both-'));
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
await mkdir(path.join(fixture, 'gates'), { recursive: true });
const verifier = await readFile(verifierPath, 'utf8');
const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n";
assert.equal(verifier.split(inventoryEntry).length - 1, 1, 'source inventory fixture drifted');
await writeFile(
path.join(fixture, 'scripts', 'gate-verify.mjs'),
verifier.replace(inventoryEntry, ''),
);
await copyFile(
path.join(root, 'gates', 'required-gates.baseline.json'),
path.join(fixture, 'gates', 'required-gates.baseline.json'),
);
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
shrinkManifest(manifest, requiredGateId);
await writeFile(
path.join(fixture, 'gates', 'gates.manifest.json'),
`${JSON.stringify(manifest)}\n`,
);
const result = spawnSync(
process.execPath,
[
path.join(fixture, 'scripts', 'gate-verify.mjs'),
'--root',
fixture,
'--manifest',
'gates/gates.manifest.json',
'--structure-only',
],
{ cwd: fixture, encoding: 'utf8' },
);
assert.notEqual(result.status, 0, 'shrinking source anchor and manifest together must go red');
assert.match(output(result), /same-checkout required-gate baseline.*hook-pre-push/i);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
test('shrinking the same-checkout baseline and manifest together is rejected by verifier inventory', async () => {
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-baseline-manifest-'));
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
await mkdir(path.join(fixture, 'gates'), { recursive: true });
await copyFile(verifierPath, path.join(fixture, 'scripts', 'gate-verify.mjs'));
const baseline = JSON.parse(
await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'),
);
baseline.gates = baseline.gates.filter((gate) => gate.id !== requiredGateId);
await writeFile(
path.join(fixture, 'gates', 'required-gates.baseline.json'),
`${JSON.stringify(baseline)}\n`,
);
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
shrinkManifest(manifest, requiredGateId);
await writeFile(
path.join(fixture, 'gates', 'gates.manifest.json'),
`${JSON.stringify(manifest)}\n`,
);
const result = spawnSync(
process.execPath,
[
path.join(fixture, 'scripts', 'gate-verify.mjs'),
'--root',
fixture,
'--manifest',
'gates/gates.manifest.json',
'--structure-only',
],
{ cwd: fixture, encoding: 'utf8' },
);
assert.notEqual(result.status, 0, 'shrinking baseline and manifest together must go red');
assert.match(output(result), /verifier inventory.*hook-pre-push.*baseline/i);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
test('every gate carries an evidence-side subject distinct from its definition', async () => {
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
for (const gate of manifest.gates) {
assert.ok(gate.cases.length > 0, `${gate.id} must have consumable evidence`);
for (const gateCase of gate.cases) {
assert.equal(
gateCase.evidence?.subject,
gate.id,
`${gate.id}/${gateCase.id} must source its subject from the evidence record`,
);
}
}
});
test('inventory claim control rejects qualified independence wording', async () => {
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-inventory-overclaim-'));
const artifacts = [
'gates/required-gates.baseline.json',
'gates/gates.manifest.json',
'docs/PRD.md',
'docs/ADMIN-GUIDE/quality-gate-registry.md',
'docs/DEVELOPER-GUIDE/quality-gate-registry.md',
'docs/remediation/GATE-CLAIMS.md',
'docs/plans/2026-08-01-rm-02-gate-registry.md',
'docs/SITEMAP.md',
];
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
await copyFile(
path.join(root, 'scripts', 'gate-inventory-claim-control.mjs'),
path.join(fixture, 'scripts', 'gate-inventory-claim-control.mjs'),
);
for (const relativePath of artifacts) {
const target = path.join(fixture, relativePath);
await mkdir(path.dirname(target), { recursive: true });
await copyFile(path.join(root, relativePath), target);
}
const prd = path.join(fixture, 'docs', 'PRD.md');
await writeFile(
prd,
`${await readFile(prd, 'utf8')}\nThis provides an independent seven-gate baseline comparison.\n`,
);
const result = spawnSync(
process.execPath,
[path.join(fixture, 'scripts', 'gate-inventory-claim-control.mjs')],
{ cwd: fixture, encoding: 'utf8' },
);
assert.equal(result.status, 84, output(result));
assert.match(output(result), /INVENTORY_CLAIM_OVERSTATED.*docs\/PRD\.md/i);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
test('evidence population control depends on production result consumption wiring', async () => {
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-evidence-consumer-inert-'));
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
await mkdir(path.join(fixture, 'gates'), { recursive: true });
const verifier = await readFile(verifierPath, 'utf8');
const consumer = ` const subjectFailure = consumeEvidenceSubject(gate, result.evidence);\n if (subjectFailure) failures.push(subjectFailure);\n`;
assert.equal(verifier.split(consumer).length - 1, 1, 'consumer fixture drifted');
await writeFile(
path.join(fixture, 'scripts', 'gate-verify.mjs'),
verifier.replace(consumer, ''),
);
await copyFile(
path.join(root, 'scripts', 'gate-population-control.mjs'),
path.join(fixture, 'scripts', 'gate-population-control.mjs'),
);
await copyFile(manifestPath, path.join(fixture, 'gates', 'gates.manifest.json'));
const result = spawnSync(
process.execPath,
[path.join(fixture, 'scripts', 'gate-population-control.mjs'), 'evidence-subject'],
{ cwd: fixture, encoding: 'utf8' },
);
assert.equal(result.status, 0, output(result));
assert.match(output(result), /did not reject every registered gate/i);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
test('production verification has a closed current-tree observation renderer', async () => {
const verifier = await readFile(verifierPath, 'utf8');
assert.doesNotMatch(verifier, /from ['"]\.\/gate-history\.mjs['"]/);
assert.doesNotMatch(verifier, /\bverifyHistory\s*\(/);
assert.doesNotMatch(verifier, /history[-_ ]?provenance/i);
const attacks = [
[
'history wording',
' /^META-NEGATIVE-CONTROL /,',
' /^HISTORY PROVENANCE VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
],
[
'renamed ancestry wording',
' /^META-NEGATIVE-CONTROL /,',
' /^COMMIT ANCESTRY VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
],
[
'provider lineage wording',
' /^META-NEGATIVE-CONTROL /,',
' /^PROVIDER LINEAGE SUCCESS /,\n /^META-NEGATIVE-CONTROL /,',
],
['writer assertion bypass', ' assertCurrentTreeObservation(output);\n', ''],
[
'final success stdout bypass',
' writeCurrentTreeOutput(\n `REGISTRY SUMMARY open behavior deltas: ${defects}; required-behavior conformance is not asserted while deltas remain`,\n );',
" process.stdout.write('COMMIT ANCESTRY VERIFIED\\n');",
],
[
'direct success stderr bypass',
" const defects = observations.filter((line) => line.startsWith('DEFECT ')).length;",
" process.stderr.write('PROVIDER LINEAGE SUCCESS\\n');\n const defects = observations.filter((line) => line.startsWith('DEFECT ')).length;",
],
];
for (const [name, find, replace] of attacks) {
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-history-renderer-'));
try {
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
assert.equal(verifier.split(find).length - 1, 1, `${name}: fixture drifted`);
await writeFile(
path.join(fixture, 'scripts', 'gate-verify.mjs'),
verifier.replace(find, replace),
);
await copyFile(
path.join(root, 'scripts', 'gate-history-exclusion-control.mjs'),
path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs'),
);
const result = spawnSync(
process.execPath,
[path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs')],
{ cwd: fixture, encoding: 'utf8' },
);
assert.equal(result.status, 79, `${name}: ${output(result)}`);
assert.match(output(result), /HISTORY_PROVENANCE_FORBIDDEN/);
} finally {
await rm(fixture, { recursive: true, force: true });
}
}
});
File diff suppressed because it is too large Load Diff
+785
View File
@@ -0,0 +1,785 @@
import assert from 'node:assert/strict';
import { chmod, copyFile, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import test from 'node:test';
const verifier = path.join(process.cwd(), 'scripts', 'gate-verify.mjs');
const fixtureRunner = path.join(
process.cwd(),
'scripts',
'test-support',
'gate-verify-fixture-runner.mjs',
);
const fixtureBase = path.join(process.cwd(), '.mosaic-test-work', `gate-verify-${process.pid}`);
async function fixture(name = 'case') {
const root = path.join(fixtureBase, name);
await rm(root, { recursive: true, force: true });
await mkdir(path.join(root, 'gates'), { recursive: true });
return root;
}
function baseManifest() {
return {
schemaVersion: 1,
gateRoots: ['gates'],
governingClaimFiles: [],
coverageBoundary: { included: ['meta fixture'], excluded: [], trackedBy: 'RM-54' },
criteria: [
{
id: 'META-CRIT-1',
originalText: 'The fixture rejects its bad input.',
currentText: 'The fixture rejects its bad input.',
claimType: 'integrity',
source: 'fixture',
meaningChanges: [],
caseRefs: ['meta-fixture/rejects-bad-input'],
},
],
compatibilityScenarios: [],
proseClaims: [],
gates: [
{
id: 'meta-fixture',
source: 'gates/meta-fixture.sh',
invocation: ['gates/meta-fixture.sh'],
deployment: { kind: 'none', reason: 'test fixture only' },
inertMutation: {
file: 'gates/meta-fixture.sh',
find: 'exit 7',
replace: 'exit 0',
expected: { exitCode: 0 },
},
cases: [
{
id: 'rejects-bad-input',
criterionIds: ['META-CRIT-1'],
mustFail: true,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 7 },
actual: { exitCode: 7 },
evidence: { subject: 'meta-fixture' },
reasonPattern: 'META_REJECT',
},
],
},
],
};
}
async function writeGate(root, contents = '#!/bin/sh\necho META_REJECT >&2\nexit 7\n') {
const target = path.join(root, 'gates', 'meta-fixture.sh');
await writeFile(target, contents);
await chmod(target, 0o755);
}
async function writeManifest(root, manifest) {
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), `${JSON.stringify(manifest)}\n`);
}
function verify(root, extraArgs = []) {
return spawnSync(
process.execPath,
[fixtureRunner, '--root', root, '--manifest', 'gates/gates.manifest.json', ...extraArgs],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
}
function verifyProductionStructure(root, extraArgs = []) {
return spawnSync(
process.execPath,
[
verifier,
'--root',
root,
'--manifest',
'gates/gates.manifest.json',
'--structure-only',
...extraArgs,
],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
}
function output(result) {
return `${result.stdout}\n${result.stderr}`;
}
test.after(async () => {
await rm(fixtureBase, { recursive: true, force: true });
});
test('universally quantified registry checks reject empty populations before evaluation', async () => {
const root = await fixture('empty-registry-populations');
await mkdir(path.join(root, 'empty-gate-root'), { recursive: true });
const manifest = baseManifest();
manifest.gateRoots = ['empty-gate-root'];
manifest.criteria = [];
manifest.proseClaims = [];
manifest.compatibilityScenarios = [];
manifest.gates = [];
await writeManifest(root, manifest);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /criteria population.*non-empty.*anchored/i);
assert.match(output(result), /gates population.*non-empty.*anchored/i);
assert.match(output(result), /proseClaims population.*non-empty.*anchored/i);
assert.match(output(result), /compatibilityScenarios population.*non-empty.*anchored/i);
});
test('production verifier exposes no fixture-profile population bypass', async () => {
const root = await fixture('no-production-fixture-profile');
const result = verifyProductionStructure(root, ['--fixture-profile']);
assert.notEqual(result.status, 0);
assert.match(output(result), /unknown option: --fixture-profile/i);
});
test('anchored gate inventory and population criteria cannot shrink together', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('shrunken-gate-population');
await copyFile(
path.join(process.cwd(), 'gates', 'required-gates.baseline.json'),
path.join(root, 'gates', 'required-gates.baseline.json'),
);
source.gates = source.gates.filter((gate) => gate.id !== 'hook-pre-push');
for (const criterion of source.criteria) {
if (criterion.gateRefs) {
criterion.gateRefs = criterion.gateRefs.filter((gateId) => gateId !== 'hook-pre-push');
}
criterion.caseRefs = criterion.caseRefs.filter(
(caseRef) => !caseRef.startsWith('hook-pre-push/'),
);
}
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
/same-checkout required-gate baseline rejects manifest drift at hook-pre-push/i,
);
});
test('general population criteria cannot delete their gateRefs binding', async () => {
const requiredCriteria = [
'RM02-EVIDENCE-SUBJECT-BINDING',
'RM02-TYPE-STRICT-SCHEMA',
'RM02-NONEMPTY-ANCHORED-QUANTIFICATION',
];
for (const criterionId of requiredCriteria) {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture(`missing-gate-refs-${criterionId}`);
delete source.criteria.find((criterion) => criterion.id === criterionId).gateRefs;
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
new RegExp(`${criterionId}.*gateRefs.*required`, 'i'),
criterionId,
);
}
});
test('general population criteria must span every registered gate', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const root = await fixture('incomplete-gate-refs');
source.criteria.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING').gateRefs =
source.criteria
.find((criterion) => criterion.id === 'RM02-EVIDENCE-SUBJECT-BINDING')
.gateRefs.filter((gateId) => gateId !== 'quality-lint');
await writeManifest(root, source);
const result = verifyProductionStructure(root);
assert.notEqual(result.status, 0);
assert.match(
output(result),
/RM02-EVIDENCE-SUBJECT-BINDING.*gate population binding is missing quality-lint/i,
);
});
test('an externally inerted failure branch makes verification nonzero and names the gate', async () => {
const root = await fixture('external-inert');
await writeGate(root, '#!/bin/sh\nexit 0\n');
await writeManifest(root, baseManifest());
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture/);
});
test('the verifier applies the declared inert mutation and observes its own control red', async () => {
const root = await fixture('internal-meta');
await writeGate(root);
await writeManifest(root, baseManifest());
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.match(output(result), /META-NEGATIVE-CONTROL.*meta-fixture.*observed red/i);
});
test('a mutation crash is rejected instead of counted as an observed-red control', async () => {
const root = await fixture('mutation-crash');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].inertMutation.replace = 'this is not valid shell (';
manifest.gates[0].inertMutation.expected = { exitCode: 0 };
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*mutation.*unexpected outcome/i);
assert.doesNotMatch(output(result), /META-NEGATIVE-CONTROL.*observed red/i);
});
test('a stale declared mutation case id is rejected instead of falling back', async () => {
const root = await fixture('stale-case-id');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].inertMutation.caseId = 'case-that-does-not-exist';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*case-that-does-not-exist.*not found/i);
});
test('duplicate stable ids and unsupported schema versions are rejected', async () => {
const root = await fixture('closed-schema');
await writeGate(root);
const manifest = baseManifest();
manifest.schemaVersion = 99;
manifest.criteria.push({ ...manifest.criteria[0] });
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /unsupported schemaVersion 99/i);
assert.match(output(result), /duplicate criterion id META-CRIT-1/i);
});
test('misspelled nested assertion fields are rejected instead of becoming optional', async () => {
const root = await fixture('nested-schema-typo');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].required.outputPatern = 'META_REJECT';
manifest.gates[0].cases[0].actual.outputPatern = 'META_REJECT';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /required.*unknown field outputPatern/i);
assert.match(output(result), /actual.*unknown field outputPatern/i);
});
test('present outcome patterns cannot be empty assertion bypasses', async () => {
const root = await fixture('nested-schema-empty-patterns');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].required.outputPattern = '';
manifest.gates[0].cases[0].actual.notOutputPattern = ' ';
await writeManifest(root, manifest);
const result = verify(root, ['--structure-only']);
assert.notEqual(result.status, 0);
assert.match(output(result), /required.outputPattern: expected a non-empty pattern/i);
assert.match(output(result), /actual.notOutputPattern: expected a non-empty pattern/i);
});
test('nested discriminator and comparison fields reject wrong types', async () => {
const root = await fixture('nested-schema-types');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].mustFail = 'true';
manifest.gates[0].cases[0].required.exitCode = '7';
manifest.gates[0].cases[0].actual.outputPattern = 7;
await writeManifest(root, manifest);
const result = verify(root, ['--structure-only']);
assert.notEqual(result.status, 0);
assert.match(output(result), /mustFail: expected a boolean/i);
assert.match(output(result), /required.exitCode: expected an integer/i);
assert.match(output(result), /actual.outputPattern: expected a string/i);
});
test('recursive closed-schema guards reject unknown fields in every nested assertion object', async () => {
const source = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
const checkout = source.gates.find((gate) => gate.id === 'checkout-preflight');
const stale = checkout.cases.find((gateCase) => gateCase.id === 'stale-build-lock');
const queue = source.gates.find((gate) => gate.id === 'ci-queue-wait');
const queueCase = queue.cases.find((gateCase) => gateCase.id === 'terminal-success');
const targets = [
['coverageBoundary', (manifest) => manifest.coverageBoundary],
['mergeAssertions', (manifest) => manifest.mergeAssertions],
[
'meaningChanges',
(manifest) =>
manifest.criteria.find((criterion) => criterion.meaningChanges.length).meaningChanges[0],
],
['proseClaims', (manifest) => manifest.proseClaims[0]],
['compatibility expected', (manifest) => manifest.compatibilityScenarios[0].expected],
[
'deployment',
(manifest) => manifest.gates.find((gate) => gate.id === 'ci-queue-wait').deployment,
],
['inertMutation', (manifest) => manifest.gates[0].inertMutation],
['inert expected', (manifest) => manifest.gates[0].inertMutation.expected],
['required', (manifest) => manifest.gates[0].cases[0].required],
['actual', (manifest) => manifest.gates[0].cases[0].actual],
[
'fixture',
(manifest) =>
manifest.gates
.find((gate) => gate.id === 'checkout-preflight')
.cases.find((gateCase) => gateCase.id === 'stale-build-lock').fixture,
],
[
'write entry',
(manifest) =>
manifest.gates
.find((gate) => gate.id === 'checkout-preflight')
.cases.find((gateCase) => gateCase.id === 'stale-build-lock').fixture.writeFiles[0],
],
[
'replace entry',
(manifest) =>
manifest.gates
.find((gate) => gate.id === 'checkout-preflight')
.cases.find((gateCase) => gateCase.id === 'criterion-misbinding').fixture.replaceFiles[0],
],
[
'defect',
(manifest) =>
manifest.gates
.find((gate) => gate.id === 'ci-queue-wait')
.cases.find((gateCase) => gateCase.id === 'terminal-success').defect,
],
];
assert.ok(stale.fixture && queueCase.defect);
for (const [name, select] of targets) {
const root = await fixture(`recursive-${name.replaceAll(' ', '-')}`);
const manifest = structuredClone(source);
select(manifest).unexpectedNestedField = true;
await writeManifest(root, manifest);
const result = verify(root, ['--structure-only']);
assert.notEqual(result.status, 0, `${name}: ${output(result)}`);
assert.match(output(result), /unknown field unexpectedNestedField/i, name);
}
});
test('manifest-controlled fixture paths cannot escape the sandbox', async () => {
const root = await fixture('path-traversal');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: '../../escaped-by-manifest', content: 'bad' }],
};
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /path escapes sandbox/i);
});
test('fixture writes reject a final symlink and preserve its outside target', async () => {
const root = await fixture('final-symlink');
await writeGate(root);
const outside = path.join(fixtureBase, 'outside-sentinel');
await writeFile(outside, 'preserve-me\n');
const linked = path.join(root, 'linked-sentinel');
await symlink(outside, linked);
const manifest = baseManifest();
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: 'linked-sentinel', content: 'overwritten\n' }],
};
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /fixture write.*symbolic link/i);
assert.equal(await readFile(outside, 'utf8'), 'preserve-me\n');
});
test('an executable below a gate root without an entry is rejected', async () => {
const root = await fixture('unregistered');
await writeGate(root);
const extra = path.join(root, 'gates', 'forgotten.sh');
await writeFile(extra, '#!/bin/sh\nexit 1\n');
await chmod(extra, 0o755);
await writeManifest(root, baseManifest());
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /unregistered gate.*forgotten\.sh/i);
});
test('a must-fail case without a reason diagnostic is rejected', async () => {
const root = await fixture('missing-reason');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].reasonPattern = '';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*rejects-bad-input.*reasonPattern/i);
});
test('a gate with zero must-fail cases is rejected', async () => {
const root = await fixture('no-negative');
await writeGate(root, '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].inertMutation = {
file: 'gates/meta-fixture.sh',
find: 'exit 0',
replace: 'exit 1',
};
manifest.gates[0].cases = [
{
id: 'positive',
criterionIds: ['META-CRIT-1'],
mustFail: false,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 0 },
actual: { exitCode: 0 },
reasonPattern: '',
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*no negative control/i);
});
test('reordered but equivalent outcome fields do not create a false behavior delta', async () => {
const root = await fixture('reordered-outcomes');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].required = { exitCode: 7, outputPattern: 'META_REJECT' };
manifest.gates[0].cases[0].actual = { outputPattern: 'META_REJECT', exitCode: 7 };
await writeManifest(root, manifest);
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.doesNotMatch(output(result), /behavior delta requires|DEFECT \(owner:/);
});
test('a required-versus-actual delta without a tracked owner is rejected', async () => {
const root = await fixture('ownerless-delta');
await writeGate(root, '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].inertMutation.find = 'exit 0';
manifest.gates[0].inertMutation.replace = 'exit 7';
manifest.gates[0].cases[0].actual.exitCode = 0;
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*delta.*tracked owner/i);
});
test('moving criterion bindings to unrelated cases is rejected', async () => {
const root = await fixture('semantic-misbinding');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'META-CRIT-2',
originalText: 'The fixture reports the second rejection reason.',
currentText: 'The fixture reports the second rejection reason.',
claimType: 'integrity',
source: 'fixture',
meaningChanges: [],
caseRefs: ['meta-fixture/rejects-second-input'],
});
manifest.gates[0].cases.push({
...manifest.gates[0].cases[0],
id: 'rejects-second-input',
criterionIds: ['META-CRIT-1'],
});
manifest.gates[0].cases[0].criterionIds = ['META-CRIT-2'];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /META-CRIT-1.*rejects-bad-input.*not bound/i);
assert.match(output(result), /META-CRIT-2.*rejects-second-input.*not bound/i);
});
test('canonical verification preserves binding diagnostics when a case fixture is also stale', async () => {
const root = await fixture('misbinding-plus-stale-fixture');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'META-CRIT-2',
originalText: 'The second case rejects its own bad input.',
currentText: 'The second case rejects its own bad input.',
claimType: 'integrity',
source: 'fixture',
meaningChanges: [],
caseRefs: ['meta-fixture/rejects-second-input'],
});
manifest.gates[0].cases.push({
...manifest.gates[0].cases[0],
id: 'rejects-second-input',
criterionIds: ['META-CRIT-1'],
});
manifest.gates[0].cases[0].criterionIds = ['META-CRIT-2'];
manifest.gates[0].cases[0].fixture = {
replaceFiles: [
{
path: 'gates/meta-fixture.sh',
find: 'text that is not present',
replace: 'irrelevant',
},
],
};
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /META-CRIT-1.*rejects-bad-input.*not bound/i);
assert.match(output(result), /META-CRIT-2.*rejects-second-input.*not bound/i);
assert.match(output(result), /fixture replace.*stale or ambiguous/i);
});
test('moving meaning and prose criteria to an unrelated type error is rejected', async () => {
const root = await fixture('real-manifest-misbinding');
const manifest = JSON.parse(
await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'),
);
for (const gate of manifest.gates) {
for (const gateCase of gate.cases) {
gateCase.criterionIds = gateCase.criterionIds.filter(
(id) => !['RM02-MEANING-PROVENANCE', 'RM02-PROSE-CONTROL'].includes(id),
);
}
}
const typeError = manifest.gates
.find((gate) => gate.id === 'quality-typecheck')
.cases.find((gateCase) => gateCase.id === 'type-error');
typeError.criterionIds.push('RM02-MEANING-PROVENANCE', 'RM02-PROSE-CONTROL');
await writeManifest(root, manifest);
const result = verify(root, ['--structure-only']);
assert.notEqual(result.status, 0);
assert.match(output(result), /RM02-MEANING-PROVENANCE.*missing-meaning-provenance.*not bound/i);
assert.match(output(result), /RM02-PROSE-CONTROL.*prose-claim-misbinding.*not bound/i);
assert.match(
output(result),
/RM02-(?:MEANING-PROVENANCE|PROSE-CONTROL).*undeclared exercising case quality-typecheck\/type-error/i,
);
});
test('a criterion with no bound case is rejected', async () => {
const root = await fixture('unbound-criterion');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'ORPHAN',
originalText: 'This criterion is not exercised.',
currentText: 'This criterion is not exercised.',
claimType: 'quality',
source: 'fixture',
meaningChanges: [],
});
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /ORPHAN.*no bound case/i);
});
test('an unbound governing prose marker is rejected', async () => {
const root = await fixture('unbound-prose');
await writeGate(root);
await mkdir(path.join(root, 'docs'), { recursive: true });
await writeFile(path.join(root, 'docs', 'governing.md'), 'GATE-CLAIM:UNBOUND\n');
const manifest = baseManifest();
manifest.governingClaimFiles = ['docs/governing.md'];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /GATE-CLAIM:UNBOUND.*unbound/i);
});
test('directly contradictory modeled scenarios are rejected', async () => {
const root = await fixture('conflict');
await writeGate(root);
const manifest = baseManifest();
manifest.compatibilityScenarios = [
{
id: 'ONE',
construction: 'same-input',
caseRefs: ['meta-fixture/rejects-bad-input'],
invocation: ['sh', '-c', 'exit 0'],
expected: { exitCode: 0 },
},
{
id: 'TWO',
construction: 'same-input',
caseRefs: ['meta-fixture/rejects-bad-input'],
invocation: ['sh', '-c', 'exit 1'],
expected: { exitCode: 1 },
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /ONE.*TWO.*conflict/i);
});
test('compatibility scenarios execute referenced conditions as one construction', async () => {
const root = await fixture('combined-compatibility');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases.push({
id: 'second-condition',
criterionIds: ['META-CRIT-1'],
mustFail: true,
invocation: ['sh', '-c', 'echo "$SECOND_REASON" >&2; exit 7'],
fixture: { writeFiles: [{ path: 'conditions/second', content: 'present\n' }] },
required: { exitCode: 7 },
actual: { exitCode: 7 },
evidence: { subject: 'meta-fixture' },
reasonPattern: 'SECOND_REASON',
environment: { SECOND_REASON: 'SECOND_REASON' },
});
manifest.criteria[0].caseRefs.push('meta-fixture/second-condition');
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: 'conditions/first', content: 'present\n' }],
};
manifest.compatibilityScenarios = [
{
id: 'BOTH-CONDITIONS',
construction: 'both-fixtures',
caseRefs: ['meta-fixture/rejects-bad-input', 'meta-fixture/second-condition'],
invocation: ['sh', '-c', 'test -f conditions/first && test -f conditions/second'],
expected: { exitCode: 0 },
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.match(output(result), /COMPATIBILITY BOTH-CONDITIONS: observed expected outcome/i);
});
test('a restated criterion without provenance is rejected', async () => {
const root = await fixture('provenance');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria[0].currentText = 'The fixture rejects only malformed input.';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /META-CRIT-1.*meaning-change provenance/i);
});
test('a security criterion bound only to a positive case is unregistered in substance', async () => {
const root = await fixture('positive-only-criterion');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'POSITIVE-ONLY',
originalText: 'A security property.',
currentText: 'A security property.',
claimType: 'security',
source: 'fixture',
meaningChanges: [],
});
manifest.gates[0].cases.push({
id: 'positive-only',
criterionIds: ['POSITIVE-ONLY'],
mustFail: false,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 7 },
actual: { exitCode: 7 },
reasonPattern: '',
});
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /POSITIVE-ONLY.*no must-fail case/i);
});
test('a registered prose claim whose marker is absent is rejected', async () => {
const root = await fixture('missing-prose-marker');
await writeGate(root);
await mkdir(path.join(root, 'docs'), { recursive: true });
await writeFile(path.join(root, 'docs', 'governing.md'), 'No marker here.\n');
const manifest = baseManifest();
manifest.governingClaimFiles = ['docs/governing.md'];
manifest.proseClaims = [{ id: 'MISSING-MARKER', criterionId: 'META-CRIT-1' }];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /GATE-CLAIM:MISSING-MARKER.*missing/i);
});
test('source-versus-deployed byte drift is rejected and names the gate', async () => {
const root = await fixture('deployment-drift');
await writeGate(root);
await mkdir(path.join(root, 'deployed'), { recursive: true });
await writeFile(path.join(root, 'deployed', 'meta-fixture.sh'), '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].deployment = { kind: 'file', path: 'deployed/meta-fixture.sh' };
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*source.*deployed.*drift/i);
});
test('deployment drift meta-control fails if the shared comparator is made inert', async () => {
const root = await fixture('deployment-comparator-inert');
await writeGate(root);
await mkdir(path.join(root, 'deployed'), { recursive: true });
await copyFile(
path.join(root, 'gates', 'meta-fixture.sh'),
path.join(root, 'deployed', 'meta-fixture.sh'),
);
const manifest = baseManifest();
manifest.gates[0].deployment = { kind: 'file', path: 'deployed/meta-fixture.sh' };
await writeManifest(root, manifest);
const alteredScripts = path.join(root, 'verifier-scripts');
await mkdir(alteredScripts, { recursive: true });
const verifierSource = await readFile(verifier, 'utf8');
const inertSource = verifierSource.replace(
'return source.equals(deployed);',
'return true; // deliberate test-only inert comparator',
);
assert.notEqual(inertSource, verifierSource, 'shared deployment comparator mutation went stale');
await writeFile(path.join(alteredScripts, 'gate-verify.mjs'), inertSource);
const result = spawnSync(
process.execPath,
[
path.join(alteredScripts, 'gate-verify.mjs'),
'--root',
root,
'--manifest',
'gates/gates.manifest.json',
],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*drift negative control was ineffective/i);
});
+104
View File
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
const root = process.cwd();
const expectedTriggers = `when:
# PR + manual CI run on any branch the pull_request pipeline is the merge gate.
# push CI is restricted to protected branches (main) so a feature-branch push no
# longer fires a redundant SECOND pipeline alongside its PR pipeline. This ~halves
# CI load on the storage-constrained runner with zero loss of gating (branch
# protection requires no push/ci status context; main still gets full push CI).
- event: [pull_request, manual]
- event: push
branch: main`;
const expectedGateStep = ` image: *node_image
commands:
- *enable_pnpm
- pnpm gate:verify
depends_on:
- install
- sanitization
- upgrade-guard`;
export function assertUnprivilegedGateStep(pipeline) {
assert.doesNotMatch(
pipeline,
/privileged/i,
'no pull-request pipeline step may declare privilege',
);
for (const line of pipeline.split('\n')) {
const candidate = line.trimStart().replace(/^-\s+/, '');
if (/^(?:["'!<].*|[A-Za-z_][A-Za-z0-9_-]*\s+):(?:\s|$)/.test(candidate)) {
assert.fail(`non-canonical or merged YAML key is forbidden: ${candidate}`);
}
}
const triggerMatches = [...pipeline.matchAll(/^when:\n([\s\S]*?)(?=\n\n)/gm)];
assert.equal(triggerMatches.length, 1, 'exactly one top-level trigger is required');
assert.equal(
`when:\n${triggerMatches[0][1].trimEnd()}`,
expectedTriggers,
'top-level triggers must match closed PR/main construction',
);
const matches = [
...pipeline.matchAll(/\n gate-verify:\n([\s\S]*?)(?=\n [a-z][a-z0-9-]+:|\nservices:|$)/g),
];
assert.equal(matches.length, 1, 'exactly one gate-verify step is required');
// Closed textual construction by design: accepting arbitrary YAML syntax here
// would require a duplicate-key-preserving parser. Exact equality rejects all
// extra keys, quoted/escaped key spellings, aliases, and mapping merges.
assert.equal(
matches[0][1].trimEnd(),
expectedGateStep,
'gate-verify step must match closed unprivileged construction',
);
}
test('package.json exposes the canonical gate:verify command', async () => {
const packageJson = JSON.parse(await readFile(`${root}/package.json`, 'utf8'));
assert.equal(packageJson.scripts['gate:verify'], 'node scripts/gate-verify.mjs');
});
test('Woodpecker runs the closed unprivileged gate construction on every pipeline', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
assertUnprivilegedGateStep(pipeline);
});
test('gate wiring rejects privilege syntax, merges, duplicate keys, and trigger narrowing', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
const additions = [
' privileged: *enabled\n',
' "privileged": true\n',
" 'privileged': true\n",
' privileged : true\n',
' "priv\\u0069leged": true\n',
' <<: *privileged-step\n',
' "<<": *privileged-step\n',
];
for (const addition of additions) {
const changed = pipeline.replace(
' gate-verify:\n image:',
` gate-verify:\n${addition} image:`,
);
assert.throws(() => assertUnprivilegedGateStep(changed));
}
const privilegedInstall = pipeline.replace(
' install:\n image:',
' install:\n privileged: true\n image:',
);
const duplicate = `${pipeline}\n gate-verify:\n image: *node_image\n`;
const noPullRequest = pipeline.replace(' - event: [pull_request, manual]', ' - event: manual');
const filteredPullRequest = pipeline.replace(
' - event: [pull_request, manual]',
' - event: [pull_request, manual]\n path: [scripts/**]',
);
assert.throws(() => assertUnprivilegedGateStep(privilegedInstall), /privilege/i);
assert.throws(() => assertUnprivilegedGateStep(duplicate), /exactly one gate-verify/);
assert.throws(() => assertUnprivilegedGateStep(noPullRequest), /closed PR\/main construction/);
assert.throws(
() => assertUnprivilegedGateStep(filteredPullRequest),
/closed PR\/main construction/,
);
});
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env node
import { access, lstat, mkdir, readFile, readdir, rename, rm } from 'node:fs/promises';
import { execFile, spawn } from 'node:child_process';
import { promisify } from 'node:util';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const execFileAsync = promisify(execFile);
function run(command, args, options) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
child.once('error', reject);
child.once('exit', (code, signal) => {
if (code === 0) resolve();
else reject(new Error(signal ? `husky terminated by ${signal}` : `husky exited ${code}`));
});
});
}
async function pathExists(target) {
try {
await access(target);
return true;
} catch (error) {
if (error.code === 'ENOENT') return false;
throw error;
}
}
async function directorySnapshot(root) {
const snapshot = [];
async function walk(current) {
const children = await readdir(current, { withFileTypes: true });
for (const child of children.sort((left, right) => left.name.localeCompare(right.name))) {
const target = path.join(current, child.name);
const relative = path.relative(root, target);
const stats = await lstat(target);
if (child.isDirectory()) {
snapshot.push([relative, 'directory', stats.mode & 0o777]);
await walk(target);
} else {
snapshot.push([
relative,
'file',
stats.mode & 0o777,
(await readFile(target)).toString('base64'),
]);
}
}
}
await walk(root);
return JSON.stringify(snapshot);
}
async function directoriesMatch(left, right) {
return (await directorySnapshot(left)) === (await directorySnapshot(right));
}
export async function installHooks({
root = process.cwd(),
disabled = process.env.HUSKY === '0',
quarantineRoot = path.join(root, '.mosaic-test-work', 'husky-quarantine'),
runHusky = async (_stagingHooks, stagingRepo) => {
await execFileAsync('git', ['init', '--quiet', stagingRepo]);
await run(path.join(root, 'node_modules', '.bin', 'husky'), ['.husky'], {
cwd: stagingRepo,
stdio: 'inherit',
});
},
activateHooks = async () => {
await run('git', ['config', 'core.hooksPath', '.husky/_'], { cwd: root, stdio: 'inherit' });
},
} = {}) {
if (disabled) return;
const huskyDir = path.join(root, '.husky');
const active = path.join(huskyDir, '_');
const nonce = `${Date.now()}-${process.pid}`;
const stagingRepo = path.join(root, '.mosaic-test-work', `husky-stage-${nonce}`);
const stagingHooks = path.join(stagingRepo, '.husky');
const quarantined = path.join(quarantineRoot, `${path.basename(root)}-${nonce}`);
await mkdir(huskyDir, { recursive: true });
await mkdir(quarantineRoot, { recursive: true });
const previousComplete = (await pathExists(active)) && (await pathExists(path.join(active, 'h')));
let previousQuarantined = false;
try {
if ((await pathExists(active)) && !previousComplete) {
await rename(active, quarantined);
previousQuarantined = true;
}
await mkdir(stagingRepo, { recursive: true });
await runHusky(stagingHooks, stagingRepo);
const staged = path.join(stagingHooks, '_');
if (!(await pathExists(path.join(staged, 'h')))) {
throw new Error('husky did not produce its required h shim');
}
if (previousComplete) {
if (!(await directoriesMatch(active, staged))) {
throw new Error('existing complete hook set differs from the installed Husky version');
}
await rm(stagingRepo, { recursive: true, force: true });
} else {
await rename(staged, active);
await rm(stagingRepo, { recursive: true, force: true });
}
await activateHooks();
if (previousQuarantined) {
try {
await rm(quarantined, { recursive: true, force: true });
} catch {
console.warn(
`Previous hook state was deactivated but remains quarantined at ${quarantined}`,
);
}
}
} catch (error) {
const cleanupFailures = [];
try {
if (await pathExists(stagingRepo)) {
await rename(stagingRepo, `${quarantined}-staging`);
}
} catch (cleanupError) {
cleanupFailures.push(`staging hooks: ${cleanupError.message}`);
}
const cleanup =
cleanupFailures.length === 0
? 'No partial hook set was activated.'
: `Automatic cleanup was incomplete (${cleanupFailures.join('; ')}).`;
throw new Error(
`Hook installation failed: ${error.message}. ${cleanup} Fix: rm -rf .husky/_ && git config core.hooksPath .husky/_ && pnpm install --frozen-lockfile`,
{ cause: error },
);
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
await installHooks();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
+208
View File
@@ -0,0 +1,208 @@
import assert from 'node:assert/strict';
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { installHooks } from './install-hooks.mjs';
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `hooks-${process.pid}`);
const quarantineRoot = path.join(fixtureRoot, 'quarantine');
async function fixture(name) {
const root = path.join(fixtureRoot, name);
await mkdir(path.join(root, '.husky'), { recursive: true });
return root;
}
async function exists(target) {
try {
await access(target);
return true;
} catch {
return false;
}
}
test.after(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
test('an interrupted install quarantines the partial active hook set and fails loudly', async () => {
const root = await fixture('interrupted');
let restoredHooksPath = 'not-called';
await assert.rejects(
installHooks({
root,
quarantineRoot,
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'partial');
throw new Error('simulated interruption');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async (value) => {
restoredHooksPath = value;
},
}),
(error) => {
assert.match(error.message, /Hook installation failed/);
assert.match(error.message, /pnpm install --frozen-lockfile/);
return true;
},
);
assert.equal(await exists(path.join(root, '.husky', '_')), false);
assert.equal(restoredHooksPath, 'not-called');
const quarantined = await readdir(quarantineRoot);
assert.equal(quarantined.length, 1);
});
test('a failed replacement restores a previously complete active hook set', async () => {
const root = await fixture('rollback');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'previous-complete');
let previousRemainedActiveDuringStaging = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'rollback-quarantine'),
runHusky: async () => {
previousRemainedActiveDuringStaging =
(await readFile(activeShim, 'utf8')) === 'previous-complete';
throw new Error('simulated replacement failure');
},
activateHooks: async () => {},
readHooksPath: async () => '.husky/_',
restoreHooksPath: async () => {},
}),
/Hook installation failed/,
);
assert.equal(previousRemainedActiveDuringStaging, true);
assert.equal(await readFile(activeShim, 'utf8'), 'previous-complete');
});
test('a mismatched complete hook set fails loudly instead of reporting a stale install as current', async () => {
const root = await fixture('mismatch');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'old-complete');
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'mismatch-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'new-complete');
},
activateHooks: async () => {},
readHooksPath: async () => '.husky/_',
restoreHooksPath: async () => {},
}),
/Hook installation failed.*pnpm install --frozen-lockfile/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'old-complete');
});
test('a competing successful installer is not removed by the losing process', async () => {
const root = await fixture('concurrent');
const activeShim = path.join(root, '.husky', '_', 'h');
let restored = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'concurrent-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'peer');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async () => {
restored = true;
},
}),
/Hook installation failed/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
assert.equal(restored, false);
});
test("a competing installer that replaces this installer's active set is preserved", async () => {
const root = await fixture('concurrent-after-rename');
const active = path.join(root, '.husky', '_');
const activeShim = path.join(active, 'h');
let restored = false;
await assert.rejects(
installHooks({
root,
quarantineRoot: path.join(fixtureRoot, 'concurrent-after-rename-quarantine'),
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
},
activateHooks: async () => {
await rm(active, { recursive: true, force: true });
await mkdir(active, { recursive: true });
await writeFile(activeShim, 'peer');
throw new Error('our activation lost to peer');
},
readHooksPath: async () => null,
restoreHooksPath: async () => {
restored = true;
},
}),
/Hook installation failed/,
);
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
assert.equal(restored, false);
});
test('an explicit interactive HUSKY=0 opt-out preserves existing hooks without running installer', async () => {
const root = await fixture('disabled');
const activeShim = path.join(root, '.husky', '_', 'h');
await mkdir(path.dirname(activeShim), { recursive: true });
await writeFile(activeShim, 'preserved');
let ran = false;
await installHooks({
root,
disabled: true,
runHusky: async () => {
ran = true;
},
});
assert.equal(ran, false);
assert.equal(await readFile(activeShim, 'utf8'), 'preserved');
});
test('a successful install leaves a complete active hook set', async () => {
const root = await fixture('success');
await installHooks({
root,
quarantineRoot,
runHusky: async (stagingHooks) => {
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
await writeFile(path.join(stagingHooks, '_', 'h'), 'complete');
},
activateHooks: async () => {},
readHooksPath: async () => null,
restoreHooksPath: async () => {},
});
assert.equal(await exists(path.join(root, '.husky', '_', 'h')), true);
});
+254
View File
@@ -0,0 +1,254 @@
#!/usr/bin/env node
import { constants } from 'node:fs';
import { access, lstat, readFile, readdir, readlink } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
export const MISSING_DEPS_EXIT = 42;
export const GENERATED_STATE_EXIT = 43;
const scriptRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
async function entries(root) {
const result = [];
async function walk(current) {
let children;
try {
children = await readdir(current, { withFileTypes: true });
} catch (error) {
if (error.code === 'ENOENT') return;
throw error;
}
for (const child of children) {
const target = path.join(current, child.name);
result.push(target);
if (child.isDirectory() && !child.isSymbolicLink()) await walk(target);
}
}
await walk(root);
return result;
}
export async function generatedSymlinkManifest(nextDir) {
const links = [];
for (const target of (await entries(nextDir)).sort()) {
const stats = await lstat(target);
if (!stats.isSymbolicLink()) continue;
links.push({
path: path.relative(nextDir, target).split(path.sep).join('/'),
target: await readlink(target),
});
}
return `${JSON.stringify({ version: 1, links })}\n`;
}
const webSourceRoots = (root) => [
path.join(root, 'apps', 'web', 'src'),
path.join(root, 'apps', 'web', 'public'),
path.join(root, 'apps', 'web', 'next-env.d.ts'),
path.join(root, 'apps', 'web', 'next.config.ts'),
path.join(root, 'apps', 'web', 'postcss.config.mjs'),
path.join(root, 'apps', 'web', 'package.json'),
path.join(root, 'apps', 'web', 'tsconfig.json'),
path.join(root, 'packages', 'design-tokens', 'src'),
path.join(root, 'packages', 'design-tokens', 'package.json'),
path.join(root, 'packages', 'design-tokens', 'tsconfig.json'),
path.join(root, 'package.json'),
path.join(root, 'tsconfig.base.json'),
path.join(root, 'pnpm-lock.yaml'),
path.join(root, 'pnpm-workspace.yaml'),
path.join(root, 'turbo.json'),
];
// next.config.ts currently reads no server-only environment. Add any future
// server-side build inputs here; all resolved NEXT_PUBLIC_* inputs are automatic.
const serverBuildEnvironmentKeys = [];
function publicBuildEnvironment(root) {
const webDir = path.join(root, 'apps', 'web');
const requireFromWeb = createRequire(path.join(scriptRoot, 'apps', 'web', 'package.json'));
const requireFromNext = createRequire(requireFromWeb.resolve('next/package.json'));
const { loadEnvConfig, resetEnv, updateInitialEnv } = requireFromNext('@next/env');
const originalEnvironment = { ...process.env };
updateInitialEnv(originalEnvironment);
try {
const { combinedEnv } = loadEnvConfig(webDir, false, { info() {}, error() {} }, true);
return Object.fromEntries(
Object.entries(combinedEnv).filter(
([key, value]) =>
value !== undefined &&
(key.startsWith('NEXT_PUBLIC_') || serverBuildEnvironmentKeys.includes(key)),
),
);
} finally {
resetEnv();
}
}
export async function sourceFingerprint(root = process.cwd()) {
const files = [];
for (const sourceRoot of webSourceRoots(root)) {
try {
const stats = await lstat(sourceRoot);
if (stats.isSymbolicLink()) {
throw new Error(
`Web build input must not be a symbolic link: ${path.relative(root, sourceRoot)}`,
);
}
if (stats.isFile()) files.push(sourceRoot);
if (stats.isDirectory()) {
for (const target of await entries(sourceRoot)) {
const targetStats = await lstat(target);
if (targetStats.isSymbolicLink()) {
throw new Error(
`Web build input must not be a symbolic link: ${path.relative(root, target)}`,
);
}
if (targetStats.isFile()) files.push(target);
}
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
const digest = createHash('sha256');
for (const [key, value] of Object.entries(publicBuildEnvironment(root)).sort()) {
digest.update(`env:${key}\0${value.length}\0${value}\0`);
}
for (const target of files.sort()) {
const contents = await readFile(target);
digest.update(path.relative(root, target).split(path.sep).join('/'));
digest.update('\0');
digest.update(String(contents.length));
digest.update('\0');
digest.update(contents);
digest.update('\0');
}
return digest.digest('hex');
}
export async function runPreflight({ root = process.cwd(), uid = process.getuid?.() } = {}) {
const binDir = path.join(root, 'node_modules', '.bin');
const requiredBinaries = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
const missingBinaries = [];
for (const binary of requiredBinaries) {
try {
await access(path.join(binDir, binary), constants.X_OK);
} catch {
missingBinaries.push(binary);
}
}
if (missingBinaries.length > 0) {
return {
code: MISSING_DEPS_EXIT,
message: `MOSAIC_PREFLIGHT_MISSING_DEPS: dependency installation is missing ${missingBinaries.join(', ')}; run pnpm install --frozen-lockfile`,
};
}
const buildLock = path.join(root, '.mosaic-test-work', 'web-build.lock');
try {
await lstat(buildLock);
return {
code: GENERATED_STATE_EXIT,
message: `MOSAIC_PREFLIGHT_GENERATED_STATE: web build is in progress or interrupted at ${buildLock}; wait for it to finish or rerun pnpm build to recover the stale lock`,
};
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
const nextDir = path.join(root, 'apps', 'web', '.next');
let generated = [];
try {
const nextStats = await lstat(nextDir);
if (!nextStats.isDirectory() || nextStats.isSymbolicLink()) {
return {
code: GENERATED_STATE_EXIT,
message:
'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next must be a real directory, not a symbolic link, and is not trustworthy; run pnpm clean:generated, then rerun the gate',
};
}
generated = [nextDir, ...(await entries(nextDir))];
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (generated.length > 0) {
const foreign = [];
for (const target of generated) {
const stats = await lstat(target);
if (uid !== undefined && stats.uid !== uid) foreign.push(path.relative(root, target));
}
// Detects accidental, independent, stale, and foreign-residue mutation of
// generated state: the class this check was born from was a five-month-stale
// .next whose validator referenced deleted pages and produced 19 phantom TS2307
// errors indistinguishable from real type errors.
//
// Does NOT defend against an actor with same-UID write access to the generated
// tree, which can regenerate both the manifest and marker consistently
// (CWE-345). No local construction can, absent a trust anchor outside that
// actor's authority. RM-59 tracks executor/spine-side attestation.
let certification = null;
let certifiedManifest = null;
try {
const [certificationContents, manifestContents] = await Promise.all([
readFile(path.join(nextDir, '.mosaic-source-hash'), 'utf8'),
readFile(path.join(nextDir, '.mosaic-symlink-manifest'), 'utf8'),
]);
try {
const parsed = JSON.parse(certificationContents);
if (
parsed.version === 1 &&
typeof parsed.sourceFingerprint === 'string' &&
typeof parsed.symlinkManifestHash === 'string'
) {
certification = parsed;
certifiedManifest = manifestContents;
}
} catch {
// Invalid certification is handled as untrusted generated state below.
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
const stale = certification?.sourceFingerprint !== (await sourceFingerprint(root));
const actualManifest = await generatedSymlinkManifest(nextDir);
const certifiedManifestHash =
certifiedManifest === null
? null
: createHash('sha256').update(certifiedManifest).digest('hex');
const changedSymlinks =
certification?.symlinkManifestHash !== certifiedManifestHash ||
certifiedManifest !== actualManifest;
if (foreign.length > 0 || stale || changedSymlinks) {
const reasons = [
foreign.length > 0 ? `foreign-owned paths: ${foreign.slice(0, 3).join(', ')}` : '',
stale ? 'generated source fingerprint does not match web source/configuration' : '',
changedSymlinks
? 'generated symbolic-link manifest does not match the certified build'
: '',
].filter(Boolean);
return {
code: GENERATED_STATE_EXIT,
message: `MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next is not trustworthy (${reasons.join('; ')}); run pnpm clean:generated, then rerun the gate`,
};
}
}
return { code: 0, message: 'checkout preflight passed' };
}
async function main() {
const result = await runPreflight();
const stream = result.code === 0 ? process.stdout : process.stderr;
stream.write(`${result.message}\n`);
process.exitCode = result.code;
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main();
}
+274
View File
@@ -0,0 +1,274 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { chmod, mkdir, rm, symlink, utimes, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { runPreflight, sourceFingerprint } from './preflight.mjs';
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `preflight-${process.pid}`);
const requiredBins = ['eslint', 'husky', 'prettier', 'tsc', 'turbo', 'vitest'];
async function fixture(name) {
const root = path.join(fixtureRoot, name);
await mkdir(path.join(root, 'apps', 'web', 'src', 'app'), { recursive: true });
await writeFile(path.join(root, 'apps', 'web', 'src', 'app', 'page.tsx'), 'export default 1;\n');
return root;
}
async function installRequiredBins(root) {
const binDir = path.join(root, 'node_modules', '.bin');
await mkdir(binDir, { recursive: true });
await Promise.all(
requiredBins.map(async (name) => {
const target = path.join(binDir, name);
await writeFile(target, '');
await chmod(target, 0o755);
}),
);
}
async function certifyGeneratedState(root, links = []) {
const nextDir = path.join(root, 'apps', 'web', '.next');
await mkdir(nextDir, { recursive: true });
const manifest = `${JSON.stringify({ version: 1, links })}\n`;
const manifestHash = createHash('sha256').update(manifest).digest('hex');
await writeFile(path.join(nextDir, '.mosaic-symlink-manifest'), manifest);
await writeFile(
path.join(nextDir, '.mosaic-source-hash'),
`${JSON.stringify({
version: 1,
sourceFingerprint: await sourceFingerprint(root),
symlinkManifestHash: manifestHash,
})}\n`,
);
}
test.after(async () => {
await rm(fixtureRoot, { recursive: true, force: true });
});
test('missing dependencies have a dedicated exit code and install remediation', async () => {
const root = await fixture('missing-deps');
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /MOSAIC_PREFLIGHT_MISSING_DEPS/);
assert.match(result.message, /run pnpm install/i);
});
test('a partial dependency install keeps the dedicated missing-deps result', async () => {
const root = await fixture('partial-deps');
await mkdir(path.join(root, 'node_modules', '.bin'), { recursive: true });
await writeFile(path.join(root, 'node_modules', '.bin', 'tsc'), '', { mode: 0o755 });
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /turbo/);
});
test('a dangling required dependency shim keeps the dedicated missing-deps result', async () => {
const root = await fixture('dangling-deps');
await installRequiredBins(root);
const turbo = path.join(root, 'node_modules', '.bin', 'turbo');
await rm(turbo);
await symlink(path.join(root, 'node_modules', 'missing-turbo'), turbo);
const result = await runPreflight({ root });
assert.equal(result.code, 42);
assert.match(result.message, /turbo/);
});
test('installed dependencies pass when generated state is absent', async () => {
const root = await fixture('clean');
await installRequiredBins(root);
assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' });
});
test('foreign-owned generated Next state is identified separately from source errors', async () => {
const root = await fixture('foreign-next');
await installRequiredBins(root);
const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts');
await mkdir(path.dirname(generated), { recursive: true });
await writeFile(generated, 'generated output');
const result = await runPreflight({ root, uid: (process.getuid?.() ?? 0) + 1 });
assert.equal(result.code, 43);
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
assert.match(result.message, /foreign-owned/);
});
test('a generated marker mismatch is identified separately from source errors', async () => {
const root = await fixture('stale-next');
await installRequiredBins(root);
const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts');
await mkdir(path.dirname(generated), { recursive: true });
await writeFile(generated, 'stale generated output');
await writeFile(path.join(root, 'apps', 'web', '.next', '.mosaic-source-hash'), 'old-source');
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
assert.match(result.message, /apps\/web\/\.next/);
assert.match(result.message, /pnpm clean:generated/);
});
test('generated-state symbolic links are accepted only when exactly build-certified', async (t) => {
await t.test('apps/web/.next itself is rejected when it is a symbolic link', async () => {
const root = await fixture('symbolic-next-root');
await installRequiredBins(root);
await writeFile(path.join(root, 'outside-generated'), 'not a Next build\n');
await symlink(path.join(root, 'outside-generated'), path.join(root, 'apps', 'web', '.next'));
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
assert.match(result.message, /symbolic link/);
});
await t.test('apps/web/.next is rejected when it is not a directory', async () => {
const root = await fixture('non-directory-next-root');
await installRequiredBins(root);
await writeFile(path.join(root, 'apps', 'web', '.next'), 'not a Next build\n');
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/);
assert.match(result.message, /real directory/);
});
await t.test('an added descendant symlink is rejected', async () => {
const root = await fixture('symbolic-next-added');
await installRequiredBins(root);
await certifyGeneratedState(root);
await symlink('/etc/hosts', path.join(root, 'apps', 'web', '.next', 'reviewer-symlink'));
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /symbolic-link manifest/);
});
await t.test('a removed certified descendant symlink is rejected', async () => {
const root = await fixture('symbolic-next-removed');
await installRequiredBins(root);
const link = path.join(root, 'apps', 'web', '.next', 'dependency-link');
await mkdir(path.dirname(link), { recursive: true });
await symlink('../dependency-one', link);
await certifyGeneratedState(root, [{ path: 'dependency-link', target: '../dependency-one' }]);
await rm(link);
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /symbolic-link manifest/);
});
await t.test('a retargeted certified descendant symlink is rejected', async () => {
const root = await fixture('symbolic-next-retargeted');
await installRequiredBins(root);
const link = path.join(root, 'apps', 'web', '.next', 'dependency-link');
await mkdir(path.dirname(link), { recursive: true });
await symlink('../dependency-one', link);
await certifyGeneratedState(root, [{ path: 'dependency-link', target: '../dependency-one' }]);
await rm(link);
await symlink('../dependency-two', link);
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /symbolic-link manifest/);
});
await t.test('a manifest edited to whitelist a rogue symlink is rejected', async () => {
const root = await fixture('symbolic-next-tampered-manifest');
await installRequiredBins(root);
await certifyGeneratedState(root);
const nextDir = path.join(root, 'apps', 'web', '.next');
await symlink('/etc/hosts', path.join(nextDir, 'reviewer-symlink'));
await writeFile(
path.join(nextDir, '.mosaic-symlink-manifest'),
`${JSON.stringify({
version: 1,
links: [{ path: 'reviewer-symlink', target: '/etc/hosts' }],
})}\n`,
);
const result = await runPreflight({ root });
assert.equal(result.code, 43);
assert.match(result.message, /symbolic-link manifest/);
});
await t.test('unchanged canonical-style descendant symlinks are accepted', async () => {
const root = await fixture('symbolic-next-certified');
await installRequiredBins(root);
const link = path.join(
root,
'apps',
'web',
'.next',
'standalone',
'node_modules',
'dependency',
);
await mkdir(path.dirname(link), { recursive: true });
await symlink('../.pnpm/dependency', link);
await certifyGeneratedState(root, [
{ path: 'standalone/node_modules/dependency', target: '../.pnpm/dependency' },
]);
assert.deepEqual(await runPreflight({ root }), {
code: 0,
message: 'checkout preflight passed',
});
});
});
test('the source fingerprint includes inherited TypeScript configuration', async () => {
const root = await fixture('inherited-typescript-config');
const config = path.join(root, 'tsconfig.base.json');
await writeFile(config, '{"compilerOptions":{"strict":true}}\n');
const first = await sourceFingerprint(root);
await writeFile(config, '{"compilerOptions":{"strict":false}}\n');
const second = await sourceFingerprint(root);
assert.notEqual(first, second);
});
test('the source fingerprint rejects symbolic-link build inputs', async () => {
const root = await fixture('symbolic-source');
await writeFile(path.join(root, 'outside.ts'), 'export default 1;\n');
await symlink(path.join(root, 'outside.ts'), path.join(root, 'apps', 'web', 'src', 'linked.ts'));
await assert.rejects(sourceFingerprint(root), /must not be a symbolic link/);
});
test('the source fingerprint includes expanded public web build environment', async () => {
const root = await fixture('public-build-environment');
const envFile = path.join(root, 'apps', 'web', '.env.production');
await writeFile(
envFile,
'RM01_GATEWAY_URL=https://one.example\nNEXT_PUBLIC_RM01_URL=$RM01_GATEWAY_URL\n',
);
const first = await sourceFingerprint(root);
await writeFile(
envFile,
'RM01_GATEWAY_URL=https://two.example\nNEXT_PUBLIC_RM01_URL=$RM01_GATEWAY_URL\n',
);
const second = await sourceFingerprint(root);
assert.notEqual(first, second);
});
test('a matching generation marker accepts incremental output with mixed mtimes', async () => {
const root = await fixture('incremental-next');
await installRequiredBins(root);
const generated = path.join(root, 'apps', 'web', '.next', 'types', 'validator.ts');
await mkdir(path.dirname(generated), { recursive: true });
await writeFile(generated, 'unchanged generated output');
await utimes(generated, new Date('2020-01-01T00:00:00Z'), new Date('2020-01-01T00:00:00Z'));
const fresh = path.join(root, 'apps', 'web', '.next', 'types', 'routes.ts');
await writeFile(fresh, 'fresh generated output');
await certifyGeneratedState(root);
assert.deepEqual(await runPreflight({ root }), { code: 0, message: 'checkout preflight passed' });
});
@@ -0,0 +1,27 @@
#!/usr/bin/env node
import path from 'node:path';
import { verifyRegistry } from '../gate-verify.mjs';
let root;
let manifest = 'gates/gates.manifest.json';
let structureOnly = false;
for (let index = 0; index < process.argv.slice(2).length; index += 1) {
const args = process.argv.slice(2);
const value = args[index];
if (value === '--root') root = path.resolve(args[++index]);
else if (value === '--manifest') manifest = args[++index];
else if (value === '--structure-only') structureOnly = true;
else throw new Error(`unknown fixture-runner option: ${value}`);
}
if (!root) throw new Error('fixture runner requires --root');
const { failures, observations } = await verifyRegistry({
root,
manifest,
structureOnly,
fixtureProfile: true,
});
for (const observation of observations) process.stdout.write(`${observation}\n`);
for (const failure of failures) process.stderr.write(`GATE VERIFY FAILED: ${failure}\n`);
if (failures.length > 0) process.exitCode = 1;