@holmes-lab/holmes-kit 0.21.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +8 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +38 -0
- package/dist/holmes/cli/agents.js +106 -4
- package/dist/holmes/cli/doctor.d.ts +1 -0
- package/dist/holmes/cli/doctor.js +42 -0
- package/dist/holmes/cli/index.js +1 -0
- package/dist/holmes/cli/init.js +18 -0
- package/dist/holmes/cpg/language-parser-walk.js +1 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +54 -1
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +29 -1
- package/dist/holmes/mcp/handlers/operator-inspection.js +68 -2
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +5 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +59 -1
- package/dist/holmes/mcp/handlers/test-execution.d.ts +4 -0
- package/dist/holmes/mcp/handlers/test-execution.js +6 -2
- package/dist/holmes/mcp/handlers.d.ts +33 -1
- package/dist/holmes/mcp/handlers.js +1 -0
- package/dist/holmes/mcp/maintenance-analyze.js +1 -0
- package/dist/holmes/mcp/supervisor.d.ts +35 -0
- package/dist/holmes/mcp/supervisor.js +105 -2
- package/dist/holmes/mcp/tool-schemas.js +1 -0
- package/dist/holmes/project/ci-runs.d.ts +46 -0
- package/dist/holmes/project/ci-runs.js +137 -0
- package/dist/holmes/project/install-scripts-policy.js +1 -0
- package/dist/holmes/review/evaluation-metrics.js +1 -0
- package/dist/holmes/review/kills-check.d.ts +40 -0
- package/dist/holmes/review/kills-check.js +147 -0
- package/dist/holmes/review/manual-baseline.js +1 -0
- package/dist/holmes/rtm/advisory-outcomes.d.ts +137 -0
- package/dist/holmes/rtm/advisory-outcomes.js +314 -0
- package/dist/holmes/rtm/declared-scope.d.ts +28 -0
- package/dist/holmes/rtm/declared-scope.js +60 -0
- package/dist/holmes/rtm/rtm-graph.js +1 -0
- package/dist/holmes/rtm/taint-benchmark.js +1 -0
- package/dist/holmes/server/dashboard-launcher.d.ts +7 -0
- package/dist/holmes/server/dashboard-launcher.js +3 -0
- package/dist/holmes/server/dashboard.js +14 -1
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,118 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.23.1] - 2026-09-18
|
|
9
|
+
|
|
10
|
+
A release that could not be published, and the documentation that had fallen three versions behind.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The release gate knows the ledgers 0.22.0 began tracking** (A-SPEC-668). `v0.23.0`'s publish was
|
|
14
|
+
refused on a dirty tree: 0.22.0 taught `.gitignore` to track `advisory-outcomes` and `ci-runs`, and
|
|
15
|
+
`verify-release.js`'s append-only exclusion list never heard about it. The first is appended by every
|
|
16
|
+
seal and every `approval_status`; the second by the launchd CI runner every fifteen minutes — between
|
|
17
|
+
them this repository could no longer publish at an arbitrary moment. Both are excused now, and a pin
|
|
18
|
+
asserts that every TRACKED ledger is excused, naming any that is not so the next person knows which
|
|
19
|
+
file to edit. The reverse slack is reported rather than asserted away (an excused-but-untracked ledger
|
|
20
|
+
costs nothing, because a gitignored file never shows as dirty).
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
- **The README lists 0.21.0 through 0.23.0.** The feature list stopped at 0.20.0 while three releases
|
|
24
|
+
shipped. The publish playbook's docs-currency gate forbids exactly that — "a stale doc is a false
|
|
25
|
+
claim" — and it is a written step with no automation, so it was skipped twice. Automating it is the
|
|
26
|
+
next candidate.
|
|
27
|
+
|
|
28
|
+
## [0.23.0] - 2026-09-18
|
|
29
|
+
|
|
30
|
+
Three things this release stops doing quietly: deleting the neighbours in a config file it shares,
|
|
31
|
+
going deaf when its own child process dies, and reporting a coverage shortfall it cannot explain.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **A re-wire no longer deletes the other tools' entries** (A-SPEC-667). `init --agent antigravity`
|
|
35
|
+
and `init --agent codex` replaced `.agents/mcp_config.json`, `.agents/hooks.json` and
|
|
36
|
+
`marketplace.json` whole. Measured with a real `--dry-run` before the change: a neighbour MCP
|
|
37
|
+
server, a neighbour hook namespace, a neighbour plugin, a marketplace's own name and an operator's
|
|
38
|
+
`disabled` flag all survived a re-wire at a rate of zero. They now survive; only the holmes-kit
|
|
39
|
+
entry is refreshed, and `init` names what it kept. A `disabled` flag an operator set is preserved
|
|
40
|
+
rather than silently switched back on, and `init` says the gate will not run while it stands. An
|
|
41
|
+
existing file that is not readable JSON is refused with a reason instead of being replaced. The
|
|
42
|
+
Claude wiring already merged (A-SPEC-179); this gives the other two harnesses the same discipline.
|
|
43
|
+
- **The MCP supervisor notices a child that died** (A-SPEC-666). With no exit handler it kept writing
|
|
44
|
+
to a dead pipe, its in-flight count never returned to zero, and the swap that would have replaced
|
|
45
|
+
the child therefore never fired — the server was permanently deaf. Reproduced against the real
|
|
46
|
+
supervisor, twice in one session and once on demand. It now answers every outstanding request with
|
|
47
|
+
a JSON-RPC error before anything else (a client must not wait for ever), resets, respawns and
|
|
48
|
+
replays the opening exchange. A child that dies having never answered spends a restart budget, so a
|
|
49
|
+
broken build stops quickly while a single crash under load restarts freely; an intentional swap or
|
|
50
|
+
`stop()` is not counted as an accident. Affects supervised runs only (`HOLMES_MCP_AUTORELOAD`).
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- **The census says what an unlinked spec declared, not just where it is anchored** (A-SPEC-665).
|
|
54
|
+
`rtm_dashboard` and `/api/rtm` now classify every unlinked approved A-SPEC on a second axis —
|
|
55
|
+
`scanned-source`, `file-anchor-target`, `test-target`, `unreachable-target`, `no-declaration` —
|
|
56
|
+
beside the existing `unlinkedByReason`, and report how many specs declare a path no anchor
|
|
57
|
+
mechanism can read. Measured here: of 52 unlinked specs, zero declare a file the scanner parses, so
|
|
58
|
+
the remaining 8.1% of `codeLinkedPct` is not a missing anchor. Bucket names state what was
|
|
59
|
+
declared; whether that is a defect remains `traceGaps`'s answer.
|
|
60
|
+
|
|
61
|
+
### Measured
|
|
62
|
+
- macOS: 528 suites / 6,607 passed / 11 skipped, every suite green; each slice recorded red-assertion → green with an official `test_run` at both ends.
|
|
63
|
+
- Linux (OrbStack Ubuntu 24.04 arm64, Node 22): judged automatically on every commit by the launchd
|
|
64
|
+
runner added in 0.22.0. Residual reds are load-dependent suites that pass when run alone —
|
|
65
|
+
`dashboard.test` was verified at 120 s alone on the VM against 370 s under load.
|
|
66
|
+
- Windows: not re-run; no Windows-specific change landed.
|
|
67
|
+
|
|
68
|
+
## [0.22.0] - 2026-09-18
|
|
69
|
+
|
|
70
|
+
The advisories learn what happened next. Until now this repository issued findings and recorded that
|
|
71
|
+
it had issued them; nothing recorded whether anyone acted. That missing half is the numerator every
|
|
72
|
+
"promote this to a hard gate once we know the false-positive rate" sentence needed.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- **Advisory reaction ledger** (A-SPEC-663). Every finding — impact-advisory, anchor-density,
|
|
76
|
+
Files-to-Touch fulfilment, trace-gap, kills-unapplicable — now has a deterministic id (kind + spec +
|
|
77
|
+
canonical payload). `spec_approve` records it as `issued` and answers with `advisoryIds`; the next
|
|
78
|
+
`approval_status` re-runs the SAME finding functions and records `resolved` when the cause is gone or
|
|
79
|
+
`persisted` when it is not, at most one row per finding per day. An author who judges a finding
|
|
80
|
+
unhelpful passes `dismiss: [id]` to `spec_approve`; an id the ledger does not know comes back in
|
|
81
|
+
`dismissUnknown` rather than inventing a row. `rtm_dashboard`'s census gains `advisoryOutcomes`
|
|
82
|
+
(issued / resolved / persisted / dismissed per kind) and `approval_status` gains `advisoryHistory`.
|
|
83
|
+
Rows live in `.ax/ledger/advisory-outcomes.<replica>.jsonl` and carry ids, spec ids, outcome words and
|
|
84
|
+
commit hashes only. Queries never issue a finding the seal did not, so the observation denominator
|
|
85
|
+
stays the seal's.
|
|
86
|
+
- **`kills` applicability** (A-SPEC-662). `test_run --mutate` now reports mutations that never applied
|
|
87
|
+
as `unapplied`, separately from `survivors` — measured here, all 22 `kills` entries in this repository
|
|
88
|
+
write `where` as a file path and `mutate` as prose, so the literal-replacement engine applied none of
|
|
89
|
+
them and the response still read `survivors: []`, the shape of a clean run. Sealing a T-SPEC (and
|
|
90
|
+
previewing it with `approval_status`) reports entries whose `where` literal is absent from the
|
|
91
|
+
A-SPEC's anchored production source as a `kills-unapplicable` finding. Never blocking. The
|
|
92
|
+
author-slice playbook now shows the grammar.
|
|
93
|
+
- **CI matrix, minimal form** (A-SPEC-664). `scripts/ci-orb-linux.sh` runs one commit's full suite on an
|
|
94
|
+
OrbStack Linux machine and appends exactly one row to `.ax/ledger/ci-runs.<host>.jsonl` — for every
|
|
95
|
+
outcome, including the ones where the run could not judge (`clone-failed`, `install-failed`,
|
|
96
|
+
`vm-unreachable`). A watch script and a launchd agent trigger it. The Stop hook reports the matrix's
|
|
97
|
+
last word on a tracked line and `doctor` gains a `ci matrix` check, and neither ever reads a missing
|
|
98
|
+
row as green: absence is "not run". The scripts are maintainer tools and are not shipped; a workspace
|
|
99
|
+
with no `ci-runs` ledger hears nothing about the matrix at all.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
- **Trace-gap anchors: 21 approved specs joined the graph.** Each declared a scanned production file in
|
|
103
|
+
its Files to Touch while anchoring only tests, so `rtm_impact` could not see them. Their anchors now
|
|
104
|
+
sit in the files they declared. Unlinked approved specs fell from 73 to 52 and trace gaps from 21 to
|
|
105
|
+
0; `codeLinkedPct` rose from 88.6 to 91.9. The remaining 52 declare no scanned production file — they
|
|
106
|
+
are test- and document-only specs, not gaps.
|
|
107
|
+
- **The runner counted focused suites as failures.** jest reports a suite that used `test.only` with
|
|
108
|
+
status `focused`; filtering on `!== 'passed'` made five healthy suites look red in the first two
|
|
109
|
+
Linux rows. Only `failed` counts now.
|
|
110
|
+
|
|
111
|
+
### Measured
|
|
112
|
+
- macOS: 524 suites / 6,581 passed / 11 skipped; every slice recorded red-assertion → green; official
|
|
113
|
+
`test_run` green at each step.
|
|
114
|
+
- Linux (OrbStack Ubuntu 24.04 arm64, Node 22): five recorded runs, the last one unattended via launchd.
|
|
115
|
+
The residual reds are load-dependent suites (`dashboard.test`, `entity-store-boundaries`,
|
|
116
|
+
`entity-git-snapshot`) that pass when run alone; the host was running other agents' work at the time.
|
|
117
|
+
- First reaction rows in this repository: 22 trace-gap findings issued, 22 resolved by the anchor move.
|
|
118
|
+
- Windows: not re-run for this release; no Windows-specific change landed.
|
|
119
|
+
|
|
8
120
|
## [0.21.0] - 2026-09-17
|
|
9
121
|
|
|
10
122
|
The backlog-hardening cycle: the RTM stops claiming coverage it cannot see, sealing reports what a
|
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🧩 **Your config files survive a re-wire** *(new in 0.23.0)*: `init --agent antigravity` and `init --agent codex` used to replace `.agents/mcp_config.json`, `.agents/hooks.json` and `marketplace.json` **whole**. Measured with a real `--dry-run` before the fix: a neighbour MCP server, a neighbour hook namespace, a neighbour plugin, a marketplace's own name and an operator's `disabled` flag all survived a re-wire at a rate of **zero**. They now survive — only the holmes-kit entry is refreshed, and `init` names what it kept. A `disabled` you set stays set (and `init` says the gate will not run while it stands, rather than switching it back on silently); an existing file that is not readable JSON is refused with a reason instead of being replaced. The Claude wiring already merged; the other two harnesses now have the same discipline.
|
|
20
|
+
- 🫀 **The MCP supervisor notices a child that died** *(new in 0.23.0)*: under `HOLMES_MCP_AUTORELOAD` the supervisor had no exit handler, so a crashed child left it writing to a dead pipe — its in-flight count never returned to zero, the swap that would have replaced the child never fired, and the server went **permanently deaf** (observed twice in one session, then reproduced on demand against the real class). It now answers every outstanding request with a JSON-RPC error **first** (a client must never wait for ever), then resets, respawns and replays the opening exchange. A child that dies having never answered spends a restart budget, so a broken build stops quickly while one crash under load restarts freely; an intentional swap or shutdown is not counted as an accident.
|
|
21
|
+
- 🧮 **Coverage you can explain** *(new in 0.23.0)*: the RTM census already said *where* an unlinked spec is anchored; it now also says *what it declared* — `scanned-source`, `file-anchor-target`, `test-target`, `unreachable-target`, `no-declaration` — plus how many specs declare a path no anchor mechanism can read. On this repository, of 52 unlinked approved specs **zero** declare a file the scanner parses, so the remaining 8.1% of `codeLinkedPct` is not a missing anchor. Bucket names state the fact; whether one is a defect stays `traceGaps`'s answer.
|
|
22
|
+
- 🤖 **A CI matrix that judges every commit, and never reads silence as green** *(new in 0.22.0)*: a maintainer-side runner takes one commit onto a Linux VM (clone → install → build → full suite) and appends exactly one row to a `ci-runs` ledger — for every outcome, including the ones where the run could not judge (`clone-failed`, `install-failed`, `vm-unreachable`). A launchd agent triggers it per commit. The Stop hook reports the matrix's last word and `doctor` gains a `ci matrix` check; a missing row reads as **"not run"**, never as a pass. Workspaces that never adopted the matrix hear nothing about it.
|
|
23
|
+
- 🔁 **Advisories learn what happened next** *(new in 0.22.0)*: every finding — impact, anchor density, Files-to-Touch fulfilment, trace gap, `kills`-unapplicable — now carries a deterministic id. Sealing records it as `issued`; the next `approval_status` re-runs the **same** functions and records `resolved` when the cause is gone or `persisted` when it is not, at most one row per finding per day. An author who judges a finding unhelpful passes `dismiss: [id]`; an unknown id comes back in `dismissUnknown` rather than inventing a row. `rtm_dashboard`'s census gains the per-kind tallies. This is the numerator every "promote to a hard gate once we know the false-positive rate" sentence was missing.
|
|
24
|
+
- 🧪 **`kills` that cannot apply say so** *(new in 0.22.0)*: `test_run --mutate` reports mutations that never applied as `unapplied`, separately from `survivors`. Measured here, all 22 `kills` entries in this repository wrote `where` as a file path and `mutate` as prose, so the literal-replacement engine applied **none** of them while the response still read `survivors: []` — the shape of a clean run. Sealing a T-SPEC now reports entries whose `where` is absent from the A-SPEC's anchored source, and the authoring playbook shows the grammar.
|
|
25
|
+
- 🔎 **The RTM stops claiming coverage it cannot see** *(new in 0.21.0)*: `rtm_dashboard` and `/api/rtm` report `codeLinkedPct`, `unlinkedCount` and `unlinkedByReason` beside the old `coveragePct`, which read 100 while 11.4% of approved specs carried no `implements` edge. The scanner now also reads every anchor the injector can write (`.sh`/`.yml`/`.toml` anchors were planted and never indexed). Sealing gained a **Files-to-Touch fulfilment advisory** (declared paths that do not exist, as `missing`/`moved`/`alternatives`, ledgered, never blocking) and `rtm_impact` gained **trace gaps** — approved specs that declare a changed production file yet anchor only tests, with `impact_gate_check` answering `trace-gap` instead of silently widening.
|
|
26
|
+
- 🩹 **`@known-defect(reason, expires=YYYY-MM-DD)` and constitution article ART-9** *(new in 0.21.0)*: a test that pins a known defect as its expected value carries a machine-readable marker. Unexpired markers are listed on the Stop hook's `tracked` channel as debt; the gate blocks only when a marker has **expired** or cannot be read (`no-reason`, `no-expires`, `bad-date`). String literals are stripped first, the expiry day itself counts as expired (UTC), and a walk that cannot start is no signal rather than a clean bill. A bypass is sometimes the right call — the marker is there so the next person can see it.
|
|
19
27
|
- 🧑🤝🧑 **Concurrent Multi-Agent Workspace** *(new in 0.20.0)*: several agents, machines and clones work on one spec store and converge through Git. A Git-shareable **workspace identity** with per-checkout replica registration (`workspace_identity`) stamps every governed write with run provenance; specs become **UUID-keyed entities** (`entity_store`) whose display ids can be **renumbered without losing identity, anchors or approval closure** (`entity_renumber`); independent work from a common ancestor is **integrated** with stable identity, per-side conflict evidence, automatic renumbering of colliding new ids and explicit derived-state discharge (`entity_integrate`); single-use approvals are spent on the writer's own ledger chain so offline clones merge cleanly, and a nonce spent twice is a structured **double-spend** that freezes every authority-spending act until `ledger_reconcile`. Evidence (RED→GREEN outcomes, timelines) follows the entity across renames. Reproduced end to end in a project outside this repository on macOS and Linux.
|
|
20
28
|
- 🗂️ **Approval decisions you can actually see** *(new in 0.20.0)*: `holmes-kit approve --status` (and the `--watch` header) shows the workspace, the delegation posture, and per request the risk grade, who asked (run · replica · workspace), the subject digest and exactly what a grant would open — plus recent decisions with their outcome (live until / consumed / expired / revoked / denied). A grant is now **bound to the workspace it was minted in and to the content the human read**: a copied grant is refused as `foreign-workspace`, a seal of changed content as `stale-subject`, and `approve --revoke` withdraws a live grant. Grants are git-ignored by `init`.
|
|
21
29
|
- 🔁 **Import cycles are governed, at the three moments that can each do something** *(new in 0.19.0)*: the spec graph has been required to be acyclic since early on; the code graph now is too. **Guidance** reaches the agent before it designs (the authoring playbooks say "share types through a third module", pinned by test). **A design-time advisory** — `approval_status`'s `graphPreview` gains `cycles` — names the cycles your declared files are *already* in, with each edge classified as `type-erasable` (TypeScript deletes it, so it is not a runtime cycle), `lazy-require` (a workaround someone already paid for) or `eager-value`; the note says in words that this is **not** a prediction, because at approval time the code that would create a cycle does not exist yet. **A ratchet** in the Stop hook's constitution catches new cycles after the code is written — shipped in `track` (records, never blocks), and the escape is a **named exception**, never a threshold, so a project carrying legacy cycles can still adopt the harness. This repository went from three cycles to zero, and the two lazy `require()` workarounds they forced are gone.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d2304488-mu6dv817
|
|
@@ -98,6 +98,44 @@ export declare function mergeAgentsMd(existing: string | null, generated: string
|
|
|
98
98
|
* 계산과 쓰기를 나누는 이유는 dry-run 이 실제 실행과 같은 집합을 예고해야 하기 때문이다
|
|
99
99
|
* (A-SPEC-190 §9). 같은 함수가 두 경로에 답하면 둘이 어긋날 수 없다.
|
|
100
100
|
*/
|
|
101
|
+
/**
|
|
102
|
+
* @implements A-SPEC-667
|
|
103
|
+
* Merging a wiring file the harness shares with other tools.
|
|
104
|
+
*
|
|
105
|
+
* Measured 2026-09-18 with a real `--dry-run`: a neighbour MCP server, a neighbour hook namespace, a
|
|
106
|
+
* neighbour plugin, a marketplace's own name and an operator's `disabled` flag all survived a
|
|
107
|
+
* re-wire at a rate of ZERO — the agy and codex writers replaced each file whole. The Claude path
|
|
108
|
+
* learned this in A-SPEC-179 ("merge, not replace"; `--force` means refresh the wiring, not turn off
|
|
109
|
+
* what I switched on). These functions give the other two harnesses the same discipline.
|
|
110
|
+
*
|
|
111
|
+
* Pure text in, pure text out, so `init` can merge during its COMPUTE phase and `--dry-run` predicts
|
|
112
|
+
* the bytes the write produces (the A-SPEC-190 §9 rule).
|
|
113
|
+
*/
|
|
114
|
+
export interface WiringMerge {
|
|
115
|
+
content: string;
|
|
116
|
+
/** Names of things that were kept because they are not ours — reported to the person verbatim. */
|
|
117
|
+
preserved: string[];
|
|
118
|
+
/** Sentences about the merged state a person needs (e.g. the wiring is present but disabled). */
|
|
119
|
+
notes: string[];
|
|
120
|
+
/** Set when the existing file could not be read; `content` is then the ORIGINAL, and init skips it. */
|
|
121
|
+
refused?: string;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* `.agents/mcp_config.json`. Neighbour servers survive whole; inside the holmes-kit entry our
|
|
125
|
+
* command, args and `HOLMES_SPECS` win while every other key a person put there — `disabled` above
|
|
126
|
+
* all — is kept. Turning someone's switch back on without a word is the surprise this prevents.
|
|
127
|
+
*/
|
|
128
|
+
export declare function mergeMcpConfigJson(before: string | null, ours: string): WiringMerge;
|
|
129
|
+
/** `.agents/hooks.json`. Top-level keys are tool namespaces; only ours is replaced. */
|
|
130
|
+
export declare function mergeHooksJson(before: string | null, ours: string): WiringMerge;
|
|
131
|
+
/**
|
|
132
|
+
* `marketplace.json`. Only the holmes-kit plugin entry is ours; the marketplace's identity and every
|
|
133
|
+
* other plugin belong to whoever created the file. Renaming someone's marketplace to ours would be
|
|
134
|
+
* the same silent theft as deleting their plugins.
|
|
135
|
+
*/
|
|
136
|
+
export declare function mergeMarketplaceJson(before: string | null, ours: string): WiringMerge;
|
|
137
|
+
/** The merged set, by basename. `null` means "no merger" — the caller writes as it always did. */
|
|
138
|
+
export declare function mergeWiringFile(basename: string, before: string | null, ours: string): WiringMerge | null;
|
|
101
139
|
export declare function agentFiles(agent: Agent, opts: AgentWiringOptions): AgentFile[];
|
|
102
140
|
export interface AgentLink {
|
|
103
141
|
path: string;
|
|
@@ -38,8 +38,13 @@ exports.antigravityHookCommand = antigravityHookCommand;
|
|
|
38
38
|
exports.shortPathIfSpaced = shortPathIfSpaced;
|
|
39
39
|
exports.antigravityHookWarnings = antigravityHookWarnings;
|
|
40
40
|
exports.mergeAgentsMd = mergeAgentsMd;
|
|
41
|
+
exports.mergeMcpConfigJson = mergeMcpConfigJson;
|
|
42
|
+
exports.mergeHooksJson = mergeHooksJson;
|
|
43
|
+
exports.mergeMarketplaceJson = mergeMarketplaceJson;
|
|
44
|
+
exports.mergeWiringFile = mergeWiringFile;
|
|
41
45
|
exports.agentFiles = agentFiles;
|
|
42
46
|
exports.agentLinks = agentLinks;
|
|
47
|
+
// @implements A-SPEC-202, A-SPEC-250, A-SPEC-341
|
|
43
48
|
// @implements A-SPEC-442
|
|
44
49
|
// @implements A-SPEC-193
|
|
45
50
|
const path = __importStar(require("node:path"));
|
|
@@ -296,12 +301,109 @@ function closeRun(runs, run) {
|
|
|
296
301
|
runs.push(run);
|
|
297
302
|
return [];
|
|
298
303
|
}
|
|
304
|
+
const HOLMES = 'holmes-kit';
|
|
305
|
+
/** Parse, or explain why not. An unreadable file is never replaced: silently deleting it is the defect. */
|
|
306
|
+
function parseOr(before, ours, what) {
|
|
307
|
+
if (before === null)
|
|
308
|
+
return { content: ours, preserved: [], notes: [] };
|
|
309
|
+
let prev;
|
|
310
|
+
try {
|
|
311
|
+
prev = JSON.parse(before);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return { content: before, preserved: [], notes: [], refused: `${what} is not readable JSON — left untouched; fix or remove it, then re-run init` };
|
|
315
|
+
}
|
|
316
|
+
if (prev === null || typeof prev !== 'object' || Array.isArray(prev)) {
|
|
317
|
+
return { content: before, preserved: [], notes: [], refused: `${what} is not a JSON object — left untouched; fix or remove it, then re-run init` };
|
|
318
|
+
}
|
|
319
|
+
return { prev: prev };
|
|
320
|
+
}
|
|
321
|
+
const isMerge = (x) => 'content' in x;
|
|
322
|
+
const asObject = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
299
323
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
|
|
324
|
+
* `.agents/mcp_config.json`. Neighbour servers survive whole; inside the holmes-kit entry our
|
|
325
|
+
* command, args and `HOLMES_SPECS` win while every other key a person put there — `disabled` above
|
|
326
|
+
* all — is kept. Turning someone's switch back on without a word is the surprise this prevents.
|
|
327
|
+
*/
|
|
328
|
+
function mergeMcpConfigJson(before, ours) {
|
|
329
|
+
const parsed = parseOr(before, ours, 'mcp_config.json');
|
|
330
|
+
if (isMerge(parsed))
|
|
331
|
+
return parsed;
|
|
332
|
+
const oursObj = asObject(JSON.parse(ours));
|
|
333
|
+
const oursEntry = asObject(asObject(oursObj.mcpServers)[HOLMES]);
|
|
334
|
+
const prevServers = asObject(parsed.prev.mcpServers);
|
|
335
|
+
const prevEntry = asObject(prevServers[HOLMES]);
|
|
336
|
+
const keptKeys = Object.keys(prevEntry).filter((k) => k !== 'command' && k !== 'args' && k !== 'env');
|
|
337
|
+
const merged = {
|
|
338
|
+
...prevEntry,
|
|
339
|
+
command: oursEntry.command,
|
|
340
|
+
args: oursEntry.args,
|
|
341
|
+
env: { ...asObject(prevEntry.env), ...asObject(oursEntry.env) },
|
|
342
|
+
};
|
|
343
|
+
const out = { ...parsed.prev, mcpServers: { ...prevServers, [HOLMES]: merged } };
|
|
344
|
+
const notes = [];
|
|
345
|
+
if (prevEntry.disabled === true || prevEntry.disabled === 'true') {
|
|
346
|
+
notes.push(`mcp_config.json: holmes-kit is wired but DISABLED in this config — the gate will not run until it is enabled.`);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
content: `${JSON.stringify(out, null, 2)}\n`,
|
|
350
|
+
preserved: [...Object.keys(prevServers).filter((k) => k !== HOLMES), ...keptKeys],
|
|
351
|
+
notes,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** `.agents/hooks.json`. Top-level keys are tool namespaces; only ours is replaced. */
|
|
355
|
+
function mergeHooksJson(before, ours) {
|
|
356
|
+
const parsed = parseOr(before, ours, 'hooks.json');
|
|
357
|
+
if (isMerge(parsed))
|
|
358
|
+
return parsed;
|
|
359
|
+
const oursObj = asObject(JSON.parse(ours));
|
|
360
|
+
const out = { ...parsed.prev, [HOLMES]: oursObj[HOLMES] };
|
|
361
|
+
return {
|
|
362
|
+
content: `${JSON.stringify(out, null, 2)}\n`,
|
|
363
|
+
preserved: Object.keys(parsed.prev).filter((k) => k !== HOLMES),
|
|
364
|
+
notes: [],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* `marketplace.json`. Only the holmes-kit plugin entry is ours; the marketplace's identity and every
|
|
369
|
+
* other plugin belong to whoever created the file. Renaming someone's marketplace to ours would be
|
|
370
|
+
* the same silent theft as deleting their plugins.
|
|
304
371
|
*/
|
|
372
|
+
function mergeMarketplaceJson(before, ours) {
|
|
373
|
+
const parsed = parseOr(before, ours, 'marketplace.json');
|
|
374
|
+
if (isMerge(parsed))
|
|
375
|
+
return parsed;
|
|
376
|
+
const oursObj = asObject(JSON.parse(ours));
|
|
377
|
+
const oursPlugin = (Array.isArray(oursObj.plugins) ? oursObj.plugins : []).find((p) => asObject(p).name === HOLMES);
|
|
378
|
+
const prevPlugins = Array.isArray(parsed.prev.plugins) ? parsed.prev.plugins : [];
|
|
379
|
+
const hadOurs = prevPlugins.some((p) => asObject(p).name === HOLMES);
|
|
380
|
+
const plugins = hadOurs
|
|
381
|
+
? prevPlugins.map((p) => (asObject(p).name === HOLMES ? oursPlugin : p))
|
|
382
|
+
: [...prevPlugins, oursPlugin];
|
|
383
|
+
const keptName = typeof parsed.prev.name === 'string' && parsed.prev.name !== '' ? parsed.prev.name : undefined;
|
|
384
|
+
const out = {
|
|
385
|
+
...parsed.prev,
|
|
386
|
+
name: keptName ?? oursObj.name,
|
|
387
|
+
interface: parsed.prev.interface ?? oursObj.interface,
|
|
388
|
+
plugins,
|
|
389
|
+
};
|
|
390
|
+
const neighbours = prevPlugins.map((p) => String(asObject(p).name ?? '')).filter((n) => n !== '' && n !== HOLMES);
|
|
391
|
+
return {
|
|
392
|
+
content: `${JSON.stringify(out, null, 2)}\n`,
|
|
393
|
+
preserved: [...neighbours, ...(keptName !== undefined && keptName !== oursObj.name ? [keptName] : [])],
|
|
394
|
+
notes: [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** The merged set, by basename. `null` means "no merger" — the caller writes as it always did. */
|
|
398
|
+
function mergeWiringFile(basename, before, ours) {
|
|
399
|
+
if (basename === 'mcp_config.json')
|
|
400
|
+
return mergeMcpConfigJson(before, ours);
|
|
401
|
+
if (basename === 'hooks.json')
|
|
402
|
+
return mergeHooksJson(before, ours);
|
|
403
|
+
if (basename === 'marketplace.json')
|
|
404
|
+
return mergeMarketplaceJson(before, ours);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
305
407
|
function agentFiles(agent, opts) {
|
|
306
408
|
const { target, packageRoot, specsDir } = opts;
|
|
307
409
|
switch (agent) {
|
|
@@ -171,6 +171,7 @@ export declare function wiringSpawnCheck(command: string, args: string[], timeou
|
|
|
171
171
|
* the same honest-diagnosis lineage as the codex-wiring WARN (never a gate).
|
|
172
172
|
*/
|
|
173
173
|
export declare function pushGateCheck(target: string): Check | null;
|
|
174
|
+
export declare function ciMatrixCheck(target: string): Check | null;
|
|
174
175
|
export declare function formatChecks(checks: Check[]): string;
|
|
175
176
|
/**
|
|
176
177
|
* Prove that EVERY wired harness can actually start a server, not merely that its file parses.
|
|
@@ -47,10 +47,12 @@ exports.resolveWiringPath = resolveWiringPath;
|
|
|
47
47
|
exports.runDoctor = runDoctor;
|
|
48
48
|
exports.wiringSpawnCheck = wiringSpawnCheck;
|
|
49
49
|
exports.pushGateCheck = pushGateCheck;
|
|
50
|
+
exports.ciMatrixCheck = ciMatrixCheck;
|
|
50
51
|
exports.formatChecks = formatChecks;
|
|
51
52
|
exports.wiringHandshakeChecks = wiringHandshakeChecks;
|
|
52
53
|
exports.semanticTierVerdict = semanticTierVerdict;
|
|
53
54
|
exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
|
|
55
|
+
// @implements A-SPEC-264, A-SPEC-423, A-SPEC-549.1, A-SPEC-590
|
|
54
56
|
// @implements A-SPEC-594
|
|
55
57
|
// @implements A-SPEC-592
|
|
56
58
|
// @implements A-SPEC-591
|
|
@@ -65,6 +67,7 @@ const native_deps_1 = require("./native-deps");
|
|
|
65
67
|
const tier_1 = require("../semantic/tier");
|
|
66
68
|
const probe_process_1 = require("./probe-process");
|
|
67
69
|
const npx_cache_check_1 = require("./npx-cache-check");
|
|
70
|
+
const ci_runs_1 = require("../project/ci-runs");
|
|
68
71
|
const path = __importStar(require("node:path"));
|
|
69
72
|
const role_policy_1 = require("../governance/role-policy");
|
|
70
73
|
const blind_spots_1 = require("../guardrail/blind-spots");
|
|
@@ -1261,6 +1264,9 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
1261
1264
|
const pg = pushGateCheck(target ?? process.cwd());
|
|
1262
1265
|
if (pg)
|
|
1263
1266
|
checks.push(pg);
|
|
1267
|
+
const ci = ciMatrixCheck(target ?? process.cwd()); // @implements A-SPEC-664
|
|
1268
|
+
if (ci)
|
|
1269
|
+
checks.push(ci);
|
|
1264
1270
|
}
|
|
1265
1271
|
if (extraChecks) {
|
|
1266
1272
|
checks.push(...extraChecks);
|
|
@@ -1554,6 +1560,42 @@ function pushGateCheck(target) {
|
|
|
1554
1560
|
return null;
|
|
1555
1561
|
} // diagnosis must never crash doctor
|
|
1556
1562
|
}
|
|
1563
|
+
// @implements A-SPEC-664
|
|
1564
|
+
// The CI matrix as doctor sees it: PASS only for a green row at HEAD; everything else is a WARN that
|
|
1565
|
+
// says exactly what the ledger says — "not run" when there is no row, the failed suites when red,
|
|
1566
|
+
// the infrastructure status when the run could not judge. Never a FAIL: the matrix informs.
|
|
1567
|
+
function ciMatrixCheck(target) {
|
|
1568
|
+
try {
|
|
1569
|
+
if (!fs.existsSync(path.join(target, '.ax')))
|
|
1570
|
+
return null;
|
|
1571
|
+
if (!(0, ci_runs_1.hasCiLedger)(target))
|
|
1572
|
+
return null; // @implements A-SPEC-664 — same adoption predicate as the hook
|
|
1573
|
+
const runs = (0, ci_runs_1.readCiRuns)(target);
|
|
1574
|
+
let head;
|
|
1575
|
+
try {
|
|
1576
|
+
head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: target, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
head = undefined;
|
|
1580
|
+
}
|
|
1581
|
+
const behind = (rev) => {
|
|
1582
|
+
try {
|
|
1583
|
+
return Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${rev}..HEAD`], { cwd: target, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim());
|
|
1584
|
+
}
|
|
1585
|
+
catch {
|
|
1586
|
+
return undefined;
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
const v = (0, ci_runs_1.ciVerdict)(runs, 'linux', head, behind);
|
|
1590
|
+
const detail = (0, ci_runs_1.ciStatusLine)(v);
|
|
1591
|
+
if (v.state === 'green' && v.behind === 0)
|
|
1592
|
+
return { name: 'ci matrix', level: 'PASS', detail };
|
|
1593
|
+
return { name: 'ci matrix', level: 'WARN', detail, fix: 'Run scripts/ci-orb-linux.sh HEAD (or install the launchd agent: scripts/ci-orb-linux-install.sh)' };
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
return null;
|
|
1597
|
+
} // diagnosis must never crash doctor
|
|
1598
|
+
}
|
|
1557
1599
|
function formatChecks(checks) {
|
|
1558
1600
|
const lines = checks.map((c) => {
|
|
1559
1601
|
const head = `${c.level.padEnd(4)} ${c.name} — ${c.detail}`;
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.isBrokenPipe = void 0;
|
|
|
37
37
|
exports.packageRoot = packageRoot;
|
|
38
38
|
exports.main = main;
|
|
39
39
|
exports.installPipeGuard = installPipeGuard;
|
|
40
|
+
// @implements A-SPEC-549.2
|
|
40
41
|
// @implements A-SPEC-591, A-SPEC-626
|
|
41
42
|
const cli_execution_1 = require("./cli-execution");
|
|
42
43
|
// @implements A-SPEC-100.2
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.settingsPathOf = exports.MATCHERS = exports.SERVER_NAME = void 0;
|
|
37
37
|
exports.buildHookPlan = buildHookPlan;
|
|
38
38
|
exports.runInit = runInit;
|
|
39
|
+
// @implements A-SPEC-340
|
|
39
40
|
// @implements A-SPEC-100.2
|
|
40
41
|
const fs = __importStar(require("node:fs"));
|
|
41
42
|
const path = __importStar(require("node:path"));
|
|
@@ -470,6 +471,23 @@ function runInit(opts) {
|
|
|
470
471
|
}
|
|
471
472
|
continue;
|
|
472
473
|
}
|
|
474
|
+
// @implements A-SPEC-667 — these files are shared with other tools. Replacing one whole
|
|
475
|
+
// deletes a neighbour's MCP server, hooks or plugins without a word (measured 2026-09-18:
|
|
476
|
+
// every third-party entry survived at a rate of zero). Merging happens HERE, in the compute
|
|
477
|
+
// phase, so `--dry-run` predicts the bytes the write produces (A-SPEC-190 §9).
|
|
478
|
+
const wiring = (0, agents_1.mergeWiringFile)(path.basename(f.path), before, f.content);
|
|
479
|
+
if (wiring) {
|
|
480
|
+
if (wiring.refused) {
|
|
481
|
+
messages.push(`${f.path}: ${wiring.refused}`);
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
changes.push({ path: f.path, before, after: wiring.content });
|
|
485
|
+
if (wiring.preserved.length > 0)
|
|
486
|
+
messages.push(`${f.path}: kept ${wiring.preserved.length} ${wiring.preserved.length === 1 ? 'entry that is' : 'entries that are'} not holmes-kit's — ${wiring.preserved.join(', ')}`);
|
|
487
|
+
for (const n of wiring.notes)
|
|
488
|
+
messages.push(n);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
473
491
|
changes.push({ path: f.path, before, after: f.content });
|
|
474
492
|
}
|
|
475
493
|
// @implements A-SPEC-586 — an antigravity hook whose path still carries a space is a hook the
|
|
@@ -2,7 +2,9 @@ import { PendingRequest } from '../governance/approval-queue';
|
|
|
2
2
|
import { Spec } from '../spec/spec-parser';
|
|
3
3
|
import type { TestOutcome } from '../review/test-runner';
|
|
4
4
|
import { type KnownDefectJudgement } from '../rtm/known-defects';
|
|
5
|
+
import { type CiVerdict } from '../project/ci-runs';
|
|
5
6
|
export declare function collectKnownDefects(root: string, now: Date): KnownDefectJudgement | undefined;
|
|
7
|
+
export declare function collectCiVerdicts(root: string, now?: Date): CiVerdict[];
|
|
6
8
|
/**
|
|
7
9
|
* @implements A-SPEC-100.2
|
|
8
10
|
* Stop-hook governance gate (Phase-2 #1: push, not pull).
|
|
@@ -31,6 +33,11 @@ export interface StopEvidence {
|
|
|
31
33
|
* `undefined` is no signal — a walk that could not run must not read as marker-free.
|
|
32
34
|
*/
|
|
33
35
|
knownDefects?: KnownDefectJudgement;
|
|
36
|
+
/**
|
|
37
|
+
* @implements A-SPEC-664 — what the CI matrix last said about this tree, per OS. Absent when the
|
|
38
|
+
* hook could not look; a `not-run` verdict when it looked and found no row (never silence).
|
|
39
|
+
*/
|
|
40
|
+
ci?: CiVerdict[];
|
|
34
41
|
/** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
|
|
35
42
|
provenance?: {
|
|
36
43
|
ok: boolean;
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
37
|
exports.collectKnownDefects = collectKnownDefects;
|
|
38
|
+
exports.collectCiVerdicts = collectCiVerdicts;
|
|
38
39
|
exports.changedAnchoredAspecs = changedAnchoredAspecs;
|
|
39
40
|
exports.unanchoredChangedSources = unanchoredChangedSources;
|
|
40
41
|
exports.dependencyReappraisals = dependencyReappraisals;
|
|
@@ -79,6 +80,7 @@ const constitution_debt_1 = require("../governance/constitution-debt");
|
|
|
79
80
|
const root_1 = require("../project/root");
|
|
80
81
|
const known_defects_1 = require("../rtm/known-defects");
|
|
81
82
|
const test_files_1 = require("../cpg/test-files");
|
|
83
|
+
const ci_runs_1 = require("../project/ci-runs");
|
|
82
84
|
// @implements A-SPEC-660 — the I/O half of the known-defect marker: walk the workspace's test files
|
|
83
85
|
// (the same directory rule and test predicate ART-4's anchor scan uses), parse each for markers,
|
|
84
86
|
// judge them against the injected clock. A walk that cannot START is NO SIGNAL (undefined) — never
|
|
@@ -126,6 +128,40 @@ function collectKnownDefects(root, now) {
|
|
|
126
128
|
return undefined;
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
// @implements A-SPEC-664 — the I/O half of the CI line: read the ci-runs ledger, judge Linux (always)
|
|
132
|
+
// and any other OS that has a row, with git measuring how far each judged commit sits behind HEAD.
|
|
133
|
+
// A tree without a ledger yields a `not-run` verdict — a line, not an absence.
|
|
134
|
+
function collectCiVerdicts(root, now = new Date()) {
|
|
135
|
+
void now;
|
|
136
|
+
// @implements A-SPEC-664 — a workspace that never ran the matrix hears nothing about it. Past this
|
|
137
|
+
// line the matrix IS adopted here, so a missing row is reported rather than passed over in silence.
|
|
138
|
+
if (!(0, ci_runs_1.hasCiLedger)(root))
|
|
139
|
+
return [];
|
|
140
|
+
let runs = [];
|
|
141
|
+
try {
|
|
142
|
+
runs = (0, ci_runs_1.readCiRuns)(root);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
runs = [];
|
|
146
|
+
}
|
|
147
|
+
let head;
|
|
148
|
+
try {
|
|
149
|
+
head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
head = undefined;
|
|
153
|
+
}
|
|
154
|
+
const behind = (rev) => {
|
|
155
|
+
try {
|
|
156
|
+
return Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${rev}..HEAD`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim());
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const oses = ['linux', ...[...new Set(runs.map((r) => r.os))].filter((o) => o !== 'linux').sort()];
|
|
163
|
+
return oses.map((os) => (0, ci_runs_1.ciVerdict)(runs, os, head, behind));
|
|
164
|
+
}
|
|
129
165
|
/**
|
|
130
166
|
* @implements A-SPEC-534.4
|
|
131
167
|
* ART-8 evidence (I/O half): the A-SPECs whose DIRTY source files carry an @implements anchor. git is
|
|
@@ -492,6 +528,7 @@ const TRACK_LABELS = {
|
|
|
492
528
|
'ART-8': 'RED-first',
|
|
493
529
|
'ART-2': 'code-graph cycles',
|
|
494
530
|
'ART-9': 'known-defect debt',
|
|
531
|
+
'CI': 'matrix',
|
|
495
532
|
};
|
|
496
533
|
/**
|
|
497
534
|
* One line per ARTICLE, each under its own name.
|
|
@@ -557,6 +594,14 @@ function evaluateStop(specs, evidence) {
|
|
|
557
594
|
const t = evidence.knownDefects.unexpired.map((k) => ({ article: 'ART-9', detail: `${k.file}:${k.line}: known defect "${k.reason}" until ${k.expires}` }));
|
|
558
595
|
tracked = [...(tracked ?? []), ...t];
|
|
559
596
|
}
|
|
597
|
+
// @implements A-SPEC-664 — the CI matrix's last word, one tracked line per OS. Never a block: a
|
|
598
|
+
// red Linux run is information for the person closing the turn, and "not run" is said out loud
|
|
599
|
+
// because the alternative — silence — reads as green (measured 2026-09-17: 136 environmental reds
|
|
600
|
+
// surfaced only because someone ran the suite by hand before a release).
|
|
601
|
+
if (evidence?.ci && evidence.ci.length > 0) {
|
|
602
|
+
const t = evidence.ci.map((v) => ({ article: 'CI', detail: (0, ci_runs_1.ciStatusLine)(v) }));
|
|
603
|
+
tracked = [...(tracked ?? []), ...t];
|
|
604
|
+
}
|
|
560
605
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
561
606
|
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
562
607
|
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
@@ -1141,7 +1186,15 @@ if (require.main === module) {
|
|
|
1141
1186
|
catch { /* maintenance, never a hook failure */ }
|
|
1142
1187
|
// @implements A-SPEC-660 — the marker walk: no signal when it cannot run (never a clean verdict).
|
|
1143
1188
|
const knownDefects = collectKnownDefects(stopProjectRoot(), new Date());
|
|
1144
|
-
|
|
1189
|
+
// @implements A-SPEC-664 — the CI matrix line rides beside the constitution's verdict.
|
|
1190
|
+
let ci;
|
|
1191
|
+
try {
|
|
1192
|
+
ci = collectCiVerdicts(stopProjectRoot());
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
ci = undefined;
|
|
1196
|
+
}
|
|
1197
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}) });
|
|
1145
1198
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
1146
1199
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
1147
1200
|
// @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
|
|
@@ -15,6 +15,11 @@ export interface OperatorInspectionContext {
|
|
|
15
15
|
reason: string;
|
|
16
16
|
};
|
|
17
17
|
fetchJson(url: string): Promise<any>;
|
|
18
|
+
/** A-SPEC-663 — the production scan, for the trace-gap finding (same closure test_run uses). */
|
|
19
|
+
cachedScan?(root: string, repoRoot?: string): Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
implementsSpecs?: string[];
|
|
22
|
+
}>;
|
|
18
23
|
/** A-SPEC-642 — the shared approval channel and queue hint, for the reconciliation act. */
|
|
19
24
|
resolveHandlerApproval(root: string | undefined, approval: Approval | undefined, action: {
|
|
20
25
|
kind: string;
|
|
@@ -46,6 +51,9 @@ export declare function createOperatorInspectionHandlers(context: OperatorInspec
|
|
|
46
51
|
ok: boolean;
|
|
47
52
|
reason: string;
|
|
48
53
|
} | {
|
|
54
|
+
advisoryHistory?: import("../../rtm/advisory-outcomes").AdvisoryState[] | undefined;
|
|
55
|
+
advisoryIds?: Partial<Record<string, string>> | undefined;
|
|
56
|
+
killsAdvisory?: import("../../review/kills-check").KillsAdvisory | undefined;
|
|
49
57
|
fttFulfilment?: import("../../rtm/ftt-fulfilment").FttFulfilment | undefined;
|
|
50
58
|
graphPreview?: {
|
|
51
59
|
impact?: import("../../rtm/impact-advisory").ImpactAdvisory;
|
|
@@ -162,7 +170,27 @@ export declare function createOperatorInspectionHandlers(context: OperatorInspec
|
|
|
162
170
|
ok: boolean;
|
|
163
171
|
url: string;
|
|
164
172
|
running: boolean;
|
|
165
|
-
census:
|
|
173
|
+
census: {
|
|
174
|
+
advisoryOutcomes?: import("../../rtm/advisory-outcomes").AdvisoryCensus | undefined;
|
|
175
|
+
codeLinkedPct: number;
|
|
176
|
+
codeLinkedCount: number;
|
|
177
|
+
unlinkedCount: number;
|
|
178
|
+
unlinkedByReason: Record<import("../../rtm/link-census").UnlinkedReason, number>;
|
|
179
|
+
excluded: {
|
|
180
|
+
total: number;
|
|
181
|
+
retired: number;
|
|
182
|
+
unmapped: number;
|
|
183
|
+
nonSpec: number;
|
|
184
|
+
};
|
|
185
|
+
declaredScopeByScope: Record<import("../../rtm/declared-scope").DeclaredScope, number>;
|
|
186
|
+
unreachableDeclaringCount: number;
|
|
187
|
+
reqCount: number;
|
|
188
|
+
pipelineCount: number;
|
|
189
|
+
coveredCount: number;
|
|
190
|
+
coveragePct: number;
|
|
191
|
+
retiredCount: number;
|
|
192
|
+
findingsScanned: boolean;
|
|
193
|
+
};
|
|
166
194
|
reason?: undefined;
|
|
167
195
|
}>;
|
|
168
196
|
};
|