@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +17 -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
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HOOK_ENFORCED_TOOLS = void 0;
4
+ // @implements A-SPEC-625, A-SPEC-564.1
4
5
  const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
6
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
7
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
@@ -35,6 +36,7 @@ const elicit = async (req) => {
35
36
  };
36
37
  const handlers = (0, handlers_1.makeHandlers)(store, {
37
38
  elicit,
39
+ observeInvocation: root => stampSession(root),
38
40
  clientName: () => { try {
39
41
  return server.getClientVersion()?.name ?? 'unknown';
40
42
  }
@@ -120,7 +122,6 @@ const TOOLS = Object.keys(handlers)
120
122
  });
121
123
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOLS }));
122
124
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
123
- stampSession(req.params.arguments?.root);
124
125
  // @implements A-SPEC-189
125
126
  // The server is the first consumer of its own advertised schemas. Before this check, 15 of 26
126
127
  // handlers threw raw internal errors at `{}` over the wire, and a one-key typo in reverse_anchor
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TOOL_SCHEMAS = exports.HOOK_ENFORCED_TOOLS = void 0;
4
- // @implements A-SPEC-277
4
+ // @implements A-SPEC-629, A-SPEC-628, A-SPEC-623, A-SPEC-277, A-SPEC-624, A-SPEC-627
5
+ // @implements A-SPEC-642
6
+ // @implements A-SPEC-641
7
+ // @implements A-SPEC-639
8
+ // @implements A-SPEC-632
5
9
  // @implements A-SPEC-269
6
10
  // @implements A-SPEC-267
7
11
  const spec_types_1 = require("../spec/spec-types");
@@ -31,6 +35,31 @@ const ROOT_ANY = str('Absolute path to the project root. Version control is opti
31
35
  // the rest of the tool metadata and has no import side effects.
32
36
  exports.HOOK_ENFORCED_TOOLS = new Set(['phase_check', 'risk_check', 'rtm_check', 'spec_validate']);
33
37
  exports.TOOL_SCHEMAS = {
38
+ entity_integrate: {
39
+ description: 'Plan integration of fixed common-ancestor Git inputs for the configured adopted store, apply an exact approved plan, inspect an operation recovery-plan, or recover it with current authority. Preserves entity UUIDs and explicit conflicts, coordinates store/source ownership, journals publication and records completion audit and derived-state obligations. Leaves Git refs/index unchanged. Completed retries verify existing evidence without republishing. discharge rebuilds this replica\'s graph and semantic cache for a completed operation and records one immutable discharge.json bound to its obligations; obligations lists completed operations with pending, discharged or invalid obligations (read-only). abandon rolls a pending operation a third state blocked back to its own before-bytes, preserves foreign bytes, releases ownership and audits the abandonment. Unsupported inputs remain explicit blockers; no implicit adoption or approval.',
40
+ inputSchema: { type: 'object', properties: { root: ROOT_ANY, operation: { type: 'string', enum: ['plan', 'apply', 'recovery-plan', 'recover', 'abandon', 'discharge', 'obligations'] },
41
+ base: str('Common ancestor Git revision.'), left: str('First input Git revision.'), right: str('Second input Git revision.'),
42
+ operationId: str('Integration operation UUID returned in the original plan; required for recovery-plan and discharge.'), plan: { type: 'object', description: 'The entire unchanged plan returned by plan or recovery-plan.' } } },
43
+ },
44
+ entity_renumber: {
45
+ description: 'Plan, apply, inspect recovery-plan and recover UUID-preserving display-ID, dependency and source-comment anchor changes for the configured adopted spec store. Reports prose and coverage diagnostics. Application and recovery require the exact inspected plan and current specification authority; recovery revalidates source and official seal transitions, preserves external edits and resumes remaining journal steps. No implicit adoption or approval.',
46
+ inputSchema: { type: 'object', properties: { root: ROOT_ANY, operation: { type: 'string', enum: ['plan', 'apply', 'recovery-plan', 'recover', 'abandon'] }, changes: { type: 'array', items: { type: 'object', properties: { entityId: { type: 'string' }, newDisplayId: { type: 'string' } }, required: ['entityId', 'newDisplayId'], additionalProperties: false } }, plan: { type: 'object' } } },
47
+ },
48
+ entity_store: {
49
+ description: 'Inspect or plan entity adoption without writes by default. Explicit adopt requires config-write approval for the exact returned plan and preserves sealed document bytes. Adopted stores retain entity identity through journaled document creation, edits and classification moves. recovery-plan reports interrupted targets without changing them. recover requires separate exact-plan approval to complete interrupted transactions or release locks for verified completed writes. Sealing, unsealing and retirement additionally require current spec-approve authority for the affected ID and the same transition policy as official spec tools. Live or unknown owners, external changes and noncanonical transitions refuse. Recovery audit completion across crashes remains under development.',
50
+ inputSchema: { type: 'object', properties: { root: ROOT_ANY, operation: { type: 'string', enum: ['status', 'plan-adoption', 'adopt', 'recovery-plan', 'recover'] }, plan: { type: 'object', description: 'The complete unmodified plan returned by plan-adoption or recovery-plan.' } } },
51
+ },
52
+ workspace_identity: {
53
+ description: 'Inspect canonical workspace and local replica identity without writes by default. Explicit initialize creates a Git-shareable .ax/workspace.json and registers this checkout; register requires an existing canonical identity. Preserve legacy specs and ledgers; these identifiers grant no approval authority. Share the manifest through Git and keep .ax/state local. Registered MCP writes include holmes-execution/1 provenance and require a reader supporting that schema; matching package versions alone do not establish this capability.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ root: ROOT_ANY,
58
+ operation: { type: 'string', enum: ['status', 'initialize', 'register'], description: 'Defaults to read-only status; initialize and register explicitly write identity records.' },
59
+ },
60
+ required: ['root'],
61
+ },
62
+ },
34
63
  // @implements A-SPEC-156
