@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,130 @@
|
|
|
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.npxPinOf = npxPinOf;
|
|
37
|
+
exports.npmCacheRoot = npmCacheRoot;
|
|
38
|
+
exports.checkNpxCache = checkNpxCache;
|
|
39
|
+
// @implements A-SPEC-652.2
|
|
40
|
+
/**
|
|
41
|
+
* doctor: is the npx cache entry the wiring will run intact?
|
|
42
|
+
*
|
|
43
|
+
* Measured 2026-09-15 on Windows: `.mcp.json` wires `npx -y -p @holmes-lab/holmes-kit@0.19.4
|
|
44
|
+
* holmes-mcp`; npx resolved it to `_npx/a739130c5322c549`, whose root `package.json` had gone, and
|
|
45
|
+
* died with `npm error ENOENT … package.json` before any handshake. The harness reported only
|
|
46
|
+
* "Connection closed". Six entries in that cache had the same shape (two of them this package).
|
|
47
|
+
*
|
|
48
|
+
* Filesystem only — no npm spawn: the cache root is `npm_config_cache`, else the platform default
|
|
49
|
+
* (`%LOCALAPPDATA%\npm-cache` on win32, `~/.npm` elsewhere). Only entries holding the PINNED version
|
|
50
|
+
* of the pinned package are judged; a corrupt entry of another version is not what this wiring runs.
|
|
51
|
+
*/
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const NAME = 'npx cache';
|
|
54
|
+
/** The `-p <pkg>@<ver>` pin of the target's holmes-kit wiring, or null when it is not an npx pin. */
|
|
55
|
+
function npxPinOf(target, deps) {
|
|
56
|
+
const mcpPath = path.join(target, '.mcp.json');
|
|
57
|
+
if (!deps.exists(mcpPath))
|
|
58
|
+
return null;
|
|
59
|
+
let server;
|
|
60
|
+
try {
|
|
61
|
+
server = JSON.parse(deps.readFile(mcpPath)).mcpServers?.['holmes-kit'];
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!server || !/^npx(\.cmd)?$/i.test(String(server.command ?? '')) || !Array.isArray(server.args))
|
|
67
|
+
return null;
|
|
68
|
+
const at = server.args.indexOf('-p');
|
|
69
|
+
const spec = at >= 0 ? server.args[at + 1] : undefined;
|
|
70
|
+
const m = spec ? /^(@?[^@]+)@([0-9][^\s]*)$/.exec(spec) : null;
|
|
71
|
+
return m ? { pkg: m[1], version: m[2] } : null;
|
|
72
|
+
}
|
|
73
|
+
function npmCacheRoot(deps) {
|
|
74
|
+
const configured = deps.env.npm_config_cache;
|
|
75
|
+
if (configured && configured.trim() !== '')
|
|
76
|
+
return configured;
|
|
77
|
+
if (deps.platform === 'win32') {
|
|
78
|
+
const local = deps.env.LOCALAPPDATA && deps.env.LOCALAPPDATA.trim() !== '' ? deps.env.LOCALAPPDATA : path.join(deps.homedir, 'AppData', 'Local');
|
|
79
|
+
return path.join(local, 'npm-cache');
|
|
80
|
+
}
|
|
81
|
+
return path.join(deps.homedir, '.npm');
|
|
82
|
+
}
|
|
83
|
+
function checkNpxCache(target, deps) {
|
|
84
|
+
const pin = npxPinOf(target, deps);
|
|
85
|
+
if (!pin)
|
|
86
|
+
return { name: NAME, level: 'PASS', detail: 'the holmes-kit wiring is not an npx pin — nothing cached to judge' };
|
|
87
|
+
const npxDir = path.join(npmCacheRoot(deps), '_npx');
|
|
88
|
+
if (!deps.exists(npxDir))
|
|
89
|
+
return { name: NAME, level: 'PASS', detail: `${pin.pkg}@${pin.version} is not cached yet (${npxDir} absent) — the first launch installs it` };
|
|
90
|
+
let hashes = [];
|
|
91
|
+
try {
|
|
92
|
+
hashes = deps.readdir(npxDir);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return { name: NAME, level: 'PASS', detail: `${npxDir} could not be listed — nothing to judge` };
|
|
96
|
+
}
|
|
97
|
+
const matching = [];
|
|
98
|
+
const corrupt = [];
|
|
99
|
+
for (const hash of hashes) {
|
|
100
|
+
const entry = path.join(npxDir, hash);
|
|
101
|
+
const pkgJson = path.join(entry, 'node_modules', ...pin.pkg.split('/'), 'package.json');
|
|
102
|
+
if (!deps.exists(pkgJson))
|
|
103
|
+
continue;
|
|
104
|
+
let version;
|
|
105
|
+
try {
|
|
106
|
+
version = JSON.parse(deps.readFile(pkgJson)).version;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (version !== pin.version)
|
|
112
|
+
continue;
|
|
113
|
+
matching.push(hash);
|
|
114
|
+
if (!deps.exists(path.join(entry, 'package.json')))
|
|
115
|
+
corrupt.push(hash);
|
|
116
|
+
}
|
|
117
|
+
if (matching.length === 0)
|
|
118
|
+
return { name: NAME, level: 'PASS', detail: `${pin.pkg}@${pin.version} is not cached yet under ${npxDir} — the first launch installs it` };
|
|
119
|
+
if (corrupt.length === 0)
|
|
120
|
+
return { name: NAME, level: 'PASS', detail: `${matching.length} intact npx cache entr${matching.length === 1 ? 'y' : 'ies'} for ${pin.pkg}@${pin.version}` };
|
|
121
|
+
const dirs = corrupt.map((h) => path.join(npxDir, h));
|
|
122
|
+
const remove = deps.platform === 'win32'
|
|
123
|
+
? dirs.map((d) => `Remove-Item -Recurse -Force "${d}"`).join('; ')
|
|
124
|
+
: dirs.map((d) => `rm -rf ${d}`).join(' && ');
|
|
125
|
+
return {
|
|
126
|
+
name: NAME, level: 'FAIL',
|
|
127
|
+
detail: `npx cache entr${corrupt.length === 1 ? 'y' : 'ies'} ${corrupt.join(', ')} for ${pin.pkg}@${pin.version} ${corrupt.length === 1 ? 'is' : 'are'} missing its package.json — the wiring dies with \`npm error ENOENT\` before the handshake and the harness reports only "Connection closed" (${dirs.join(', ')})`,
|
|
128
|
+
fix: `Delete the corrupt entr${corrupt.length === 1 ? 'y' : 'ies'} and let the next launch reinstall: ${remove}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
* Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
|
|
12
12
|
* still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* is exactly what a user's own skill of the same name would occupy, and overwriting that would be
|
|
17
|
-
* the same class of error as clobbering their settings.
|
|
18
|
-
*/
|
|
19
|
-
export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edits will be replaced on upgrade -->";
|
|
14
|
+
import { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd } from '../project/installer-markers';
|
|
15
|
+
export { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd };
|
|
20
16
|
/**
|
|
21
17
|
* @implements A-SPEC-190 (round 7)
|
|
22
18
|
* The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
|
|
@@ -28,22 +24,6 @@ export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edit
|
|
|
28
24
|
* while doctor certified the layout as `current`. The marker now follows the frontmatter block.
|
|
29
25
|
*/
|
|
30
26
|
export declare function installedSkillBytes(body: string): string;
|
|
31
|
-
/**
|
|
32
|
-
* @implements A-SPEC-190 (round 7)
|
|
33
|
-
* Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
|
|
34
|
-
* document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
|
|
35
|
-
* ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
|
|
36
|
-
* backup. A quotation is not a claim of ownership.
|
|
37
|
-
*/
|
|
38
|
-
export declare function carriesMarker(content: string): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* @implements A-SPEC-190 (round 8)
|
|
41
|
-
* Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
|
|
42
|
-
* regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
|
|
43
|
-
* trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
|
|
44
|
-
* round-7 fix existed to remove while doctor certified the result as `current`.
|
|
45
|
-
*/
|
|
46
|
-
export declare function frontmatterEnd(content: string): number | null;
|
|
47
27
|
/**
|
|
48
28
|
* @implements A-SPEC-172
|
|
49
29
|
* What the user types to reach a playbook.
|
|
@@ -33,10 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PLAYBOOK_SKILL_MARKER = void 0;
|
|
36
|
+
exports.frontmatterEnd = exports.carriesMarker = exports.PLAYBOOK_SKILL_MARKER = void 0;
|
|
37
37
|
exports.installedSkillBytes = installedSkillBytes;
|
|
38
|
-
exports.carriesMarker = carriesMarker;
|
|
39
|
-
exports.frontmatterEnd = frontmatterEnd;
|
|
40
38
|
exports.invocableSkillName = invocableSkillName;
|
|
41
39
|
exports.skillPathFor = skillPathFor;
|
|
42
40
|
exports.installPlaybookSkills = installPlaybookSkills;
|
|
@@ -62,12 +60,10 @@ const path = __importStar(require("node:path"));
|
|
|
62
60
|
* Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
|
|
63
61
|
* still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
|
|
64
62
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*/
|
|
70
|
-
exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be replaced on upgrade -->';
|
|
63
|
+
const installer_markers_1 = require("../project/installer-markers");
|
|
64
|
+
Object.defineProperty(exports, "PLAYBOOK_SKILL_MARKER", { enumerable: true, get: function () { return installer_markers_1.PLAYBOOK_SKILL_MARKER; } });
|
|
65
|
+
Object.defineProperty(exports, "carriesMarker", { enumerable: true, get: function () { return installer_markers_1.carriesMarker; } });
|
|
66
|
+
Object.defineProperty(exports, "frontmatterEnd", { enumerable: true, get: function () { return installer_markers_1.frontmatterEnd; } });
|
|
71
67
|
/**
|
|
72
68
|
* @implements A-SPEC-190 (round 7)
|
|
73
69
|
* The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
|
|
@@ -79,54 +75,14 @@ exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be rep
|
|
|
79
75
|
* while doctor certified the layout as `current`. The marker now follows the frontmatter block.
|
|
80
76
|
*/
|
|
81
77
|
function installedSkillBytes(body) {
|
|
82
|
-
const end = frontmatterEnd(body);
|
|
78
|
+
const end = (0, installer_markers_1.frontmatterEnd)(body);
|
|
83
79
|
if (end === null)
|
|
84
|
-
return `${
|
|
80
|
+
return `${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${body}`;
|
|
85
81
|
// The offset is computed on normalised text; a body with CRLF or a BOM is rewritten normalised so
|
|
86
82
|
// the marker cannot land inside the frontmatter it must follow.
|
|
87
83
|
const text = body.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
88
84
|
const head = text.slice(0, end);
|
|
89
|
-
return `${head}${head.endsWith('\n') ? '' : '\n'}${
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* @implements A-SPEC-190 (round 7)
|
|
93
|
-
* Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
|
|
94
|
-
* document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
|
|
95
|
-
* ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
|
|
96
|
-
* backup. A quotation is not a claim of ownership.
|
|
97
|
-
*/
|
|
98
|
-
function carriesMarker(content) {
|
|
99
|
-
// Round-8: judging on raw bytes made ownership brittle in the direction that cannot be repaired.
|
|
100
|
-
// A CRLF normalisation (a Windows editor, `core.autocrlf`, `.gitattributes eol=crlf`), a BOM, or a
|
|
101
|
-
// trailing space on the marker line each turned a file the kit itself wrote into `foreign` —
|
|
102
|
-
// permanently, since install then skips it forever and doctor advises deleting it as "yours".
|
|
103
|
-
// Whitespace and line endings are not ownership; the marker is.
|
|
104
|
-
const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
105
|
-
const markerAt = (s) => {
|
|
106
|
-
const line = s.split('\n', 1)[0];
|
|
107
|
-
return line.trimEnd() === exports.PLAYBOOK_SKILL_MARKER;
|
|
108
|
-
};
|
|
109
|
-
if (markerAt(text))
|
|
110
|
-
return true; // 옛 배치(마커 먼저) — 우리 것이므로 고칠 수 있다
|
|
111
|
-
const fm = frontmatterEnd(text);
|
|
112
|
-
// 빈 줄 하나가 소유를 지우지 않는다(round-9: frontmatter 뒤 개행 하나로 영구 foreign 이 됐다).
|
|
113
|
-
return fm !== null && markerAt(text.slice(fm).replace(/^\s*\n/, ''));
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* @implements A-SPEC-190 (round 8)
|
|
117
|
-
* Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
|
|
118
|
-
* regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
|
|
119
|
-
* trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
|
|
120
|
-
* round-7 fix existed to remove while doctor certified the result as `current`.
|
|
121
|
-
*/
|
|
122
|
-
function frontmatterEnd(content) {
|
|
123
|
-
const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
124
|
-
if (!/^---[ \t]*(\n|$)/.test(text))
|
|
125
|
-
return null;
|
|
126
|
-
const m = /\n---[ \t]*(\n|$)/.exec(text.slice(3));
|
|
127
|
-
if (!m)
|
|
128
|
-
return null;
|
|
129
|
-
return 3 + m.index + m[0].length;
|
|
85
|
+
return `${head}${head.endsWith('\n') ? '' : '\n'}${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${text.slice(end)}`;
|
|
130
86
|
}
|
|
131
87
|
/**
|
|
132
88
|
* @implements A-SPEC-172
|
|
@@ -168,7 +124,7 @@ function playbookNames(packageRoot) {
|
|
|
168
124
|
}
|
|
169
125
|
const isOurs = (file) => {
|
|
170
126
|
try {
|
|
171
|
-
return carriesMarker(fs.readFileSync(file, 'utf8'));
|
|
127
|
+
return (0, installer_markers_1.carriesMarker)(fs.readFileSync(file, 'utf8'));
|
|
172
128
|
}
|
|
173
129
|
catch {
|
|
174
130
|
return false;
|
|
@@ -637,7 +593,7 @@ function playbookSkillStates(packageRoot, target) {
|
|
|
637
593
|
out.push({ name, state: err.code === 'ENOENT' && !lstatPresent ? 'missing' : 'foreign' });
|
|
638
594
|
continue;
|
|
639
595
|
}
|
|
640
|
-
const state = !carriesMarker(installed) ? 'foreign'
|
|
596
|
+
const state = !(0, installer_markers_1.carriesMarker)(installed) ? 'foreign'
|
|
641
597
|
: installed === installedSkillBytes(body) ? 'current' : 'drifted';
|
|
642
598
|
out.push({ name, state });
|
|
643
599
|
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { type ChildProcess } from 'node:child_process';
|
|
1
|
+
import { execFile as nodeExecFile, type ChildProcess } from 'node:child_process';
|
|
2
2
|
/** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
|
|
3
3
|
export declare const PROBE_SPAWN_OPTIONS: {
|
|
4
4
|
detached: boolean;
|
|
5
5
|
windowsHide: boolean;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* A-SPEC-651.2 — budgets measured, not guessed. taskkill /T /F finished in 0.56–0.67 s on a quiet
|
|
9
|
+
* box (2026-09-14); the old 2.5 s budget, spent once, read "cleanup could not be confirmed" on a
|
|
10
|
+
* healthy install in 1 of 4 quiet doctor runs and 2 of 3 loaded ones. 8 s each, and a timed-out
|
|
11
|
+
* taskkill gets exactly one more try — a refused one does not (A-SPEC-592: a failed tree kill is never
|
|
12
|
+
* success).
|
|
13
|
+
*/
|
|
14
|
+
export declare const PROBE_KILL_TIMEOUT_MS = 8000;
|
|
15
|
+
export declare const PROBE_CLOSE_TIMEOUT_MS = 8000;
|
|
16
|
+
export interface ProbeStopDeps {
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
execFile?: typeof nodeExecFile;
|
|
19
|
+
}
|
|
20
|
+
/** Why the most recent stopProbeProcess answered false; undefined after a confirmed cleanup. */
|
|
21
|
+
export declare function lastProbeStopReason(): string | undefined;
|
|
7
22
|
export declare function trackProbeProcess(child: ChildProcess): void;
|
|
8
|
-
export declare function stopProbeProcess(child?: ChildProcess): Promise<boolean>;
|
|
23
|
+
export declare function stopProbeProcess(child?: ChildProcess, deps?: ProbeStopDeps): Promise<boolean>;
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PROBE_SPAWN_OPTIONS = void 0;
|
|
3
|
+
exports.PROBE_CLOSE_TIMEOUT_MS = exports.PROBE_KILL_TIMEOUT_MS = exports.PROBE_SPAWN_OPTIONS = void 0;
|
|
4
|
+
exports.lastProbeStopReason = lastProbeStopReason;
|
|
4
5
|
exports.trackProbeProcess = trackProbeProcess;
|
|
5
6
|
exports.stopProbeProcess = stopProbeProcess;
|
|
6
7
|
// @implements A-SPEC-592
|
|
8
|
+
// @implements A-SPEC-651.2
|
|
7
9
|
const node_child_process_1 = require("node:child_process");
|
|
8
10
|
/** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
|
|
9
11
|
exports.PROBE_SPAWN_OPTIONS = { detached: process.platform !== 'win32', windowsHide: true };
|
|
12
|
+
/**
|
|
13
|
+
* A-SPEC-651.2 — budgets measured, not guessed. taskkill /T /F finished in 0.56–0.67 s on a quiet
|
|
14
|
+
* box (2026-09-14); the old 2.5 s budget, spent once, read "cleanup could not be confirmed" on a
|
|
15
|
+
* healthy install in 1 of 4 quiet doctor runs and 2 of 3 loaded ones. 8 s each, and a timed-out
|
|
16
|
+
* taskkill gets exactly one more try — a refused one does not (A-SPEC-592: a failed tree kill is never
|
|
17
|
+
* success).
|
|
18
|
+
*/
|
|
19
|
+
exports.PROBE_KILL_TIMEOUT_MS = 8000;
|
|
20
|
+
exports.PROBE_CLOSE_TIMEOUT_MS = 8000;
|
|
10
21
|
const states = new WeakMap();
|
|
22
|
+
let lastReason;
|
|
23
|
+
/** Why the most recent stopProbeProcess answered false; undefined after a confirmed cleanup. */
|
|
24
|
+
function lastProbeStopReason() { return lastReason; }
|
|
11
25
|
function trackProbeProcess(child) {
|
|
12
26
|
if (states.has(child))
|
|
13
27
|
return;
|
|
@@ -15,30 +29,45 @@ function trackProbeProcess(child) {
|
|
|
15
29
|
states.set(child, state);
|
|
16
30
|
child.once('close', () => { state.closed = true; });
|
|
17
31
|
}
|
|
18
|
-
|
|
32
|
+
const isTimeout = (error) => {
|
|
33
|
+
const e = error;
|
|
34
|
+
return !!e && (e.killed === true || e.code === null || e.code === undefined);
|
|
35
|
+
};
|
|
36
|
+
async function stopProbeProcess(child, deps = {}) {
|
|
37
|
+
lastReason = undefined;
|
|
19
38
|
if (!child)
|
|
20
39
|
return true;
|
|
21
40
|
trackProbeProcess(child);
|
|
41
|
+
const platform = deps.platform ?? process.platform;
|
|
42
|
+
const execFile = deps.execFile ?? node_child_process_1.execFile;
|
|
22
43
|
let closed = states.get(child).closed;
|
|
23
44
|
let onClose = () => { };
|
|
24
45
|
let closeTimer;
|
|
25
46
|
const closure = new Promise(resolve => {
|
|
26
47
|
onClose = () => { closed = true; clearTimeout(closeTimer); resolve(true); };
|
|
27
48
|
child.once('close', onClose);
|
|
28
|
-
closeTimer = setTimeout(() => resolve(false),
|
|
49
|
+
closeTimer = setTimeout(() => resolve(false), exports.PROBE_CLOSE_TIMEOUT_MS);
|
|
29
50
|
if (closed || !child.pid)
|
|
30
51
|
onClose();
|
|
31
52
|
});
|
|
32
53
|
let stopped = true;
|
|
54
|
+
let stopReason;
|
|
33
55
|
try {
|
|
34
|
-
if (child.pid &&
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
resolve(!error);
|
|
38
|
-
});
|
|
56
|
+
if (child.pid && platform === 'win32' && !closed) {
|
|
57
|
+
const taskkill = () => new Promise(resolve => {
|
|
58
|
+
execFile('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: exports.PROBE_KILL_TIMEOUT_MS }, error => resolve(error ?? null));
|
|
39
59
|
});
|
|
60
|
+
let error = await taskkill();
|
|
61
|
+
if (error && isTimeout(error))
|
|
62
|
+
error = await taskkill(); // one retry, for a timeout only
|
|
63
|
+
if (error) {
|
|
64
|
+
stopped = false;
|
|
65
|
+
stopReason = isTimeout(error)
|
|
66
|
+
? `taskkill timed out twice (${exports.PROBE_KILL_TIMEOUT_MS} ms each)`
|
|
67
|
+
: `taskkill exited ${String(error.code)}`;
|
|
68
|
+
}
|
|
40
69
|
}
|
|
41
|
-
else if (child.pid &&
|
|
70
|
+
else if (child.pid && platform !== 'win32') {
|
|
42
71
|
const signalGroup = (signal) => {
|
|
43
72
|
try {
|
|
44
73
|
process.kill(-child.pid, signal);
|
|
@@ -56,8 +85,9 @@ async function stopProbeProcess(child) {
|
|
|
56
85
|
signalGroup('SIGKILL');
|
|
57
86
|
}
|
|
58
87
|
}
|
|
59
|
-
catch {
|
|
88
|
+
catch (error) {
|
|
60
89
|
stopped = false;
|
|
90
|
+
stopReason = `kill failed: ${error.message}`;
|
|
61
91
|
}
|
|
62
92
|
// Terminate the tree before closing stdin: an EOF could make the wrapper exit first,
|
|
63
93
|
// leaving Windows taskkill with no parent from which to enumerate its descendants.
|
|
@@ -69,5 +99,8 @@ async function stopProbeProcess(child) {
|
|
|
69
99
|
child.removeListener('close', onClose);
|
|
70
100
|
if (!released)
|
|
71
101
|
child.unref();
|
|
72
|
-
|
|
102
|
+
const ok = stopped && released;
|
|
103
|
+
if (!ok)
|
|
104
|
+
lastReason = stopReason ?? `child never closed within ${exports.PROBE_CLOSE_TIMEOUT_MS} ms`;
|
|
105
|
+
return ok;
|
|
73
106
|
}
|
|
@@ -33,6 +33,27 @@ export declare function readGrants(root: string): {
|
|
|
33
33
|
grants: Approval[];
|
|
34
34
|
ignored: number;
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* @implements A-SPEC-649
|
|
38
|
+
* A minted grant may carry its BINDING: the workspace it was minted in and the digest of the subject
|
|
39
|
+
* the operator read. Neither is part of the guardrail's `Approval` (an operator's env token has no
|
|
40
|
+
* binding); both are optional so legacy grant files keep covering exactly as before (stated).
|
|
41
|
+
*/
|
|
42
|
+
export type BoundGrant = Approval & {
|
|
43
|
+
workspace?: string;
|
|
44
|
+
subject?: string;
|
|
45
|
+
requestedBy?: string;
|
|
46
|
+
};
|
|
47
|
+
export type BindingRefusal = 'foreign-workspace' | 'stale-subject';
|
|
48
|
+
/** @implements A-SPEC-649 — the grants that WOULD cover this action but are refused by their binding, by name. */
|
|
49
|
+
export declare function refusedGrantReasons(root: string, action: {
|
|
50
|
+
kind: string;
|
|
51
|
+
target: string;
|
|
52
|
+
subject?: string;
|
|
53
|
+
}, now: string): Array<{
|
|
54
|
+
nonce: string;
|
|
55
|
+
reason: BindingRefusal;
|
|
56
|
+
}>;
|
|
36
57
|
/** First grant that covers the action — judged by `approvalCovers` and nothing else. Pure. */
|
|
37
58
|
export declare function pickCoveringGrant(grants: Approval[], action: {
|
|
38
59
|
kind: string;
|
|
@@ -48,6 +69,7 @@ export declare function pickCoveringGrant(grants: Approval[], action: {
|
|
|
48
69
|
export declare function resolveApproval(root: string, envApproval: Approval | undefined, action: {
|
|
49
70
|
kind: string;
|
|
50
71
|
target: string;
|
|
72
|
+
subject?: string;
|
|
51
73
|
}, now: string): {
|
|
52
74
|
approval: Approval;
|
|
53
75
|
source: 'env' | 'grant';
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.GRANTS_RELDIR = void 0;
|
|
37
37
|
exports.readGrants = readGrants;
|
|
38
|
+
exports.refusedGrantReasons = refusedGrantReasons;
|
|
38
39
|
exports.pickCoveringGrant = pickCoveringGrant;
|
|
39
40
|
exports.resolveApproval = resolveApproval;
|
|
40
41
|
exports.consumeGrantFile = consumeGrantFile;
|
|
@@ -42,6 +43,8 @@ exports.consumeGrantFile = consumeGrantFile;
|
|
|
42
43
|
const fs = __importStar(require("node:fs"));
|
|
43
44
|
const path = __importStar(require("node:path"));
|
|
44
45
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
46
|
+
const workspace_identity_1 = require("../project/workspace-identity");
|
|
47
|
+
const approval_queue_1 = require("./approval-queue");
|
|
45
48
|
/**
|
|
46
49
|
* The grant file channel — approval delivery with no file editing and no reconnect.
|
|
47
50
|
*
|
|
@@ -102,6 +105,47 @@ function readGrants(root) {
|
|
|
102
105
|
}
|
|
103
106
|
return { grants, ignored };
|
|
104
107
|
}
|
|
108
|
+
/** The current workspace id, or undefined for a legacy workspace / an unreadable identity. */
|
|
109
|
+
function currentWorkspaceId(root) {
|
|
110
|
+
try {
|
|
111
|
+
const id = (0, workspace_identity_1.workspaceIdentity)(root, 'status');
|
|
112
|
+
return id.state === 'legacy' ? undefined : id.workspaceId;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Why this grant must NOT cover this action here, or null. Measured 2026-09-14 on the shipped dist:
|
|
120
|
+
* a grant file copied into another workspace with the same request id sealed there, and a grant
|
|
121
|
+
* read against one spec body sealed a different one. A grant that names a workspace is refused in
|
|
122
|
+
* any other (or in a workspace with no identity to compare); a grant that names a subject is
|
|
123
|
+
* refused when the action NAMES a subject and it differs. An action that names no subject is
|
|
124
|
+
* judged by its consumer at the point where the subject is known: `spec_approve` compares the
|
|
125
|
+
* grant's subject with the digest of its ONE versioned read (an extra read here to learn the
|
|
126
|
+
* subject moved the optimistic-write window and let an external edit be sealed — measured by the
|
|
127
|
+
* A-SPEC-188 TOCTOU pin), and every other decision kind carries a content-addressed target.
|
|
128
|
+
*/
|
|
129
|
+
function bindingRefusal(grant, action, workspace) {
|
|
130
|
+
if (typeof grant.workspace === 'string' && grant.workspace !== '' && grant.workspace !== workspace)
|
|
131
|
+
return 'foreign-workspace';
|
|
132
|
+
if (typeof grant.subject === 'string' && grant.subject !== '' && action.subject !== undefined && grant.subject !== action.subject)
|
|
133
|
+
return 'stale-subject';
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/** @implements A-SPEC-649 — the grants that WOULD cover this action but are refused by their binding, by name. */
|
|
137
|
+
function refusedGrantReasons(root, action, now) {
|
|
138
|
+
const workspace = currentWorkspaceId(root);
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const g of readGrants(root).grants) {
|
|
141
|
+
if (!(0, risk_gate_1.approvalCovers)(g, action, now))
|
|
142
|
+
continue;
|
|
143
|
+
const reason = bindingRefusal(g, action, workspace);
|
|
144
|
+
if (reason && typeof g.nonce === 'string')
|
|
145
|
+
out.push({ nonce: g.nonce, reason });
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
105
149
|
/** First grant that covers the action — judged by `approvalCovers` and nothing else. Pure. */
|
|
106
150
|
function pickCoveringGrant(grants, action, now) {
|
|
107
151
|
for (const g of Array.isArray(grants) ? grants : []) {
|
|
@@ -120,7 +164,9 @@ function pickCoveringGrant(grants, action, now) {
|
|
|
120
164
|
function resolveApproval(root, envApproval, action, now) {
|
|
121
165
|
if ((0, risk_gate_1.approvalCovers)(envApproval, action, now))
|
|
122
166
|
return { approval: envApproval, source: 'env' };
|
|
123
|
-
|
|
167
|
+
// @implements A-SPEC-649 — a bound grant is a candidate only in its own workspace and for its own subject.
|
|
168
|
+
const workspace = currentWorkspaceId(root);
|
|
169
|
+
const grant = pickCoveringGrant(readGrants(root).grants.filter((g) => bindingRefusal(g, action, workspace) === null), action, now);
|
|
124
170
|
return grant ? { approval: grant, source: 'grant' } : undefined;
|
|
125
171
|
}
|
|
126
172
|
/**
|
|
@@ -157,6 +203,14 @@ function consumeGrantFile(root, nonce) {
|
|
|
157
203
|
// No directory, or it cannot be read: there is nothing to spend, which is an ordinary state.
|
|
158
204
|
return;
|
|
159
205
|
}
|
|
206
|
+
// @implements A-SPEC-649 — the spend is a queue event too, so the decision surface can show the
|
|
207
|
+
// grant's outcome without reading the provenance ledger. Best-effort, like the removal itself.
|
|
208
|
+
if (removed > 0) {
|
|
209
|
+
try {
|
|
210
|
+
(0, approval_queue_1.appendQueueEvent)(root, { event: 'consumed', id: wanted });
|
|
211
|
+
}
|
|
212
|
+
catch { /* fire-and-forget */ }
|
|
213
|
+
}
|
|
160
214
|
if (removed === 0) {
|
|
161
215
|
// Consumption runs AFTER the act succeeded, so this must never throw — but it must not be silent
|
|
162
216
|
// either: an unspent single-use grant is still spendable, and nobody would know.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExecutionContext } from '../project/execution-context';
|
|
1
2
|
/**
|
|
2
3
|
* The approval request queue — the review list a human batches decisions over.
|
|
3
4
|
*
|
|
@@ -53,6 +54,17 @@ export interface PendingRequest {
|
|
|
53
54
|
* reads it, so an absent or wrong value can never change a verdict.
|
|
54
55
|
*/
|
|
55
56
|
replica?: string;
|
|
57
|
+
/**
|
|
58
|
+
* @implements A-SPEC-649
|
|
59
|
+
* What the FIRST `requested` event of this id said about itself: the execution that asked
|
|
60
|
+
* (workspace/replica/run — absent for the CLI, hooks and legacy events), the digest of the subject
|
|
61
|
+
* the human will read (`spec-approve`: `specDigest` of the spec as read), and the risk grade. A
|
|
62
|
+
* grant minted from this entry copies `subject` so a seal of DIFFERENT content is refused — measured
|
|
63
|
+
* 2026-09-14: a grant read against "Original title" sealed "CHANGED after the grant".
|
|
64
|
+
*/
|
|
65
|
+
execution?: ExecutionContext;
|
|
66
|
+
subject?: string;
|
|
67
|
+
risk?: string;
|
|
56
68
|
}
|
|
57
69
|
export interface QueueState {
|
|
58
70
|
pending: PendingRequest[];
|
|
@@ -75,10 +87,24 @@ export interface QueueState {
|
|
|
75
87
|
* An invisible decision cannot stop a retry loop.
|
|
76
88
|
*/
|
|
77
89
|
decisions: Record<string, {
|
|
78
|
-
event: 'granted' | 'denied';
|
|
90
|
+
event: 'granted' | 'denied' | 'revoked';
|
|
79
91
|
reason?: string;
|
|
80
92
|
ts: string;
|
|
81
93
|
covered?: number;
|
|
94
|
+
execution?: ExecutionContext;
|
|
95
|
+
consumedTs?: string;
|
|
96
|
+
expires?: string;
|
|
97
|
+
actor?: string;
|
|
98
|
+
}>;
|
|
99
|
+
/**
|
|
100
|
+
* @implements A-SPEC-650
|
|
101
|
+
* kind/target per request id, for EVERY `requested` event the fold saw (first wins) — the decision
|
|
102
|
+
* surface names what a past decision was about without re-reading the file. Present only when at
|
|
103
|
+
* least one request was seen, so the empty-state pins (A-SPEC-244) keep their exact shape.
|
|
104
|
+
*/
|
|
105
|
+
subjects?: Record<string, {
|
|
106
|
+
kind: string;
|
|
107
|
+
target: string;
|
|
82
108
|
}>;
|
|
83
109
|
/**
|
|
84
110
|
* @implements A-SPEC-563.2
|
|
@@ -89,6 +115,7 @@ export interface QueueState {
|
|
|
89
115
|
holds: Record<string, {
|
|
90
116
|
question?: string;
|
|
91
117
|
ts: string;
|
|
118
|
+
execution?: ExecutionContext;
|
|
92
119
|
}>;
|
|
93
120
|
}
|
|
94
121
|
/**
|
|
@@ -121,12 +148,17 @@ export declare function foldQueue(lines: string[], opts?: {
|
|
|
121
148
|
* dead queue could change a verdict, killing the queue becomes a way to manipulate the gate. The
|
|
122
149
|
* caller uses the boolean only to decide whether to print the review hint.
|
|
123
150
|
*/
|
|
151
|
+
/** @implements A-SPEC-649 — what a request may say about its subject and risk, beyond kind/target/why. */
|
|
152
|
+
export interface RequestDetails {
|
|
153
|
+
subject?: string;
|
|
154
|
+
risk?: string;
|
|
155
|
+
}
|
|
124
156
|
export declare function enqueueApprovalRequest(root: string, req: {
|
|
125
157
|
kind: string;
|
|
126
158
|
target: string;
|
|
127
159
|
why: string;
|
|
128
160
|
reasonBytes?: number;
|
|
129
|
-
}): boolean;
|
|
161
|
+
} & RequestDetails): boolean;
|
|
130
162
|
/**
|
|
131
163
|
* The same act, told in full: what happened and why.
|
|
132
164
|
*
|
|
@@ -140,10 +172,17 @@ export declare function enqueueApprovalRequestDetailed(root: string, req: {
|
|
|
140
172
|
target: string;
|
|
141
173
|
why: string;
|
|
142
174
|
reasonBytes?: number;
|
|
143
|
-
}): {
|
|
175
|
+
} & RequestDetails): {
|
|
144
176
|
written: boolean;
|
|
145
177
|
reason: 'written' | 'duplicate' | 'no-project' | 'unwritable';
|
|
146
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* @implements A-SPEC-649
|
|
181
|
+
* Append a lifecycle event (`consumed`, `revoked`) to the tracked queue: `.ax` must exist, the
|
|
182
|
+
* queue must be a plain file, the timestamp is this write's, and the active execution (if any) is
|
|
183
|
+
* attached — the same discipline as the CLI's decision writer. Fire-and-forget: `false` on failure.
|
|
184
|
+
*/
|
|
185
|
+
export declare function appendQueueEvent(root: string, event: Record<string, unknown>): boolean;
|
|
147
186
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
148
187
|
export declare function readQueue(root: string, opts?: {
|
|
149
188
|
now?: number;
|
|
@@ -160,6 +199,6 @@ export declare function queueHint(root: string, req: {
|
|
|
160
199
|
kind: string;
|
|
161
200
|
target: string;
|
|
162
201
|
why: string;
|
|
163
|
-
}, opts?: {
|
|
202
|
+
} & RequestDetails, opts?: {
|
|
164
203
|
baseReasonBytes?: number;
|
|
165
204
|
}): string;
|