@holmes-lab/holmes-kit 0.1.12 → 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 +36 -0
- 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/package.json +1 -1
|
@@ -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());
|
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",
|