@holmes-lab/holmes-kit 0.23.3 → 0.24.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 +61 -0
- package/README.md +45 -42
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/index.js +41 -1
- package/dist/holmes/cli/open-url.d.ts +30 -0
- package/dist/holmes/cli/open-url.js +38 -0
- package/dist/holmes/cli/report.d.ts +20 -0
- package/dist/holmes/cli/report.js +115 -0
- package/dist/holmes/cli/reported-log.d.ts +23 -0
- package/dist/holmes/cli/reported-log.js +31 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +35 -1
- package/dist/holmes/project/analysis-currency.d.ts +22 -0
- package/dist/holmes/project/analysis-currency.js +96 -0
- package/dist/holmes/project/field-report-url.d.ts +47 -0
- package/dist/holmes/project/field-report-url.js +63 -0
- package/dist/holmes/project/field-report.d.ts +54 -0
- package/dist/holmes/project/field-report.js +107 -0
- package/dist/holmes/update/workspaces.d.ts +53 -2
- package/dist/holmes/update/workspaces.js +77 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,67 @@ 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.24.1] - 2026-09-19
|
|
9
|
+
|
|
10
|
+
Documentation only. No code changed; `dist/` is byte-identical in behaviour to 0.24.0.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **The README feature list stops growing without bound.** Accumulating one full paragraph per
|
|
14
|
+
shipped feature since 0.16.0 had taken the list to 30,905 characters, and the cost fell on the
|
|
15
|
+
reader who opens the page to find out what the CURRENT release is: the two 0.24.0 entries sat
|
|
16
|
+
above forty paragraphs of equal visual weight. The three most recent releases (0.23.2, 0.23.3,
|
|
17
|
+
0.24.0) keep their full account; everything from 0.16.0 through 0.23.0 is condensed to one line
|
|
18
|
+
each under **Earlier releases**, and the older foundations are grouped under **Foundations** with
|
|
19
|
+
duplicated descriptions merged. Measured: 30,905 → 17,264 characters, a 44% reduction.
|
|
20
|
+
Each release's full account remains in this file, which the README now links to.
|
|
21
|
+
The unflattering measurements were kept rather than trimmed away — the wrong census number
|
|
22
|
+
0.23.0 published and its correction, the 63.3% of candidate slots that were going to files that
|
|
23
|
+
could not be the answer, the 1,404 single-shot refusals that were burying a two-item inbox.
|
|
24
|
+
A summary that keeps only the favourable half is not a summary.
|
|
25
|
+
npm renders the README captured at publish time, so this release is what carries the shorter
|
|
26
|
+
page to the registry; the GitHub copy has been current since the commit itself.
|
|
27
|
+
|
|
28
|
+
## [0.24.0] - 2026-09-19
|
|
29
|
+
|
|
30
|
+
A way for a consumer's defect to reach us, and two rules that existed only in prose.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- **`holmes-kit report`** (A-SPEC-675, A-SPEC-676, A-SPEC-679, A-SPEC-680). A consumer project had no channel by which a
|
|
34
|
+
holmes-kit defect could reach us: the one field defect we knew about arrived because a user pasted
|
|
35
|
+
a transcript, and it had reproduced for every consumer on every slice. The command writes a
|
|
36
|
+
**redacted** report to `.ax/reports/<fingerprint>.md` and prints a prefilled GitHub issue link —
|
|
37
|
+
carrying the title, the repository owner as assignee, and the body — plus a search link for the
|
|
38
|
+
same fingerprint so a duplicate is visible before filing. `--open` opens it; a headless box, an SSH
|
|
39
|
+
session or CI simply keeps the printed link. **No token, no API, no automatic submission**: the
|
|
40
|
+
person presses Submit on GitHub's own page with the body visible and editable, which puts consent
|
|
41
|
+
where the bytes are. Redaction is an **allowlist**, not a scrubber, because a denylist has been
|
|
42
|
+
punctured here before; the machine's hostname, username, home directory and replica ids are
|
|
43
|
+
searched for in the composed body by the pins themselves. A spec id passes by shape and a spec
|
|
44
|
+
TITLE does not — that is unreleased product intent. Observations take numbers only.
|
|
45
|
+
What holmes-kit cannot know, it says: no ledger keeps the product's refusal text, so a run without
|
|
46
|
+
`--message` writes the report, states that no description was given, explains why it cannot be
|
|
47
|
+
recovered, and exits non-zero rather than handing a maintainer a version string.
|
|
48
|
+
- **A stale graph analysis is visible** (A-SPEC-681). The Stop hook's tracked channel reports source
|
|
49
|
+
changed with no `maintenance_analyze` standing open. Measured here: the procedure had been skipped
|
|
50
|
+
for seventeen consecutive commits, and the one run that followed found child-process precedents a
|
|
51
|
+
name search had missed entirely. Non-blocking, silent in a workspace that never analysed anything,
|
|
52
|
+
and judged by commit rather than by clock.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- **The workspace registry no longer fills with dead temp directories** (A-SPEC-677, 678). Measured:
|
|
56
|
+
603 entries, 458 (76%) pointing at paths that no longer existed and 599 of them temp directories —
|
|
57
|
+
**three** were real projects. `upgrade` believed the list: it would print 458 skip lines and then
|
|
58
|
+
run `runInit({force:true})` against 142 abandoned temp directories, writing wiring files into them.
|
|
59
|
+
A temp target is now never recorded, and recording prunes what has vanished. The pruning is
|
|
60
|
+
deliberately asymmetric: a vanished temp path is gone by construction, while a vanished normal path
|
|
61
|
+
may be an unmounted volume, so it is counted and kept — `upgrade` already skips it harmlessly, and
|
|
62
|
+
deleting it is the one outcome a user cannot undo.
|
|
63
|
+
- **Tests can no longer write into the developer's real registry** (A-SPEC-678). That is how 599 of
|
|
64
|
+
those entries arrived. Inside jest, `process.env` is a sandbox copy, so setting `HOME` never
|
|
65
|
+
reaches `setenv` and `os.homedir()` keeps answering the real home — an in-process test cannot
|
|
66
|
+
isolate a module that calls it directly. `HOLMES_HOME` is the seam; consumers set nothing and
|
|
67
|
+
behaviour is unchanged for them.
|
|
68
|
+
|
|
8
69
|
## [0.23.3] - 2026-09-18
|
|
9
70
|
|
|
10
71
|
A suite that takes half as long, a build that admits when it is stale, and a CI matrix that stopped
|
package/README.md
CHANGED
|
@@ -16,51 +16,54 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 📮 **`holmes-kit report` — a defect can reach the maintainers** *(new in 0.24.0)*: until now a consumer's holmes-kit defect had no way back to us; the one we learned about arrived because someone pasted a transcript, and it had been reproducing for every consumer on every slice. The command writes a **redacted** report to `.ax/reports/<fingerprint>.md` and prints a prefilled GitHub issue link — title, assignee, body — plus a search link for the same fingerprint so you can see whether it is already known. `--open` opens it; on a headless box or over SSH the printed link is the whole of it. **No token, no API, nothing sent automatically**: you press Submit on GitHub's own page with the body in front of you and editable. Redaction is an allowlist rather than a scrubber — this project's own remote carries a token before the `@`, its replica ids carry a person's name, and its spec titles are unreleased product intent, so a path, a credential or a machine identifier withholds the field and the report says which. A spec id passes by shape; a spec title does not. And what holmes-kit cannot know, it says: no ledger keeps its own refusal text, so a report with no description states that rather than pretending.
|
|
20
|
+
- 🧭 **A skipped graph analysis is visible** *(new in 0.24.0)*: `AGENTS.md` asks for `maintenance_analyze` before editing source, and nothing checked. Measured on this repository, the step had been skipped for seventeen consecutive commits — and the run that followed named child-process precedents a name search had missed completely, because the question was "who opens a browser" while the answer lived under "who spawns a child". The Stop hook now reports source changed with no analysis standing open: non-blocking, judged by commit rather than by clock, and silent in a workspace that never adopted the habit.
|
|
19
21
|
- 🧱 **A stale build is told, not discovered** *(new in 0.23.3)*: thirty suites in this project load `dist/` while they run, and nothing asserted that it still represented the source — only the release gate compared the build id to HEAD, and only at publish time. A stale build does not go red; it verifies old code and returns green. The Stop hook now reports it on the non-blocking `tracked` channel, judged by the **build id and never by mtime**: measured here, `.build-id` had a newer mtime than every source file while naming a commit nine behind HEAD, with two changed sources missing from `dist` entirely. A workspace that does not build hears nothing, `fresh` says nothing, and the two states that cannot be judged say **that** rather than passing quietly.
|
|
20
22
|
- 📐 **Declarations are read as written** *(new in 0.23.2)*: `Files to Touch` is where a spec declares the files it will touch, and three things read it — fulfilment advisories, the declaration census and the approval impact note. The parser took only the **first word of a list item**, so measured over 678 approved specs here, **88 specs and 172 paths were declared and never read**; 24 of them parsed to zero while naming files plainly. Several paths on one line, an indented continuation, a Korean first word, a prose paragraph — all invisible. They are read now, wherever they sit, and a bare `name.ext` keeps its old position rule so a property access like `module.exports` is still not a file. A declared path that does not exist but is the suffix of exactly one repository file is reported as an abbreviation rather than a defect; two candidates stays an ambiguity and nothing is guessed. Cost, measured across the whole corpus: **zero** new `missing` findings.
|
|
21
23
|
- 📄 **The publish gate reads the docs** *(new in 0.23.2)*: the publish playbook has demanded "bring README and CHANGELOG up to this release" since 0.16.0 and only prose enforced it, so it failed four times — including 0.21.0, 0.22.0 and 0.23.0, which each shipped with a feature list frozen at 0.20.0. The release gate now refuses two things it can decide: a missing CHANGELOG entry for the version being published, and an entry with `### Added` while `README.md` has not changed since the previous release. Replayed over eight releases it refuses exactly the three that were stale and passes the other five. What needs judgement — is the old wording still true? — stays with the person and is **reported**, never faked; a check that could not run says so instead of reading as a pass.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
24
|
+
|
|
25
|
+
**Earlier releases** — condensed to one line each; every release's full account lives in [CHANGELOG.md](CHANGELOG.md).
|
|
26
|
+
|
|
27
|
+
- 🧩 **Your config files survive a re-wire** *(0.23.0)*: `init --agent antigravity|codex` refreshes only the holmes-kit entry instead of replacing `.agents/mcp_config.json`, `hooks.json` and `marketplace.json` whole — a neighbour server, hook, plugin and your own `disabled` flag all survive, and an unreadable JSON file is refused with a reason rather than overwritten.
|
|
28
|
+
- 🫀 **The MCP supervisor notices a child that died** *(0.23.0)*: a crashed child used to leave the server **permanently deaf**; outstanding requests now get a JSON-RPC error first, then the supervisor resets, respawns and replays the opening exchange — with a restart budget spent only by a child that never answered.
|
|
29
|
+
- 🧮 **Coverage you can explain** *(0.23.0, corrected in 0.23.2)*: the RTM census says *what each unlinked spec declared* (`scanned-source`, `file-anchor-target`, `test-target`, `unreachable-target`, `no-declaration`). The "zero" 0.23.0 published here was the Files-to-Touch parser's, not the corpus's — the corrected census reads three, with five real trace gaps behind it.
|
|
30
|
+
- 🤖 **A CI matrix that judges every commit** *(0.22.0)*: a maintainer-side Linux runner appends exactly one `ci-runs` row per commit for **every** outcome, including the ones it could not judge; a missing row reads as "not run", never as a pass. Workspaces that never adopted it hear nothing about it.
|
|
31
|
+
- 🔁 **Advisories learn what happened next** *(0.22.0)*: every finding carries a deterministic id, and the next `approval_status` re-runs the same functions to record `resolved` or `persisted` — the numerator every "promote to a hard gate once we know the false-positive rate" sentence was missing. `dismiss: [id]` retires one an author judges unhelpful.
|
|
32
|
+
- 🧪 **`kills` that cannot apply say so** *(0.22.0)*: `test_run --mutate` reports `unapplied` separately from `survivors` — measured here, all 22 `kills` entries in this repository applied **none** while the response still read `survivors: []`, the shape of a clean run.
|
|
33
|
+
- 🔎 **The RTM stops claiming coverage it cannot see** *(0.21.0)*: `codeLinkedPct`, `unlinkedCount` and `unlinkedByReason` join the old `coveragePct`, which read 100 while 11.4% of approved specs carried no `implements` edge — plus a Files-to-Touch fulfilment advisory and `rtm_impact` **trace gaps** (a spec that declares a changed production file yet anchors only tests).
|
|
34
|
+
- 🩹 **`@known-defect(reason, expires=YYYY-MM-DD)` and article ART-9** *(0.21.0)*: a test that pins a known defect as its expected value carries a machine-readable marker — listed as debt while live, blocking only once it has **expired** or cannot be read. A bypass is sometimes right; the marker is there so the next person can see it.
|
|
35
|
+
- 🧑🤝🧑 **Concurrent multi-agent workspace** *(0.20.0)*: several agents, machines and clones converge on one spec store through Git — replica-stamped provenance, UUID-keyed entities that renumber without losing identity or approval closure, `entity_integrate` with per-side conflict evidence, and single-use approvals whose double-spend freezes every authority-spending act until `ledger_reconcile`. Reproduced end to end outside this repository on macOS and Linux.
|
|
36
|
+
- 🗂️ **Approval decisions you can actually see** *(0.20.0)*: `approve --status` shows who asked (run · replica · workspace), the risk grade, the subject digest and exactly what a grant would open. A grant is bound to the workspace it was minted in and to the content the human read — a copied one is `foreign-workspace`, changed content is `stale-subject` — and `--revoke` withdraws it.
|
|
37
|
+
- 🔁 **Import cycles are governed** *(0.19.0)*: guidance reaches the agent before it designs, a `graphPreview.cycles` advisory names the cycles your declared files are already in (each edge classified `type-erasable` / `lazy-require` / `eager-value`), and a Stop-hook ratchet in `track` catches new ones — the escape is a **named exception**, never a threshold, so a project carrying legacy cycles can still adopt the harness. This repository went three cycles → zero.
|
|
38
|
+
- 📐 **Size and fan-in, shown but never judged** *(0.19.0)*: lines, symbols, longest function, fan-in and fan-out per declared file. Numbers only — a test pins the **absence** of a severity field, because one would grow into the gate the evidence does not support.
|
|
39
|
+
- 🎯 **Candidates you could actually act on** *(0.19.0)*: history-derived candidates must be able to be source (**63.3%** of emission slots were going to files that cannot be the answer), vendored trees are demoted, and def-use ranking orders symbols inside a file the search already found.
|
|
40
|
+
- 🧭 **The graph speaks BEFORE you commit to a scope** *(0.18.0)*: `approval_status` answers `graphPreview` — `impact` (what calls into your declared scope from outside it) and `density` — computed by the **same functions the sealing advisory uses**, so the preview can never disagree with the seal. `maintenance_analyze` candidates ride with the ADRs constraining each file. Information only: tests pin that no ranking or gate reads them.
|
|
41
|
+
- 📜 **ADR as a first-class governed document** *(0.18.0)*: `spec_create(type: "ADR")` scaffolds a decision document under full authoring governance, with its **own number space** and a **hitl-only seal** (autonomy never self-approves a decision). `ADR-XXXX` citations become `constrained_by` edges; legacy `.ax/decisions/` entries coexist. Migration guide at `docs/adr-migration.md`.
|
|
42
|
+
- 📣 **Impact advisory at sealing time** *(0.16.0, hardened in 0.17.0)*: approving an A-SPEC returns what your Files-to-Touch declaration **missed** — files whose symbols call into the declared scope from outside it. Advisory, never verdict: it rides the response after the seal commits, degrades to absence on failure, and every emission is ledgered so its false-positive rate is **measured before** anyone proposes a hard gate. Approved-only, capped (a hub-grade response shrank −82%), with an anchor-density advisory alongside.
|
|
43
|
+
- 🗣️ **The graph speaks intent** *(0.16.0)*: every SPEC node stores a one-sentence intent summary, extracted deterministically and **never generated**, so an advisory shows *which intent* is at risk without a spec-store round trip. Information only.
|
|
44
|
+
- 📇 **Session-context observability** *(0.16.0)*: the ledger records which agent/model drove a session and what the governance overhead cost, per replica — field reports grounded in machine attribution instead of guesswork.
|
|
45
|
+
|
|
46
|
+
**Foundations** — in place since the early releases, still load-bearing.
|
|
47
|
+
|
|
48
|
+
- 📋 **Requirements & specification governance**: strict **"No Spec, No Code"** across a 4-tier chain (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) with `// @implements A-SPEC-XXX` code anchors — comma-lists and every anchor in a file participate in the gate.
|
|
49
|
+
- 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(0.9.0)*: constitution article **ART-8** requires a recorded `red-assertion → green` sequence in the ledger, and a `red-error` (a test that could not run) is not a valid RED — so "the covering test failed *correctly*" is judged mechanically, not on trust. Ships observe-first (`redFirstEvidence: track`). Where superpowers *asks* for RED-first, holmes-kit *proves* it.
|
|
50
|
+
- 🧱 **Deterministic gate, hardened** *(0.8.0, 0.13.0)*: shell writes are judged at the segment's **effective working directory**, the governing anchor is the whole set rather than the first match, a project is governed when **any** spec exists, and `cp`/`mv` are classified by **destination**. Every gate change ships with two consecutive clean adversarial rounds.
|
|
51
|
+
- 🤖 **Autonomous approval — three layers, always bounded** *(0.8.0, reworked 0.13.0)*: a project default (`init --autonomy`) and an expiring per-session envelope let the agent seal **low-risk** specs itself under an `autonomous:<client>` actor; every governance-critical, high-risk or irreversible decision is refused and routed to the out-of-band `holmes-kit approve` queue. The agent can never grant it to itself, the posture is surfaced at every session start, and off is byte-identical to a fully human-gated project.
|
|
52
|
+
- 🧭 **Compatibility and evolution gates** *(0.14.0, 0.15.0)*: a new A-SPEC seals only with `harness_impact:` and `os_impact:` declared and machine-cross-checked against Files-to-Touch; separately, a changed in-scope source that **newly introduces an external dependency** raises a spec-reappraisal — a warning when manual, a queued item under autonomy, never a blocked turn.
|
|
53
|
+
- 🚢 **Release autonomy + docs-currency gate** *(0.13.0, machine-checked in 0.23.2)*: `npm publish` stays **human-approved by default** while a deterministic classifier lets a low-risk release self-publish under the ledger; a major bump or any gate-behavior/security/architecture spec forces HITL. The publish gate refuses a release whose docs never caught up — a stale doc is a false claim.
|
|
54
|
+
- 🧠 **3-tier semantic layer** *(0.3.0)*: an explicit consent ladder — `none` (default, **zero egress**), `local` (bge-m3, no egress), `cloud` (gemini-embedding-001, opt-in). Measured on 305 traceability cases: recall 0.486 → 0.667 → **0.887**; on lexical-zero requests 0% → 52% → **92%**. Surfaced additively, never as a hard filter.
|
|
55
|
+
- 🎯 **Graded impact surface** *(0.3.0)*: `rankedImpact` (personalized PageRank over the spec/code graph) beat its pre-registered naive baseline on **both** recall and precision across 3 corpora (×1.6–×17) — measured before claimed.
|
|
56
|
+
- 🐞 **Causal defect localization & CPG** *(equalized in 0.5–0.7)*: AST code property graph (CFG/DDG/CDG) and dataflow taint reachability across 7 languages — **42 language×layer cells graded on measured evidence** (11 corpora, 39,344 functions, zero invariant violations; C++ conditional on 67.9% parse coverage, disclosed in the matrix).
|
|
57
|
+
- 📏 **Measured, not claimed** *(0.3.x)*: performance is judged against a pre-registered modeled-human band (R 0.67–0.78 / P ≈0.9±). Current official grade: **band entry on recall; division-of-labor precision 0.727 = 81% of the modeled human**, reproduced by an independent context-free judge on a fresh blind window. No superhuman claims until both metrics exceed the band.
|
|
58
|
+
- 📊 **RTM dashboard & heatmaps** *(0.12.0–0.12.1)*: `holmes-kit serve` renders a real 2D coverage matrix (requirements × pipeline stages) with an honesty census, drills into a symbol's **CFG as a layered DAG with PDG colour overlays**, and a non-CFG language is named rather than faked. Standalone HTML/SVG reports (`generateRtmHeatmap`) cover spec coverage and taint reachability.
|
|
59
|
+
- 🔔 **Approval UX** *(0.3.1; inbox split 0.15.0)*: dialogs forewarn their 120s deadline and, on expiry, say exactly where the decision went. The tracked queue holds **decision-seeking requests only** — plain gate refusals live in a local per-machine log (raw commands never leave the machine), after 1,404 single-shot refusals were measured burying a 2-item inbox.
|
|
60
|
+
- ⬆️ **Zero-config upgrades & session banner** *(0.8.0–0.12.2)*: `holmes-kit upgrade` re-pins **every** recorded workspace in one command (`--dry-run`/`--yes`), and every session start states the version, the governance rule and any newer published version — from every harness's MCP startup, not just Claude's. The write stays your explicit choice, never a silent auto-install.
|
|
61
|
+
- 🧰 **Governance UX tools** *(0.10.0)*: `spec_unseal` (return a sealed spec to editable `draft` in one act, out-of-band approval required), `approval_status` and `ledger_timeline` for read-only observability, and a structured `conflict` on optimistic-concurrency refusal.
|
|
62
|
+
- 🚦 **Push & server-side re-validation** *(hardened in 0.8.0)*: a local `pre-push` evidence gate (test-run ledger head == push HEAD, green, executed > 0) plus a server-side workflow that re-runs `npm ci → build → full suite → tarball install probe`, so a `--no-verify` push or a hook-less clone is still caught.
|
|
63
|
+
- 🧪 **Self-healing & diagnostic doctor**: integrity checks and auto-fix remediation (`doctor --fix`, `spec_remediate`), plus a live report of holmes-kit's own advertised MCP schema token cost.
|
|
64
|
+
- 🔢 **Sensible spec numbering** *(0.12.1)*: a brand-new project's first slice is **REQ-100**; existing projects keep `max(existing)+1` exactly, and 4-digit ids (including `ADR-1000+`) work cleanly.
|
|
65
|
+
- 🌐 **English CLI & hook surface** *(0.13.0)*: the operator-facing CLI, `doctor` output, hook `deny` reasons and interactive prompts are English, guarded by a hangul-absence test over the **rendered runtime output** rather than a source scan.
|
|
66
|
+
- 🤖 **CLI-first AI harness matrix**: native process hook gating for Claude Code, Antigravity CLI (AGY), Codex CLI, and the Google Antigravity SDK.
|
|
64
67
|
|
|
65
68
|
---
|
|
66
69
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
22a30fa1-mu77dmb2
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -234,6 +234,8 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
|
234
234
|
Setting the key is consent to egress (spec prose, paths, symbol names sent externally).
|
|
235
235
|
|
|
236
236
|
holmes-kit doctor [flags] diagnose the install
|
|
237
|
+
holmes-kit report --message "<what happened>" [--harness <name>]
|
|
238
|
+
write a redacted field report and print a prefilled issue link
|
|
237
239
|
--target <dir> also verify that target's wiring points at this install
|
|
238
240
|
--json machine-readable output
|
|
239
241
|
|
|
@@ -347,6 +349,33 @@ async function main(argv) {
|
|
|
347
349
|
return r.exitCode;
|
|
348
350
|
});
|
|
349
351
|
}
|
|
352
|
+
// @implements A-SPEC-679 — the surface that calls the report composer. Nothing here reaches the
|
|
353
|
+
// network: it writes a file and prints links, and a person decides whether to open them.
|
|
354
|
+
if (cmd === 'report') {
|
|
355
|
+
const { runReport } = require('./report');
|
|
356
|
+
const { machineIdentity } = require('../project/field-report');
|
|
357
|
+
const os3 = require('node:os');
|
|
358
|
+
const mIdx = argv.indexOf('--message');
|
|
359
|
+
const message = mIdx >= 0 ? argv[mIdx + 1] : undefined;
|
|
360
|
+
const reported = runReport({ message, open: argv.includes('--open'), root: process.cwd(), packageRoot: packageRoot(), harness: (() => { const i = argv.indexOf('--harness'); return i >= 0 ? argv[i + 1] : undefined; })() }, {
|
|
361
|
+
write: (p2, c) => fs.writeFileSync(p2, c),
|
|
362
|
+
mkdir: (p2) => { fs.mkdirSync(p2, { recursive: true }); },
|
|
363
|
+
stdout: (t) => process.stdout.write(t),
|
|
364
|
+
readFile: (p2) => fs.readFileSync(p2, 'utf8'),
|
|
365
|
+
now: () => new Date(),
|
|
366
|
+
identity: machineIdentity({ hostname: os3.hostname(), username: os3.userInfo().username, homedir: os3.homedir() }),
|
|
367
|
+
});
|
|
368
|
+
// @implements A-SPEC-680 — the flag now does what it says. Failing to open is the NORMAL case on
|
|
369
|
+
// a headless box and never changes the report's own exit code: the URL is already on stdout.
|
|
370
|
+
if (argv.includes('--open')) {
|
|
371
|
+
const { openUrl } = require('./open-url');
|
|
372
|
+
const { spawn } = require('node:child_process');
|
|
373
|
+
const opened = openUrl(reported.url, process.platform, (f, a, o) => spawn(f, a, o).unref());
|
|
374
|
+
process.stdout.write(opened ? '\nopened in your browser — press Submit there.\n'
|
|
375
|
+
: '\ncould not open a browser here; the link above is the whole of it.\n');
|
|
376
|
+
}
|
|
377
|
+
return reported.code;
|
|
378
|
+
}
|
|
350
379
|
if (cmd === 'doctor') {
|
|
351
380
|
// A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
|
|
352
381
|
// (round-6) — the typo is the diagnosis, so say it.
|
|
@@ -1183,7 +1212,18 @@ async function main(argv) {
|
|
|
1183
1212
|
return '';
|
|
1184
1213
|
}
|
|
1185
1214
|
})();
|
|
1186
|
-
|
|
1215
|
+
// @implements A-SPEC-677 — the recorder prunes what has vanished while it is already here.
|
|
1216
|
+
// A deletion the user cannot see is a deletion they cannot check, so the count is printed.
|
|
1217
|
+
// @implements A-SPEC-678 — the seam a test can reach; consumers set nothing and get homedir().
|
|
1218
|
+
const { holmesHome } = require('../update/workspaces');
|
|
1219
|
+
recordWorkspace(holmesHome(process.env, os2.homedir()), { target: opts.target, agents: [...agents], version: pkgVersion, ts: new Date().toISOString() }, {
|
|
1220
|
+
read: (p2) => fs.readFileSync(p2, 'utf8'),
|
|
1221
|
+
write: (p2, c) => fs.writeFileSync(p2, c),
|
|
1222
|
+
mkdir: (p2) => { fs.mkdirSync(p2, { recursive: true }); },
|
|
1223
|
+
exists: (p2) => fs.existsSync(p2),
|
|
1224
|
+
tmpdir: () => os2.tmpdir(),
|
|
1225
|
+
onPrune: (n) => process.stdout.write(` registry: dropped ${n} workspace entr${n === 1 ? 'y' : 'ies'} whose temporary path no longer exists\n`),
|
|
1226
|
+
});
|
|
1187
1227
|
}
|
|
1188
1228
|
catch { /* registry failure never fails init */ }
|
|
1189
1229
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening a URL in whatever the operating system calls a browser.
|
|
3
|
+
*
|
|
4
|
+
* `report --open` used to parse the flag and do nothing: A-SPEC-679 said "opening is left to the
|
|
5
|
+
* caller" and that caller was never written. A flag accepted and ignored is worse than no flag —
|
|
6
|
+
* the user believes the browser opened and walks away. This repository has the lesson on file as
|
|
7
|
+
* "left as an integration point means dead".
|
|
8
|
+
*
|
|
9
|
+
* Judgement is separated from doing so all three platforms can be pinned from one machine. Wiring
|
|
10
|
+
* being present has never been the same thing as wiring that works.
|
|
11
|
+
*
|
|
12
|
+
* NO SHELL. The URL carries `&` and `#`, and a shell would split the command at them; it goes in as
|
|
13
|
+
* a single argument, which is the same discipline `Supervisor.spawnChild` and doctor's spawn checks
|
|
14
|
+
* already follow here.
|
|
15
|
+
*/
|
|
16
|
+
export interface OpenCommand {
|
|
17
|
+
file: string;
|
|
18
|
+
args: string[];
|
|
19
|
+
}
|
|
20
|
+
export type SpawnLike = (file: string, args: string[], opts: Record<string, unknown>) => unknown;
|
|
21
|
+
/** `null` when the platform cannot be judged: not opening is better than guessing a command. */
|
|
22
|
+
export declare function openCommand(platform: string): OpenCommand | null;
|
|
23
|
+
/**
|
|
24
|
+
* True only when the spawn was actually attempted and did not throw.
|
|
25
|
+
*
|
|
26
|
+
* A missing browser is the NORMAL case on a headless box, an SSH session or CI. It costs the user
|
|
27
|
+
* nothing — the URL is already on stdout — so it returns false rather than throwing, and the
|
|
28
|
+
* caller's exit code does not change.
|
|
29
|
+
*/
|
|
30
|
+
export declare function openUrl(url: string, platform: string, spawn: SpawnLike): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openCommand = openCommand;
|
|
4
|
+
exports.openUrl = openUrl;
|
|
5
|
+
/** `null` when the platform cannot be judged: not opening is better than guessing a command. */
|
|
6
|
+
function openCommand(platform) {
|
|
7
|
+
if (typeof platform !== 'string' || platform.trim() === '')
|
|
8
|
+
return null;
|
|
9
|
+
if (platform === 'darwin')
|
|
10
|
+
return { file: 'open', args: [] };
|
|
11
|
+
// `start` reads its first quoted argument as the WINDOW TITLE, so a URL passed without the empty
|
|
12
|
+
// title becomes a title and nothing opens.
|
|
13
|
+
if (platform === 'win32')
|
|
14
|
+
return { file: 'cmd', args: ['/c', 'start', ''] };
|
|
15
|
+
return { file: 'xdg-open', args: [] };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True only when the spawn was actually attempted and did not throw.
|
|
19
|
+
*
|
|
20
|
+
* A missing browser is the NORMAL case on a headless box, an SSH session or CI. It costs the user
|
|
21
|
+
* nothing — the URL is already on stdout — so it returns false rather than throwing, and the
|
|
22
|
+
* caller's exit code does not change.
|
|
23
|
+
*/
|
|
24
|
+
function openUrl(url, platform, spawn) {
|
|
25
|
+
if (typeof url !== 'string' || url.trim() === '')
|
|
26
|
+
return false;
|
|
27
|
+
const cmd = openCommand(platform);
|
|
28
|
+
if (!cmd)
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
// detached + ignored stdio: the browser outlives this process and never holds the terminal.
|
|
32
|
+
spawn(cmd.file, [...cmd.args, url], { detached: true, stdio: 'ignore' });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ReportOptions {
|
|
2
|
+
message?: string;
|
|
3
|
+
open?: boolean;
|
|
4
|
+
root: string;
|
|
5
|
+
packageRoot: string;
|
|
6
|
+
harness?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ReportIo {
|
|
9
|
+
write(p: string, c: string): void;
|
|
10
|
+
mkdir(p: string): void;
|
|
11
|
+
stdout(s: string): void;
|
|
12
|
+
readFile(p: string): string;
|
|
13
|
+
now(): Date;
|
|
14
|
+
/** Strings that identify this machine or person, injected so a pin can search for them. */
|
|
15
|
+
identity: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function runReport(o: ReportOptions, io: ReportIo): {
|
|
18
|
+
code: number;
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runReport = runReport;
|
|
37
|
+
// @implements A-SPEC-679
|
|
38
|
+
/**
|
|
39
|
+
* `holmes-kit report` — the surface that finally calls the report composer.
|
|
40
|
+
*
|
|
41
|
+
* Two measurements decided what this command can honestly claim. No ledger keeps the product's
|
|
42
|
+
* refusal text: it is returned to the caller and vanishes, so holmes-kit cannot know what went
|
|
43
|
+
* wrong. And a report assembled from only what IS auto-fillable — version, harness, OS, node, a
|
|
44
|
+
* spec id out of a ledger — cannot be triaged; a maintainer receives a version string and a
|
|
45
|
+
* fingerprint.
|
|
46
|
+
*
|
|
47
|
+
* So the human's one sentence IS the report, and everything here is context wrapped around it. That
|
|
48
|
+
* is still worth having: a pasted transcript leaks paths, spec titles and commit messages and can
|
|
49
|
+
* never be deduplicated, while this carries a redacted context and a fingerprint that recognises
|
|
50
|
+
* the same defect reported by someone else.
|
|
51
|
+
*
|
|
52
|
+
* Nothing here reaches the network. Opening a browser is the caller's act, which keeps this module
|
|
53
|
+
* pure enough to pin and keeps "going out" a decision a person makes.
|
|
54
|
+
*/
|
|
55
|
+
const path = __importStar(require("node:path"));
|
|
56
|
+
const field_report_1 = require("../project/field-report");
|
|
57
|
+
const field_report_url_1 = require("../project/field-report-url");
|
|
58
|
+
const reported_log_1 = require("./reported-log");
|
|
59
|
+
/** `unknown` rather than an empty string: a blank version reads as a version we know to be blank. */
|
|
60
|
+
const versionOf = (io, packageRoot) => {
|
|
61
|
+
try {
|
|
62
|
+
return String(JSON.parse(io.readFile(path.join(packageRoot, 'package.json'))).version ?? 'unknown');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function runReport(o, io) {
|
|
69
|
+
const described = typeof o.message === 'string' && o.message.trim() !== '';
|
|
70
|
+
const report = (0, field_report_1.composeFieldReport)({
|
|
71
|
+
kit: versionOf(io, o.packageRoot),
|
|
72
|
+
harness: o.harness ?? 'unknown',
|
|
73
|
+
os: process.platform,
|
|
74
|
+
arch: process.arch,
|
|
75
|
+
node: process.version,
|
|
76
|
+
...(described ? { message: o.message.trim() } : {}),
|
|
77
|
+
identity: io.identity,
|
|
78
|
+
});
|
|
79
|
+
let body = (0, field_report_1.fieldReportBody)(report);
|
|
80
|
+
if (!described) {
|
|
81
|
+
// Incomplete, not failed. A quiet empty report hands a maintainer a version string and calls it
|
|
82
|
+
// a bug report; saying so is the difference between a report and a shrug.
|
|
83
|
+
body += '\n\nno description was given — rerun with `--message "<what happened>"`; holmes-kit cannot'
|
|
84
|
+
+ ' recover what it said to you, because no ledger keeps its refusal text.';
|
|
85
|
+
}
|
|
86
|
+
const dir = path.join(o.root, '.ax', 'reports');
|
|
87
|
+
const file = path.join(dir, `${report.fingerprint}.md`);
|
|
88
|
+
io.mkdir(dir);
|
|
89
|
+
io.write(file, `${body}\n`);
|
|
90
|
+
// @implements A-SPEC-679 — leave the fingerprint of THIS report, so a future briefing can match it.
|
|
91
|
+
// Best-effort: a log problem must never fail the report it accompanies.
|
|
92
|
+
try {
|
|
93
|
+
const logFile = path.join(dir, 'reported.jsonl');
|
|
94
|
+
let prior = null;
|
|
95
|
+
try {
|
|
96
|
+
prior = io.readFile(logFile);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
prior = null;
|
|
100
|
+
}
|
|
101
|
+
io.write(logFile, (0, reported_log_1.mergeReported)(prior, { fingerprint: report.fingerprint, kit: report.kit, ts: io.now().toISOString() }).lines);
|
|
102
|
+
}
|
|
103
|
+
catch { /* the report itself still stands */ }
|
|
104
|
+
const url = (0, field_report_url_1.issueUrl)(body, { localPath: file, title: (0, field_report_url_1.issueTitle)(report), assignee: field_report_url_1.ISSUE_ASSIGNEE });
|
|
105
|
+
io.stdout(`report written: ${file}\n`);
|
|
106
|
+
io.stdout(`\nopen this to file it (you press Submit, and you can edit the body first):\n${url.url}\n`);
|
|
107
|
+
io.stdout(`\ncheck whether it is already known:\n${(0, field_report_url_1.searchUrl)(report.fingerprint)}\n`);
|
|
108
|
+
if (url.truncated)
|
|
109
|
+
io.stdout('\n(the URL body was truncated; the file above holds the whole report)\n');
|
|
110
|
+
if (!described)
|
|
111
|
+
io.stdout('\nno description was given — this report says so rather than pretending.\n');
|
|
112
|
+
// @implements A-SPEC-680 — the URL is returned rather than rebuilt by the caller: two places
|
|
113
|
+
// composing it would drift, and the caller needs the exact bytes it printed.
|
|
114
|
+
return { code: described ? 0 : 2, url: url.url };
|
|
115
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this machine has reported — a SET of fingerprints, not a history.
|
|
3
|
+
*
|
|
4
|
+
* It exists for a briefing that does not exist yet: "the thing you reported is fixed in 0.24.0".
|
|
5
|
+
* That can only work if the fingerprint from the MOMENT of reporting is on disk, because a
|
|
6
|
+
* fingerprint is derived from content and recomputing one later can disagree — redaction may
|
|
7
|
+
* withhold a different field next time. A line costs nothing now; without it such a briefing would
|
|
8
|
+
* stay silent about every report made before it shipped.
|
|
9
|
+
*
|
|
10
|
+
* A set rather than a log because the question is "what have I reported", not "when". Reporting the
|
|
11
|
+
* same defect twice adds nothing.
|
|
12
|
+
*
|
|
13
|
+
* Local only. Nothing here sends anything anywhere.
|
|
14
|
+
*/
|
|
15
|
+
export interface ReportedEntry {
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
kit: string;
|
|
18
|
+
ts: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function mergeReported(existing: string | null, entry: ReportedEntry): {
|
|
21
|
+
lines: string;
|
|
22
|
+
added: boolean;
|
|
23
|
+
};
|