35
64
  // The full basis behind every other answer. It is a separate tool, not a field, because carrying
36
65
  // the whole object on all responses is what REQ-122 forbids — the digest rides along, the detail
@@ -126,7 +155,7 @@ exports.TOOL_SCHEMAS = {
126
155
  },
127
156
  },
128
157
  approval_status: {
129
- description: "Read-only: report ONE spec's approval/seal state without the caller parsing files — { sealed, approvedDigest?, status, parents: [{ id, status, sealed, resolved }], blockers: [] }. `sealed` is the same approved_digest predicate the code gate reads; `blockers` is the exact list spec_approve would refuse on (empty once approved), so the report cannot drift from the acts it describes. Each parent entry shows that parent's OWN live seal state (an unresolved depends_on id is reported as status:'missing', resolved:false — not dropped). No writes, no ledger append. Refuses a duplicate or unknown id.",
158
+ description: "Read-only: report ONE spec's approval/seal state without the caller parsing files — { sealed, approvedDigest?, status, parents: [{ id, status, sealed, resolved }], blockers: [], resealBlockers: [] }. `sealed` is the same approved_digest predicate the code gate reads; `blockers` is the exact list spec_approve would refuse on (empty once approved), so the report cannot drift from the acts it describes; `resealBlockers` is that same predicate evaluated on the document as it stands, reported for approved documents too — what a re-seal (spec_approve again, entity_renumber, entity_integrate, store recovery) would be refused on, e.g. a hand-sealed chain whose parent kind violates the rule, or a legacy A-SPEC without its declarations. Each parent entry shows that parent's OWN live seal state (an unresolved depends_on id is reported as status:'missing', resolved:false — not dropped). No writes, no ledger append. Refuses a duplicate or unknown id.",
130
159
  inputSchema: {
131
160
  type: 'object',
132
161
  properties: {
@@ -146,6 +175,18 @@ exports.TOOL_SCHEMAS = {
146
175
  },
147
176
  },
148
177
  },
178
+ ledger_reconcile: {
179
+ description: "Reconcile ONE cross-replica double-spend in the merged provenance ledger: a single-use approval nonce consumed on two or more replicas while offline. Requires an out-of-band HOLMES_APPROVAL (or grant) of kind \"ledger-reconcile\" whose target is that nonce input (e.g. \"nonce:91ad2a8ad9a1\"); appends `nonce-double-spend-reconciled` on this replica's chain referencing the nonce and every consuming event's hash, after which verifyAll, the Stop hook, baseline trust and the ledger-conflict refusals (spec_approve, spec_unseal, spec_retire, review_record, entity acts, risk_check, the shell gate) clear. { ok, reconciled, nonce, events, remaining } — `reconciled: false` with `pending` when the nonce is not an unreconciled double-spend. Refuses while any chain is broken (audit-conflict). Never erases a consumption.",
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {
183
+ nonce: str('The nonce input exactly as verifyAll / doubleSpends names it (usually the fingerprint form "nonce:<12 hex>").'),
184
+ rationale: str('Why the double-spend is accepted as reconciled (recorded on the event; defaults to the approval rationale).'),
185
+ root: str('Optional when the server is bound to a file store; if supplied it must resolve to the SAME project.'),
186
+ },
187
+ required: ['nonce'],
188
+ },
189
+ },
149
190
  rtm_dashboard: {
150
191
  description: "Launch (idempotently) the interactive RTM/CPG dashboard the agent would otherwise start by hand, and return { ok, url, running, census } — use it when a human asks to SEE the RTM heatmap / coverage matrix / dashboard. `url` is the localhost address to open; `running` is true when an already-live server for this project was reused (a second call never starts a second server). `census` is an honesty summary of what the dashboard shows — { reqCount, pipelineCount, coveredCount, coveragePct, retiredCount, findingsScanned } — so the caller can report coverage without scraping the page. No spec writes, no ledger append. Refuses when a supplied `root` points at a different project than the server is bound to.",
151
192
  inputSchema: {
@@ -0,0 +1,17 @@
1
+ export interface ExecutionContext {
2
+ readonly schema: 'holmes-execution/1';
3
+ readonly workspaceId: string;
4
+ readonly replicaId: string;
5
+ readonly runId: string;
6
+ }
7
+ export declare function validateExecution(value: unknown): ExecutionContext;
8
+ /** Internal entry-point boundary; an unavailable operator identity explicitly clears ambient scope. */
9
+ export declare function withOwnedExecution<T>(root: string, execution: ExecutionContext | undefined, act: () => T): T;
10
+ /** Writers may inherit only the context of the actual checkout that owns them. */
11
+ export declare function executionForRoot(root: string): ExecutionContext | undefined;
12
+ /** A handler factory owns one run; neither request payload nor a global last-root can set it. */
13
+ export declare function createExecutionScope(storeRoot?: string, observe?: (root: string) => void | Promise<void>): <T>(askedRoot: string | undefined, act: () => Promise<T>) => Promise<T>;
14
+ /** Validate observation attribution before an append; absence is never inferred from bad input. */
15
+ export declare function observationExecution(root: string, record: {
16
+ execution?: unknown;
17
+ }): ExecutionContext | undefined;
@@ -0,0 +1,120 @@
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.validateExecution = validateExecution;
37
+ exports.withOwnedExecution = withOwnedExecution;
38
+ exports.executionForRoot = executionForRoot;
39
+ exports.createExecutionScope = createExecutionScope;
40
+ exports.observationExecution = observationExecution;
41
+ // @implements A-SPEC-624, A-SPEC-625, A-SPEC-626
42
+ const node_async_hooks_1 = require("node:async_hooks");
43
+ const node_crypto_1 = require("node:crypto");
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const root_1 = require("./root");
47
+ const workspace_identity_1 = require("./workspace-identity");
48
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
49
+ function validateExecution(value) {
50
+ if (!value || typeof value !== 'object' || Array.isArray(value))
51
+ throw new Error('Invalid execution context');
52
+ const v = value;
53
+ if (Object.keys(v).length !== 4 || v.schema !== 'holmes-execution/1' ||
54
+ !['workspaceId', 'replicaId', 'runId'].every(k => typeof v[k] === 'string' && uuid.test(v[k]))) {
55
+ throw new Error('Invalid execution context schema, fields or UUID');
56
+ }
57
+ return Object.freeze({ schema: 'holmes-execution/1', workspaceId: v.workspaceId, replicaId: v.replicaId, runId: v.runId });
58
+ }
59
+ const scope = new node_async_hooks_1.AsyncLocalStorage();
60
+ /** Internal entry-point boundary; an unavailable operator identity explicitly clears ambient scope. */
61
+ function withOwnedExecution(root, execution, act) {
62
+ const owned = execution ? { root: fs.realpathSync(root), execution: validateExecution(execution) } : undefined;
63
+ return scope.run(owned, act);
64
+ }
65
+ /** Writers may inherit only the context of the actual checkout that owns them. */
66
+ function executionForRoot(root) {
67
+ const active = scope.getStore();
68
+ if (!active)
69
+ return undefined;
70
+ let actual = path.resolve(root);
71
+ if (fs.existsSync(actual))
72
+ actual = fs.realpathSync(actual);
73
+ if (actual !== active.root)
74
+ throw new workspace_identity_1.WorkspaceIdentityError('foreign-execution', 'The writer belongs to a different execution workspace.');
75
+ return active.execution;
76
+ }
77
+ /** A handler factory owns one run; neither request payload nor a global last-root can set it. */
78
+ function createExecutionScope(storeRoot, observe) {
79
+ const runId = (0, node_crypto_1.randomUUID)();
80
+ return async (askedRoot, act) => {
81
+ if (storeRoot === undefined)
82
+ return scope.run(undefined, act);
83
+ let ancestor = path.resolve(storeRoot);
84
+ while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor)
85
+ ancestor = path.dirname(ancestor);
86
+ const bound = (0, root_1.resolveProjectRoot)(ancestor);
87
+ if (bound.marker !== '.ax')
88
+ return scope.run(undefined, act);
89
+ const invoke = async () => {
90
+ try {
91
+ await observe?.(bound.root);
92
+ }
93
+ catch { /* observation does not change the act */ }
94
+ return act();
95
+ };
96
+ const identity = (0, workspace_identity_1.workspaceIdentity)(bound.root);
97
+ if (identity.state === 'legacy')
98
+ return scope.run(undefined, invoke);
99
+ if (askedRoot !== undefined && (0, root_1.resolveProjectRoot)(askedRoot).root !== bound.root) {
100
+ throw new workspace_identity_1.WorkspaceIdentityError('foreign-workspace', 'The requested workspace differs from the configured spec store.');
101
+ }
102
+ if (identity.state !== 'registered')
103
+ throw new workspace_identity_1.WorkspaceIdentityError('unregistered-replica', 'Register this replica through workspace_identity before governance writes.');
104
+ const execution = validateExecution({ schema: 'holmes-execution/1', workspaceId: identity.workspaceId, replicaId: identity.replicaId, runId });
105
+ return scope.run({ root: fs.realpathSync(bound.root), execution }, invoke);
106
+ };
107
+ }
108
+ /** Validate observation attribution before an append; absence is never inferred from bad input. */
109
+ function observationExecution(root, record) {
110
+ const active = executionForRoot(root);
111
+ const supplied = 'execution' in record ? validateExecution(record.execution) : undefined;
112
+ if (active && supplied && JSON.stringify(active) !== JSON.stringify(supplied))
113
+ throw new Error('Observation execution differs from owned run');
114
+ if (!active && supplied) {
115
+ const identity = (0, workspace_identity_1.workspaceIdentity)(root);
116
+ if (identity.state !== 'registered' || identity.workspaceId !== supplied.workspaceId || identity.replicaId !== supplied.replicaId)
117
+ throw new Error('Observation execution differs from workspace registration');
118
+ }
119
+ return active ?? supplied;
120
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The marks by which holmes-kit's installer claims text as ITS OWN — the `.gitignore`/`.gitattributes`
3
+ * block delimiters and the installed-skill marker — with the ownership judgement built on them.
4
+ *
5
+ * They live under `project/` rather than `cli/` because two layers need one definition: the installer
6
+ * (cli) writes them, and the entity source walk (spec/) must ignore exactly what the installer wrote
7
+ * (A-SPEC-646). Measured 2026-09-14: with the definitions in `cli/`, the walk's import was a C-SPEC-224
8
+ * forbidden edge (`spec/ -x-> cli/`); duplicating the strings would let what is written and what is
9
+ * ignored drift apart.
10
+ */
11
+ export declare const HOLMES_BLOCK_BEGIN = "# >>> holmes-kit >>>";
12
+ export declare const HOLMES_BLOCK_END = "# <<< holmes-kit <<<";
13
+ /**
14
+ * Marks a file as OURS. Ownership is decided by this marker and never by the path, because the path
15
+ * is exactly what a user's own skill of the same name would occupy, and overwriting that would be
16
+ * the same class of error as clobbering their settings.
17
+ */
18
+ export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edits will be replaced on upgrade -->";
19
+ /**
20
+ * @implements A-SPEC-190 (round 7)
21
+ * Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
22
+ * document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
23
+ * ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
24
+ * backup. A quotation is not a claim of ownership.
25
+ */
26
+ export declare function carriesMarker(content: string): boolean;
27
+ /**
28
+ * @implements A-SPEC-190 (round 8)
29
+ * Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
30
+ * regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
31
+ * trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
32
+ * round-7 fix existed to remove while doctor certified the result as `current`.
33
+ */
34
+ export declare function frontmatterEnd(content: string): number | null;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ // @implements A-SPEC-646
3
+ // @implements A-SPEC-190
4
+ /**
5
+ * The marks by which holmes-kit's installer claims text as ITS OWN — the `.gitignore`/`.gitattributes`
6
+ * block delimiters and the installed-skill marker — with the ownership judgement built on them.
7
+ *
8
+ * They live under `project/` rather than `cli/` because two layers need one definition: the installer
9
+ * (cli) writes them, and the entity source walk (spec/) must ignore exactly what the installer wrote
10
+ * (A-SPEC-646). Measured 2026-09-14: with the definitions in `cli/`, the walk's import was a C-SPEC-224
11
+ * forbidden edge (`spec/ -x-> cli/`); duplicating the strings would let what is written and what is
12
+ * ignored drift apart.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.PLAYBOOK_SKILL_MARKER = exports.HOLMES_BLOCK_END = exports.HOLMES_BLOCK_BEGIN = void 0;
16
+ exports.carriesMarker = carriesMarker;
17
+ exports.frontmatterEnd = frontmatterEnd;
18
+ exports.HOLMES_BLOCK_BEGIN = '# >>> holmes-kit >>>';
19
+ exports.HOLMES_BLOCK_END = '# <<< holmes-kit <<<';
20
+ /**
21
+ * Marks a file as OURS. Ownership is decided by this marker and never by the path, because the path
22
+ * is exactly what a user's own skill of the same name would occupy, and overwriting that would be
23
+ * the same class of error as clobbering their settings.
24
+ */
25
+ exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be replaced on upgrade -->';
26
+ /**
27
+ * @implements A-SPEC-190 (round 7)
28
+ * Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
29
+ * document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
30
+ * ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
31
+ * backup. A quotation is not a claim of ownership.
32
+ */
33
+ function carriesMarker(content) {
34
+ // Round-8: judging on raw bytes made ownership brittle in the direction that cannot be repaired.
35
+ // A CRLF normalisation (a Windows editor, `core.autocrlf`, `.gitattributes eol=crlf`), a BOM, or a
36
+ // trailing space on the marker line each turned a file the kit itself wrote into `foreign` —
37
+ // permanently, since install then skips it forever and doctor advises deleting it as "yours".
38
+ // Whitespace and line endings are not ownership; the marker is.
39
+ const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
40
+ const markerAt = (s) => {
41
+ const line = s.split('\n', 1)[0];
42
+ return line.trimEnd() === exports.PLAYBOOK_SKILL_MARKER;
43
+ };
44
+ if (markerAt(text))
45
+ return true; // 옛 배치(마커 먼저) — 우리 것이므로 고칠 수 있다
46
+ const fm = frontmatterEnd(text);
47
+ // 빈 줄 하나가 소유를 지우지 않는다(round-9: frontmatter 뒤 개행 하나로 영구 foreign 이 됐다).
48
+ return fm !== null && markerAt(text.slice(fm).replace(/^\s*\n/, ''));
49
+ }
50
+ /**
51
+ * @implements A-SPEC-190 (round 8)
52
+ * Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
53
+ * regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
54
+ * trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
55
+ * round-7 fix existed to remove while doctor certified the result as `current`.
56
+ */
57
+ function frontmatterEnd(content) {
58
+ const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
59
+ if (!/^---[ \t]*(\n|$)/.test(text))
60
+ return null;
61
+ const m = /\n---[ \t]*(\n|$)/.exec(text.slice(3));
62
+ if (!m)
63
+ return null;
64
+ return 3 + m.index + m[0].length;
65
+ }
@@ -45,4 +45,6 @@ export declare const MARKER = ".ax";
45
45
  * makes: one spelling per directory, the spelling every path key is built from.
46
46
  */
47
47
  export declare const canonicalPath: (p: string) => string;
48
- export declare function resolveProjectRoot(dir: string): ProjectRoot;
48
+ export declare function resolveProjectRoot(dir: string, opts?: {
49
+ includeGit?: boolean;
50
+ }): ProjectRoot;
@@ -37,7 +37,7 @@ exports.canonicalPath = exports.MARKER = void 0;
37
37
  exports.cleanSubprocessEnv = cleanSubprocessEnv;
38
38
  exports.cleanTestEnv = cleanTestEnv;
39
39
  exports.resolveProjectRoot = resolveProjectRoot;
40
- // @implements A-SPEC-205
40
+ // @implements A-SPEC-205, A-SPEC-627
41
41
  // @implements A-SPEC-128
42
42
  const fs = __importStar(require("node:fs"));
43
43
  const path = __importStar(require("node:path"));
@@ -127,7 +127,7 @@ function gitTopLevel(dir) {
127
127
  return undefined;
128
128
  }
129
129
  }
130
- function resolveProjectRoot(dir) {
130
+ function resolveProjectRoot(dir, opts = {}) {
131
131
  let start;
132
132
  try {
133
133
  start = canonical(path.resolve(dir));
@@ -165,6 +165,7 @@ function resolveProjectRoot(dir) {
165
165
  break;
166
166
  }
167
167
  const root = found ?? start;
168
- const topLevel = gitTopLevel(root);
168
+ // Storage readers need the canonical workspace boundary, not a subprocess on every document.
169
+ const topLevel = opts.includeGit === false ? undefined : gitTopLevel(root);
169
170
  return { root, marker: found ? exports.MARKER : 'given', ...(topLevel ? { git: { topLevel } } : {}) };
170
171
  }
@@ -0,0 +1,29 @@
1
+ export type IdentityOperation = 'status' | 'initialize' | 'register';
2
+ export interface WorkspaceManifest {
3
+ schema: 'holmes-workspace/1';
4
+ workspaceId: string;
5
+ }
6
+ export interface WorkspaceReplica {
7
+ schema: 'holmes-workspace-replica/1';
8
+ workspaceId: string;
9
+ replicaId: string;
10
+ }
11
+ export type WorkspaceIdentityStatus = {
12
+ ok: true;
13
+ state: 'legacy';
14
+ } | {
15
+ ok: true;
16
+ state: 'initialized';
17
+ workspaceId: string;
18
+ } | {
19
+ ok: true;
20
+ state: 'registered';
21
+ workspaceId: string;
22
+ replicaId: string;
23
+ };
24
+ export declare class WorkspaceIdentityError extends Error {
25
+ readonly code: string;
26
+ readonly holmesRefusal = true;
27
+ constructor(code: string, message: string);
28
+ }
29
+ export declare function workspaceIdentity(rootArg: string, operation?: IdentityOperation): WorkspaceIdentityStatus;
@@ -0,0 +1,181 @@
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.WorkspaceIdentityError = void 0;
37
+ exports.workspaceIdentity = workspaceIdentity;
38
+ // @implements A-SPEC-623
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const node_crypto_1 = require("node:crypto");
42
+ const root_1 = require("./root");
43
+ class WorkspaceIdentityError extends Error {
44
+ code;
45
+ holmesRefusal = true;
46
+ constructor(code, message) {
47
+ super(message);
48
+ this.code = code;
49
+ this.name = 'WorkspaceIdentityError';
50
+ }
51
+ }
52
+ exports.WorkspaceIdentityError = WorkspaceIdentityError;
53
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
54
+ const errno = (e, code) => e?.code === code;
55
+ function directory(file, optional = false) {
56
+ let stat;
57
+ try {
58
+ stat = fs.lstatSync(file);
59
+ }
60
+ catch (e) {
61
+ if (optional && errno(e, 'ENOENT'))
62
+ return false;
63
+ throw e;
64
+ }
65
+ if (!stat.isDirectory() || stat.isSymbolicLink())
66
+ throw new WorkspaceIdentityError('unsafe-path', 'Identity directories must be real directories, not symlinks.');
67
+ return true;
68
+ }
69
+ function readRecord(file, local) {
70
+ let stat;
71
+ try {
72
+ stat = fs.lstatSync(file);
73
+ }
74
+ catch (e) {
75
+ if (errno(e, 'ENOENT'))
76
+ return null;
77
+ throw e;
78
+ }
79
+ if (!stat.isFile() || stat.isSymbolicLink())
80
+ throw new WorkspaceIdentityError('unsafe-path', 'Identity must be a regular file, not a symlink.');
81
+ let value;
82
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
83
+ try {
84
+ const opened = fs.fstatSync(fd);
85
+ if (!opened.isFile() || opened.dev !== stat.dev || opened.ino !== stat.ino)
86
+ throw new WorkspaceIdentityError('identity-changed', 'Identity changed while opening; retry.');
87
+ try {
88
+ value = JSON.parse(fs.readFileSync(fd, 'utf8'));
89
+ }
90
+ catch (e) {
91
+ if (e instanceof SyntaxError)
92
+ throw new WorkspaceIdentityError('invalid-identity', 'Identity contains invalid JSON; preserve and repair it explicitly.');
93
+ throw e;
94
+ }
95
+ }
96
+ finally {
97
+ fs.closeSync(fd);
98
+ }
99
+ const schema = local ? 'holmes-workspace-replica/1' : 'holmes-workspace/1';
100
+ if (!value || typeof value !== 'object' || Array.isArray(value))
101
+ throw new WorkspaceIdentityError('invalid-identity', 'Identity must be a schema-versioned object.');
102
+ const record = value, keys = local ? ['schema', 'workspaceId', 'replicaId'] : ['schema', 'workspaceId'];
103
+ if (record.schema !== schema || Object.keys(record).length !== keys.length || keys.some(k => !(k in record))
104
+ || typeof record.workspaceId !== 'string' || !uuid.test(record.workspaceId)
105
+ || (local && (typeof record.replicaId !== 'string' || !uuid.test(record.replicaId)))) {
106
+ throw new WorkspaceIdentityError('invalid-identity', 'Unsupported schema, fields or identifier; existing identity was not replaced.');
107
+ }
108
+ return local ? record : record;
109
+ }
110
+ /** Complete bytes first, then one atomic no-replace link. Never fall back to overwriting a winner. */
111
+ function publish(file, record) {
112
+ const temp = path.join(path.dirname(file), `.identity-${(0, node_crypto_1.randomUUID)()}.tmp`);
113
+ let owned = false;
114
+ try {
115
+ const fd = fs.openSync(temp, 'wx', 0o600);
116
+ owned = true;
117
+ try {
118
+ fs.writeFileSync(fd, JSON.stringify(record) + '\n');
119
+ fs.fsyncSync(fd);
120
+ }
121
+ finally {
122
+ fs.closeSync(fd);
123
+ }
124
+ try {
125
+ fs.linkSync(temp, file);
126
+ }
127
+ catch (e) {
128
+ if (!errno(e, 'EEXIST'))
129
+ throw e;
130
+ }
131
+ }
132
+ finally {
133
+ if (owned)
134
+ fs.unlinkSync(temp);
135
+ }
136
+ }
137
+ function workspaceIdentity(rootArg, operation = 'status') {
138
+ if (!['status', 'initialize', 'register'].includes(operation))
139
+ throw new WorkspaceIdentityError('invalid-operation', 'Use status, initialize or register.');
140
+ const resolved = (0, root_1.resolveProjectRoot)(rootArg, { includeGit: false });
141
+ if (resolved.marker !== '.ax')
142
+ throw new WorkspaceIdentityError('missing-workspace', 'An existing Holmes .ax workspace marker is required.');
143
+ const ax = path.join(resolved.root, '.ax'), state = path.join(ax, 'state');
144
+ directory(ax);
145
+ const canonicalFile = path.join(ax, 'workspace.json'), localFile = path.join(state, 'workspace-replica.json');
146
+ let canonical = readRecord(canonicalFile, false);
147
+ let local = directory(state, true) ? readRecord(localFile, true) : null;
148
+ if (local && (!canonical || local.workspaceId !== canonical.workspaceId))
149
+ throw new WorkspaceIdentityError('workspace-mismatch', 'Local registration does not belong to the canonical workspace; it was preserved.');
150
+ if (!canonical) {
151
+ if (operation === 'status')
152
+ return { ok: true, state: 'legacy' };
153
+ if (operation === 'register')
154
+ throw new WorkspaceIdentityError('uninitialized', 'Initialize and share the canonical workspace identity before registering a replica.');
155
+ publish(canonicalFile, { schema: 'holmes-workspace/1', workspaceId: (0, node_crypto_1.randomUUID)() });
156
+ canonical = readRecord(canonicalFile, false);
157
+ if (!canonical)
158
+ throw new WorkspaceIdentityError('identity-changed', 'Canonical identity disappeared during initialization; retry.');
159
+ }
160
+ if (!local && operation !== 'status') {
161
+ try {
162
+ fs.mkdirSync(state);
163
+ }
164
+ catch (e) {
165
+ if (!errno(e, 'EEXIST'))
166
+ throw e;
167
+ }
168
+ directory(state);
169
+ publish(localFile, { schema: 'holmes-workspace-replica/1', workspaceId: canonical.workspaceId, replicaId: (0, node_crypto_1.randomUUID)() });
170
+ local = readRecord(localFile, true);
171
+ if (!local)
172
+ throw new WorkspaceIdentityError('identity-changed', 'Local registration disappeared during initialization; retry.');
173
+ }
174
+ const current = readRecord(canonicalFile, false);
175
+ if (!current || current.workspaceId !== canonical.workspaceId || (local && local.workspaceId !== canonical.workspaceId)) {
176
+ throw new WorkspaceIdentityError('workspace-mismatch', 'Workspace identity changed during registration; preserve records and resolve the mismatch.');
177
+ }
178
+ return local
179
+ ? { ok: true, state: 'registered', workspaceId: canonical.workspaceId, replicaId: local.replicaId }
180
+ : { ok: true, state: 'initialized', workspaceId: canonical.workspaceId };
181
+ }
@@ -1,3 +1,4 @@
1
+ import { ExecutionContext } from '../project/execution-context';
1
2
  import { TestOutcome } from './test-runner';
2
3
  export declare const OUTCOMES_FILE: string;
3
4
  export declare function outcomesFilename(replica: string): string;
@@ -10,6 +11,7 @@ export declare function isOutcomesFilename(name: string): boolean;
10
11
  * still dirty, which is exactly when the Stop gate reads them.
11
12
  */
12
13
  export interface OutcomeRecord {
14
+ execution?: ExecutionContext;
13
15
  aspec: string;
14
16
  outcome: TestOutcome;
15
17
  ts: string;
@@ -44,7 +46,10 @@ export declare function buildOutcomeRecords(outcomeByFile: Record<string, TestOu
44
46
  * Group outcomes by A-SPEC, keeping only records stamped with the given baseline `head` — a stale
45
47
  * record from another commit cannot vouch for the current work (the isFresh discipline). Pure.
46
48
  */
47
- export declare function groupOutcomesByAspec(records: OutcomeRecord[], head: string): Record<string, Array<{
49
+ export declare function groupOutcomesByAspec(records: OutcomeRecord[], head: string,
50
+ /** @implements A-SPEC-645 — maps a record's display id to the id the entity carries now (from the ledger's renames). */
51
+ canonical?: (id: string) => string): Record<string, Array<{
48
52
  outcome: TestOutcome;
49
53
  ts: string;
54
+ execution?: ExecutionContext;
50
55
  }>>;