fix(governance): gate the real repository against its own lifecycle machine truth - #485
Conversation
…achine truth scripts/lib/openspec-machine-truth.mjs already owns a complete lifecycle comparator, but its CLI needs a GitHub observation and a pinned openspec binary, so CI could only ever exercise it against synthetic temporary fixtures. The one assertion that touched the real tree was a hand-written regex for a single change id, so drift between openspec/changes, the machine ledger and the NOW projection was corrected by hand after landing (#477, #481, #482) instead of being refused at PR time. This adds the repository-local subset of that comparison as a gate that needs no network, git or external binary, and wires it into the already required agent-governance check. It compares the change directories and their proposal markers, openspec/lifecycle-ledger.json, and the NOW projection, reports disagreement rather than resolving it, and fails closed on any input it cannot read as declared. Proven on a real historical commit: run against main at c41e29d the gate reports now_unexpected_change for introduce-executable-architecture-contracts, drift that existed on main and was only fixed later by #481. Claude-Session: cowork scheduled task ai-bim-geo Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 15 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds network-free OpenSpec lifecycle parity validation. The verifier parses repository-local lifecycle sources, compares current and archived state, reports deterministic mismatches, and runs through governance checks. ChangesRepository lifecycle parity
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant VerifierCLI
participant LifecycleEvaluator
participant RepositorySources
participant LifecycleComparator
VerifierCLI->>LifecycleEvaluator: evaluate repository root
LifecycleEvaluator->>RepositorySources: read ledger, NOW, changes, archives, proposals
RepositorySources-->>LifecycleEvaluator: lifecycle observation
LifecycleEvaluator->>LifecycleComparator: compare observed state
LifecycleComparator-->>VerifierCLI: parity report and exit status
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82d1c70026
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
This PR closes a governance hole in OpenSpec lifecycle enforcement. The existing full comparator (scripts/lib/openspec-machine-truth.mjs) can only run against synthetic fixtures in CI because its CLI requires a GitHub observation and a pinned openspec binary (network + external tool). As a result, drift between the three repository-local lifecycle truth sources — openspec/changes/<id>/ directories + proposal markers, openspec/lifecycle-ledger.json, and the docs/plans/NOW.md projection — was only ever corrected by hand after landing (#477, #481, #482). This change adds a network-free, git-free, binary-free parity gate and wires it into the already-required agent-governance check.
Changes:
- New comparator library
openspec-repository-lifecycle.mjsthat reads the three repository-local sources and reports targeted mismatches (input errors vs. mismatches distinguished with distinct exit codes 3/2/0), deliberately mirroring the marker/status contract ofopenspec-machine-truth.mjsrather than importing it (to avoid self-referential mechanism-surface coupling). - New CLI
verify-openspec-repository-lifecycle.mjsand a 36-casenode --testfixture suite, wired intotest-agent-governance-check.ps1. - Documents the new gate as a MUST in
openspec/AGENTS.md.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
scripts/lib/openspec-repository-lifecycle.mjs |
New comparator: parses ledger/NOW/proposal markers, enforces symlink/DoS/schema bounds, emits deterministic mismatch reports. |
scripts/tests/verify-openspec-repository-lifecycle.mjs |
New CLI wrapper with arg parsing, text/JSON rendering, exit-code discipline, and link-safe direct-invocation detection. |
scripts/tests/test-openspec-repository-lifecycle.mjs |
36 fixtures proving every mismatch/input-error code fires and pointing the comparator at the real repository. |
scripts/tests/test-agent-governance-check.ps1 |
Wires the fixtures + real-repo verify into the required agent-governance static check. |
openspec/AGENTS.md |
Documents the three-source parity requirement and the new verify command. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
scripts/tests/test-openspec-repository-lifecycle.mjs (1)
36-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winWrite the BOM as an escape sequence.
The literal holds an invisible U+FEFF character. An editor, a formatter, or a
.gitattributesnormalisation can drop it. The BOM test at Line 557 would then still pass while proving nothing.'\uFEFF'is visible in review and stable in transit.♻️ Proposed change
-const BOM = ''; +const BOM = '\uFEFF';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/tests/test-openspec-repository-lifecycle.mjs` at line 36, Update the BOM constant to use the explicit \uFEFF escape sequence instead of embedding the invisible character literally, while preserving the existing BOM test behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/lib/openspec-repository-lifecycle.mjs`:
- Around line 466-470: Update the mismatch() call for
directory_without_ledger_row so the source labels and values report
openspec_changes as expected/present and lifecycle_ledger.current as
actual/absent, matching the directory’s presence and missing ledger row. Keep
the mismatch type, entry.name, and message unchanged.
---
Nitpick comments:
In `@scripts/tests/test-openspec-repository-lifecycle.mjs`:
- Line 36: Update the BOM constant to use the explicit \uFEFF escape sequence
instead of embedding the invisible character literally, while preserving the
existing BOM test behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 502bf2e6-e221-4e9c-872d-5d8cae1429be
📒 Files selected for processing (5)
openspec/AGENTS.mdscripts/lib/openspec-repository-lifecycle.mjsscripts/tests/test-agent-governance-check.ps1scripts/tests/test-openspec-repository-lifecycle.mjsscripts/tests/verify-openspec-repository-lifecycle.mjs
…y gate Codex tri-adversarial gate on PR #485 returned SHIP with five non-blocking survivors. Four are closed here; the fifth is disclosed. - L1-1: the current-change cap counted the mandatory archive namespace, so the filesystem observation could not represent the same 512-row boundary the NOW parser accepts. One raw slot is now reserved and the filtered set is bounded instead. - SEC-001: path-derived values reached the text renderers verbatim, so a directory name carrying a newline or ESC byte could forge extra report lines. Values with control characters are now JSON-quoted; JSON output is unchanged. - TG-001: the input_too_large budgets had no coverage. Added the accepted boundary and one past it for change directories, NOW rows, ledger rows and file bytes. - TG-003: the non-regular proposal.md predicate was only reachable through a skippable symlink fixture. Added a portable directory-named-proposal.md case. Not closed: SEC-002 (readdirSync materialises the full Dirent list before the entry cap is applied). Rated low by the apex; incremental opendirSync iteration is a larger refactor and is left as disclosed follow-up. Claude-Session: cowork scheduled task ai-bim-geo Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…gate run Second Codex tri-adversarial run on head 40d93c6 returned SHIP with four survivors; the two medium ones were real fail-open defects in the marker parser, which this gate promotes to merge-blocking authority. - L1-1: the canonical marker regex had no trailing token boundary, so `> **Status: active123**` captured `active` and read as a clean active row. The status token must now end at `**`, whitespace or end of line; anything else is reported as an unreadable marker. - L1-2: near-miss detection ran only when no canonical marker was found, so a proposal carrying two disagreeing declarations resolved silently to the canonical one. Every bolded status-like line in the prologue is now inventoried and an unconsumed one is reported even beside a canonical marker. - L1-3: the raw namespace budget is now separate from the canonical change budget, so one stray entry beside a full change set stays a targeted `change_entry_not_a_directory` mismatch instead of a generic budget error. - TG-1: added a control-character assertion for the text input-error path. Verified against all 120 real proposals: the ten current ones are unchanged; only one archived proposal reads `invalid` (`Status: closeout reconciled`), and archived markers are outside this gate's scope. Claude-Session: cowork scheduled task ai-bim-geo Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1ee148353f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Codex Tri-Adversarial Bot
Automated tri-adversarial ship-gate (L0 terra triage / L1 tier-routed lens fanout / L2 refute-by-default / L3 sol apex — Codex models).
Mapped event: COMMENT
Codex Tri-Adversarial ship-gate — PR #485
- Repo head:
fix/openspec-lifecycle-projection-gate@1ee1483 - Base:
main@f1757b2 - Files changed: 5
- Engine: four-model tri-adversarial gate on Codex — L0 triage
gpt-5.6-terra/low; L1 lens finders routedgpt-5.6-terra/low →gpt-5.6-luna/medium →gpt-5.5/xhigh (security floorgpt-5.5); L2 refute-by-defaultgpt-5.5/xhigh, top-tier findings refuted bygpt-5.6-sol/xhigh (every refutation cross-model); L3 apexgpt-5.6-sol/max. 誠實聲明:層級與 Claude 三層 gate 同構(terra≈haiku、luna≈sonnet、gpt-5.5≈opus、sol≈fable),但模型池是 Codex 的,非 Anthropic 的。
Verdict
SHIP
- 阻擋門檻 severity:
critical, high - mapped GitHub event:
COMMENT - ℹ️ 判定為 SHIP,但刻意不送 APPROVE:GitHub App 的 approving review 不計入
required_approving_review_count(2026-07-31 實測)。本報告是證據,approving 那一票請由真人帳號投。
Difficulty & routing
- overall:
high(source: terra-triage) - lens tiers: correctness→
gpt-5.5, security→gpt-5.5, simplification→gpt-5.5, test-gap→gpt-5.5
Layer stats
- L1: raw=4 deduped=4 finder_failures=0
- L2: confirmed=2 refuted=2 unverified=0
- L3 final: 2
Findings (final, after apex)
[low] Raw namespace entry cap is not pinned
- id:
TG-001lens:test-gapfile:scripts/tests/test-openspec-repository-lifecycle.mjs - provenance: finder=
gpt-5.5refuter=gpt-5.6-sol(cross-model-guard) L2=confirmed - evidence: The implementation separately caps raw
openspec/changesentries atMAX_CURRENT_CHANGES + 65, but tests exercise only the 512/513 canonical-directory boundary and 512 canonical directories plus one malformed entry. No fixture exceeds the 577-entry raw cap. - why: Removing or materially shifting the raw namespace cap would leave the current suite passing. This leaves the deliberately bounded malformed-entry path without a regression guard.
- proposed fix: Create 512 canonical changes plus 65 malformed entries and the mandatory
archivedirectory (578 raw entries), then assertRepositoryLifecycleInputErrorwith codeinput_too_large. Optionally also pin the accepted 577-entry boundary.
[low] Spawned CLI input-error path lacks coverage
- id:
TG-002lens:test-gapfile:scripts/tests/test-openspec-repository-lifecycle.mjs - provenance: finder=
gpt-5.5refuter=gpt-5.6-sol(cross-model-guard) L2=confirmed - evidence: Spawned-process tests cover parity and mismatch only. Input errors are tested through
main(..., sink), which bypasses the default{ out: process.stdout, err: process.stderr }streams used by direct CLI invocation. - why: The existing tests would not catch a regression that routes process-level input errors to stdout while retaining the correct exit code. Direct invocation and nonzero
process.exitCodeare already covered, so the surviving gap is specifically default stderr/stdout routing for exit 3. - proposed fix: Spawn the CLI against a malformed ledger or missing required input and assert status 3, an expected error on stderr, and empty stdout.
Killed (did not survive L2/L3)
L1-1[medium] Canonical marker parser accepts unclosed bold status markers — The finding assumes the contract requires**immediately after the status token, but the diff does not establish that. The governing text allowsStatus: deferred <日期>, and the explicit test confirms trailing date metadata is intentional. This gate extracts lifecycle status; it does not claim toL1-SIMP-1[medium] New gate duplicates lifecycle parsing/comparison logic instead of extracting a shared core — The finding demonstrates only a narrow mirrored contract (CHANGE_ID,STATUS, and baseline marker semantics), not duplicated lifecycle comparison logic broadly. The new parser is materially different: it adds near-miss detection, token-boundary hardening, and detection of multiple status-like dec
Summary
KEEP both survivors as low-severity test gaps. Add one boundary test for the distinct raw namespace cap and one spawned malformed-input test that verifies exit 3 and stderr/stdout routing.
Agent calls
- 10/10 ok, engine wall-clock 396.5s
VERDICT
SHIP
VERDICT: SHIP
…ion marker counting Both from PR #485 review threads. - The `directory_without_ledger_row` record had its observations reversed: the directory is present in `openspec/changes` and absent from the ledger, so the record now names the directory as the observed value and `absent` as what the ledger was expected to carry. - Status-like declarations are now counted per declaration rather than per line, so `> **Status: active** **Status: deferred**` on a single blockquote line is reported as an unreadable marker instead of resolving to `active`. Claude-Session: cowork scheduled task ai-bim-geo Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 992c140ca7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Codex Tri-Adversarial Bot
Automated tri-adversarial ship-gate (L0 terra triage / L1 tier-routed lens fanout / L2 refute-by-default / L3 sol apex — Codex models).
Mapped event: COMMENT
Codex Tri-Adversarial ship-gate — PR #485
- Repo head:
fix/openspec-lifecycle-projection-gate@992c140 - Base:
main@f1757b2 - Files changed: 5
- Engine: four-model tri-adversarial gate on Codex — L0 triage
gpt-5.6-terra/low; L1 lens finders routedgpt-5.6-terra/low →gpt-5.6-luna/medium →gpt-5.5/xhigh (security floorgpt-5.5); L2 refute-by-defaultgpt-5.5/xhigh, top-tier findings refuted bygpt-5.6-sol/xhigh (every refutation cross-model); L3 apexgpt-5.6-sol/max. 誠實聲明:層級與 Claude 三層 gate 同構(terra≈haiku、luna≈sonnet、gpt-5.5≈opus、sol≈fable),但模型池是 Codex 的,非 Anthropic 的。
Verdict
SHIP
- 阻擋門檻 severity:
critical, high - mapped GitHub event:
COMMENT - ℹ️ 判定為 SHIP,但刻意不送 APPROVE:GitHub App 的 approving review 不計入
required_approving_review_count(2026-07-31 實測)。本報告是證據,approving 那一票請由真人帳號投。
Difficulty & routing
- overall:
critical(source: terra-triage) - lens tiers: correctness→
gpt-5.5, security→gpt-5.5, simplification→gpt-5.5, test-gap→gpt-5.5
Layer stats
- L1: raw=6 deduped=6 finder_failures=0
- L2: confirmed=3 refuted=3 unverified=0
- L3 final: 3
Findings (final, after apex)
[medium] Bolded ordinary Status prose is treated as a lifecycle marker near-miss
- id:
L1-COR-002lens:correctnessfile:scripts/lib/openspec-repository-lifecycle.mjsline:336 - provenance: finder=
gpt-5.5refuter=gpt-5.6-sol(cross-model-guard) L2=confirmed - evidence: The final
statusLikeinventory counts every**Status\boccurrence on a blockquote line, while the canonical regex requires**Status:. Re-executing the exact final logic yieldsnear-missfor> **Status quo** is unacceptable., butactivefor the unbolded control and> **Implementation status** — partial. - why: The documented canonical lifecycle syntax includes a colon; ordinary bold prose beginning with “Status” is not necessarily a declaration. This false positive becomes
proposal_marker_unreadableand fails the required governance gate for an otherwise consistent active proposal. - proposed fix: Restrict
statusLiketo marker-shaped text, such asStatusfollowed by a colon or a recognized lifecycle token. Add tests proving> **Status quo** ...remains ordinary prose while malformed forms such as**Status:** deferred,**Status : deferred**, and missing-colon declarations still fail closed as intended.
[low] Text renderer escaping is tested for newline only, not ESC/control bytes it explicitly claims to defend
- id:
TG-002lens:test-gapfile:scripts/tests/test-openspec-repository-lifecycle.mjsline:720 - provenance: finder=
gpt-5.5refuter=gpt-5.6-sol(cross-model-guard) L2=confirmed - evidence: Both renderer-hardening tests inject
\n. The implementation and its comment explicitly cover the entireU+0000–U+001Frange, including ESC, but no test exercises\x1b; therefore a regression narrowing the predicate to newline would leave both tests green. - why: ESC is the terminal-control threat specifically called out by the implementation. Without an ESC assertion, the tests do not protect the full security behavior the change claims to establish.
- proposed fix: Add
\x1b[31mto a mismatch value and/or input-error field, then assert text output contains escaped\u001b, contains no raw ESC byte, and remains one physical line.
[low] No boundary test for the raw namespace entry cap added above the canonical change budget
- id:
TG-003lens:test-gapfile:scripts/tests/test-openspec-repository-lifecycle.mjsline:773 - provenance: finder=
gpt-5.5refuter=gpt-5.6-sol(cross-model-guard) L2=confirmed - evidence:
MAX_CHANGE_NAMESPACE_ENTRIESis512 + 65, andlistChildEntriescounts the mandatoryarchiveentry before filtering it. Existing tests cover 512 canonical changes and 512 plus one malformed entry, but never reach the 577-entry raw cap. - why: The canonical-count tests cannot detect drift in this separate raw-entry budget. Raising it or lowering it while still accepting the single-stray fixture would remain untested.
- proposed fix: Use the corrected boundaries including
archive: verify 512 canonical changes + 64 malformed entries +archive(577 raw entries) reaches targeted mismatch reporting, while 512 + 65 malformed +archive(578) throwsinput_too_large.
Killed (did not survive L2/L3)
L1-COR-001[high] New fixture uses literal newline text and makes the required node test fail — The finding overlooks Patch 4’s per-declaration counting. Even with literal\\n, the direct parser input contains one canonical declaration but two status-like declarations, so it returnsnear-miss, notactive. In the repository fixture,status.includes('\n')is false, sowithRepositorypaTG-001[medium]parseArgumentscan silently ignore a trailing flag value in untested argument shapes — The loop processes every argv position in two-element steps. An odd trailing token is visited withvalue === undefined; an even surplus pair is visited with an unrecognizedflag. Both throwinvalid_argument, andmainmaps that toEXIT_INPUT_ERROR. Existing tests already exercise those sameTG-004[medium] Per-declaration status counting lacks a canonical-on-same-line invalid-marker case — The finding assumes both same-line spans are canonical, but the canonical grammar is anchored at the start of a blockquote line (^>...**Status:). In> **Status: active** **Status: deferred**, only the first span is canonical; the second is an unconsumed status-like near-miss. The added unit test
Summary
KEEP all three survivors. L1-COR-002 remains a medium correctness defect because valid bold prose can falsely fail the required lifecycle gate. TG-002 and TG-003 remain low-severity gaps covering the explicit ESC-hardening behavior and the separate raw-namespace cap boundary.
Agent calls
- 12/12 ok, engine wall-clock 664.7s
VERDICT
SHIP
VERDICT: SHIP
monkey1sai-blip
left a comment
There was a problem hiding this comment.
Approved by monkey1sai-blip (the reviewer account pinned by the repo's merge governance).
Submitted through scripts/blip_review.py — a scripted approval carrying the operator's authority, pinned to head 992c140ca7e2ea627d1df4e178fe6982fa56e4ee. This is the mechanism the GitHub App cannot satisfy: an App's approving review does not count toward required_approving_review_count.
Summary
scripts/lib/openspec-machine-truth.mjsalready owns a complete OpenSpec lifecycle comparator, but its CLI (scripts/tests/verify-openspec-machine-truth.mjs) requires a GitHub observation and anopenspec list --jsoncapture from a pinned binary. Both need network access and an external tool, so CI only ever runs that comparator against synthetic temporary fixtures. The only assertion that touched the real repository was a hand-written regex for one change id inscripts/tests/test-openspec-machine-truth.mjs.Consequence: the three lifecycle truth sources could drift and no gate would refuse it. That is why the drift has repeatedly been corrected by hand after landing (#477, #481, #482).
This PR adds the repository-local subset of that comparison as a gate that needs no network, no git and no external binary, and wires it into the already required
agent-governancecheck.What it compares
> **Status:proposal markersopenspec/changes/<id>/openspec/lifecycle-ledger.jsondocs/plans/NOW.mdlifecycle-ledger blockMismatch codes:
now_unexpected_change,now_change_missing,now_lifecycle_disagreement,ledger_current_without_directory,directory_without_ledger_row,duplicate_active_archive,change_entry_not_a_directory,change_directory_malformed,ledger_archived_without_archive_directory,archive_directory_without_ledger_row,archive_directory_duplicate,archive_directory_malformed,archive_entry_not_a_directory,proposal_missing,proposal_unreadable,proposal_marker_unreadable,proposal_marker_invalid,proposal_status_disagreement.Failure discipline: anything an author can legitimately write is a mismatch (exit 2, names the row); only an input that cannot be read as declared is an input error (exit 3). One odd file never takes the whole required check down in place of pointing at the row that is wrong.
RED/GREEN evidence on the real repository
Against a real historical commit of
main(c41e29d, the tip before #481):That drift was real, sat on
main, and was only fixed later by #481. The existingscripts/tests/verify-openspec-lifecycle.ps1returnsexit 0on the same tree (openspec lifecycle OK: non_deferred=5; deferred=5), which is the hole this PR closes.Against this PR head and against
origin/main(f1757b2) and the open PR branchchore/deploy-linux-test-skill:exit 0,all three sources agree. Injecting a one-word drift intodocs/plans/NOW.mdturns the requiredagent-governancestatic check red end to end (ASSERT FAILED: repository-scoped OpenSpec lifecycle parity fixtures pass).Verification
node --test scripts/tests/test-openspec-repository-lifecycle.mjsnode scripts/tests/verify-openspec-repository-lifecycle.mjscurrent=10 archived=110 now=10 change_dirs=10 archive_dirs=110pwsh -File scripts/tests/test-agent-governance-check.ps1pwsh -File scripts/tests/invoke-powershell-static.ps1pwsh -File scripts/tests/scan-secret-patterns.ps1git diff --checkReviewed by a three-layer adversarial pass (four lenses, cross-model refutation). Round-1/round-2 findings fixed in this head: non-directory and linked entries under
openspec/changesare reported instead of silently filtered out (the Windows symlink-as-plain-file bypass); the CLI resolvesprocess.argv[1]through links so it cannot become a silent no-op exit 0; the status vocabulary matchesopenspec-machine-truth.mjsexactly (held/completed/archived) so a legal row cannot take the gate down; link checks cover every path component below the root while a checkout legitimately reached through a junction is still evaluated; the fenced-block parse is linear instead of super-linear on an unterminated fence; near-miss and invalid markers are reported instead of silently defaulting toactive.Change Classification
openspec/AGENTS.mdlifecycle rules anddocs/agents/self-referential-bootstrap.mdAI Coding Governance
openspec/AGENTS.mdlifecycle rules/scripts/and/openspec/fall under@monkey1sai-blip; owner review requiredgitnexus detect-changes --scope compare --base-ref mainfails in this worktree withMultiple repositories indexedand the worktree has no.gitnexus/run.cjs; perdocs/agents/gitnexus-usage.mdthis is declared unavailable, not passed. Blast radius is bounded by construction: three new files, no existing symbol modified, no rename; the only edit to existing code appends two commands to the end ofscripts/tests/test-agent-governance-check.ps1and oneAssert-FileContainsline.github/workflows/ci.ymlchanged-path classifier does not selectdesign-semantic-visualorfunctional-runtime-convfor these pathsagent-governancestatic check gains two new commandsagent-governance,agent governance contracts,powershell static analysis,secret pattern scan,root contracts and fakes,changed path classifier,pr-metadata-contract-diagnostic,governance-base-auditSelf-referential bootstrap
scripts/tests/test-agent-governance-check.ps1is on theGet-SelfReferentialMechanismPathslist, so the declaration is required. It isnobecause there is no bootstrap paradox here: the file is not inSelfReferentialAdjudicatorPaths, no workflow YAML, adjudicator, deploy path or evidence harness is modified, and the pre-change mechanism validates the post-change behaviour completely — running the edited script is itself the evidence. The change deliberately avoided the mechanism surface for exactly this reason: the new comparator mirrors the marker/status contract fromscripts/lib/openspec-machine-truth.mjsinstead of importing it, because importing would pull mechanism surface into a read-only helper and open bootstrap debt that would block the next mechanism PR. The bootstrap ledger currently carries zero open entries and this PR adds none.Known boundaries
scripts/tests/verify-openspec-lifecycle.ps1already refuses a deferred marker stored in the completed archive.subject_commitfreshness and GitHub PR state stay withscripts/lib/openspec-machine-truth.mjs; the WIP budget stays withverify-openspec-lifecycle.ps1.openspec-machine-truth.mjsmust update both. This is a deliberate trade against opening bootstrap debt.scripts/**/*.mjsis not linted anywhere in CI (no root ESLint config); pre-existing repository gap, not introduced here.Summary by CodeRabbit
New Features
Documentation
Tests