@holmes-lab/holmes-kit 0.1.11 → 0.1.13
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 +52 -0
- package/README.md +26 -49
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +9 -2
- package/dist/holmes/cli/agents.js +9 -2
- package/dist/holmes/cli/approve.d.ts +46 -0
- package/dist/holmes/cli/approve.js +200 -0
- package/dist/holmes/cli/index.js +78 -1
- package/dist/holmes/governance/approval-grants.d.ts +63 -0
- package/dist/holmes/governance/approval-grants.js +142 -0
- package/dist/holmes/governance/approval-queue.d.ts +93 -0
- package/dist/holmes/governance/approval-queue.js +230 -0
- package/dist/holmes/guardrail/risk-classifier.js +12 -5
- package/dist/holmes/guardrail/write-target.js +2 -0
- package/dist/holmes/hooks/pre-tool-use.js +34 -12
- package/dist/holmes/hooks/stop.d.ts +39 -0
- package/dist/holmes/hooks/stop.js +100 -1
- package/dist/holmes/mcp/handlers.js +70 -11
- package/dist/holmes/mcp/server.js +9 -0
- package/docs/install-guide.md +131 -0
- package/package.json +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PendingRequest } from '../governance/approval-queue';
|
|
1
2
|
import { Spec } from '../spec/spec-parser';
|
|
2
3
|
/**
|
|
3
4
|
* @implements A-SPEC-100.2
|
|
@@ -57,6 +58,10 @@ export declare function evaluateStop(specs: Spec[], evidence?: StopEvidence): {
|
|
|
57
58
|
block: boolean;
|
|
58
59
|
reason?: string;
|
|
59
60
|
articles?: string[];
|
|
61
|
+
violations?: {
|
|
62
|
+
article: string;
|
|
63
|
+
detail: string;
|
|
64
|
+
}[];
|
|
60
65
|
};
|
|
61
66
|
/**
|
|
62
67
|
* @implements A-SPEC-134
|
|
@@ -76,6 +81,40 @@ export declare function stopDebtAction(evaluation: {
|
|
|
76
81
|
articles: string[];
|
|
77
82
|
};
|
|
78
83
|
export declare const MAX_CONSECUTIVE_BLOCKS = 3;
|
|
84
|
+
/**
|
|
85
|
+
* Should the stop gate keep re-blocking a turn, or has every unresolved debt already been queued
|
|
86
|
+
* for the owner's decision?
|
|
87
|
+
*
|
|
88
|
+
* @implements A-SPEC-247
|
|
89
|
+
* Measured (Notion UX-2): the stop hook re-blocks a violation whose only fix is an owner approval on
|
|
90
|
+
* every turn — six in a row — even when the request is already waiting in the queue, giving the
|
|
91
|
+
* agent nothing to do and the user the same wall repeatedly. This answers "is anything left that the
|
|
92
|
+
* agent or a NON-waiting approval could resolve?".
|
|
93
|
+
*
|
|
94
|
+
* ASYMMETRY IS THE POINT, and it is what keeps this from becoming amnesty: acknowledged is true ONLY
|
|
95
|
+
* when nothing is blocking AND at least one thing is waiting. A single non-waiting violation — a
|
|
96
|
+
* code debt, or an approval debt nobody queued — and the gate blocks exactly as before. Waiting is
|
|
97
|
+
* read from the pending list (REQ-244's fold), not re-derived. A violation whose detail names no
|
|
98
|
+
* spec id cannot be matched to a request, so it blocks — the safe direction.
|
|
99
|
+
*/
|
|
100
|
+
export declare function acknowledgeStop(violations: {
|
|
101
|
+
article: string;
|
|
102
|
+
detail: string;
|
|
103
|
+
}[], pending: PendingRequest[]): {
|
|
104
|
+
acknowledged: boolean;
|
|
105
|
+
blocking: string[];
|
|
106
|
+
waiting: string[];
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* One-line health of the env approval token, for the server's startup log.
|
|
110
|
+
*
|
|
111
|
+
* @implements A-SPEC-247
|
|
112
|
+
* Measured (Notion UX-4): an invalid token gives no signal until an approval is attempted, so the
|
|
113
|
+
* operator round-trips without knowing why. This is a LOG, not a gate — an absent or invalid token
|
|
114
|
+
* is a legitimate read-only posture, and the server must start regardless. The reason distinguishes
|
|
115
|
+
* "not JSON" from "wrong shape" so the operator knows which to fix.
|
|
116
|
+
*/
|
|
117
|
+
export declare function describeTokenHealth(raw: string | undefined): string;
|
|
79
118
|
export declare function decideStopGuard(wantsBlock: boolean, priorConsecutiveBlocks: number, cap?: number): {
|
|
80
119
|
block: boolean;
|
|
81
120
|
nextCount: number;
|
|
@@ -37,12 +37,15 @@ exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
|
37
37
|
exports.governanceLostPreflight = governanceLostPreflight;
|
|
38
38
|
exports.evaluateStop = evaluateStop;
|
|
39
39
|
exports.stopDebtAction = stopDebtAction;
|
|
40
|
+
exports.acknowledgeStop = acknowledgeStop;
|
|
41
|
+
exports.describeTokenHealth = describeTokenHealth;
|
|
40
42
|
exports.decideStopGuard = decideStopGuard;
|
|
41
43
|
exports.__setWiredSpecsForTest = __setWiredSpecsForTest;
|
|
42
44
|
exports.guardCountOrZero = guardCountOrZero;
|
|
43
45
|
exports.readGuardCount = readGuardCount;
|
|
44
46
|
exports.writeGuardCount = writeGuardCount;
|
|
45
47
|
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
46
49
|
const json_state_1 = require("../project/json-state");
|
|
47
50
|
const node_child_process_1 = require("node:child_process");
|
|
48
51
|
const path = __importStar(require("node:path"));
|
|
@@ -79,15 +82,20 @@ function evaluateStop(specs, evidence) {
|
|
|
79
82
|
// The articles live in ONE place (governance/constitution.ts); this gate merely executes them.
|
|
80
83
|
const violations = (0, constitution_1.verifyConstitution)({ specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings });
|
|
81
84
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
85
|
+
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
86
|
+
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
87
|
+
const structured = violations.map((x) => ({ article: x.article, detail: x.detail }));
|
|
82
88
|
// Tampered audit trail blocks finishing — wires the provenance chain's verify into the live gate
|
|
83
89
|
// (review finding: verify() previously had no product caller, so the chain's guarantee was inert).
|
|
84
90
|
if (evidence?.provenance && !evidence.provenance.ok) {
|
|
85
91
|
problems.push(`[ART-2] provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}`);
|
|
92
|
+
structured.push({ article: 'ART-2', detail: `provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}` });
|
|
86
93
|
}
|
|
87
94
|
// @implements A-SPEC-191 (§4a) — an existing-but-unreadable findings ledger is not a clean turn:
|
|
88
95
|
// it may hold an open critical, and a "clean" verdict here would also CLEAR standing ART-7 debt.
|
|
89
96
|
if (evidence?.findingsUnreadable) {
|
|
90
97
|
problems.push('[ART-7] findings 원장을 읽을 수 없습니다 — 열린 치명 발견의 존재를 확인할 수 없는 턴은 깨끗한 턴이 아닙니다 (원장 파일의 권한·형식을 복구하십시오)');
|
|
98
|
+
structured.push({ article: 'ART-7', detail: 'findings 원장을 읽을 수 없습니다' });
|
|
91
99
|
}
|
|
92
100
|
if (problems.length === 0)
|
|
93
101
|
return { block: false };
|
|
@@ -100,6 +108,7 @@ function evaluateStop(specs, evidence) {
|
|
|
100
108
|
return {
|
|
101
109
|
block: true,
|
|
102
110
|
articles,
|
|
111
|
+
violations: structured,
|
|
103
112
|
reason: `[Holmes-Kit] constitution gate: ${problems.length} article violation(s) must be ` +
|
|
104
113
|
`fixed before finishing:\n${shown.join('\n')}${more}`,
|
|
105
114
|
};
|
|
@@ -125,6 +134,76 @@ function stopDebtAction(evaluation, guard) {
|
|
|
125
134
|
// blocking while problems persist, up to MAX_CONSECUTIVE_BLOCKS, then yields with a loud warning
|
|
126
135
|
// (bounded, so an unfixable state cannot infinite-loop the session). Any clean stop resets the count.
|
|
127
136
|
exports.MAX_CONSECUTIVE_BLOCKS = 3;
|
|
137
|
+
/**
|
|
138
|
+
* Constitution violation codes whose ONLY resolution is an owner's approval, not the agent's code.
|
|
139
|
+
*
|
|
140
|
+
* @implements A-SPEC-247
|
|
141
|
+
* stale-parent and post-approval-edit are cleared by re-approving the spec — a human act, out of
|
|
142
|
+
* band. ART-4 (no anchored test), a dangling RTM edge, an unmet obligation: those the agent fixes
|
|
143
|
+
* by writing code, so they are NOT here. Acknowledging a code-resolvable violation would bury a
|
|
144
|
+
* debt the agent is supposed to pay.
|
|
145
|
+
*/
|
|
146
|
+
const APPROVAL_RESOLVABLE = ['stale-parent', 'post-approval-edit'];
|
|
147
|
+
const SPEC_ID_RE = /\b([A-Z]-SPEC-\d+(?:\.\d+)?|REQ-\d+)\b/;
|
|
148
|
+
/**
|
|
149
|
+
* Should the stop gate keep re-blocking a turn, or has every unresolved debt already been queued
|
|
150
|
+
* for the owner's decision?
|
|
151
|
+
*
|
|
152
|
+
* @implements A-SPEC-247
|
|
153
|
+
* Measured (Notion UX-2): the stop hook re-blocks a violation whose only fix is an owner approval on
|
|
154
|
+
* every turn — six in a row — even when the request is already waiting in the queue, giving the
|
|
155
|
+
* agent nothing to do and the user the same wall repeatedly. This answers "is anything left that the
|
|
156
|
+
* agent or a NON-waiting approval could resolve?".
|
|
157
|
+
*
|
|
158
|
+
* ASYMMETRY IS THE POINT, and it is what keeps this from becoming amnesty: acknowledged is true ONLY
|
|
159
|
+
* when nothing is blocking AND at least one thing is waiting. A single non-waiting violation — a
|
|
160
|
+
* code debt, or an approval debt nobody queued — and the gate blocks exactly as before. Waiting is
|
|
161
|
+
* read from the pending list (REQ-244's fold), not re-derived. A violation whose detail names no
|
|
162
|
+
* spec id cannot be matched to a request, so it blocks — the safe direction.
|
|
163
|
+
*/
|
|
164
|
+
function acknowledgeStop(violations, pending) {
|
|
165
|
+
const waitingTargets = new Set((Array.isArray(pending) ? pending : []).map((p) => p.target));
|
|
166
|
+
const blocking = [];
|
|
167
|
+
const waiting = [];
|
|
168
|
+
for (const vln of Array.isArray(violations) ? violations : []) {
|
|
169
|
+
const detail = String(vln?.detail ?? '');
|
|
170
|
+
const approvalResolvable = APPROVAL_RESOLVABLE.some((k) => detail.includes(k));
|
|
171
|
+
if (!approvalResolvable) {
|
|
172
|
+
blocking.push(`[${vln.article}] ${detail}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const id = (SPEC_ID_RE.exec(detail) ?? [])[1];
|
|
176
|
+
if (id && waitingTargets.has(id))
|
|
177
|
+
waiting.push(id);
|
|
178
|
+
else
|
|
179
|
+
blocking.push(`[${vln.article}] ${detail}`); // named no id, or not queued → block
|
|
180
|
+
}
|
|
181
|
+
return { acknowledged: blocking.length === 0 && waiting.length > 0, blocking, waiting };
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* One-line health of the env approval token, for the server's startup log.
|
|
185
|
+
*
|
|
186
|
+
* @implements A-SPEC-247
|
|
187
|
+
* Measured (Notion UX-4): an invalid token gives no signal until an approval is attempted, so the
|
|
188
|
+
* operator round-trips without knowing why. This is a LOG, not a gate — an absent or invalid token
|
|
189
|
+
* is a legitimate read-only posture, and the server must start regardless. The reason distinguishes
|
|
190
|
+
* "not JSON" from "wrong shape" so the operator knows which to fix.
|
|
191
|
+
*/
|
|
192
|
+
function describeTokenHealth(raw) {
|
|
193
|
+
if (raw === undefined || raw === '')
|
|
194
|
+
return 'HOLMES_APPROVAL 토큰 없음 (읽기 전용 — 승인이 필요한 행위만 막힙니다)';
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return 'HOLMES_APPROVAL 무효: JSON 파싱 실패 — 승인이 필요한 행위가 막힙니다';
|
|
201
|
+
}
|
|
202
|
+
if (!(0, risk_gate_1.isValidApproval)(parsed)) {
|
|
203
|
+
return 'HOLMES_APPROVAL 무효: 필수 필드(actor·token·rationale) 누락 — 승인이 필요한 행위가 막힙니다';
|
|
204
|
+
}
|
|
205
|
+
return 'HOLMES_APPROVAL 유효';
|
|
206
|
+
}
|
|
128
207
|
function decideStopGuard(wantsBlock, priorConsecutiveBlocks, cap = exports.MAX_CONSECUTIVE_BLOCKS) {
|
|
129
208
|
if (!wantsBlock)
|
|
130
209
|
return { block: false, nextCount: 0, capped: false };
|
|
@@ -413,7 +492,22 @@ if (require.main === module) {
|
|
|
413
492
|
catch {
|
|
414
493
|
findingsUnreadable = true;
|
|
415
494
|
} // @implements A-SPEC-191 (§4a) — list() absorbs ENOENT; a throw means the ledger EXISTS and cannot be read, which must block, not launder
|
|
416
|
-
|
|
495
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable });
|
|
496
|
+
// @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
|
|
497
|
+
// already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
|
|
498
|
+
// non-waiting violation and we block exactly as before.
|
|
499
|
+
let ackWaiting = [];
|
|
500
|
+
if (out.block) {
|
|
501
|
+
try {
|
|
502
|
+
const { readQueue } = require('../governance/approval-queue');
|
|
503
|
+
const ack = acknowledgeStop(out.violations ?? [], readQueue(stopProjectRoot()).pending);
|
|
504
|
+
if (ack.acknowledged) {
|
|
505
|
+
out = { block: false };
|
|
506
|
+
ackWaiting = ack.waiting;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch { /* awareness is best-effort; a failure leaves the block intact */ }
|
|
510
|
+
}
|
|
417
511
|
const guard = decideStopGuard(out.block, guardCountOrZero(sessionId));
|
|
418
512
|
const persisted = writeGuardCount(sessionId, guard.nextCount);
|
|
419
513
|
// @implements A-SPEC-134 — the cap-yield is no longer silent: a clean turn clears any debt, a
|
|
@@ -438,6 +532,11 @@ if (require.main === module) {
|
|
|
438
532
|
process.stdout.write(JSON.stringify({ decision: 'block', reason: out.reason }));
|
|
439
533
|
process.stderr.write(`${out.reason}\n`);
|
|
440
534
|
}
|
|
535
|
+
else if (ackWaiting.length > 0) {
|
|
536
|
+
// @implements A-SPEC-247 — acknowledged, not clean: name what is waiting so the user sees
|
|
537
|
+
// the standing approval debt exactly once, and the agent knows the ball is not in its court.
|
|
538
|
+
process.stderr.write(`[Holmes-Kit] 승인 대기 중 — 오너 결정을 기다리는 항목: ${ackWaiting.join(', ')}. npx holmes-kit approve 에서 결재하십시오. (이 부채로는 재차단하지 않습니다)\n`);
|
|
539
|
+
}
|
|
441
540
|
else if (guard.capped) {
|
|
442
541
|
process.stderr.write(`[Holmes-Kit] governance gate YIELDING after ${exports.MAX_CONSECUTIVE_BLOCKS} consecutive blocks — issues remain UNRESOLVED:\n${out.reason ?? ''}\n`);
|
|
443
542
|
}
|
|
@@ -99,6 +99,8 @@ const findings_1 = require("../review/findings");
|
|
|
99
99
|
const package_1 = require("../review/package");
|
|
100
100
|
const risk_classifier_1 = require("../guardrail/risk-classifier");
|
|
101
101
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
102
|
+
const approval_queue_1 = require("../governance/approval-queue");
|
|
103
|
+
const approval_grants_1 = require("../governance/approval-grants");
|
|
102
104
|
const spec_digest_1 = require("../spec/spec-digest");
|
|
103
105
|
const spec_store_2 = require("../spec/spec-store");
|
|
104
106
|
const breaking_change_1 = require("../spec/breaking-change");
|
|
@@ -148,6 +150,46 @@ const anchor_1 = require("../reverse/anchor");
|
|
|
148
150
|
// The hazard that guard addressed is real but belongs to the GIT CHANGE SOURCE: gitChangedFiles
|
|
149
151
|
// yields top-level-relative paths while scan(root, root) tags root-relative ones, so a disagreement
|
|
150
152
|
// silently produces wrong impact sets. `GitChangeSource` now enforces exactly that, and only there.
|
|
153
|
+
/**
|
|
154
|
+
* @implements A-SPEC-244
|
|
155
|
+
* File the review request from a handler refusal. A failure to resolve the root files nothing —
|
|
156
|
+
* the queue is information, and a refusal path must not gain a new failure mode from it.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* @implements A-SPEC-245
|
|
160
|
+
* Env-or-grant resolution for handler gates. With no resolvable root there is no disk to read
|
|
161
|
+
* grants from, so the env channel alone answers — same fail-closed posture as before this slice.
|
|
162
|
+
*/
|
|
163
|
+
function resolveHandlerApproval(rootArg, store, envApproval, action, now) {
|
|
164
|
+
let root;
|
|
165
|
+
try {
|
|
166
|
+
const storeRoot = store?.specsRoot;
|
|
167
|
+
const base = rootArg !== undefined ? rootArg : (typeof storeRoot === 'string' ? storeRoot : undefined);
|
|
168
|
+
root = base !== undefined ? projectRootOf(base) : undefined;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
root = undefined;
|
|
172
|
+
}
|
|
173
|
+
if (root === undefined) {
|
|
174
|
+
return (0, risk_gate_1.approvalCovers)(envApproval, action, now) ? { approval: envApproval, source: 'env' } : undefined;
|
|
175
|
+
}
|
|
176
|
+
const r = (0, approval_grants_1.resolveApproval)(root, envApproval, action, now);
|
|
177
|
+
return r ? { ...r, root } : undefined;
|
|
178
|
+
}
|
|
179
|
+
function refusalQueueHint(rootArg, store, req) {
|
|
180
|
+
try {
|
|
181
|
+
// The SpecStore interface carries no root; the file-backed store exposes one. A store without a
|
|
182
|
+
// root files nothing — the queue lives on disk, and with no disk location there is nothing to do.
|
|
183
|
+
const storeRoot = store?.specsRoot;
|
|
184
|
+
const base = rootArg !== undefined ? rootArg : (typeof storeRoot === 'string' ? storeRoot : undefined);
|
|
185
|
+
if (base === undefined)
|
|
186
|
+
return '';
|
|
187
|
+
return (0, approval_queue_1.queueHint)(projectRootOf(base), req);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return '';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
151
193
|
function projectRootOf(root) {
|
|
152
194
|
return (0, root_2.resolveProjectRoot)(root).root;
|
|
153
195
|
}
|
|
@@ -771,16 +813,22 @@ function makeRawHandlers(store) {
|
|
|
771
813
|
catch {
|
|
772
814
|
approval = undefined;
|
|
773
815
|
}
|
|
774
|
-
const
|
|
816
|
+
const retireResolved = resolveHandlerApproval(a.root, store, approval, { kind: 'spec-approve', target: a.id }, new Date().toISOString());
|
|
817
|
+
const covered = retireResolved !== undefined;
|
|
775
818
|
const sealed = typeof spec.frontmatter.approved_digest === 'string';
|
|
776
819
|
if (sealed && !covered) {
|
|
777
820
|
return {
|
|
778
821
|
ok: false,
|
|
779
822
|
reason: `${a.id}은(는) 봉인된 문서입니다 — 폐기는 이 행위를 덮는 유효한 대역외 HOLMES_APPROVAL 이 필요합니다.`
|
|
780
823
|
+ ' 코드 게이트를 막고 있는 approved T-SPEC 을 폐기하면 그 게이트가 열리므로, 폐기가 승인 우회 경로가 되지 않도록 fail-closed 로 막습니다.'
|
|
781
|
-
+ ' (범위를 쓰면 kind "spec-approve")'
|
|
824
|
+
+ ' (범위를 쓰면 kind "spec-approve")'
|
|
825
|
+
+ refusalQueueHint(a.root, store, { kind: 'spec-approve', target: a.id, why: '봉인된 스펙의 폐기' }),
|
|
782
826
|
};
|
|
783
827
|
}
|
|
828
|
+
// @implements A-SPEC-245 — a grant that authorized breaking a seal is spent by it.
|
|
829
|
+
if (sealed && retireResolved?.source === 'grant' && retireResolved.root && retireResolved.approval.nonce) {
|
|
830
|
+
(0, approval_grants_1.consumeGrantFile)(retireResolved.root, retireResolved.approval.nonce);
|
|
831
|
+
}
|
|
784
832
|
const dependents = all.filter((s) => s.id !== a.id && s.dependsOn.includes(a.id));
|
|
785
833
|
const blocking = dependents.filter((s) => s.status === 'approved').map((s) => s.id);
|
|
786
834
|
if (blocking.length > 0) {
|
|
@@ -835,8 +883,10 @@ function makeRawHandlers(store) {
|
|
|
835
883
|
// covers, not merely well-formed (round-3 escalation: a token scoped to review-resolve — or
|
|
836
884
|
// expired outright — completed a FULL SEAL here, so A-SPEC-133's narrowing was decorative at
|
|
837
885
|
// the most consequential consumer). Unscoped {actor,token,rationale} stays the session key.
|
|
838
|
-
|
|
839
|
-
|
|
886
|
+
const approveResolved = resolveHandlerApproval(a.root, store, approval, { kind: 'spec-approve', target: a.id }, new Date().toISOString());
|
|
887
|
+
if (approveResolved === undefined) {
|
|
888
|
+
return { ok: false, reason: 'spec_approve requires an out-of-band HOLMES_APPROVAL that COVERS this act — a request-payload approval is not a channel, and an expired or elsewhere-scoped token does not open this door (scoped approvals need kind "spec-approve"). (fail-closed)'
|
|
889
|
+
+ refusalQueueHint(a.root, store, { kind: 'spec-approve', target: a.id, why: '스펙 봉인 승인' }) };
|
|
840
890
|
}
|
|
841
891
|
// @implements A-SPEC-188 — destination BEFORE seal.
|
|
842
892
|
// The old order (seal at :481, resolve the ledger at :483) produced both measured harms: a
|
|
@@ -1025,12 +1075,12 @@ function makeRawHandlers(store) {
|
|
|
1025
1075
|
const chain = new ledger_store_1.FileLedgerStore(path.join(ledgerRoot, '.ax', 'ledger'));
|
|
1026
1076
|
chain.append({
|
|
1027
1077
|
ts: new Date().toISOString(),
|
|
1028
|
-
actor: approval.actor,
|
|
1078
|
+
actor: approveResolved.approval.actor,
|
|
1029
1079
|
kind: 'spec-approved',
|
|
1030
1080
|
summary: `approved ${a.id} sealing ${digest}`,
|
|
1031
1081
|
inputs: [a.id, digest],
|
|
1032
|
-
rationale: approval.rationale,
|
|
1033
|
-
authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
|
|
1082
|
+
rationale: approveResolved.approval.rationale,
|
|
1083
|
+
authorization: (0, provenance_chain_1.authorizationRef)(approveResolved.approval.actor, approveResolved.approval.token),
|
|
1034
1084
|
});
|
|
1035
1085
|
// @implements A-SPEC-135
|
|
1036
1086
|
// P4 routing signal: a re-approval whose content CHANGED (prior seal existed and differs) mints
|
|
@@ -1042,14 +1092,18 @@ function makeRawHandlers(store) {
|
|
|
1042
1092
|
try {
|
|
1043
1093
|
const files = (0, review_targets_1.anchoredForReview)(a.id, cachedScan(ledgerRoot, ledgerRoot), specs);
|
|
1044
1094
|
chain.append({
|
|
1045
|
-
ts: new Date().toISOString(), actor: approval.actor, kind: 'review-needed',
|
|
1095
|
+
ts: new Date().toISOString(), actor: approveResolved.approval.actor, kind: 'review-needed',
|
|
1046
1096
|
summary: `targeted review needed: ${a.id} content moved — re-examine ${files.length} anchored file(s) against the new spec content`,
|
|
1047
1097
|
inputs: [a.id, priorDigest, digest, ...files],
|
|
1048
|
-
rationale: approval.rationale, authorization: '',
|
|
1098
|
+
rationale: approveResolved.approval.rationale, authorization: '',
|
|
1049
1099
|
});
|
|
1050
1100
|
}
|
|
1051
1101
|
catch { /* a missed routing signal is not a broken seal */ }
|
|
1052
1102
|
}
|
|
1103
|
+
// @implements A-SPEC-245 — single-use: the grant is spent by the seal it authorized.
|
|
1104
|
+
if (approveResolved.source === 'grant' && approveResolved.root && approveResolved.approval.nonce) {
|
|
1105
|
+
(0, approval_grants_1.consumeGrantFile)(approveResolved.root, approveResolved.approval.nonce);
|
|
1106
|
+
}
|
|
1053
1107
|
return { approved: a.id, digest };
|
|
1054
1108
|
},
|
|
1055
1109
|
async spec_list(a) {
|
|
@@ -1602,8 +1656,13 @@ function makeRawHandlers(store) {
|
|
|
1602
1656
|
// not a master key — an EXPIRED or elsewhere-scoped approval must not lift a critical.
|
|
1603
1657
|
// An unscoped {actor,token,rationale} stays the operator's session key (unchanged).
|
|
1604
1658
|
const nowTs = new Date().toISOString();
|
|
1605
|
-
|
|
1606
|
-
|
|
1659
|
+
const rrResolved = resolveHandlerApproval(a.root, store, approval, { kind: 'review-resolve', target: f.id }, nowTs);
|
|
1660
|
+
if (rrResolved === undefined) {
|
|
1661
|
+
throw new HandlerRefusal(`review_record: id ${f.id} 의 열린 치명 발견을 해소하는 기록은 이 행위를 덮는 유효한 대역외 승인이 필요합니다 — 차단당한 쪽이 스스로 이빨을 뽑을 수 없어야 하고, 만료·다른 범위의 승인은 덮지 않습니다. HOLMES_APPROVAL='{"actor":"<you>","token":"<any>","rationale":"<why fixed>"}' (범위를 쓰면 kind "review-resolve") 를 서버 환경에 설정하고 다시 기록하십시오`
|
|
1662
|
+
+ refusalQueueHint(a.root, store, { kind: 'review-resolve', target: f.id, why: '열린 치명 발견의 해소 기록' }));
|
|
1663
|
+
}
|
|
1664
|
+
if (rrResolved.source === 'grant' && rrResolved.root && rrResolved.approval.nonce) {
|
|
1665
|
+
(0, approval_grants_1.consumeGrantFile)(rrResolved.root, rrResolved.approval.nonce);
|
|
1607
1666
|
}
|
|
1608
1667
|
// @implements A-SPEC-191 §11 — 한 배치가 같은 id 를 다시 열고 다시 닫으면 lift 는 두 번
|
|
1609
1668
|
// 일어난다. 소비는 호출당 1회이므로, 세 번의 호출이면 거부됐을 일이 한 배치에서는
|
|
@@ -68,4 +68,13 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
68
68
|
}
|
|
69
69
|
return { content: [{ type: 'text', text: JSON.stringify(out) }] };
|
|
70
70
|
});
|
|
71
|
+
// @implements A-SPEC-247 — surface the approval token's health once at startup, so an invalid
|
|
72
|
+
// token is known before an approval is attempted (Notion UX-4). STDERR only: stdout is the MCP
|
|
73
|
+
// protocol channel, and a single stray byte there breaks the handshake. This is a log, never a
|
|
74
|
+
// gate — the server starts with any token, or none.
|
|
75
|
+
try {
|
|
76
|
+
const { describeTokenHealth } = require('../hooks/stop');
|
|
77
|
+
process.stderr.write(`[Holmes-Kit] ${describeTokenHealth(process.env.HOLMES_APPROVAL)}\n`);
|
|
78
|
+
}
|
|
79
|
+
catch { /* a diagnostic must never keep the server from starting */ }
|
|
71
80
|
void server.connect(new stdio_js_1.StdioServerTransport());
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Install Guide — by account type and permissions
|
|
2
|
+
|
|
3
|
+
This guide exists because of a measured failure, not a hypothetical one: an adopter on Windows ran
|
|
4
|
+
`npm install -g` three times — once from an elevated PowerShell — and hit the same `EPERM` every
|
|
5
|
+
time, because their npm global prefix pointed inside `C:\Program Files\nodejs`. The local install
|
|
6
|
+
worked on the first try. Find your row, run its one command.
|
|
7
|
+
|
|
8
|
+
## Which situation are you in?
|
|
9
|
+
|
|
10
|
+
| Situation | Privileges needed | Command |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| **Using it in one project** (most people) | none | `npm install --save-dev @holmes-lab/holmes-kit` then `npx holmes-kit init` |
|
|
13
|
+
| Company-managed PC / restricted account | none | same as above — no system directory is touched |
|
|
14
|
+
| CI / container | none | same as above, plus `--prefer-online` right after a release |
|
|
15
|
+
| Want the CLI across many projects (`-g`) | depends on your prefix | **check first**: `npm config get prefix` ↓ |
|
|
16
|
+
|
|
17
|
+
The local install is the path verified end to end, on macOS and Windows, against the public
|
|
18
|
+
registry. The wiring `init` writes uses absolute paths, so nothing needs to be on `PATH`.
|
|
19
|
+
|
|
20
|
+
## Before `npm install -g`: check your prefix
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm config get prefix
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Result looks like | Verdict |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `%APPDATA%\npm`, `~/.npm-global`, `/opt/homebrew`, an nvm/fnm/volta directory | user-writable — `npm install -g @holmes-lab/holmes-kit` works as-is |
|
|
29
|
+
| `C:\Program Files\nodejs`, `/usr/local` | protected — `-g` dies with `EPERM` **before any package file arrives**. Move the prefix (below). **Do not elevate.** |
|
|
30
|
+
|
|
31
|
+
`npx holmes-kit doctor` (after a local install) runs this exact check for you — the
|
|
32
|
+
`global prefix` line names the directory and the remedy.
|
|
33
|
+
|
|
34
|
+
### Moving the prefix to user space — one-time setup
|
|
35
|
+
|
|
36
|
+
Windows (PowerShell):
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
npm config set prefix "$env:APPDATA\npm"
|
|
40
|
+
[Environment]::SetEnvironmentVariable('Path', "$([Environment]::GetEnvironmentVariable('Path','User'));$env:APPDATA\npm", 'User')
|
|
41
|
+
# open a NEW terminal, then:
|
|
42
|
+
npm install -g @holmes-lab/holmes-kit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
macOS / Linux:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm config set prefix "$HOME/.npm-global"
|
|
49
|
+
export PATH="$HOME/.npm-global/bin:$PATH" # add to your shell profile too
|
|
50
|
+
npm install -g @holmes-lab/holmes-kit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A Node version manager (`nvm`, `fnm`, `volta`) achieves the same by keeping the whole toolchain
|
|
54
|
+
under your home directory.
|
|
55
|
+
|
|
56
|
+
### Why elevation is the wrong fix
|
|
57
|
+
|
|
58
|
+
npm's own error text ends with *"try running the command again as root/Administrator."* Do not
|
|
59
|
+
follow it here, for two reasons:
|
|
60
|
+
|
|
61
|
+
1. **It may not even work.** The reported failure recurred from an elevated PowerShell — antivirus
|
|
62
|
+
and Windows Controlled Folder Access block protected-folder writes regardless of elevation.
|
|
63
|
+
2. **When it works, it is worse.** `better-sqlite3` declares
|
|
64
|
+
`install: prebuild-install || node-gyp rebuild` — under an elevated `-g`, that downloads and
|
|
65
|
+
executes, or invokes a compiler, **with system privileges**. Keeping installs in user space is
|
|
66
|
+
what contains a compromised dependency.
|
|
67
|
+
|
|
68
|
+
## Troubleshooting, by the error you actually see
|
|
69
|
+
|
|
70
|
+
### `EPERM … mkdir C:\Program Files\nodejs\node_modules\@holmes-lab`
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
npm error code EPERM
|
|
74
|
+
npm error syscall mkdir
|
|
75
|
+
npm error path C:\Program Files\nodejs\node_modules\@holmes-lab
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Your global prefix is a protected directory. The failure happens while npm creates the scope
|
|
79
|
+
folder — **before a single package file is transferred** — so no package version can fix it, and
|
|
80
|
+
neither can this one. Either drop `-g` (the local install needs none of this) or move the prefix
|
|
81
|
+
(one-time setup above).
|
|
82
|
+
|
|
83
|
+
### `notarget No matching version found`
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
npm error code ETARGET
|
|
87
|
+
npm error notarget No matching version found for @holmes-lab/holmes-kit@<version>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Your npm metadata cache predates the release — measured minutes after publishing 0.1.11, the
|
|
91
|
+
registry already listed the version while a default-cache install still refused it. Add
|
|
92
|
+
`--prefer-online`, or retry in a few minutes.
|
|
93
|
+
|
|
94
|
+
### `better-sqlite3` fails to build
|
|
95
|
+
|
|
96
|
+
The one dependency that may need a toolchain. The 8 tree-sitter grammars ship prebuilt binaries
|
|
97
|
+
(`darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`) and compile nothing; `better-sqlite3`
|
|
98
|
+
downloads a prebuild at install time and **falls back to compiling** when none matches your
|
|
99
|
+
platform and Node ABI. If it compiles, you need:
|
|
100
|
+
|
|
101
|
+
| Platform | Toolchain |
|
|
102
|
+
|---|---|
|
|
103
|
+
| Windows | Visual Studio Build Tools (C++ workload) |
|
|
104
|
+
| macOS | Xcode Command Line Tools (`xcode-select --install`) |
|
|
105
|
+
| Alpine | `apk add --no-cache python3 make g++` |
|
|
106
|
+
|
|
107
|
+
### `spawn sh ENOENT` during a git-URL install
|
|
108
|
+
|
|
109
|
+
`npm i -g git+ssh://…` is not a supported path: npm 11 clones the repository into its cache and
|
|
110
|
+
runs `prepare` there without installing dependencies, so the build tooling is missing. Install
|
|
111
|
+
from the registry or from a packed tarball.
|
|
112
|
+
|
|
113
|
+
## Verify — the last step of every path
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
npx holmes-kit doctor # local install
|
|
117
|
+
holmes-kit doctor # global install
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Expect `10 pass, 1 warn, 0 fail` on a healthy install. The lines that matter most:
|
|
121
|
+
|
|
122
|
+
- `global prefix` — whether `-g` would work on this machine, and the remedy if not
|
|
123
|
+
- `tree-sitter grammars` / `better-sqlite3` — whether the native modules actually load
|
|
124
|
+
|
|
125
|
+
## What we deliberately do NOT do
|
|
126
|
+
|
|
127
|
+
| Idea | Why not |
|
|
128
|
+
|---|---|
|
|
129
|
+
| A `postinstall` script that prints guidance | Triggers npm 11's `allow-scripts` warning and forfeits this package's current property of running no install scripts at all |
|
|
130
|
+
| Recommending `npx @holmes-lab/holmes-kit init` with no install | `init` writes wiring with absolute paths; under bare `npx` those point into the npx cache and break when it is pruned |
|
|
131
|
+
| Fixing your npm prefix from inside the package | A package rewriting your npm configuration is exactly the supply-chain behaviour this guide warns about |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.13",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"bin/",
|
|
14
14
|
"dist/",
|
|
15
15
|
"playbooks/",
|
|
16
|
+
"docs/install-guide.md",
|
|
16
17
|
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"engines": {
|