@holmes-lab/holmes-kit 0.19.6 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/README.md +9 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +91 -13
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +121 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +12 -1
- package/dist/holmes/project/root.js +22 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +136 -5
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/rtm/test-scope.js +4 -1
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +5 -2
|
@@ -0,0 +1,71 @@
|
|
|
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.DEFAULT_SERVER_SPECS = void 0;
|
|
37
|
+
exports.resolveServerSpecsDir = resolveServerSpecsDir;
|
|
38
|
+
// @implements A-SPEC-652.2
|
|
39
|
+
/**
|
|
40
|
+
* Where the MCP server's spec store is. `HOLMES_SPECS` is written RELATIVE by `init` (`.ax/specs`,
|
|
41
|
+
* so the wiring file stays portable across machines) and used to be resolved against whatever cwd
|
|
42
|
+
* the server got. Measured 2026-09-14: a session's server refused every store-bound tool with
|
|
43
|
+
* "바인딩된 스펙 디렉터리 .ax/specs 가 존재하지 않습니다" — its cwd was not the project.
|
|
44
|
+
*
|
|
45
|
+
* The base for a relative path is, in order: `CLAUDE_PROJECT_DIR` (Claude Code sets it for every
|
|
46
|
+
* stdio MCP server to the stable project root — code.claude.com/docs/en/mcp), then the nearest
|
|
47
|
+
* ancestor of cwd holding `.mcp.json` or `.ax` (a server started in a sub-directory, or by a harness
|
|
48
|
+
* that sets no variable), then cwd itself (the old behaviour, for a project that has not run `init`).
|
|
49
|
+
* The answer is always absolute, so every refusal can name the directory it looked at.
|
|
50
|
+
*/
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
exports.DEFAULT_SERVER_SPECS = '.ax/specs';
|
|
54
|
+
function resolveServerSpecsDir(env, cwd, exists = fs.existsSync) {
|
|
55
|
+
const configured = env.HOLMES_SPECS && env.HOLMES_SPECS.trim() !== '' ? env.HOLMES_SPECS : exports.DEFAULT_SERVER_SPECS;
|
|
56
|
+
if (path.isAbsolute(configured))
|
|
57
|
+
return path.normalize(configured);
|
|
58
|
+
const projectDir = env.CLAUDE_PROJECT_DIR;
|
|
59
|
+
if (projectDir && projectDir.trim() !== '' && exists(projectDir))
|
|
60
|
+
return path.resolve(projectDir, configured);
|
|
61
|
+
let dir = path.resolve(cwd);
|
|
62
|
+
for (;;) {
|
|
63
|
+
if (exists(path.join(dir, '.mcp.json')) || exists(path.join(dir, '.ax')))
|
|
64
|
+
return path.resolve(dir, configured);
|
|
65
|
+
const parent = path.dirname(dir);
|
|
66
|
+
if (parent === dir)
|
|
67
|
+
break;
|
|
68
|
+
dir = parent;
|
|
69
|
+
}
|
|
70
|
+
return path.resolve(cwd, configured);
|
|
71
|
+
}
|
|
@@ -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");
|
|
@@ -10,7 +11,16 @@ const tool_schemas_1 = require("./tool-schemas");
|
|
|
10
11
|
Object.defineProperty(exports, "HOOK_ENFORCED_TOOLS", { enumerable: true, get: function () { return tool_schemas_1.HOOK_ENFORCED_TOOLS; } });
|
|
11
12
|
const validate_args_1 = require("./validate-args");
|
|
12
13
|
// @implements A-SPEC-100.2
|
|
13
|
-
|
|
14
|
+
// @implements A-SPEC-652.2 — the store is opened at an ABSOLUTE path resolved against the project
|
|
15
|
+
// root (CLAUDE_PROJECT_DIR, then the nearest .mcp.json/.ax ancestor, then cwd), and the path is
|
|
16
|
+
// logged once to stderr (stdout is the protocol channel) so a mis-bound server is diagnosable.
|
|
17
|
+
const { resolveServerSpecsDir } = require('./server-specs-dir');
|
|
18
|
+
const SPECS_DIR = resolveServerSpecsDir(process.env, process.cwd());
|
|
19
|
+
try {
|
|
20
|
+
process.stderr.write(`[Holmes-Kit] spec store: ${SPECS_DIR}\n`);
|
|
21
|
+
}
|
|
22
|
+
catch { /* a diagnostic must never keep the server from starting */ }
|
|
23
|
+
const store = new spec_store_1.LocalMarkdownRepository(SPECS_DIR);
|
|
14
24
|
// @implements A-SPEC-263.1 — the elicitation approval channel's wiring. The capability arrives at
|
|
15
25
|
// initialize, AFTER this factory runs, so it is consulted lazily at CALL time.
|
|
16
26
|
// @implements A-SPEC-497.1 — the three fates, told apart at THIS seam (the only place the SDK error
|
|
@@ -35,6 +45,7 @@ const elicit = async (req) => {
|
|
|
35
45
|
};
|
|
36
46
|
const handlers = (0, handlers_1.makeHandlers)(store, {
|
|
37
47
|
elicit,
|
|
48
|
+
observeInvocation: root => stampSession(root),
|
|
38
49
|
clientName: () => { try {
|
|
39
50
|
return server.getClientVersion()?.name ?? 'unknown';
|
|
40
51
|
}
|
|
@@ -120,7 +131,6 @@ const TOOLS = Object.keys(handlers)
|
|
|
120
131
|
});
|
|
121
132
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
122
133
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
123
|
-
stampSession(req.params.arguments?.root);
|
|
124
134
|
// @implements A-SPEC-189
|
|
125
135
|
// The server is the first consumer of its own advertised schemas. Before this check, 15 of 26
|
|
126
136
|
// 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,121 @@
|
|
|
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
|
+
// @implements A-SPEC-651.1 — the scope's root and every root compared against it share one spelling.
|
|
63
|
+
const owned = execution ? { root: (0, root_1.canonicalPath)(root), execution: validateExecution(execution) } : undefined;
|
|
64
|
+
return scope.run(owned, act);
|
|
65
|
+
}
|
|
66
|
+
/** Writers may inherit only the context of the actual checkout that owns them. */
|
|
67
|
+
function executionForRoot(root) {
|
|
68
|
+
const active = scope.getStore();
|
|
69
|
+
if (!active)
|
|
70
|
+
return undefined;
|
|
71
|
+
let actual = path.resolve(root);
|
|
72
|
+
if (fs.existsSync(actual))
|
|
73
|
+
actual = (0, root_1.canonicalPath)(actual);
|
|
74
|
+
if (actual !== active.root)
|
|
75
|
+
throw new workspace_identity_1.WorkspaceIdentityError('foreign-execution', 'The writer belongs to a different execution workspace.');
|
|
76
|
+
return active.execution;
|
|
77
|
+
}
|
|
78
|
+
/** A handler factory owns one run; neither request payload nor a global last-root can set it. */
|
|
79
|
+
function createExecutionScope(storeRoot, observe) {
|
|
80
|
+
const runId = (0, node_crypto_1.randomUUID)();
|
|
81
|
+
return async (askedRoot, act) => {
|
|
82
|
+
if (storeRoot === undefined)
|
|
83
|
+
return scope.run(undefined, act);
|
|
84
|
+
let ancestor = path.resolve(storeRoot);
|
|
85
|
+
while (!fs.existsSync(ancestor) && path.dirname(ancestor) !== ancestor)
|
|
86
|
+
ancestor = path.dirname(ancestor);
|
|
87
|
+
const bound = (0, root_1.resolveProjectRoot)(ancestor);
|
|
88
|
+
if (bound.marker !== '.ax')
|
|
89
|
+
return scope.run(undefined, act);
|
|
90
|
+
const invoke = async () => {
|
|
91
|
+
try {
|
|
92
|
+
await observe?.(bound.root);
|
|
93
|
+
}
|
|
94
|
+
catch { /* observation does not change the act */ }
|
|
95
|
+
return act();
|
|
96
|
+
};
|
|
97
|
+
const identity = (0, workspace_identity_1.workspaceIdentity)(bound.root);
|
|
98
|
+
if (identity.state === 'legacy')
|
|
99
|
+
return scope.run(undefined, invoke);
|
|
100
|
+
if (askedRoot !== undefined && (0, root_1.resolveProjectRoot)(askedRoot).root !== bound.root) {
|
|
101
|
+
throw new workspace_identity_1.WorkspaceIdentityError('foreign-workspace', 'The requested workspace differs from the configured spec store.');
|
|
102
|
+
}
|
|
103
|
+
if (identity.state !== 'registered')
|
|
104
|
+
throw new workspace_identity_1.WorkspaceIdentityError('unregistered-replica', 'Register this replica through workspace_identity before governance writes.');
|
|
105
|
+
const execution = validateExecution({ schema: 'holmes-execution/1', workspaceId: identity.workspaceId, replicaId: identity.replicaId, runId });
|
|
106
|
+
return scope.run({ root: (0, root_1.canonicalPath)(bound.root), execution }, invoke);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Validate observation attribution before an append; absence is never inferred from bad input. */
|
|
110
|
+
function observationExecution(root, record) {
|
|
111
|
+
const active = executionForRoot(root);
|
|
112
|
+
const supplied = 'execution' in record ? validateExecution(record.execution) : undefined;
|
|
113
|
+
if (active && supplied && JSON.stringify(active) !== JSON.stringify(supplied))
|
|
114
|
+
throw new Error('Observation execution differs from owned run');
|
|
115
|
+
if (!active && supplied) {
|
|
116
|
+
const identity = (0, workspace_identity_1.workspaceIdentity)(root);
|
|
117
|
+
if (identity.state !== 'registered' || identity.workspaceId !== supplied.workspaceId || identity.replicaId !== supplied.replicaId)
|
|
118
|
+
throw new Error('Observation execution differs from workspace registration');
|
|
119
|
+
}
|
|
120
|
+
return active ?? supplied;
|
|
121
|
+
}
|
|
@@ -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,15 @@ 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
|
-
|
|
48
|
+
/**
|
|
49
|
+
* @implements A-SPEC-651.1 — "the same directory" judged by ONE spelling. Every path that names a
|
|
50
|
+
* workspace or a store is canonicalised the way `resolveProjectRoot` canonicalises (`realpath.native`,
|
|
51
|
+
* so 8.3 aliases and symlinks collapse); a path that does not exist compares by its resolved spelling.
|
|
52
|
+
* `realpath` is injectable so the 8.3 scenario runs on every OS in the tests. Measured 2026-09-14
|
|
53
|
+
* (WIN0200-20260914-SHORTPATH): `entityStoreBinding` compared JS `fs.realpathSync` output against a
|
|
54
|
+
* native-canonical root, so a workspace entered as `C:\Users\SUNGNA~1\…` was refused as foreign.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sameCanonicalDirectory(a: string, b: string, realpath?: (p: string) => string): boolean;
|
|
57
|
+
export declare function resolveProjectRoot(dir: string, opts?: {
|
|
58
|
+
includeGit?: boolean;
|
|
59
|
+
}): ProjectRoot;
|
|
@@ -36,8 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.canonicalPath = exports.MARKER = void 0;
|
|
37
37
|
exports.cleanSubprocessEnv = cleanSubprocessEnv;
|
|
38
38
|
exports.cleanTestEnv = cleanTestEnv;
|
|
39
|
+
exports.sameCanonicalDirectory = sameCanonicalDirectory;
|
|
39
40
|
exports.resolveProjectRoot = resolveProjectRoot;
|
|
40
|
-
// @implements A-SPEC-205
|
|
41
|
+
// @implements A-SPEC-205, A-SPEC-627
|
|
41
42
|
// @implements A-SPEC-128
|
|
42
43
|
const fs = __importStar(require("node:fs"));
|
|
43
44
|
const path = __importStar(require("node:path"));
|
|
@@ -111,6 +112,23 @@ exports.MARKER = '.ax';
|
|
|
111
112
|
const canonicalPath = (p) => fs.realpathSync.native(p);
|
|
112
113
|
exports.canonicalPath = canonicalPath;
|
|
113
114
|
const canonical = exports.canonicalPath;
|
|
115
|
+
/**
|
|
116
|
+
* @implements A-SPEC-651.1 — "the same directory" judged by ONE spelling. Every path that names a
|
|
117
|
+
* workspace or a store is canonicalised the way `resolveProjectRoot` canonicalises (`realpath.native`,
|
|
118
|
+
* so 8.3 aliases and symlinks collapse); a path that does not exist compares by its resolved spelling.
|
|
119
|
+
* `realpath` is injectable so the 8.3 scenario runs on every OS in the tests. Measured 2026-09-14
|
|
120
|
+
* (WIN0200-20260914-SHORTPATH): `entityStoreBinding` compared JS `fs.realpathSync` output against a
|
|
121
|
+
* native-canonical root, so a workspace entered as `C:\Users\SUNGNA~1\…` was refused as foreign.
|
|
122
|
+
*/
|
|
123
|
+
function sameCanonicalDirectory(a, b, realpath = exports.canonicalPath) {
|
|
124
|
+
const spell = (p) => { try {
|
|
125
|
+
return realpath(p);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return path.resolve(p);
|
|
129
|
+
} };
|
|
130
|
+
return spell(a) === spell(b);
|
|
131
|
+
}
|
|
114
132
|
function gitTopLevel(dir) {
|
|
115
133
|
try {
|
|
116
134
|
// stderr silenced: "not a git repository" is an expected branch here, not a fault worth printing.
|
|
@@ -127,7 +145,7 @@ function gitTopLevel(dir) {
|
|
|
127
145
|
return undefined;
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
|
-
function resolveProjectRoot(dir) {
|
|
148
|
+
function resolveProjectRoot(dir, opts = {}) {
|
|
131
149
|
let start;
|
|
132
150
|
try {
|
|
133
151
|
start = canonical(path.resolve(dir));
|
|
@@ -165,6 +183,7 @@ function resolveProjectRoot(dir) {
|
|
|
165
183
|
break;
|
|
166
184
|
}
|
|
167
185
|
const root = found ?? start;
|
|
168
|
-
|
|
186
|
+
// Storage readers need the canonical workspace boundary, not a subprocess on every document.
|
|
187
|
+
const topLevel = opts.includeGit === false ? undefined : gitTopLevel(root);
|
|
169
188
|
return { root, marker: found ? exports.MARKER : 'given', ...(topLevel ? { git: { topLevel } } : {}) };
|
|
170
189
|
}
|
|
@@ -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;
|