@holmes-lab/holmes-kit 0.19.6 → 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.
Files changed (128) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +8 -1
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,102 @@ 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
+
8
104
  ## [0.19.6] - 2026-09-11
9
105
 
10
106
  A correction. 0.19.5 claimed a security improvement that does not reach the people it was written
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).
@@ -240,6 +242,11 @@ flowchart LR
240
242
  | `holmes-kit semantic-setup` | Download and verify the local `bge-m3` model (opt-in; shared across projects) |
241
243
  | `holmes-kit semantic-check` | Verify local inference **offline**, without downloading anything |
242
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 |
243
250
  | `holmes-kit doctor` | Comprehensive health check of specs, hooks, MCP, and anchors |
244
251
  | `holmes-kit doctor --fix` | Automatically self-heal and repair broken hooks or missing skills |
245
252
  | `holmes-kit ci` | Run non-interactive headless governance gate for GitHub Actions / GitLab CI |
@@ -293,7 +300,7 @@ Full per-cell bases (and every stated limit) live in [docs/language-support.md](
293
300
  | OS / Platform | Architecture | Status | Notes |
294
301
  | :--- | :--- | :---: | :--- |
295
302
  | **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
296
- | **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 |
297
304
  | **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
298
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) |
299
306
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- b6de96c3-mtx2uyhs
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
- const DEFAULT_TTL_MINUTES = 30;
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,2 @@
1
+ /** Operator recovery predates registration: lookup failure removes attribution, never authority checks. */
2
+ export declare function runOperatorExecution<T>(target: string, act: () => T): T;
@@ -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
+ }
@@ -1,8 +1,3 @@
1
- /**
2
- * PURE .gitignore block merge. holmes-kit writes derived state under `.ax/` in the TARGET project
3
- * (ledger, cpg_cache, reports) that must never be committed. The block is delimited so it can be
4
- * added, refreshed, and removed exactly — never touching the user's own lines.
5
- */
6
1
  export declare const BEGIN = "# >>> holmes-kit >>>";
7
2
  export declare const END = "# <<< holmes-kit <<<";
8
3
  /** Derived/runtime paths holmes-kit creates in a target project. */
@@ -11,8 +11,9 @@ exports.hasGitignoreBlock = hasGitignoreBlock;
11
11
  * (ledger, cpg_cache, reports) that must never be committed. The block is delimited so it can be
12
12
  * added, refreshed, and removed exactly — never touching the user's own lines.
13
13
  */
14
- exports.BEGIN = '# >>> holmes-kit >>>';
15
- exports.END = '# <<< holmes-kit <<<';
14
+ const installer_markers_1 = require("../project/installer-markers");
15
+ exports.BEGIN = installer_markers_1.HOLMES_BLOCK_BEGIN; // A-SPEC-646 — the walk that must ignore this block reads the same definition
16
+ exports.END = installer_markers_1.HOLMES_BLOCK_END;
16
17
  /** Derived/runtime paths holmes-kit creates in a target project. */
17
18
  exports.IGNORE_LINES = [
18
19
  // @implements A-SPEC-148
@@ -41,6 +42,9 @@ exports.IGNORE_LINES = [
41
42
  // the human saw" — round-14, exact match). Raw-and-local is the only shape that serves both ADR-012
42
43
  // and approvals: the file never leaves the machine, and `holmes-kit approve` reads it right there.
43
44
  '.ax/approvals/refusals*.jsonl',
45
+ // @implements A-SPEC-649 — a minted grant is LIVE AUTHORITY for one workspace; committed and
46
+ // pulled, it would open the same door in every clone (measured: a copied grant sealed elsewhere).
47
+ '.ax/approvals/grants/',
44
48
  // @implements A-SPEC-193 — 하네스 배선물은 이 **설치본의 절대 경로**를 담는다(훅은 hooks.json
45
49
  // 이 있는 디렉터리를 cwd 로 돌고, 패키지가 어디 설치됐는지는 기계마다 다르다). 그 경로가
46
50
  // VCS 에 들어가면 다른 기계에서 남의 설치를 부르거나 아무것도 부르지 못한다. 팀과 공유하고