@holmes-lab/holmes-kit 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +5 -3
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.d.ts +14 -0
- package/dist/holmes/cli/approve.js +60 -3
- package/dist/holmes/cli/gitignore-merge.js +5 -0
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/governance/approval-queue.d.ts +33 -0
- package/dist/holmes/governance/approval-queue.js +94 -7
- package/dist/holmes/governance/autonomy.d.ts +1 -0
- package/dist/holmes/governance/autonomy.js +26 -1
- package/dist/holmes/guardrail/dependency-delta.d.ts +3 -0
- package/dist/holmes/guardrail/dependency-delta.js +118 -0
- package/dist/holmes/hooks/stop.d.ts +21 -0
- package/dist/holmes/hooks/stop.js +164 -2
- package/dist/holmes/mcp/handlers.js +19 -0
- package/dist/holmes/project/root.js +9 -1
- package/dist/holmes/review/test-outcomes.d.ts +17 -2
- package/dist/holmes/review/test-outcomes.js +54 -15
- package/dist/holmes/spec/approval-blockers.js +8 -0
- package/dist/holmes/spec/compat-impact.d.ts +26 -0
- package/dist/holmes/spec/compat-impact.js +140 -0
- package/dist/holmes/spec/spec-types.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CELL_VERDICTS = exports.OS_CELLS = exports.HARNESS_CELLS = void 0;
|
|
4
|
+
exports.checkCompatDeclared = checkCompatDeclared;
|
|
5
|
+
/**
|
|
6
|
+
* REQ-565's enforcement device: the obligation to declare HARNESS (claude/codex/agy) and OS
|
|
7
|
+
* (windows/mac/linux) impact lives on the ACT of approval — the exact shape ADR-013 gave
|
|
8
|
+
* `breaking_change`, and for the same measured reason (a static `requiredFields` predicate turned
|
|
9
|
+
* 38 already-approved specs into ART-3 violations and bricked the harness; act-time converges
|
|
10
|
+
* instead: any spec that changes re-approves and acquires the fields, a spec that never changes
|
|
11
|
+
* can introduce no new incompatibility).
|
|
12
|
+
*
|
|
13
|
+
* WHY a gate and not a habit, measured twice in one day: an observability design came out
|
|
14
|
+
* Claude-biased (Stop hook + transcript_path is a Claude-only channel — the owner caught it), and
|
|
15
|
+
* the Stop hook's own header records that voluntarily-invoked discipline fired 0/143 times. Memory
|
|
16
|
+
* is rationale storage; control is a gate.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS DOES NOT DO: verify the declarations are TRUE. Truth belongs to the layers that measure
|
|
19
|
+
* it — adapter parity (A-SPEC-336~338), doctor's wiring checks, on-device E2E. This gate makes
|
|
20
|
+
* skipping the thought impossible and makes a false declaration an auditable record.
|
|
21
|
+
*/
|
|
22
|
+
exports.HARNESS_CELLS = ['claude', 'codex', 'agy'];
|
|
23
|
+
exports.OS_CELLS = ['windows', 'mac', 'linux'];
|
|
24
|
+
exports.CELL_VERDICTS = ['supported', 'unavailable', 'n-a'];
|
|
25
|
+
/** Directory-or-file prefixes that ARE the harness surface. A trailing '/' is a directory boundary;
|
|
26
|
+
* without it the rule names a file stem (`cli/init` catches `cli/init.ts`), and the boundary test
|
|
27
|
+
* below keeps `hooks-util.ts` from matching `hooks/`. */
|
|
28
|
+
const HARNESS_SURFACES = [
|
|
29
|
+
'src/holmes/hooks/',
|
|
30
|
+
'src/holmes/cli/init',
|
|
31
|
+
'src/holmes/cli/agents',
|
|
32
|
+
'src/holmes/cli/interactive-prompt',
|
|
33
|
+
'src/holmes/mcp/server',
|
|
34
|
+
];
|
|
35
|
+
/** Conservative OS-sensitivity signals — content properties, not directories. Grown by measurement,
|
|
36
|
+
* never by guess (the goal records this as an explicitly open list). */
|
|
37
|
+
const OS_SIGNALS = ['process.platform', "'win32'", '"win32"', 'spawn(', 'spawnSync(', 'execFileSync(', '.ps1'];
|
|
38
|
+
const FORMAT_HINT = (field, cells) => `${field} 형식: 'none: <실이유>' 또는 3셀 매핑 { ${cells.map((c) => `${c}: '<supported|unavailable|n-a>: <근거>'`).join(', ')} }`;
|
|
39
|
+
/** A reason that says nothing is not a reason: empty, or still carrying the scaffold's TODO/TBD. */
|
|
40
|
+
const emptyReason = (reason) => reason.trim() === '' || /\bTODO\b|\bTBD\b/i.test(reason);
|
|
41
|
+
/**
|
|
42
|
+
* Validate one axis' declaration. Returns the refusal (naming the field) or the parsed shape:
|
|
43
|
+
* `none` (an irrelevance claim — cross-checkable) or `cells` (the axis was faced — exempt).
|
|
44
|
+
*/
|
|
45
|
+
function parseAxis(field, raw, cells) {
|
|
46
|
+
const err = (why) => ({ kind: 'error', message: `${field} ${why} — ${FORMAT_HINT(field, cells)}` });
|
|
47
|
+
if (raw === undefined || raw === null)
|
|
48
|
+
return err('선언이 없습니다 (REQ-565: 호환 고려는 봉인 의무)');
|
|
49
|
+
if (typeof raw === 'string') {
|
|
50
|
+
const i = raw.indexOf(':');
|
|
51
|
+
const grade = (i === -1 ? raw : raw.slice(0, i)).trim();
|
|
52
|
+
const reason = i === -1 ? '' : raw.slice(i + 1).trim();
|
|
53
|
+
if (grade !== 'none')
|
|
54
|
+
return err(`알 수 없는 문자열 선언 '${grade}'`);
|
|
55
|
+
if (emptyReason(reason))
|
|
56
|
+
return err('의 none 사유가 비었거나 placeholder(TODO/TBD)입니다');
|
|
57
|
+
return { kind: 'none' };
|
|
58
|
+
}
|
|
59
|
+
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
|
60
|
+
const m = raw;
|
|
61
|
+
const keys = Object.keys(m);
|
|
62
|
+
for (const c of cells)
|
|
63
|
+
if (!(c in m))
|
|
64
|
+
return err(`매핑에 '${c}' 셀이 없습니다 (3셀 전부 필수)`);
|
|
65
|
+
for (const k of keys)
|
|
66
|
+
if (!cells.includes(k))
|
|
67
|
+
return err(`매핑에 알 수 없는 셀 '${k}'`);
|
|
68
|
+
for (const c of cells) {
|
|
69
|
+
const v = m[c];
|
|
70
|
+
if (typeof v !== 'string')
|
|
71
|
+
return err(`의 '${c}' 셀이 문자열이 아닙니다`);
|
|
72
|
+
const i = v.indexOf(':');
|
|
73
|
+
const verdict = (i === -1 ? v : v.slice(0, i)).trim();
|
|
74
|
+
const reason = i === -1 ? '' : v.slice(i + 1).trim();
|
|
75
|
+
if (!exports.CELL_VERDICTS.includes(verdict)) {
|
|
76
|
+
return err(`의 '${c}' 셀 어휘 '${verdict}' 는 supported|unavailable|n-a 가 아닙니다`);
|
|
77
|
+
}
|
|
78
|
+
if (emptyReason(reason))
|
|
79
|
+
return err(`의 '${c}' 셀 근거가 비었거나 placeholder 입니다`);
|
|
80
|
+
}
|
|
81
|
+
return { kind: 'cells' };
|
|
82
|
+
}
|
|
83
|
+
return err('의 형태가 문자열도 매핑도 아닙니다');
|
|
84
|
+
}
|
|
85
|
+
/** `src/…` path tokens out of the Files to Touch section — backticks, bullets and commas tolerated.
|
|
86
|
+
* Backslashes normalize to `/` FIRST (adversarial round-1): a Windows author legitimately writes
|
|
87
|
+
* `src\holmes\hooks\stop.ts`, and un-normalized it walked straight past the surface prefixes —
|
|
88
|
+
* an OS-compat gate defeated by an OS path convention would be its own counterexample. */
|
|
89
|
+
function filesToTouch(spec) {
|
|
90
|
+
const body = (spec.sections?.['Files to Touch'] ?? '')
|
|
91
|
+
.replace(/\\/g, '/')
|
|
92
|
+
.replace(/\/{2,}/g, '/'); // round-2: `src\\holmes` normalized to `src//holmes` and slid past the prefix
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const m of body.matchAll(/src\/[\w./-]+/g))
|
|
95
|
+
out.push(m[0]);
|
|
96
|
+
return [...new Set(out)];
|
|
97
|
+
}
|
|
98
|
+
function checkCompatDeclared(spec, opts) {
|
|
99
|
+
if (spec.type !== 'A-SPEC')
|
|
100
|
+
return null;
|
|
101
|
+
const fm = (spec.frontmatter ?? {});
|
|
102
|
+
const harness = parseAxis('harness_impact', fm.harness_impact, exports.HARNESS_CELLS);
|
|
103
|
+
if (harness.kind === 'error')
|
|
104
|
+
return harness.message;
|
|
105
|
+
const os = parseAxis('os_impact', fm.os_impact, exports.OS_CELLS);
|
|
106
|
+
if (os.kind === 'error')
|
|
107
|
+
return os.message;
|
|
108
|
+
// Cross-checks apply ONLY to `none` — a 3-cell mapping already faced the axis, and re-litigating
|
|
109
|
+
// it here would punish exactly the declaration this gate exists to elicit (C1's lesson is that a
|
|
110
|
+
// DECLARATION must not be the only wall; an irrelevance CLAIM is what the machine can contradict).
|
|
111
|
+
const ftt = filesToTouch(spec);
|
|
112
|
+
if (harness.kind === 'none') {
|
|
113
|
+
for (const p of ftt) {
|
|
114
|
+
const hit = HARNESS_SURFACES.find((s) => (s.endsWith('/') ? p.startsWith(s) : p === s || p.startsWith(`${s}.`) || p.startsWith(`${s}/`)));
|
|
115
|
+
if (hit) {
|
|
116
|
+
return `harness_impact 는 none 인데 Files to Touch 의 '${p}' 는 하네스 표면(${hit})입니다 — 모순. 3셀 매핑으로 각 하네스의 영향을 기술하십시오.`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (os.kind === 'none' && opts?.readFile) {
|
|
121
|
+
for (const p of ftt) {
|
|
122
|
+
// round-2: an injected reader that THROWS (permissions, FIFO, anything) must degrade to
|
|
123
|
+
// "unreadable = skip", never crash the approval act — this check is a gate, not a hostage.
|
|
124
|
+
let text;
|
|
125
|
+
try {
|
|
126
|
+
text = opts.readFile(p);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
text = null;
|
|
130
|
+
}
|
|
131
|
+
if (text === null || text === undefined)
|
|
132
|
+
continue; // a file that does not exist yet has no character
|
|
133
|
+
const sig = OS_SIGNALS.find((s) => text.includes(s));
|
|
134
|
+
if (sig) {
|
|
135
|
+
return `os_impact 는 none 인데 '${p}' 의 내용이 OS 신호 '${sig}' 를 담고 있습니다 — 모순. 3셀 매핑으로 각 OS 의 영향을 기술하십시오.`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
@@ -76,7 +76,9 @@ exports.SPEC_TYPES = {
|
|
|
76
76
|
// blocked every turn, because all 38 governed A-SPECs are already approved. The duty belongs to
|
|
77
77
|
// the ACT of approval (see spec/breaking-change.ts); this line exists so the field is visible
|
|
78
78
|
// where someone looks up "what fields does an A-SPEC have", instead of hiding in a check.
|
|
79
|
-
|
|
79
|
+
// @implements A-SPEC-565.1 — same shape, same reason: the compat duty (REQ-565) lives on the
|
|
80
|
+
// act of approval (spec/compat-impact.ts), and these lines exist for the reader, not the check.
|
|
81
|
+
stubOnlyFields: ['breaking_change', 'harness_impact', 'os_impact'],
|
|
80
82
|
requiredSections: ['Objective', 'Inputs / Outputs', 'Behavior', 'Test Points', 'Files to Touch', 'Done When'],
|
|
81
83
|
},
|
|
82
84
|
'C-SPEC': {
|
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.
|
|
4
|
+
"version": "0.15.0",
|
|
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",
|