@holmes-lab/holmes-kit 0.9.0 → 0.10.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 +46 -1
- package/README.md +2 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +17 -16
- package/dist/holmes/cli/doctor.js +7 -7
- package/dist/holmes/governance/ledger-timeline.d.ts +21 -0
- package/dist/holmes/governance/ledger-timeline.js +21 -0
- package/dist/holmes/mcp/elicit-approval.d.ts +9 -0
- package/dist/holmes/mcp/elicit-approval.js +15 -0
- package/dist/holmes/mcp/handlers.d.ts +89 -0
- package/dist/holmes/mcp/handlers.js +153 -0
- package/dist/holmes/mcp/tool-schemas.js +32 -0
- package/dist/holmes/spec/approval-status.d.ts +29 -0
- package/dist/holmes/spec/approval-status.js +33 -0
- package/dist/holmes/spec/version-conflict.d.ts +21 -0
- package/dist/holmes/spec/version-conflict.js +21 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,52 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.10.0] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
Governance UX tools for multi-agent round-trips and observability, a cross-harness approval fix, and
|
|
11
|
+
an Antigravity portability fix. All additive and backward-compatible; existing behaviour is unchanged
|
|
12
|
+
at the default settings.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Governance UX tools** (REQ-538): three new read/act MCP tools plus a conflict-report enrichment.
|
|
17
|
+
- **`spec_unseal`** — the inverse of `spec_approve`: return a sealed (approved) spec to an editable
|
|
18
|
+
`draft` in one act, clearing `approved_digest` and `parent_digests`, and record `spec-unsealed`
|
|
19
|
+
in the provenance ledger. Un-sealing withdraws a seal, so it requires the same out-of-band
|
|
20
|
+
`HOLMES_APPROVAL` as retiring a sealed doc (fail-closed), and it refuses when an **approved** spec
|
|
21
|
+
depends on the target (naming the blockers — their `parent_digests` would go stale). Idempotent on
|
|
22
|
+
an already-draft spec. Distinct from `spec_retire` (which withdraws authority to `outdated`);
|
|
23
|
+
un-seal keeps the spec alive and editable.
|
|
24
|
+
- **`approval_status`** *(read-only)* — report a spec's approval/seal state without parsing files:
|
|
25
|
+
`{ sealed, approvedDigest?, status, parents: [{ id, status, sealed, resolved }], blockers }`.
|
|
26
|
+
`sealed` is the same predicate the code gate reads and `blockers` is the exact list `spec_approve`
|
|
27
|
+
would refuse on, so the report cannot drift from the acts it describes.
|
|
28
|
+
- **`ledger_timeline`** *(read-only)* — return the provenance ledger's events in time order
|
|
29
|
+
(`{ ts, kind, actor, summary, inputs }`), optionally narrowed to one spec, so the governance
|
|
30
|
+
history (approved / unsealed / retired / review-needed …) is legible at a glance. Chain-integrity
|
|
31
|
+
fields are projected away.
|
|
32
|
+
- **`spec_approve` conflict detail** — the optimistic-concurrency refusal now also carries a
|
|
33
|
+
structured `conflict: { id, expectedVersion, currentVersion, retry }` (the version this act read,
|
|
34
|
+
the version now on disk, and what to retry). The refusal itself — refuse, write nothing, the edit
|
|
35
|
+
wins — is unchanged; this only exposes what happened.
|
|
36
|
+
- **Cross-harness approval opt-out** (REQ-540): a new server-env switch **`HOLMES_ELICIT=off`** makes
|
|
37
|
+
the server skip the in-client `elicitInput` prompt entirely and fold to the byte-identical
|
|
38
|
+
refuse+enqueue path — the same path clients that do not advertise elicitation already take. With it
|
|
39
|
+
on, **claude/agy/codex converge on one out-of-band decision surface** (`holmes-kit approve`, whose
|
|
40
|
+
default readline input is robust over SSH) instead of each client's own prompt rendering. Env-only
|
|
41
|
+
(a session cannot set it); unset = byte-identical to before. Not an approval bypass — it routes the
|
|
42
|
+
prompt to the queue; approval still comes only from a grant or `HOLMES_APPROVAL`.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Antigravity hooks survive a package path with spaces** (REQ-539): the `.agents/hooks.json`
|
|
47
|
+
wiring now emits `command: "node"` + `args: ["<abs path>"]` instead of a single
|
|
48
|
+
`node "<quoted path>"` string. agy's hook launcher tokenizes the command on whitespace **without
|
|
49
|
+
honoring quotes**, so a Windows default path (`C:\Program Files\nodejs\…`) split at the space and
|
|
50
|
+
the PreToolUse hook never loaded (the gate silently off). Passing the path as an `args` element —
|
|
51
|
+
the array form `mcp_config.json` already used — sidesteps tokenization on every platform. `doctor`'s
|
|
52
|
+
Antigravity wiring check reads `args[0]` too (older string wirings still handled). *Real Windows/agy
|
|
53
|
+
execution remains unverified in CI; validated by the emitted JSON shape on the dev environment.*
|
|
9
54
|
|
|
10
55
|
Internalized TDD discipline — RED-first evidence enforced by a new constitution article (ART-8),
|
|
11
56
|
not a prompt — plus a doctor transparency check and a BUG-1 authoring fix. All additive and
|
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
- 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` code anchors (comma-lists and every anchor in a file participate in the gate).
|
|
20
20
|
- 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion → green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
|
|
21
|
-
-
|
|
21
|
+
- 🧰 **Governance UX tools** *(new in 0.10.0)*: `spec_unseal` (the inverse of `spec_approve` — return a sealed spec to editable `draft` in one act, out-of-band approval required, refuses approved dependents), `approval_status` and `ledger_timeline` (read-only observability into a spec's seal state and the governance history), and a structured `conflict` on `spec_approve`'s optimistic-concurrency refusal (read vs. current version + retry). See CHANGELOG for details.
|
|
22
|
+
- 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) — while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before. *(new in 0.10.0)* Set **`HOLMES_ELICIT=off`** to skip the in-session prompt entirely and route every decision to the out-of-band `holmes-kit approve` queue instead — one decision surface that behaves identically across Claude Code / Antigravity / Codex.
|
|
22
23
|
- 🪧 **Session Banner + Update Notice** *(new in 0.8.0)*: every session start emits an English intro (version + governance rule + npm URL) to both the human transcript and the agent context (SessionStart hook + MCP `instructions`); when a newer published version is cached, an install-mode-aware update command is appended. Registry check is detached, fail-silent, and opts out via `HOLMES_NO_UPDATE_CHECK`/`CI`.
|
|
23
24
|
- 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. Every gate change ships with two consecutive clean adversarial rounds.
|
|
24
25
|
- 🧠 **3-Tier Semantic Layer** *(new in 0.3.0)*: knowledge-graph semantic search with an explicit consent ladder — `none` (default, **zero egress**), `local` (bge-m3, no egress, optional module), `cloud` (gemini-embedding-001, opt-in via `GEMINI_API_KEY`). Measured on 305 traceability cases: recall 0.486 (lexical) → 0.667 (local) → **0.887 (cloud)**; on lexical-zero requests: 0% → 52% → **92%**. Surfaced only additively — rerank, evidence (`semCos`), and `semanticAlternates` — never as a hard filter.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0d70dec-mtn1jk14
|
|
@@ -107,12 +107,21 @@ const mcpConfig = (packageRoot, specsDir, launcher) => {
|
|
|
107
107
|
* (Claude 배선이 `guardrail` 모드에서 좁은 매처를 쓰는 것과 다른 선택인데, 그쪽은 그 좁힘이
|
|
108
108
|
* 무엇을 뜻하는지 문서화된 모드 선택이고 여기는 기본 배선이기 때문이다.)
|
|
109
109
|
*/
|
|
110
|
-
// A
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
110
|
+
// @implements A-SPEC-539.1
|
|
111
|
+
// agy's hook launcher tokenizes the `command` string on whitespace and does NOT honor quotes, so an
|
|
112
|
+
// earlier quoted `node "<path>"` (A-SPEC-193 §2b-2) still broke on a path with spaces: measured on a
|
|
113
|
+
// Windows machine whose Node lives at `C:\Program Files\nodejs`, reported from agy 2026-09-04 — the
|
|
114
|
+
// launcher split `node "C:\Program Files\...` at the space, node took `"C:\Program` for a module
|
|
115
|
+
// (MODULE_NOT_FOUND), and the gate loaded not at all (silently off). Passing the script path as an
|
|
116
|
+
// `args` ELEMENT — the same array form mcp_config already uses — sidesteps tokenization entirely, on
|
|
117
|
+
// every platform. This supersedes the quoted-string form for the antigravity wiring; quoting only
|
|
118
|
+
// ever helped launchers that honor quotes, which agy's does not.
|
|
119
|
+
const antigravityHookEntry = (packageRoot, script) => ({
|
|
120
|
+
type: 'command',
|
|
121
|
+
command: 'node',
|
|
122
|
+
args: [path.join(packageRoot, 'bin', script)],
|
|
123
|
+
timeout: 30,
|
|
124
|
+
});
|
|
116
125
|
/**
|
|
117
126
|
* @implements A-SPEC-442
|
|
118
127
|
* Codex 훅이 부를 것: **Claude 와 같은 정책 커널**.
|
|
@@ -128,17 +137,9 @@ const hooksJson = (packageRoot) => `${JSON.stringify({
|
|
|
128
137
|
'holmes-kit': {
|
|
129
138
|
PreToolUse: [{
|
|
130
139
|
matcher: '*',
|
|
131
|
-
hooks: [
|
|
132
|
-
type: 'command',
|
|
133
|
-
command: hookCommand(packageRoot, 'holmes-hook-antigravity.js'),
|
|
134
|
-
timeout: 30,
|
|
135
|
-
}],
|
|
136
|
-
}],
|
|
137
|
-
Stop: [{
|
|
138
|
-
type: 'command',
|
|
139
|
-
command: hookCommand(packageRoot, 'holmes-stop-antigravity.js'),
|
|
140
|
-
timeout: 30,
|
|
140
|
+
hooks: [antigravityHookEntry(packageRoot, 'holmes-hook-antigravity.js')],
|
|
141
141
|
}],
|
|
142
|
+
Stop: [antigravityHookEntry(packageRoot, 'holmes-stop-antigravity.js')],
|
|
142
143
|
},
|
|
143
144
|
}, null, 2)}\n`;
|
|
144
145
|
const AGENTS_MD = (enforced) => `# Holmes-Kit — Workspace Operational Discipline
|
|
@@ -810,17 +810,17 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
810
810
|
const h = JSON.parse(fs.readFileSync(agyHooks, 'utf8'));
|
|
811
811
|
const entry = h['holmes-kit'];
|
|
812
812
|
const pre = entry?.PreToolUse?.[0];
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
813
|
+
// @implements A-SPEC-539.1 — the hook script path now lives in args[0] (command is bare
|
|
814
|
+
// 'node'); older wirings joined it into the command string. Read args first, fall back to
|
|
815
|
+
// parsing the string so a pre-539 hooks.json is still judged correctly.
|
|
816
|
+
const scriptOf = (e) => (Array.isArray(e?.args) && typeof e?.args[0] === 'string') ? e.args[0] : (0, settings_merge_1.hookScriptPath)(e?.command ?? '');
|
|
817
|
+
const stale = [scriptOf(pre?.hooks?.[0]), scriptOf(entry?.Stop?.[0])]
|
|
818
|
+
.filter((p2) => !resolvesToPackage(p2, packageRoot));
|
|
819
819
|
if (entry === undefined) {
|
|
820
820
|
add('antigravity wiring', 'WARN', `${agyHooks} 에 holmes-kit 항목이 없습니다`, 'holmes-kit init --target <dir> --agent antigravity');
|
|
821
821
|
}
|
|
822
822
|
else if (stale.length > 0) {
|
|
823
|
-
add('antigravity wiring', 'FAIL', `이 설치본을 가리키지 않는
|
|
823
|
+
add('antigravity wiring', 'FAIL', `이 설치본을 가리키지 않는 훅 경로: ${stale.join(' | ')}`, 'holmes-kit init --target <dir> --agent antigravity --force 로 절대 경로를 갱신하십시오.');
|
|
824
824
|
}
|
|
825
825
|
else if ((pre?.matcher ?? '') !== '*') {
|
|
826
826
|
// 매처가 좁으면 그 밖의 도구가 게이트를 지나간다 — 조용한 구멍이므로 말한다.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProvenanceEvent } from './provenance-chain';
|
|
2
|
+
/** One governance event, projected to what a timeline shows — chain-integrity fields dropped. */
|
|
3
|
+
export interface TimelineEntry {
|
|
4
|
+
ts: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
actor: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
inputs: string[];
|
|
9
|
+
}
|
|
10
|
+
/** The subset of a ledger event this projection reads (structurally typed so callers can pass
|
|
11
|
+
* FileLedgerStore.loadAll() results directly). */
|
|
12
|
+
type TimelineSource = Pick<ProvenanceEvent, 'ts' | 'kind' | 'actor' | 'summary'> & Partial<Pick<ProvenanceEvent, 'inputs' | 'seq'>>;
|
|
13
|
+
/**
|
|
14
|
+
* @implements A-SPEC-538.3
|
|
15
|
+
* Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
|
|
16
|
+
* (ISO strings sort lexically), tiebroken by `seq` so same-timestamp events keep chain order. When
|
|
17
|
+
* `id` is given, only events whose `inputs` reference it survive. Chain-integrity fields
|
|
18
|
+
* (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
|
|
19
|
+
*/
|
|
20
|
+
export declare function timelineFrom(events: TimelineSource[], id?: string): TimelineEntry[];
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.timelineFrom = timelineFrom;
|
|
4
|
+
/**
|
|
5
|
+
* @implements A-SPEC-538.3
|
|
6
|
+
* Pure: order ledger events into a timeline, optionally narrowed to one spec. Ascending by `ts`
|
|
7
|
+
* (ISO strings sort lexically), tiebroken by `seq` so same-timestamp events keep chain order. When
|
|
8
|
+
* `id` is given, only events whose `inputs` reference it survive. Chain-integrity fields
|
|
9
|
+
* (hash/prevHash/seq/replicaId) are plumbing, not timeline content, and are projected away.
|
|
10
|
+
*/
|
|
11
|
+
function timelineFrom(events, id) {
|
|
12
|
+
const selected = id ? events.filter((e) => (e.inputs ?? []).includes(id)) : events.slice();
|
|
13
|
+
selected.sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : (a.seq ?? 0) - (b.seq ?? 0)));
|
|
14
|
+
return selected.map((e) => ({
|
|
15
|
+
ts: e.ts,
|
|
16
|
+
kind: e.kind,
|
|
17
|
+
actor: e.actor,
|
|
18
|
+
summary: e.summary,
|
|
19
|
+
inputs: e.inputs ?? [],
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -20,6 +20,15 @@
|
|
|
20
20
|
*/
|
|
21
21
|
/** Approval kinds that may ask in-session. Widening this set is a spec revision, not a drive-by. */
|
|
22
22
|
export declare const ELICITABLE_KINDS: ReadonlySet<string>;
|
|
23
|
+
/**
|
|
24
|
+
* @implements A-SPEC-540.1
|
|
25
|
+
* True when the operator has opted OUT of in-client elicitation via `HOLMES_ELICIT`. Then the server
|
|
26
|
+
* folds every elicitation to `silent` (refuse+enqueue) — the same path a client that never advertised
|
|
27
|
+
* the capability takes — so claude/agy/codex converge on ONE out-of-band decision surface
|
|
28
|
+
* (`holmes-kit approve`) instead of depending on each client's prompt rendering. Unset, or any
|
|
29
|
+
* non-disabling value, keeps elicitation ON — byte-identical to before.
|
|
30
|
+
*/
|
|
31
|
+
export declare function elicitationDisabled(env: NodeJS.ProcessEnv): boolean;
|
|
23
32
|
export interface ElicitApprovalRequest {
|
|
24
33
|
kind: string;
|
|
25
34
|
target: string;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @implements A-SPEC-263.1
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.MAX_PRESENTATIONS = exports.ELICIT_TIMEOUT_MS = exports.ELICITABLE_KINDS = void 0;
|
|
5
|
+
exports.elicitationDisabled = elicitationDisabled;
|
|
5
6
|
exports.elicitTimeoutMsFor = elicitTimeoutMsFor;
|
|
6
7
|
exports.classifyElicitError = classifyElicitError;
|
|
7
8
|
exports.expiredNotice = expiredNotice;
|
|
@@ -31,6 +32,20 @@ exports.interpretElicitResult = interpretElicitResult;
|
|
|
31
32
|
*/
|
|
32
33
|
/** Approval kinds that may ask in-session. Widening this set is a spec revision, not a drive-by. */
|
|
33
34
|
exports.ELICITABLE_KINDS = new Set(['spec-approve', 'review-resolve']);
|
|
35
|
+
/** The `HOLMES_ELICIT` values that turn the in-client prompt OFF. */
|
|
36
|
+
const ELICIT_DISABLED_TOKENS = new Set(['off', '0', 'false', 'no', 'disabled']);
|
|
37
|
+
/**
|
|
38
|
+
* @implements A-SPEC-540.1
|
|
39
|
+
* True when the operator has opted OUT of in-client elicitation via `HOLMES_ELICIT`. Then the server
|
|
40
|
+
* folds every elicitation to `silent` (refuse+enqueue) — the same path a client that never advertised
|
|
41
|
+
* the capability takes — so claude/agy/codex converge on ONE out-of-band decision surface
|
|
42
|
+
* (`holmes-kit approve`) instead of depending on each client's prompt rendering. Unset, or any
|
|
43
|
+
* non-disabling value, keeps elicitation ON — byte-identical to before.
|
|
44
|
+
*/
|
|
45
|
+
function elicitationDisabled(env) {
|
|
46
|
+
const v = env.HOLMES_ELICIT;
|
|
47
|
+
return typeof v === 'string' && ELICIT_DISABLED_TOKENS.has(v.trim().toLowerCase());
|
|
48
|
+
}
|
|
34
49
|
/**
|
|
35
50
|
* The lock-holding path's timeout truth (and the stage-2 reason dialog's): review-resolve holds
|
|
36
51
|
* the findings-ledger lock while the dialog waits, so its ceiling never grows (REQ-497 기아 방지).
|
|
@@ -158,6 +158,85 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
158
158
|
dependents: string[];
|
|
159
159
|
reason?: undefined;
|
|
160
160
|
}>;
|
|
161
|
+
/**
|
|
162
|
+
* @implements A-SPEC-538.1
|
|
163
|
+
* The inverse of spec_approve: returns a SEALED (approved) spec to an editable `draft`, clearing
|
|
164
|
+
* `approved_digest` and `parent_digests` in ONE act, and records `spec-unsealed`. Hand-editing
|
|
165
|
+
* only `status` leaves seal residue that later trips validation; this atomizes the reverse.
|
|
166
|
+
*
|
|
167
|
+
* Un-sealing WITHDRAWS a seal, so — like retiring a sealed document — it demands a covering
|
|
168
|
+
* out-of-band HOLMES_APPROVAL (fail-closed): un-sealing an approved T-SPEC removes the code
|
|
169
|
+
* gate's demand, so an unguarded un-seal would be an approval bypass. And it refuses when an
|
|
170
|
+
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
171
|
+
* drift detection compares against — would silently go stale.
|
|
172
|
+
*/
|
|
173
|
+
spec_unseal(a: {
|
|
174
|
+
root?: string;
|
|
175
|
+
id: string;
|
|
176
|
+
}): Promise<{
|
|
177
|
+
ok: boolean;
|
|
178
|
+
reason: string;
|
|
179
|
+
unsealed?: undefined;
|
|
180
|
+
id?: undefined;
|
|
181
|
+
dependents?: undefined;
|
|
182
|
+
} | {
|
|
183
|
+
ok: boolean;
|
|
184
|
+
unsealed: boolean;
|
|
185
|
+
id: string;
|
|
186
|
+
dependents: never[];
|
|
187
|
+
reason: string;
|
|
188
|
+
} | {
|
|
189
|
+
ok: boolean;
|
|
190
|
+
unsealed: boolean;
|
|
191
|
+
id: string;
|
|
192
|
+
dependents: string[];
|
|
193
|
+
reason?: undefined;
|
|
194
|
+
}>;
|
|
195
|
+
/**
|
|
196
|
+
* @implements A-SPEC-538.2
|
|
197
|
+
* Read-only: report a spec's approval/seal state — sealed?, approved_digest, each parent's seal
|
|
198
|
+
* state, and the concrete blockers still standing between it and approval — so a caller need not
|
|
199
|
+
* parse files to ask "what is the approval state right now". Reuses `sealOf` and `approvalBlockers`
|
|
200
|
+
* (the same predicates the code gate and spec_approve read) so the report cannot drift from the
|
|
201
|
+
* acts it describes. No writes, no ledger append.
|
|
202
|
+
*/
|
|
203
|
+
approval_status(a: {
|
|
204
|
+
root?: string;
|
|
205
|
+
id: string;
|
|
206
|
+
}): Promise<{
|
|
207
|
+
ok: boolean;
|
|
208
|
+
reason: string;
|
|
209
|
+
} | {
|
|
210
|
+
id: string;
|
|
211
|
+
type?: string;
|
|
212
|
+
status: string;
|
|
213
|
+
sealed: boolean;
|
|
214
|
+
approvedDigest?: string;
|
|
215
|
+
parents: import("../spec/approval-status").ParentApproval[];
|
|
216
|
+
blockers: string[];
|
|
217
|
+
ok: boolean;
|
|
218
|
+
reason?: undefined;
|
|
219
|
+
}>;
|
|
220
|
+
/**
|
|
221
|
+
* @implements A-SPEC-538.3
|
|
222
|
+
* Read-only: return the provenance ledger's events in time order (optionally narrowed to one
|
|
223
|
+
* spec) so the governance history — approved / unsealed / retired / review-needed … — is legible
|
|
224
|
+
* at a glance without reading raw JSONL. Reuses FileLedgerStore.loadAll(); the ordering/filtering/
|
|
225
|
+
* projection is the pure `timelineFrom`. No writes. An absent ledger is an empty history, not an
|
|
226
|
+
* error.
|
|
227
|
+
*/
|
|
228
|
+
ledger_timeline(a: {
|
|
229
|
+
root?: string;
|
|
230
|
+
id?: string;
|
|
231
|
+
}): Promise<{
|
|
232
|
+
ok: boolean;
|
|
233
|
+
reason: string;
|
|
234
|
+
events?: undefined;
|
|
235
|
+
} | {
|
|
236
|
+
ok: boolean;
|
|
237
|
+
events: import("../governance/ledger-timeline").TimelineEntry[];
|
|
238
|
+
reason?: undefined;
|
|
239
|
+
}>;
|
|
161
240
|
spec_approve(a: {
|
|
162
241
|
root?: string;
|
|
163
242
|
id: string;
|
|
@@ -165,12 +244,21 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
165
244
|
ok: boolean;
|
|
166
245
|
reason: string;
|
|
167
246
|
findings?: undefined;
|
|
247
|
+
conflict?: undefined;
|
|
168
248
|
approved?: undefined;
|
|
169
249
|
digest?: undefined;
|
|
170
250
|
} | {
|
|
171
251
|
ok: boolean;
|
|
172
252
|
reason: string;
|
|
173
253
|
findings: import("../spec/validator").Finding[];
|
|
254
|
+
conflict?: undefined;
|
|
255
|
+
approved?: undefined;
|
|
256
|
+
digest?: undefined;
|
|
257
|
+
} | {
|
|
258
|
+
ok: boolean;
|
|
259
|
+
reason: string;
|
|
260
|
+
conflict: import("../spec/version-conflict").ConflictDetail;
|
|
261
|
+
findings?: undefined;
|
|
174
262
|
approved?: undefined;
|
|
175
263
|
digest?: undefined;
|
|
176
264
|
} | {
|
|
@@ -179,6 +267,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
179
267
|
ok?: undefined;
|
|
180
268
|
reason?: undefined;
|
|
181
269
|
findings?: undefined;
|
|
270
|
+
conflict?: undefined;
|
|
182
271
|
}>;
|
|
183
272
|
spec_list(a: any): Promise<{
|
|
184
273
|
specs: {
|
|
@@ -189,6 +189,9 @@ const spec_digest_1 = require("../spec/spec-digest");
|
|
|
189
189
|
const spec_store_2 = require("../spec/spec-store");
|
|
190
190
|
const breaking_change_1 = require("../spec/breaking-change");
|
|
191
191
|
const approval_blockers_1 = require("../spec/approval-blockers");
|
|
192
|
+
const approval_status_1 = require("../spec/approval-status");
|
|
193
|
+
const ledger_timeline_1 = require("../governance/ledger-timeline");
|
|
194
|
+
const version_conflict_1 = require("../spec/version-conflict");
|
|
192
195
|
const ledger_store_1 = require("../governance/ledger-store");
|
|
193
196
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
194
197
|
const ledger_lock_1 = require("../governance/ledger-lock");
|
|
@@ -703,6 +706,11 @@ function makeRawHandlers(store, opts) {
|
|
|
703
706
|
// stays byte-identical to the pre-elicitation one — nothing is ever worse than before the
|
|
704
707
|
// channel existed.
|
|
705
708
|
const tryElicit = async (kind, target, summary) => {
|
|
709
|
+
// @implements A-SPEC-540.1 — operator opt-out: with HOLMES_ELICIT=off the server never shows the
|
|
710
|
+
// in-client prompt, folding to the byte-identical `silent` (refuse+enqueue) path so claude/agy/
|
|
711
|
+
// codex all converge on one out-of-band surface (holmes-kit approve). Unset ⇒ unchanged.
|
|
712
|
+
if ((0, elicit_approval_1.elicitationDisabled)(process.env))
|
|
713
|
+
return { kind: 'silent' };
|
|
706
714
|
if (!opts?.elicit || !elicit_approval_1.ELICITABLE_KINDS.has(kind))
|
|
707
715
|
return { kind: 'silent' };
|
|
708
716
|
try {
|
|
@@ -1174,6 +1182,146 @@ function makeRawHandlers(store, opts) {
|
|
|
1174
1182
|
});
|
|
1175
1183
|
return { ok: true, retired: true, id: a.id, dependents: dependents.map((s) => s.id) };
|
|
1176
1184
|
},
|
|
1185
|
+
/**
|
|
1186
|
+
* @implements A-SPEC-538.1
|
|
1187
|
+
* The inverse of spec_approve: returns a SEALED (approved) spec to an editable `draft`, clearing
|
|
1188
|
+
* `approved_digest` and `parent_digests` in ONE act, and records `spec-unsealed`. Hand-editing
|
|
1189
|
+
* only `status` leaves seal residue that later trips validation; this atomizes the reverse.
|
|
1190
|
+
*
|
|
1191
|
+
* Un-sealing WITHDRAWS a seal, so — like retiring a sealed document — it demands a covering
|
|
1192
|
+
* out-of-band HOLMES_APPROVAL (fail-closed): un-sealing an approved T-SPEC removes the code
|
|
1193
|
+
* gate's demand, so an unguarded un-seal would be an approval bypass. And it refuses when an
|
|
1194
|
+
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
1195
|
+
* drift detection compares against — would silently go stale.
|
|
1196
|
+
*/
|
|
1197
|
+
async spec_unseal(a) {
|
|
1198
|
+
const all = await store.list();
|
|
1199
|
+
if (all.filter((s) => s.id === a.id).length > 1) {
|
|
1200
|
+
return {
|
|
1201
|
+
ok: false,
|
|
1202
|
+
reason: `${a.id}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본이 진본인지 도구가 고를 수 없어 봉인 해제 전에 거부합니다.`,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
const cur = await store.read(a.id);
|
|
1206
|
+
if (!cur) {
|
|
1207
|
+
const dir = store.specsRoot;
|
|
1208
|
+
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
1209
|
+
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
1210
|
+
}
|
|
1211
|
+
const spec = cur.spec;
|
|
1212
|
+
if (!spec.type) {
|
|
1213
|
+
return {
|
|
1214
|
+
ok: false,
|
|
1215
|
+
reason: `${a.id}에는 \`type:\` 선언이 없습니다(구형식 문서) — 어느 폴더에 속하는지 알 수 없어 봉인 해제할 수 없습니다.`
|
|
1216
|
+
+ ` 먼저 spec_upgrade({ id: "${a.id}" })로 형식을 올린 뒤 다시 시도하십시오.`,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
// Sealed ⇔ carries approved_digest. Un-sealing an already-draft spec writes NOTHING (a second
|
|
1220
|
+
// un-seal is history, not graffiti) — mirrors spec_retire's idempotent no-op.
|
|
1221
|
+
const sealed = typeof spec.frontmatter.approved_digest === 'string';
|
|
1222
|
+
if (!sealed) {
|
|
1223
|
+
return { ok: true, unsealed: false, id: a.id, dependents: [], reason: `${a.id}은(는) 이미 미봉인(draft) 상태입니다` };
|
|
1224
|
+
}
|
|
1225
|
+
const approvalRaw = process.env.HOLMES_APPROVAL;
|
|
1226
|
+
let approval;
|
|
1227
|
+
try {
|
|
1228
|
+
approval = approvalRaw ? JSON.parse(approvalRaw) : undefined;
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
approval = undefined;
|
|
1232
|
+
}
|
|
1233
|
+
const resolved = resolveHandlerApproval(a.root, store, approval, { kind: 'spec-approve', target: a.id }, new Date().toISOString());
|
|
1234
|
+
if (resolved === undefined) {
|
|
1235
|
+
return {
|
|
1236
|
+
ok: false,
|
|
1237
|
+
reason: `${a.id}은(는) 봉인된 문서입니다 — 봉인 해제는 이 행위를 덮는 유효한 대역외 HOLMES_APPROVAL 이 필요합니다.`
|
|
1238
|
+
+ ' 봉인을 해제하면 코드 게이트가 요구하던 approved 스펙의 봉인이 사라지므로, 해제가 승인 우회 경로가 되지 않도록 fail-closed 로 막습니다.'
|
|
1239
|
+
+ ' (범위를 쓰면 kind "spec-approve")'
|
|
1240
|
+
+ refusalQueueHint(a.root, store, { kind: 'spec-approve', target: a.id, why: '봉인된 스펙의 해제' }),
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
// @implements A-SPEC-245 — a grant that authorized breaking a seal is spent by it.
|
|
1244
|
+
if (resolved.source === 'grant' && resolved.root && resolved.approval.nonce) {
|
|
1245
|
+
(0, approval_grants_1.consumeGrantFile)(resolved.root, resolved.approval.nonce);
|
|
1246
|
+
}
|
|
1247
|
+
const dependents = all.filter((s) => s.id !== a.id && s.dependsOn.includes(a.id));
|
|
1248
|
+
const blocking = dependents.filter((s) => s.status === 'approved').map((s) => s.id);
|
|
1249
|
+
if (blocking.length > 0) {
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
reason: `${a.id}의 봉인을 해제하면 approved 문서 ${blocking.join(', ')}의 parent_digests 가 stale 이 됩니다 — 지금 그 사슬을 지탱하고 있으므로 거부합니다.`
|
|
1253
|
+
+ ' 해당 문서를 먼저 해제/폐기하거나 부모를 다른 문서로 옮기십시오.',
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
// Destination BEFORE the write (A-SPEC-188 order): a resolution failure leaves nothing behind.
|
|
1257
|
+
const dest = resolveLedgerRoot(a.root);
|
|
1258
|
+
if (!dest.ok)
|
|
1259
|
+
return { ok: false, reason: dest.reason };
|
|
1260
|
+
const ledgerRoot = dest.root;
|
|
1261
|
+
const fm = { ...spec.frontmatter };
|
|
1262
|
+
delete fm.approved_digest; // the seal this act withdraws
|
|
1263
|
+
delete fm.parent_digests;
|
|
1264
|
+
try {
|
|
1265
|
+
await store.write({ ...spec, status: 'draft', frontmatter: fm }, { expectedVersion: cur.version });
|
|
1266
|
+
}
|
|
1267
|
+
catch (e) {
|
|
1268
|
+
if (e instanceof spec_store_2.SpecVersionConflictError) {
|
|
1269
|
+
return { ok: false, reason: `봉인 해제 진행 중 ${a.id}이(가) 바뀌었습니다 — 확인 후 다시 시도하십시오. 이번 해제는 아무것도 쓰지 않았습니다.` };
|
|
1270
|
+
}
|
|
1271
|
+
throw e;
|
|
1272
|
+
}
|
|
1273
|
+
new ledger_store_1.FileLedgerStore(path.join(ledgerRoot, '.ax', 'ledger')).append({
|
|
1274
|
+
ts: new Date().toISOString(),
|
|
1275
|
+
actor: approval?.actor ?? 'unattributed',
|
|
1276
|
+
kind: 'spec-unsealed',
|
|
1277
|
+
summary: `unsealed ${a.id}`,
|
|
1278
|
+
inputs: [a.id],
|
|
1279
|
+
rationale: approval?.rationale ?? 'unsealed',
|
|
1280
|
+
...(approval ? { authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token) } : {}),
|
|
1281
|
+
});
|
|
1282
|
+
return { ok: true, unsealed: true, id: a.id, dependents: dependents.map((s) => s.id) };
|
|
1283
|
+
},
|
|
1284
|
+
/**
|
|
1285
|
+
* @implements A-SPEC-538.2
|
|
1286
|
+
* Read-only: report a spec's approval/seal state — sealed?, approved_digest, each parent's seal
|
|
1287
|
+
* state, and the concrete blockers still standing between it and approval — so a caller need not
|
|
1288
|
+
* parse files to ask "what is the approval state right now". Reuses `sealOf` and `approvalBlockers`
|
|
1289
|
+
* (the same predicates the code gate and spec_approve read) so the report cannot drift from the
|
|
1290
|
+
* acts it describes. No writes, no ledger append.
|
|
1291
|
+
*/
|
|
1292
|
+
async approval_status(a) {
|
|
1293
|
+
assertSpecStoreReachable('approval_status', store, a.root); // @implements A-SPEC-419
|
|
1294
|
+
const all = await store.list();
|
|
1295
|
+
if (all.filter((s) => s.id === a.id).length > 1) {
|
|
1296
|
+
return {
|
|
1297
|
+
ok: false,
|
|
1298
|
+
reason: `${a.id}이(가) 스토어에 두 번 이상 존재합니다 — 어느 사본의 상태를 물었는지 도구가 고를 수 없어 거부합니다.`,
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const cur = await store.read(a.id);
|
|
1302
|
+
if (!cur) {
|
|
1303
|
+
const dir = store.specsRoot;
|
|
1304
|
+
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
1305
|
+
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
1306
|
+
}
|
|
1307
|
+
return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, resolver(all)) };
|
|
1308
|
+
},
|
|
1309
|
+
/**
|
|
1310
|
+
* @implements A-SPEC-538.3
|
|
1311
|
+
* Read-only: return the provenance ledger's events in time order (optionally narrowed to one
|
|
1312
|
+
* spec) so the governance history — approved / unsealed / retired / review-needed … — is legible
|
|
1313
|
+
* at a glance without reading raw JSONL. Reuses FileLedgerStore.loadAll(); the ordering/filtering/
|
|
1314
|
+
* projection is the pure `timelineFrom`. No writes. An absent ledger is an empty history, not an
|
|
1315
|
+
* error.
|
|
1316
|
+
*/
|
|
1317
|
+
async ledger_timeline(a) {
|
|
1318
|
+
const dest = resolveLedgerRoot(a.root);
|
|
1319
|
+
if (!dest.ok)
|
|
1320
|
+
return { ok: false, reason: dest.reason };
|
|
1321
|
+
const dir = path.join(dest.root, '.ax', 'ledger');
|
|
1322
|
+
const events = fs.existsSync(dir) ? new ledger_store_1.FileLedgerStore(dir).loadAll() : [];
|
|
1323
|
+
return { ok: true, events: (0, ledger_timeline_1.timelineFrom)(events, a.id) };
|
|
1324
|
+
},
|
|
1177
1325
|
async spec_approve(a) {
|
|
1178
1326
|
const approvalRaw = process.env.HOLMES_APPROVAL;
|
|
1179
1327
|
let approval;
|
|
@@ -1415,10 +1563,15 @@ function makeRawHandlers(store, opts) {
|
|
|
1415
1563
|
}
|
|
1416
1564
|
catch (e) {
|
|
1417
1565
|
if (e instanceof spec_store_2.SpecVersionConflictError) {
|
|
1566
|
+
// @implements A-SPEC-538.4 — the refusal is unchanged (refuse, write nothing, the edit
|
|
1567
|
+
// wins); it now also CARRIES the conflict: the version this act read, the version now on
|
|
1568
|
+
// disk, and what to retry. Re-read to learn the current version (null if it vanished).
|
|
1569
|
+
const now = await store.read(a.id).catch(() => null);
|
|
1418
1570
|
return {
|
|
1419
1571
|
ok: false,
|
|
1420
1572
|
reason: `승인 진행 중 ${a.id}이(가) 바뀌었습니다 — 바뀐 내용을 확인하고 다시 승인하십시오.`
|
|
1421
1573
|
+ ' 이번 승인은 아무것도 쓰지 않았습니다.',
|
|
1574
|
+
conflict: (0, version_conflict_1.conflictDetail)({ id: a.id, expected: cur.version, current: now?.version ?? null }),
|
|
1422
1575
|
};
|
|
1423
1576
|
}
|
|
1424
1577
|
// @implements A-SPEC-188 — approval can RELOCATE the file (req_type classification moves
|
|
@@ -101,6 +101,38 @@ exports.TOOL_SCHEMAS = {
|
|
|
101
101
|
required: ['id'],
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
|
+
spec_unseal: {
|
|
105
|
+
description: "The inverse of spec_approve: return ONE sealed (approved) spec to an editable `status: draft`, clearing `approved_digest` and `parent_digests` in a single ACT, and record `spec-unsealed` in the provenance ledger. Un-sealing WITHDRAWS a seal, so it requires the same out-of-band HOLMES_APPROVAL as spec_approve/spec_retire's sealed path (fail-closed): un-sealing an approved T-SPEC removes the code gate's demand, which unguarded would be an approval bypass. Refuses when an APPROVED spec depends on the target — that dependent's parent_digests would silently go stale — naming the blockers. Idempotent: un-sealing a spec that is already draft (never sealed) writes nothing and returns unsealed:false. Written only at the version this act read; a concurrent edit wins and the un-seal is refused for retry. Distinct from spec_retire (which withdraws authority to status:outdated); un-seal keeps the spec alive and editable.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
id: str('Id of the spec to un-seal (return to draft).'),
|
|
110
|
+
root: str('Optional when the server is bound to a file store — the ledger location is derived from the store itself; if supplied it must resolve to the SAME project.'),
|
|
111
|
+
},
|
|
112
|
+
required: ['id'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
approval_status: {
|
|
116
|
+
description: "Read-only: report ONE spec's approval/seal state without the caller parsing files — { sealed, approvedDigest?, status, parents: [{ id, status, sealed, resolved }], blockers: [] }. `sealed` is the same approved_digest predicate the code gate reads; `blockers` is the exact list spec_approve would refuse on (empty once approved), so the report cannot drift from the acts it describes. Each parent entry shows that parent's OWN live seal state (an unresolved depends_on id is reported as status:'missing', resolved:false — not dropped). No writes, no ledger append. Refuses a duplicate or unknown id.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
id: str('Id of the spec whose approval/seal state to report.'),
|
|
121
|
+
root: str('Optional when the server is bound to a file store; if supplied it must resolve to the SAME project.'),
|
|
122
|
+
},
|
|
123
|
+
required: ['id'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
ledger_timeline: {
|
|
127
|
+
description: "Read-only: return the provenance ledger's events in time order — { events: [{ ts, kind, actor, summary, inputs }] } ascending by ts (tiebroken by chain seq). Pass an `id` to narrow to one spec (only events whose `inputs` reference it: spec-approved, spec-unsealed, spec-retired, review-needed …). The chain-integrity fields (hash/prevHash/seq/replicaId) are plumbing and are projected away — this is the governance history, not the tamper-evidence chain. No writes. An absent ledger is an empty history, not an error.",
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
id: str('Optional spec id — narrow the timeline to events that reference it.'),
|
|
132
|
+
root: str('Optional when the server is bound to a file store; the ledger location is derived from the store, or from this root.'),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
104
136
|
spec_approve: {
|
|
105
137
|
description: 'Approve a spec as a sealing ACT: confirm the ledger destination BEFORE sealing → validate (zero errors) → record approved_digest + parent_digests snapshots → status: approved (written only at the version this act read; a concurrent edit wins and the approval is refused for retry) → append spec-approved to the provenance ledger. Requires a valid out-of-band HOLMES_APPROVAL in the SERVER environment (fail-closed; nothing in the request can substitute). Refuses an unsealed approved parent — seal parents first.',
|
|
106
138
|
inputSchema: {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Spec } from './spec-parser';
|
|
2
|
+
/** One parent's approval/seal state as seen from a child. */
|
|
3
|
+
export interface ParentApproval {
|
|
4
|
+
id: string;
|
|
5
|
+
status: string;
|
|
6
|
+
sealed: boolean;
|
|
7
|
+
/** false when the resolver could not find the parent (a dangling depends_on). */
|
|
8
|
+
resolved: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A spec's approval/seal state, answerable without the caller parsing any file. */
|
|
11
|
+
export interface ApprovalStatus {
|
|
12
|
+
id: string;
|
|
13
|
+
type?: string;
|
|
14
|
+
status: string;
|
|
15
|
+
/** carries approved_digest — the code gate treats it as sealed. */
|
|
16
|
+
sealed: boolean;
|
|
17
|
+
approvedDigest?: string;
|
|
18
|
+
parents: ParentApproval[];
|
|
19
|
+
/** the concrete reasons this spec cannot be approved right now (empty once approved). */
|
|
20
|
+
blockers: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* @implements A-SPEC-538.2
|
|
24
|
+
* Pure: describe a spec's approval/seal state from the spec object and a parent resolver. Reuses
|
|
25
|
+
* `sealOf` (the same seal predicate the code gate reads) and `approvalBlockers` (the exact list
|
|
26
|
+
* spec_approve would refuse on) so the report cannot drift from the acts it describes. Touches no
|
|
27
|
+
* filesystem — the handler reads the store and hands the objects in.
|
|
28
|
+
*/
|
|
29
|
+
export declare function describeApproval(spec: Spec, resolve: (id: string) => Spec | null): ApprovalStatus;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeApproval = describeApproval;
|
|
4
|
+
const spec_digest_1 = require("./spec-digest");
|
|
5
|
+
const approval_blockers_1 = require("./approval-blockers");
|
|
6
|
+
/**
|
|
7
|
+
* @implements A-SPEC-538.2
|
|
8
|
+
* Pure: describe a spec's approval/seal state from the spec object and a parent resolver. Reuses
|
|
9
|
+
* `sealOf` (the same seal predicate the code gate reads) and `approvalBlockers` (the exact list
|
|
10
|
+
* spec_approve would refuse on) so the report cannot drift from the acts it describes. Touches no
|
|
11
|
+
* filesystem — the handler reads the store and hands the objects in.
|
|
12
|
+
*/
|
|
13
|
+
function describeApproval(spec, resolve) {
|
|
14
|
+
const seal = (0, spec_digest_1.sealOf)(spec);
|
|
15
|
+
const parents = (spec.dependsOn ?? []).map((pid) => {
|
|
16
|
+
const parent = resolve(pid);
|
|
17
|
+
return parent
|
|
18
|
+
? { id: pid, status: parent.status, sealed: !!(0, spec_digest_1.sealOf)(parent).approvedDigest, resolved: true }
|
|
19
|
+
: { id: pid, status: 'missing', sealed: false, resolved: false };
|
|
20
|
+
});
|
|
21
|
+
// An approved spec has, by definition, cleared every blocker; recomputing would only re-report
|
|
22
|
+
// the placeholder/parent checks against a document that already passed them.
|
|
23
|
+
const blockers = spec.status === 'approved' ? [] : (0, approval_blockers_1.approvalBlockers)(spec, resolve);
|
|
24
|
+
return {
|
|
25
|
+
id: spec.id,
|
|
26
|
+
type: spec.type,
|
|
27
|
+
status: spec.status,
|
|
28
|
+
sealed: !!seal.approvedDigest,
|
|
29
|
+
approvedDigest: seal.approvedDigest,
|
|
30
|
+
parents,
|
|
31
|
+
blockers,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A structured report of an optimistic-concurrency conflict during a sealing act. */
|
|
2
|
+
export interface ConflictDetail {
|
|
3
|
+
id: string;
|
|
4
|
+
/** the version this act READ (and tried to write at). */
|
|
5
|
+
expectedVersion: string;
|
|
6
|
+
/** the version now on disk — null when the spec was removed between read and write. */
|
|
7
|
+
currentVersion: string | null;
|
|
8
|
+
/** one non-empty sentence: what happened and what to do next. */
|
|
9
|
+
retry: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @implements A-SPEC-538.4
|
|
13
|
+
* Pure: describe a version conflict so the refusal names what won and what to retry. The act's
|
|
14
|
+
* refusal behaviour (refuse, write nothing, the edit wins) is unchanged — this only exposes the
|
|
15
|
+
* detail. `current` is null when the spec vanished between read and write.
|
|
16
|
+
*/
|
|
17
|
+
export declare function conflictDetail(a: {
|
|
18
|
+
id: string;
|
|
19
|
+
expected: string;
|
|
20
|
+
current: string | null;
|
|
21
|
+
}): ConflictDetail;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-538.4
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.conflictDetail = conflictDetail;
|
|
5
|
+
/**
|
|
6
|
+
* @implements A-SPEC-538.4
|
|
7
|
+
* Pure: describe a version conflict so the refusal names what won and what to retry. The act's
|
|
8
|
+
* refusal behaviour (refuse, write nothing, the edit wins) is unchanged — this only exposes the
|
|
9
|
+
* detail. `current` is null when the spec vanished between read and write.
|
|
10
|
+
*/
|
|
11
|
+
function conflictDetail(a) {
|
|
12
|
+
const what = a.current === null
|
|
13
|
+
? `${a.id} was removed since this act read it (read ${a.expected})`
|
|
14
|
+
: `${a.id} changed since this act read it (read ${a.expected}, now ${a.current})`;
|
|
15
|
+
return {
|
|
16
|
+
id: a.id,
|
|
17
|
+
expectedVersion: a.expected,
|
|
18
|
+
currentVersion: a.current,
|
|
19
|
+
retry: `${what} — nothing was written; re-read ${a.id} and try the approval again.`,
|
|
20
|
+
};
|
|
21
|
+
}
|
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.10.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",
|