@holmes-lab/holmes-kit 0.19.5 → 0.20.0
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 +135 -0
- package/README.md +17 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +86 -12
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +653 -424
- package/dist/holmes/mcp/handlers.js +267 -3250
- package/dist/holmes/mcp/server.js +2 -1
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +120 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +3 -1
- package/dist/holmes/project/root.js +4 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +132 -4
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +207 -0
- package/dist/holmes/spec/entity-integration.js +747 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1051 -0
- package/dist/holmes/spec/entity-transaction.d.ts +89 -0
- package/dist/holmes/spec/entity-transaction.js +701 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,11 +5,146 @@ 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.20.0] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
The Concurrent Multi-Agent Workspace release. Several agents, machines and clones now work on one spec
|
|
11
|
+
store at the same time and converge through Git without losing identity, authority or evidence. Every
|
|
12
|
+
mechanism below was reproduced first as a failure on the shipped package (in temp workspaces, in real
|
|
13
|
+
Git clones, and in a project OUTSIDE this repository on macOS and Linux), then specified, sealed,
|
|
14
|
+
RED-first tested and shipped — 49 A-SPECs (602–650), each with an official regression run.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Workspace and run identity** (`workspace_identity`, REQ-623–626): a Git-shareable canonical
|
|
18
|
+
workspace id (`.ax/workspace.json`) plus a local replica registration (`.ax/state/`, never shared).
|
|
19
|
+
Every registered MCP write carries `holmes-execution/1` provenance — workspace, replica, run — and
|
|
20
|
+
CLI operator decisions and autonomy audit are attributed to owned runs. Legacy records without an
|
|
21
|
+
execution are reported as legacy, never inferred.
|
|
22
|
+
- **Canonical entity identity for specs** (`entity_store`, REQ-627): a spec store can be *adopted*
|
|
23
|
+
into UUID-keyed entities with journaled, recoverable document transactions (create/edit/move),
|
|
24
|
+
atomic lock publication, aborted-before-journal recovery, verified-aside publication of updates and
|
|
25
|
+
deletions, and explicit abandonment of an operation blocked by a third party's state. Sealed bytes
|
|
26
|
+
are preserved; sealing/unsealing/retirement still require spec-approve authority.
|
|
27
|
+
- **Recoverable display-id renumbering** (`entity_renumber`, REQ-628): rename `REQ-1000 → REQ-1500`
|
|
28
|
+
while keeping the entity UUID, dependency links, source `@implements` anchors and approval closure;
|
|
29
|
+
journaled, resumable after a crash, refuses on source-coverage diagnostics it cannot rewrite. The
|
|
30
|
+
raw legacy `spec_renumber` is journaled and rolls back, and is refused on an activated store.
|
|
31
|
+
- **Git integration of concurrent work** (`entity_integrate`, REQ-629 + 632–637): plan a merge of two
|
|
32
|
+
Git inputs from a common ancestor over an adopted store, with stable entity identity, per-side
|
|
33
|
+
structured conflict evidence for every refusal, automatic renumbering of colliding new ids, rename
|
|
34
|
+
and shape cases, apply under exact-plan approval, recover, abandon, and **discharge** the derived
|
|
35
|
+
state obligations (graph, semantic cache) with an immutable `discharge.json`. Integration seal
|
|
36
|
+
transitions land in the provenance ledger as `spec-unsealed`/`spec-approved` events.
|
|
37
|
+
- **Cross-replica double-spend reconciliation** (`ledger_reconcile`, REQ-642/643): single-use approval
|
|
38
|
+
nonces are consumed on the writer's own replica chain, so offline clones converge through Git
|
|
39
|
+
without conflicts; a nonce spent on two replicas is detected as a structured double-spend, every
|
|
40
|
+
authority-spending act (seal, unseal, retire, review resolution, entity acts, `risk_check`, the
|
|
41
|
+
shell gate) refuses with `ledger-conflict` until an operator reconciles it.
|
|
42
|
+
- **Evidence follows the entity** (REQ-645): red-first test evidence, `ledger_timeline` and
|
|
43
|
+
integration audits resolve display-id history, so a renumbered A-SPEC keeps its RED→GREEN record.
|
|
44
|
+
- **Approval grants bound to what the human read** (REQ-649): a grant minted by `holmes-kit approve`
|
|
45
|
+
now names its workspace and, for `spec-approve`, the digest of the spec as the operator read it.
|
|
46
|
+
A grant copied into another clone is refused as `foreign-workspace`; a seal of content that changed
|
|
47
|
+
after the decision is refused as `stale-subject` and re-filed. Requests carry the asking run, the
|
|
48
|
+
subject digest and the risk grade; the queue records `consumed` and `revoked`;
|
|
49
|
+
`holmes-kit approve --revoke <id> --reason <text>` withdraws a live grant; the grants directory is
|
|
50
|
+
git-ignored by `init` (a live grant is authority for one workspace, never a shared artifact).
|
|
51
|
+
- **The decision surface** (`holmes-kit approve --status`, REQ-650): workspace and delegation posture
|
|
52
|
+
(project default / session envelope / off), and per request the risk grade, who asked (run, replica,
|
|
53
|
+
workspace), the subject digest and exactly what a grant would open (kind, exact target, duration,
|
|
54
|
+
workspace); recent decisions with their outcome — live until, consumed, expired, revoked, denied.
|
|
55
|
+
`approve --watch` prints the same picture before each pass that presents new items. The list, the
|
|
56
|
+
interactive keys and the non-TTY hint are byte-identical to 0.19.
|
|
57
|
+
- **pytest is a first-class RED-first runner** (REQ-647): `test_run` classifies pytest outcomes per file
|
|
58
|
+
from the JUnit report (`red-error` / `red-assertion` / `green`), a planned file the failed run never
|
|
59
|
+
reached is `red-error`, and a full-tier run executes the ecosystems the project actually has
|
|
60
|
+
(jest is assumed only when nothing is detected). Measured in an external Python project: before,
|
|
61
|
+
a ranged run executed the anchored case and recorded nothing.
|
|
62
|
+
- **Approval status reports re-seal blockers** (`approval_status.resealBlockers`, REQ-641): what a
|
|
63
|
+
re-seal (spec_approve again, renumber, integration, recovery) would be refused on, evaluated on
|
|
64
|
+
approved documents too; every re-seal refusal names the document and its findings.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- **The MCP handler module is split** into typed groups (REQ-602–622: phase, risk, adoption, slice
|
|
68
|
+
orchestration, workspace queries, taint, maintenance, graph, context/review, test execution, review
|
|
69
|
+
evidence, spec authoring, lifecycle, approval) with public key order, refusal texts, conformance and
|
|
70
|
+
evidence contracts pinned byte-for-byte; a foreign `root` is refused before graph and review
|
|
71
|
+
derivation. Full-suite wall time dropped to 285 s.
|
|
72
|
+
- **Derived state converges** (REQ-630/631/644): the semantic vector cache is published in flush
|
|
73
|
+
segments with owned compaction; the RTM graph is published by atomic replacement and consumers
|
|
74
|
+
report `current`/`stale`/`unverified`; incremental updates rebuild FILE nodes and import edges from
|
|
75
|
+
the whole scan. A corrupted `rtm.sqlite` is reported `unreadable` and rebuilt by `rtm_reindex`.
|
|
76
|
+
- **Store boundaries** (REQ-638–640): overlapping store registrations are refused; symlinked store
|
|
77
|
+
aliases are `unsafe-path` everywhere; the publication window (check → write) is closed with a
|
|
78
|
+
verified aside and transient leftovers are settled before recovery.
|
|
79
|
+
- **Installer-owned text is never a workspace reference** (REQ-646): the `.gitattributes` marker
|
|
80
|
+
block and installed skill files that mention `@implements` no longer block integration — measured
|
|
81
|
+
in every `init`-wired workspace. The marker definitions moved to `project/installer-markers.ts`.
|
|
82
|
+
- **In-workspace view links are not unsafe sources** (REQ-648): the `.agents/skills → .claude/skills`
|
|
83
|
+
link `init --agent all` writes for Antigravity no longer blocks `entity_renumber`; file links,
|
|
84
|
+
dangling links and links leaving the workspace keep their diagnostic.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Explicit contract citations keep priority in lexical localization (REQ-619); graph reachability is
|
|
88
|
+
distinguished from ranking-exclusive contribution (REQ-620).
|
|
89
|
+
- A grant read against one spec body could seal a different one; a copied grant could seal in another
|
|
90
|
+
workspace (both measured on 0.19.6, closed by REQ-649).
|
|
91
|
+
|
|
92
|
+
### Measured, stated limits
|
|
93
|
+
- External matrix (`docs/goals/probes/external-matrix-2026-09-14.js`): tarball installed outside this
|
|
94
|
+
repository, `init --agent all`, doctor, pytest RED→GREEN, adoption, two registered clones, Git
|
|
95
|
+
integration, renumber, derived-state corruption/recovery, the approval round-trip (status → grant →
|
|
96
|
+
stale refusal → revoke → seal → deny) — every step passes on **macOS 26 arm64** and **Ubuntu 24.04
|
|
97
|
+
aarch64 (OrbStack)**. Windows native and live Codex / Claude Code / Antigravity sessions in the
|
|
98
|
+
external workspace were not exercised in this release cycle (previous Windows field validation:
|
|
99
|
+
2026-09-05, 0.10.1).
|
|
100
|
+
- The session autonomy marker is project-wide (not run-scoped); a grant records the requesting run
|
|
101
|
+
but binds to the workspace and the subject, not the run. Integration snapshots are bounded at
|
|
102
|
+
64 MiB of Git objects per read. `decisions.jsonl` does not converge through Git by design.
|
|
103
|
+
|
|
104
|
+
## [0.19.6] - 2026-09-11
|
|
105
|
+
|
|
106
|
+
A correction. 0.19.5 claimed a security improvement that does not reach the people it was written
|
|
107
|
+
for, and a claim that does not hold is worse than no claim.
|
|
108
|
+
|
|
109
|
+
### Fixed
|
|
110
|
+
- **The `protobufjs` override does not protect consumers, and 0.19.5 said it did.** `overrides` is a
|
|
111
|
+
ROOT-manifest field: npm applies it from the top-level project only and ignores a dependency's.
|
|
112
|
+
The field ships inside this package's `package.json` and has no effect there. Measured against the
|
|
113
|
+
published 0.19.5 from a fresh consumer project: `protobufjs@6.11.6`, critical 1 / high 5 —
|
|
114
|
+
unchanged. This repository had already recorded that `allowScripts` is root-only and does not
|
|
115
|
+
transfer; the same property of `overrides` was not carried across, and the claim was written before
|
|
116
|
+
a third-party install was checked.
|
|
117
|
+
|
|
118
|
+
The override is kept, because it does protect this repository's own tree — development, CI and the
|
|
119
|
+
release gate. What changed is the description.
|
|
120
|
+
|
|
121
|
+
### Security
|
|
122
|
+
- **Consumers who want the critical advisory gone must declare the override themselves.** In the
|
|
123
|
+
CONSUMING project's `package.json`:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{ "overrides": { "protobufjs": "^7.6.3" } }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Measured from a fresh consumer project: `protobufjs@7.6.6`, **critical 0 / high 3**, with the
|
|
130
|
+
local model's real inference intact. The remaining three highs (`@xenova/transformers`, `js-yaml`,
|
|
131
|
+
`sharp`) are unaffected by this and stay recorded, with their re-evaluation triggers, in
|
|
132
|
+
`docs/goals/supply-chain-xenova-2026-09.md`. The durable fix is upstream — an
|
|
133
|
+
`@xenova/transformers` line that does not pull `onnxruntime-web`'s 6.x `protobufjs` — and is not
|
|
134
|
+
something this package can deliver on a consumer's behalf.
|
|
135
|
+
|
|
8
136
|
## [0.19.5] - 2026-09-11
|
|
9
137
|
|
|
10
138
|
One critical supply-chain advisory removed, by measuring a prediction this project had written down
|
|
11
139
|
and found to be wrong.
|
|
12
140
|
|
|
141
|
+
> **Correction (0.19.6):** the entry below overstates its reach. `overrides` is a ROOT-manifest
|
|
142
|
+
> field — npm applies it only from the top-level project, never from a dependency — so installing
|
|
143
|
+
> this package does **not** change what a consumer resolves. Measured on the published 0.19.5:
|
|
144
|
+
> a consumer still gets `protobufjs@6.11.6`, critical 1 / high 5. The override protects THIS
|
|
145
|
+
> repository's tree (development, CI, the release gate) and nothing else. A consumer who wants it
|
|
146
|
+
> must declare it themselves; see the Security note in 0.19.6.
|
|
147
|
+
|
|
13
148
|
### Security
|
|
14
149
|
- **`protobufjs` is overridden to 7.x, and the critical advisory is gone** (REQ-601). The arbitrary
|
|
15
150
|
code execution lives in protobufjs's 6.x line, reached three levels down —
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🧑🤝🧑 **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
|
+
- 🗂️ **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`.
|
|
19
21
|
- 🔁 **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.
|
|
20
22
|
- 📐 **Size and fan-in, shown but never judged** *(new in 0.19.0)*: the same design-time advisory carries `architecture` — lines, symbols, longest function, fan-in and fan-out for each declared file. Numbers only: no grade, no threshold, no participation in any verdict. A test pins the *absence* of a severity field, because one would grow into the gate the evidence does not support (the size/defect correlation in the literature is largely "there is more code"). Practitioners still reject inspecting more than five things, so the number is worth seeing — the person decides.
|
|
21
23
|
- 🎯 **Candidates you could actually act on** *(new in 0.19.0)*: the commit-history channel used to admit whatever git named, so ledger files and build baselines took emission slots — measured, **63.3% of candidate slots** went to files that cannot be the answer, one at the head of the list. Now history-derived candidates must be able to be source, vendored trees are demoted, and inside a file the search already found, def-use ranking puts the functions the request reaches through data flow first (symbol Top-10 recall 0.087 → 0.287 on this repository; unchanged on two others, and reported as conditional rather than general).
|
|
@@ -165,6 +167,15 @@ To prepare during installation instead, set `HOLMES_AUTO_MODEL_INSTALL=1` before
|
|
|
165
167
|
is never overridden. Models live in `~/.holmes/models` (`%USERPROFILE%\.holmes\models` on Windows);
|
|
166
168
|
`HOLMES_MODEL_CACHE` overrides that, including for a prepopulated offline cache.
|
|
167
169
|
|
|
170
|
+
> **Security note.** This package's dependency chain pulls `protobufjs@6.x` through
|
|
171
|
+
> `@xenova/transformers → onnxruntime-web → onnx-proto`, which carries a critical advisory. npm's
|
|
172
|
+
> `overrides` is a ROOT-manifest field, so we cannot fix this on your behalf — declare it in YOUR
|
|
173
|
+
> `package.json` to resolve it:
|
|
174
|
+
> ```json
|
|
175
|
+
> { "overrides": { "protobufjs": "^7.6.3" } }
|
|
176
|
+
> ```
|
|
177
|
+
> Measured from a fresh install: `critical 1 → 0`, `high 5 → 3`, with local inference intact.
|
|
178
|
+
|
|
168
179
|
> On **ARM Linux**, install with Node 20 or 22: `tree-sitter@0.21.1` ships no `linux-arm64` prebuild,
|
|
169
180
|
> so it compiles from source, and that compile fails against Node 24 headers. The failure happens
|
|
170
181
|
> while npm is still installing dependencies — before Holmes-Kit exists — so `doctor` cannot diagnose
|
|
@@ -231,6 +242,11 @@ flowchart LR
|
|
|
231
242
|
| `holmes-kit semantic-setup` | Download and verify the local `bge-m3` model (opt-in; shared across projects) |
|
|
232
243
|
| `holmes-kit semantic-check` | Verify local inference **offline**, without downloading anything |
|
|
233
244
|
| `holmes-kit semantic-key set` | Store the cloud-tier key outside the tree (stdin only, never argv) |
|
|
245
|
+
| `holmes-kit approve --status` | The whole approval picture: workspace, delegation, per-request risk/run/subject/grant preview, recent outcomes |
|
|
246
|
+
| `holmes-kit approve --watch` | Resident decision surface: decide with one key as requests arrive (prints the status picture before each new item) |
|
|
247
|
+
| `holmes-kit approve --grant <ref>` | Mint the narrow, workspace- and subject-bound grant for one request (`--ttl`, `--rationale`) |
|
|
248
|
+
| `holmes-kit approve --deny <ref> --reason <text>` | Record a denial the agent sees on its next attempt |
|
|
249
|
+
| `holmes-kit approve --revoke <id> --reason <text>` | Withdraw a live grant; the request must be decided again |
|
|
234
250
|
| `holmes-kit doctor` | Comprehensive health check of specs, hooks, MCP, and anchors |
|
|
235
251
|
| `holmes-kit doctor --fix` | Automatically self-heal and repair broken hooks or missing skills |
|
|
236
252
|
| `holmes-kit ci` | Run non-interactive headless governance gate for GitHub Actions / GitLab CI |
|
|
@@ -284,7 +300,7 @@ Full per-cell bases (and every stated limit) live in [docs/language-support.md](
|
|
|
284
300
|
| OS / Platform | Architecture | Status | Notes |
|
|
285
301
|
| :--- | :--- | :---: | :--- |
|
|
286
302
|
| **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
|
|
287
|
-
| **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL |
|
|
303
|
+
| **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL. **Field-validated 2026-09-14** (Ubuntu 24.04 aarch64, OrbStack): tarball install, `init --agent all`, doctor, pytest RED→GREEN, Git integration, renumber, derived-state recovery and the approval round-trip, all from the external matrix probe |
|
|
288
304
|
| **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
|
|
289
305
|
| **Windows Native** | x86_64 | ✅ Tier 1 | Windows 10/11 (Node.js 20+; prebuilt natives, no build tools needed in the common case). **Field-validated 2026-08-31** on a real user machine: registry install, natives (better-sqlite3 + 7 tree-sitter grammars), both OS gates, MCP handshake (30 tools), interactive init TUI, out-of-band approval channel (doctor 25 PASS; the 4 false FAILs it also showed were doctor's own win32 spawn bug, fixed in 0.3.2). See ADR-015 (platform tier is decided by executed verification — internal decision record) for tier criteria and residual risks (NTFS 8.3 names, reserved device names, 260-char paths; no Windows CI yet) |
|
|
290
306
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e2d9bd59-mu0pzf41
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { QueueState } from '../governance/approval-queue';
|
|
2
|
+
export interface StatusContext {
|
|
3
|
+
now: string;
|
|
4
|
+
workspace: {
|
|
5
|
+
state: 'legacy' | 'initialized' | 'registered';
|
|
6
|
+
workspaceId?: string;
|
|
7
|
+
replicaId?: string;
|
|
8
|
+
};
|
|
9
|
+
delegation: {
|
|
10
|
+
mode: 'project-default' | 'session' | 'off';
|
|
11
|
+
until?: string;
|
|
12
|
+
};
|
|
13
|
+
/** Nonces that still have a grant file — a granted decision is LIVE only while its file exists. */
|
|
14
|
+
liveGrants: ReadonlySet<string>;
|
|
15
|
+
}
|
|
16
|
+
/** What the screen needs from disk: identity (status only, never initializing), delegation, live grant files. */
|
|
17
|
+
export declare function statusContext(root: string, now?: string): StatusContext;
|
|
18
|
+
/**
|
|
19
|
+
* The whole picture, template-owned: every agent-controlled field goes through the same screen-safe
|
|
20
|
+
* cells the list uses (A-SPEC-262.1 — a target must never own a line).
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderStatus(state: QueueState, ctx: StatusContext, opts?: {
|
|
23
|
+
recent?: number;
|
|
24
|
+
}): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
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.statusContext = statusContext;
|
|
37
|
+
exports.renderStatus = renderStatus;
|
|
38
|
+
// @implements A-SPEC-650
|
|
39
|
+
//
|
|
40
|
+
// The DECISION PICTURE (GOAL §3-A) on the operator's existing terminal surface, chosen by the owner
|
|
41
|
+
// 2026-09-14 over a browser page or harness-only dialogs. Measured on the shipped dist before this
|
|
42
|
+
// slice: `approve --list` showed kind/target/reason/count/since/id; a live grant read as "No requests
|
|
43
|
+
// awaiting approval."; consumed/revoked/denied/expired outcomes and the delegation posture appeared
|
|
44
|
+
// nowhere. Everything here is READ-ONLY and pure over the folded queue + a small context; decisions
|
|
45
|
+
// still go through grantRequest/denyRequest/holdRequest/revokeGrant, and no pinned line of the list,
|
|
46
|
+
// the interactive loop or the key surface changes.
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
const approval_grants_1 = require("../governance/approval-grants");
|
|
50
|
+
const workspace_identity_1 = require("../project/workspace-identity");
|
|
51
|
+
const autonomy_1 = require("../governance/autonomy");
|
|
52
|
+
const approve_1 = require("./approve");
|
|
53
|
+
const screen_safe_1 = require("./screen-safe");
|
|
54
|
+
/** What the screen needs from disk: identity (status only, never initializing), delegation, live grant files. */
|
|
55
|
+
function statusContext(root, now = new Date().toISOString()) {
|
|
56
|
+
let workspace = { state: 'legacy' };
|
|
57
|
+
try {
|
|
58
|
+
const id = (0, workspace_identity_1.workspaceIdentity)(root, 'status');
|
|
59
|
+
workspace = id.state === 'legacy' ? { state: 'legacy' } : { state: id.state, workspaceId: id.workspaceId, ...(id.state === 'registered' ? { replicaId: id.replicaId } : {}) };
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
workspace = { state: 'legacy' };
|
|
63
|
+
}
|
|
64
|
+
let delegation = { mode: 'off' };
|
|
65
|
+
try {
|
|
66
|
+
if ((0, autonomy_1.projectDefaultAutonomy)(root))
|
|
67
|
+
delegation = { mode: 'project-default' };
|
|
68
|
+
else if ((0, autonomy_1.sessionAutonomyActive)(root, now)) {
|
|
69
|
+
let until;
|
|
70
|
+
try {
|
|
71
|
+
until = String(JSON.parse(fs.readFileSync(path.join(root, ...autonomy_1.SESSION_AUTONOMY_MARKER), 'utf8')).expires);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
until = undefined;
|
|
75
|
+
}
|
|
76
|
+
delegation = { mode: 'session', ...(until ? { until } : {}) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
delegation = { mode: 'off' };
|
|
81
|
+
}
|
|
82
|
+
const liveGrants = new Set();
|
|
83
|
+
try {
|
|
84
|
+
for (const g of (0, approval_grants_1.readGrants)(root).grants)
|
|
85
|
+
if (typeof g.nonce === 'string')
|
|
86
|
+
liveGrants.add(g.nonce);
|
|
87
|
+
}
|
|
88
|
+
catch { /* no grants dir */ }
|
|
89
|
+
return { now, workspace, delegation, liveGrants };
|
|
90
|
+
}
|
|
91
|
+
const short = (s, n) => (typeof s === 'string' && s !== '' ? (0, screen_safe_1.rowField)(s.slice(0, n), n) : '');
|
|
92
|
+
const digest12 = (subject) => (typeof subject === 'string' && /^sha256:[0-9a-f]{64}$/.test(subject) ? subject.slice(7, 19) : 'none');
|
|
93
|
+
function askedBy(p) {
|
|
94
|
+
if (p.execution)
|
|
95
|
+
return `run ${short(p.execution.runId, 8)} \u00b7 replica ${short(p.execution.replicaId, 8)} \u00b7 workspace ${short(p.execution.workspaceId, 8)}`;
|
|
96
|
+
return `no run recorded${p.replica && p.replica !== 'unknown' ? ` (replica ${(0, screen_safe_1.rowField)(p.replica, 24)})` : ''}`;
|
|
97
|
+
}
|
|
98
|
+
/** The outcome of a granted decision, judged from the fold and the live grant files. Pure. */
|
|
99
|
+
function grantedOutcome(id, d, ctx) {
|
|
100
|
+
if (d.consumedTs)
|
|
101
|
+
return `consumed ${(0, screen_safe_1.rowField)(d.consumedTs, 28)}`;
|
|
102
|
+
const exp = d.expires ? Date.parse(d.expires) : NaN;
|
|
103
|
+
const nowMs = Date.parse(ctx.now);
|
|
104
|
+
const expired = Number.isFinite(exp) && Number.isFinite(nowMs) && nowMs > exp;
|
|
105
|
+
if (ctx.liveGrants.has(id) && !expired)
|
|
106
|
+
return `live until ${(0, screen_safe_1.rowField)(d.expires ?? '(no expiry)', 28)}`;
|
|
107
|
+
if (expired)
|
|
108
|
+
return `expired ${(0, screen_safe_1.rowField)(d.expires ?? '', 28)}`;
|
|
109
|
+
return 'spent (no consumption record)';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The whole picture, template-owned: every agent-controlled field goes through the same screen-safe
|
|
113
|
+
* cells the list uses (A-SPEC-262.1 — a target must never own a line).
|
|
114
|
+
*/
|
|
115
|
+
function renderStatus(state, ctx, opts) {
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (ctx.workspace.state === 'legacy')
|
|
118
|
+
lines.push('\u25c6 legacy workspace (no identity) \u2014 grants bind to no workspace here');
|
|
119
|
+
// A replica id is a UUID on a registered clone (measured in the external matrix: the 36-char id clipped
|
|
120
|
+
// mid-row) and a hostname on legacy chains — show the same 8-char prefix the request rows use for ids.
|
|
121
|
+
else
|
|
122
|
+
lines.push(`\u25c6 workspace ${short(ctx.workspace.workspaceId, 8)}${ctx.workspace.replicaId ? ` \u00b7 replica ${/^[0-9a-f-]{36}$/.test(ctx.workspace.replicaId) ? short(ctx.workspace.replicaId, 8) : (0, screen_safe_1.rowField)(ctx.workspace.replicaId, 32)}` : ''}`);
|
|
123
|
+
if (ctx.delegation.mode === 'project-default')
|
|
124
|
+
lines.push('\u25c6 delegation: autonomous spec approval ON (project default, .mcp.json) \u2014 governance-critical, high-risk and irreversible still ask you');
|
|
125
|
+
else if (ctx.delegation.mode === 'session')
|
|
126
|
+
lines.push(`\u25c6 delegation: autonomous spec approval ON (this session, until ${(0, screen_safe_1.rowField)(ctx.delegation.until ?? '?', 28)}) \u2014 end early: holmes-kit autonomy off`);
|
|
127
|
+
else
|
|
128
|
+
lines.push('\u25c6 delegation: autonomous spec approval OFF \u2014 every spec approval is human');
|
|
129
|
+
lines.push('');
|
|
130
|
+
if (state.pending.length === 0)
|
|
131
|
+
lines.push('No requests awaiting approval.');
|
|
132
|
+
else
|
|
133
|
+
lines.push(`\u25c6 ${state.pending.length} request(s) awaiting approval`);
|
|
134
|
+
const bind = ctx.workspace.state === 'legacy' ? 'any workspace (legacy)' : 'this workspace only';
|
|
135
|
+
state.pending.forEach((p, i) => {
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(`[${i + 1}] ${(0, approve_1.subjectCells)(`[${i + 1}] `, p)}`);
|
|
138
|
+
lines.push(` reason: ${(0, screen_safe_1.rowField)(p.why, screen_safe_1.ROW_COLS)}`);
|
|
139
|
+
lines.push(` risk: ${(0, screen_safe_1.rowField)(p.risk ?? 'unknown', 12)} asked by: ${askedBy(p)}`);
|
|
140
|
+
lines.push(` subject: ${digest12(p.subject)} grant would be: ${(0, approve_1.subjectCells)(' subject: xxxxxxxxxxxx grant would be: ', p)} exact \u00b7 ${approve_1.DEFAULT_TTL_MINUTES} min \u00b7 ${bind}`);
|
|
141
|
+
const since = p.firstTs ? ` \u00b7 since ${(0, screen_safe_1.rowField)(p.firstTs, 28)}` : '';
|
|
142
|
+
lines.push(` blocked ${p.count}x${since}${p.hold ? ' (on hold)' : ''}`);
|
|
143
|
+
if (p.hold)
|
|
144
|
+
lines.push(` question: ${(0, screen_safe_1.rowField)(p.question ?? '(no question)', screen_safe_1.ROW_COLS)}`);
|
|
145
|
+
lines.push(` id: ${(0, screen_safe_1.rowField)(p.id, 40)}`);
|
|
146
|
+
});
|
|
147
|
+
const decided = Object.entries(state.decisions).sort((a, b) => (Date.parse(b[1].ts) || 0) - (Date.parse(a[1].ts) || 0)).slice(0, opts?.recent ?? 10);
|
|
148
|
+
if (decided.length > 0) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(`\u25c6 recent decisions (newest first, up to ${opts?.recent ?? 10})`);
|
|
151
|
+
for (const [id, d] of decided) {
|
|
152
|
+
const subject = state.subjects?.[id];
|
|
153
|
+
const cells = subject ? (0, approve_1.decisionSubject)({ id, kind: subject.kind, target: subject.target, why: '', count: 0, firstTs: '', lastTs: '' }, ' \u2713 granted ') : (0, screen_safe_1.rowField)(id, 40);
|
|
154
|
+
if (d.event === 'granted')
|
|
155
|
+
lines.push(` \u2713 granted ${cells} ${grantedOutcome(id, d, ctx)}`);
|
|
156
|
+
else if (d.event === 'revoked')
|
|
157
|
+
lines.push(` \u2717 revoked ${cells} ${(0, screen_safe_1.rowField)(d.reason ?? '(no reason)', screen_safe_1.ROW_COLS)}`);
|
|
158
|
+
else
|
|
159
|
+
lines.push(` \u2717 denied ${cells} ${(0, screen_safe_1.rowField)(d.reason ?? '(no reason)', screen_safe_1.ROW_COLS)}`);
|
|
160
|
+
lines.push(` id: ${(0, screen_safe_1.rowField)(id, 40)}${d.actor ? ` \u00b7 by ${(0, screen_safe_1.rowField)(d.actor, 24)}` : ''}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (state.malformedLines > 0)
|
|
164
|
+
lines.push(`(${state.malformedLines} unreadable line(s) in the queue \u2014 check for corruption)`);
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
@@ -45,6 +45,12 @@ export interface WatchOpts {
|
|
|
45
45
|
keyStream?: KeyStream;
|
|
46
46
|
/** Called once per NEW pending item (lastPresented dedup) — a throwing notifier is swallowed. */
|
|
47
47
|
notify?: (item: PendingRequest) => void;
|
|
48
|
+
/**
|
|
49
|
+
* @implements A-SPEC-650
|
|
50
|
+
* The status header (workspace, delegation, the per-request picture, recent outcomes), printed once
|
|
51
|
+
* before each pass that presents NEW items — never on an idle poll. Absent: byte-identical output.
|
|
52
|
+
*/
|
|
53
|
+
status?: () => string;
|
|
48
54
|
}
|
|
49
55
|
/** Exported for test: the shipped idle wait must resolve at once on abort, not after the timer. */
|
|
50
56
|
export declare function defaultWaitIdle(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -161,6 +161,12 @@ async function runWatch(root, io, actor, opts = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
const shown = new Set(state.pending.map((p) => p.id)); // what this pass presents
|
|
164
|
+
if (opts.status) {
|
|
165
|
+
try {
|
|
166
|
+
io.print(opts.status());
|
|
167
|
+
}
|
|
168
|
+
catch { /* the header is not a lever: a failing renderer never blocks a decision */ }
|
|
169
|
+
}
|
|
164
170
|
try {
|
|
165
171
|
if (opts.keyStream)
|
|
166
172
|
await presentByKey(root, io, actor, opts.keyStream, state);
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { QueueState, PendingRequest } from '../governance/approval-queue';
|
|
2
|
+
/**
|
|
3
|
+
* `holmes-kit approve` — the surface where a human decides.
|
|
4
|
+
*
|
|
5
|
+
* @implements A-SPEC-246
|
|
6
|
+
* The queue makes the review list (REQ-244) and grant files deliver approvals with no editing and
|
|
7
|
+
* no reconnect (REQ-245); what was missing is the surface where the decision happens. Without it
|
|
8
|
+
* the operator hand-writes grant JSON — scope grammar, expiry format, nonce rules — the exact
|
|
9
|
+
* "human types values into files" this programme exists to zero out.
|
|
10
|
+
*
|
|
11
|
+
* THE DECISION API IS THE HEART; THE TUI IS A SHELL. `grantRequest`/`denyRequest`/`holdRequest`
|
|
12
|
+
* write the files and events; both the interactive loop and the flags call the same three, so a
|
|
13
|
+
* CI or remote operator without a TTY is never locked out.
|
|
14
|
+
*
|
|
15
|
+
* A GRANT IS DERIVED, NEVER TYPED. kind→scope.kind, target→pattern, id→nonce, now+TTL→expires —
|
|
16
|
+
* all from the queue entry. The human's inputs are the decision, an optional rationale, and a TTL.
|
|
17
|
+
* The moment the human must know scope syntax, this UI has no reason to exist.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DEFAULT_TTL_MINUTES = 30;
|
|
2
20
|
export type DecisionResult = {
|
|
3
21
|
ok: true;
|
|
4
22
|
expires?: string;
|
|
@@ -11,6 +29,15 @@ export declare function grantRequest(root: string, id: string, opts: {
|
|
|
11
29
|
ttlMinutes?: number;
|
|
12
30
|
rationale?: string;
|
|
13
31
|
}): DecisionResult;
|
|
32
|
+
/**
|
|
33
|
+
* @implements A-SPEC-649
|
|
34
|
+
* Withdraw a live grant. The operator changed their mind, or the request's subject moved on: the
|
|
35
|
+
* file goes, and the queue says so — a `revoked` decision the fold reads like a denial. Refuses
|
|
36
|
+
* an empty reason (a revocation the agent cannot read is invisible) and an id that was never
|
|
37
|
+
* granted here (nothing to withdraw). A grant already spent has no file; revoking it records the
|
|
38
|
+
* intent for the audit trail but cannot un-seal — the ledger is the record of what happened.
|
|
39
|
+
*/
|
|
40
|
+
export declare function revokeGrant(root: string, id: string, reason: string, actor: string): DecisionResult;
|
|
14
41
|
export declare function denyRequest(root: string, id: string, reason: string, actor: string): DecisionResult;
|
|
15
42
|
/** Hold with a question — approve/deny later. The agent carries the question to the user. */
|
|
16
43
|
export declare function holdRequest(root: string, id: string, question: string, actor: string): DecisionResult;
|
|
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_TTL_MINUTES = void 0;
|
|
36
37
|
exports.grantRequest = grantRequest;
|
|
38
|
+
exports.revokeGrant = revokeGrant;
|
|
37
39
|
exports.denyRequest = denyRequest;
|
|
38
40
|
exports.holdRequest = holdRequest;
|
|
39
41
|
exports.staleView = staleView;
|
|
@@ -45,13 +47,16 @@ exports.decisionDetail = decisionDetail;
|
|
|
45
47
|
exports.renderRefusals = renderRefusals;
|
|
46
48
|
exports.renderNonTtyHint = renderNonTtyHint;
|
|
47
49
|
exports.runInteractive = runInteractive;
|
|
48
|
-
// @implements A-SPEC-246
|
|
50
|
+
// @implements A-SPEC-246, A-SPEC-626
|
|
51
|
+
const execution_context_1 = require("../project/execution-context");
|
|
52
|
+
const root_1 = require("../project/root");
|
|
49
53
|
const fs = __importStar(require("node:fs"));
|
|
50
54
|
const npx_bin_1 = require("../project/npx-bin");
|
|
51
55
|
const path = __importStar(require("node:path"));
|
|
52
56
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
53
57
|
const screen_safe_1 = require("./screen-safe");
|
|
54
58
|
const approval_grants_1 = require("../governance/approval-grants");
|
|
59
|
+
const workspace_identity_1 = require("../project/workspace-identity");
|
|
55
60
|
/**
|
|
56
61
|
* `holmes-kit approve` — the surface where a human decides.
|
|
57
62
|
*
|
|
@@ -69,7 +74,7 @@ const approval_grants_1 = require("../governance/approval-grants");
|
|
|
69
74
|
* all from the queue entry. The human's inputs are the decision, an optional rationale, and a TTL.
|
|
70
75
|
* The moment the human must know scope syntax, this UI has no reason to exist.
|
|
71
76
|
*/
|
|
72
|
-
|
|
77
|
+
exports.DEFAULT_TTL_MINUTES = 30; // A-SPEC-650: the status surface names the default grant duration
|
|
73
78
|
// @implements A-SPEC-262.1 — round-5: the guard bounded TTL below only. `--ttl 1e12` threw an
|
|
74
79
|
// uncaught RangeError out of the interactive loop (killing every remaining decision on the surface
|
|
75
80
|
// whose purpose is making decisions cheap), and `--ttl 1e10` minted a grant expiring in the year
|
|
@@ -143,13 +148,14 @@ const isWritablePath = (file) => {
|
|
|
143
148
|
};
|
|
144
149
|
const appendEvent = (root, event) => {
|
|
145
150
|
try {
|
|
151
|
+
const execution = (0, execution_context_1.executionForRoot)((0, root_1.resolveProjectRoot)(root).root);
|
|
146
152
|
const file = path.join(root, approval_queue_1.QUEUE_RELPATH);
|
|
147
153
|
if (!fs.existsSync(path.join(root, '.ax')))
|
|
148
154
|
return false;
|
|
149
155
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
150
156
|
if (!isWritablePath(file))
|
|
151
157
|
return false;
|
|
152
|
-
fs.appendFileSync(file, JSON.stringify({ ...event, ts: new Date().toISOString() }) + '\n');
|
|
158
|
+
fs.appendFileSync(file, JSON.stringify({ ...event, ts: new Date().toISOString(), ...(execution ? { execution } : {}) }) + '\n');
|
|
153
159
|
return true;
|
|
154
160
|
}
|
|
155
161
|
catch {
|
|
@@ -181,7 +187,7 @@ function recordIsSelfConsistent(entry) {
|
|
|
181
187
|
return entry.id === (0, approval_queue_1.approvalRequestId)(entry.kind, entry.target);
|
|
182
188
|
}
|
|
183
189
|
function grantRequest(root, id, opts) {
|
|
184
|
-
const ttl = opts.ttlMinutes ?? DEFAULT_TTL_MINUTES;
|
|
190
|
+
const ttl = opts.ttlMinutes ?? exports.DEFAULT_TTL_MINUTES;
|
|
185
191
|
if (typeof ttl !== 'number' || !Number.isFinite(ttl) || !Number.isInteger(ttl) || ttl <= 0 || ttl > MAX_TTL_MINUTES) {
|
|
186
192
|
return { ok: false, reason: `TTL must be an integer number of minutes in 1..${MAX_TTL_MINUTES} — received: ${(0, screen_safe_1.rowField)(String(ttl), 40)}` };
|
|
187
193
|
}
|
|
@@ -196,7 +202,25 @@ function grantRequest(root, id, opts) {
|
|
|
196
202
|
// second guard is not defence in depth, it is a line nobody can prove still works (round-3
|
|
197
203
|
// mutation test: neutering it left the suite green).
|
|
198
204
|
const expires = new Date(Date.now() + ttl * 60_000).toISOString();
|
|
205
|
+
// @implements A-SPEC-649 — the grant names WHERE it was minted and WHAT the operator read, so a
|
|
206
|
+
// copy in another clone and a seal of changed content are refused by name (measured 2026-09-14:
|
|
207
|
+
// both were accepted). A legacy workspace has no id to name; a request filed without a subject
|
|
208
|
+
// (shell, config-write: the target already IS the content) binds to the workspace only.
|
|
209
|
+
let workspace;
|
|
210
|
+
try {
|
|
211
|
+
const identity = (0, workspace_identity_1.workspaceIdentity)(root, 'status');
|
|
212
|
+
workspace = identity.state === 'legacy' ? undefined : identity.workspaceId;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
workspace = undefined;
|
|
216
|
+
}
|
|
217
|
+
const binding = {
|
|
218
|
+
...(workspace ? { workspace } : {}),
|
|
219
|
+
...(entry.subject ? { subject: entry.subject } : {}),
|
|
220
|
+
...(entry.execution?.runId ? { requestedBy: entry.execution.runId } : {}),
|
|
221
|
+
};
|
|
199
222
|
const grant = {
|
|
223
|
+
...binding,
|
|
200
224
|
actor: opts.actor,
|
|
201
225
|
token: 'grant',
|
|
202
226
|
rationale: opts.rationale?.trim() ? opts.rationale : DEFAULT_RATIONALE,
|
|
@@ -229,7 +253,7 @@ function grantRequest(root, id, opts) {
|
|
|
229
253
|
// class round-6 closed in findingContext, three files over.
|
|
230
254
|
return { ok: false, reason: `cannot write the grant file:\n ${(0, screen_safe_1.rowField)(e instanceof Error ? e.message : String(e), 76)}` };
|
|
231
255
|
}
|
|
232
|
-
if (!appendEvent(root, { event: 'granted', id, actor: opts.actor, expires, covered: entry.count })) {
|
|
256
|
+
if (!appendEvent(root, { event: 'granted', id, actor: opts.actor, expires, covered: entry.count, ...binding })) {
|
|
233
257
|
try {
|
|
234
258
|
fs.rmSync(tmp, { force: true });
|
|
235
259
|
}
|
|
@@ -244,6 +268,43 @@ function grantRequest(root, id, opts) {
|
|
|
244
268
|
}
|
|
245
269
|
return { ok: true, expires };
|
|
246
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* @implements A-SPEC-649
|
|
273
|
+
* Withdraw a live grant. The operator changed their mind, or the request's subject moved on: the
|
|
274
|
+
* file goes, and the queue says so — a `revoked` decision the fold reads like a denial. Refuses
|
|
275
|
+
* an empty reason (a revocation the agent cannot read is invisible) and an id that was never
|
|
276
|
+
* granted here (nothing to withdraw). A grant already spent has no file; revoking it records the
|
|
277
|
+
* intent for the audit trail but cannot un-seal — the ledger is the record of what happened.
|
|
278
|
+
*/
|
|
279
|
+
function revokeGrant(root, id, reason, actor) {
|
|
280
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
281
|
+
return { ok: false, reason: 'a revocation needs a --reason the agent can read' };
|
|
282
|
+
const decided = (0, approval_queue_1.readQueue)(root, { includeAllKinds: true }).decisions[id];
|
|
283
|
+
const dir = path.join(root, approval_grants_1.GRANTS_RELDIR);
|
|
284
|
+
let hadFile = false;
|
|
285
|
+
try {
|
|
286
|
+
hadFile = fs.readdirSync(dir).some((n) => n === `${id}.json` && fs.lstatSync(path.join(dir, n)).isFile());
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
hadFile = false;
|
|
290
|
+
}
|
|
291
|
+
if (!hadFile && decided?.event !== 'granted')
|
|
292
|
+
return { ok: false, reason: `no grant to revoke for ${(0, screen_safe_1.rowField)(id, 40)} — nothing was granted here` };
|
|
293
|
+
if (hadFile) {
|
|
294
|
+
try {
|
|
295
|
+
fs.rmSync(path.join(dir, `${id}.json`), { force: true });
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
return { ok: false, reason: `could not remove the grant file:\n ${(0, screen_safe_1.rowField)(e instanceof Error ? e.message : String(e), 76)}` };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!appendEvent(root, { event: 'revoked', id, reason, actor })) {
|
|
302
|
+
return { ok: false, reason: hadFile
|
|
303
|
+
? 'the grant file is gone but the revocation could not be recorded (queue event) — check write permission on .ax/approvals'
|
|
304
|
+
: 'could not write the revocation record (queue event) — check write permission on .ax/approvals. Nothing was recorded' };
|
|
305
|
+
}
|
|
306
|
+
return { ok: true };
|
|
307
|
+
}
|
|
247
308
|
function denyRequest(root, id, reason, actor) {
|
|
248
309
|
const { entry } = findPending(root, id);
|
|
249
310
|
if (!entry)
|
|
@@ -570,8 +631,8 @@ async function runInteractive(root, io, actor) {
|
|
|
570
631
|
}
|
|
571
632
|
else if (answer === 'e') {
|
|
572
633
|
const rationale = (await io.ask(`reason (enter = "${DEFAULT_RATIONALE}") > `)).trim();
|
|
573
|
-
const ttlRaw = (await io.ask(`ttl (minutes, enter = ${DEFAULT_TTL_MINUTES}) > `)).trim();
|
|
574
|
-
const ttl = ttlRaw === '' ? DEFAULT_TTL_MINUTES : Number(ttlRaw);
|
|
634
|
+
const ttlRaw = (await io.ask(`ttl (minutes, enter = ${exports.DEFAULT_TTL_MINUTES}) > `)).trim();
|
|
635
|
+
const ttl = ttlRaw === '' ? exports.DEFAULT_TTL_MINUTES : Number(ttlRaw);
|
|
575
636
|
const r = grantRequest(root, p.id, { actor, rationale, ttlMinutes: ttl });
|
|
576
637
|
// @implements A-SPEC-262.1 — round-6: this branch alone RETURNED on failure, so a mistyped TTL
|
|
577
638
|
// ("30min") ended the whole session with exit 0 and every remaining item undecided — measured on
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runOperatorExecution = runOperatorExecution;
|
|
4
|
+
// @implements A-SPEC-626
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const root_1 = require("../project/root");
|
|
7
|
+
const workspace_identity_1 = require("../project/workspace-identity");
|
|
8
|
+
const execution_context_1 = require("../project/execution-context");
|
|
9
|
+
/** Operator recovery predates registration: lookup failure removes attribution, never authority checks. */
|
|
10
|
+
function runOperatorExecution(target, act) {
|
|
11
|
+
let root = target;
|
|
12
|
+
let execution;
|
|
13
|
+
try {
|
|
14
|
+
const resolved = (0, root_1.resolveProjectRoot)(target);
|
|
15
|
+
root = resolved.root;
|
|
16
|
+
if (resolved.marker === '.ax') {
|
|
17
|
+
const identity = (0, workspace_identity_1.workspaceIdentity)(root);
|
|
18
|
+
if (identity.state === 'registered')
|
|
19
|
+
execution = { schema: 'holmes-execution/1', workspaceId: identity.workspaceId, replicaId: identity.replicaId, runId: (0, node_crypto_1.randomUUID)() };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch { /* identity lookup only; the actual command's errors are not caught here */ }
|
|
23
|
+
return (0, execution_context_1.withOwnedExecution)(root, execution, act);
|
|
24
|
+
}
|