@holmes-lab/holmes-kit 0.15.0 → 0.17.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 +118 -0
- package/README.md +5 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.js +15 -1
- package/dist/holmes/cli/mcp-version.d.ts +4 -1
- package/dist/holmes/cli/mcp-version.js +5 -2
- package/dist/holmes/governance/approval-queue.d.ts +6 -0
- package/dist/holmes/governance/approval-queue.js +11 -3
- package/dist/holmes/governance/autonomy.js +16 -1
- package/dist/holmes/governance/session-context.d.ts +74 -0
- package/dist/holmes/governance/session-context.js +179 -0
- package/dist/holmes/hooks/pre-tool-use.js +5 -2
- package/dist/holmes/hooks/rtm-refresh-child.d.ts +1 -0
- package/dist/holmes/hooks/rtm-refresh-child.js +56 -0
- package/dist/holmes/hooks/rtm-refresh.d.ts +13 -0
- package/dist/holmes/hooks/rtm-refresh.js +76 -0
- package/dist/holmes/hooks/stop.js +42 -0
- package/dist/holmes/mcp/handlers.d.ts +8 -7
- package/dist/holmes/mcp/handlers.js +114 -5
- package/dist/holmes/mcp/server.js +12 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -1
- package/dist/holmes/review/judgement-bundle.d.ts +49 -0
- package/dist/holmes/review/judgement-bundle.js +108 -0
- package/dist/holmes/review/run-replay.d.ts +5 -0
- package/dist/holmes/review/run-replay.js +32 -0
- package/dist/holmes/rtm/anchor-density.d.ts +43 -0
- package/dist/holmes/rtm/anchor-density.js +117 -0
- package/dist/holmes/rtm/impact-advisory.d.ts +52 -0
- package/dist/holmes/rtm/impact-advisory.js +182 -0
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +50 -1
- package/dist/holmes/rtm/rtm-graph.d.ts +28 -1
- package/dist/holmes/rtm/rtm-graph.js +61 -8
- package/dist/holmes/spec/compat-impact.d.ts +5 -0
- package/dist/holmes/spec/compat-impact.js +1 -0
- package/package.json +1 -1
- package/playbooks/publish/PLAYBOOK.md +16 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,124 @@ 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.17.0] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
An adversarial review of 0.16.0's own new surfaces, run against the shipped tarball the day it
|
|
11
|
+
went out, drove this release: the newest repair is always the next target, and this time the
|
|
12
|
+
findings were sealed before anyone else could find them. The release gate itself also gets the
|
|
13
|
+
fix for the incident that let 0.16.0 ship with a frozen README.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Graph-row forgery via spec prose (REQ-569 S1, HIGH)** — a DRAFT spec (no approval needed to
|
|
17
|
+
exist) whose YAML double-quoted title carried `\n`/`\t` escapes could forge rows in the graph's
|
|
18
|
+
canonical dump, which the PPR view parsed as **real call edges** — poisoning `rankedImpact` and
|
|
19
|
+
`maintenance_analyze` (reproduced against the shipped 0.16.0 artifact, then killed). Sealed at
|
|
20
|
+
BOTH boundaries independently: `specSummary` now folds whitespace over the whole summary (title
|
|
21
|
+
included — the sentence-only fold was the hole), and `RtmGraph.addNode` folds structural
|
|
22
|
+
characters at the storage boundary so no future caller can break a dump row either. Each face
|
|
23
|
+
verified alone. A high-effort review of this very fix then widened it: tabs riding in via spec
|
|
24
|
+
`id:`/`depends_on:` still shifted dump columns, so `dumpCanonical` now folds EVERY text column
|
|
25
|
+
at emission (single choke point; the 13-cell row invariant is pinned by test), and the graph
|
|
26
|
+
extractor version bumped to `holmes-rtm/2` so a persisted store written by unfixed 0.16.0 —
|
|
27
|
+
whose summary column may already carry forged rows — is force-rebuilt instead of reused.
|
|
28
|
+
- **Grader/doctor parity pair (REQ-569 S4)** — `isHighRiskPath` folds `.` path segments before
|
|
29
|
+
judging (`src/holmes/./governance/x.ts` no longer dodges the risk roots; `..` is deliberately
|
|
30
|
+
NOT folded — the grader widens sight, never impersonates path resolution, and the enforcer
|
|
31
|
+
stays byte-literal). The codex doctor's npx-pin branch now compares the pin against the
|
|
32
|
+
installed version and WARNs on a stale pin (parity with the `.mcp.json` drift check) — an old
|
|
33
|
+
fixture that had enshrined the gap as PASS was repinned to the current version. Review
|
|
34
|
+
follow-ups sealed in the same release: a `..`-bearing Files-to-Touch token is now FLAGGED
|
|
35
|
+
high-risk (the hook does not canonicalize relative paths, so a `src/app/../holmes/...` spelling
|
|
36
|
+
stayed admissible at the byte-literal enforcer while the grader called it benign — flagging the
|
|
37
|
+
ambiguous ascent token closes the self-approval hole without impersonating path resolution);
|
|
38
|
+
non-semver pins (`@latest`, `^x.y.z`) judge `unknown` instead of an inverted "stale pin" WARN;
|
|
39
|
+
and the sealing-time advisory now closes its SQLite handle (a per-approval native-handle leak in
|
|
40
|
+
the long-lived MCP server, and a file-lock risk on Windows).
|
|
41
|
+
- **The docs-currency gate is bidirectional now (REQ-570)** — 0.16.0 shipped with README's
|
|
42
|
+
feature list frozen at "v0.14.x" and zero 0.16.0 entries, because the gate's instruction was
|
|
43
|
+
"grep for stale phrases": a feature never written produces zero hits, and zero hits read as
|
|
44
|
+
"no drift". The publish playbook now demands three checks per user-facing change — ADDITION
|
|
45
|
+
(the entry must exist; zero grep hits are a missing-entry signal, not a pass), drift (the old
|
|
46
|
+
wording), and stale markers (version-pinned section labels are drift generators and get
|
|
47
|
+
removed, not policed). The incident is recorded in the playbook itself, the README repaired
|
|
48
|
+
retroactively, and the new wording pinned by tests.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
- **The impact/advisory graph is approved-only (REQ-569 S2)** — the persisted RTM graph that
|
|
52
|
+
feeds `rtm_impact`, the sealing-time advisory and the intent summaries now builds from SEALED
|
|
53
|
+
specs only: a draft needs no approval to exist, so it can no longer reach the agent-visible
|
|
54
|
+
channels (that was the forgery's delivery vehicle). The closure walks edges, so the channel
|
|
55
|
+
filter is node existence — an anchor naming a draft keeps its id but carries no prose until
|
|
56
|
+
approval. Diagnosis (`rtm_check`) and matching (`issue_localize`) keep their existing
|
|
57
|
+
populations; old `rtm.sqlite` files rebuild automatically on basis drift. Replay pins unmoved
|
|
58
|
+
to the digit.
|
|
59
|
+
- **Annotation caps (REQ-569 S3)** — `impactedSummaries` caps at 40 (`summariesOmitted` counts
|
|
60
|
+
the rest; `impacted` itself is never truncated) and advisory anchors cap at 10 per file
|
|
61
|
+
(`anchorsOmitted`). Measured on this repository's hub-grade impact (342 specs): the response
|
|
62
|
+
shrank **104,706 → 18,708 bytes (−82%)**, and the omitted tail costs no summary lookups.
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
- **Anchor-density advisory (REQ-569 S5, observation only)** — sealing an A-SPEC whose
|
|
66
|
+
Files-to-Touch contains an anchor-dense file (live `implements` count ≥ max(8, p90 of the
|
|
67
|
+
store's distribution)) annotates the response with `anchorDensity: [{path, anchors, p90}]` and
|
|
68
|
+
records it to `anchor-density.<replica>.jsonl` (paths and integers only). Grounded in the
|
|
69
|
+
measured precision tax of anchor accumulation (authoring one spec moved replay recall
|
|
70
|
+
0.5476→0.5060); a count GATE was considered and refused — the thresholds are prose constants,
|
|
71
|
+
and promotion or rejection will be decided by this ledger, the impactAdvisory lifecycle.
|
|
72
|
+
|
|
73
|
+
### Notes
|
|
74
|
+
- One inherited finding was honestly killed instead of "fixed": the recorded
|
|
75
|
+
"scanTestAnchors only sees a file's first anchor" defect does **not reproduce** at HEAD (all
|
|
76
|
+
standalone-comment anchors are consumed by ART-4 and the execution-evidence attribution alike);
|
|
77
|
+
the only non-recognition is the trailing-comment form, which is the sealed anchor idiom rule.
|
|
78
|
+
Stale findings get re-measured, not re-fixed.
|
|
79
|
+
|
|
80
|
+
## [0.16.0] - 2026-09-07
|
|
81
|
+
|
|
82
|
+
The graph learns to speak intent, and the call graph learns to speak up at sealing time. An A-SPEC
|
|
83
|
+
approval now returns what your Files-to-Touch declaration *missed* — and every spec the graph
|
|
84
|
+
names arrives with its one-sentence intent beside the id, extracted (never generated) from the
|
|
85
|
+
spec store. Two more mechanisms were benchmarked under pre-registration and honestly discarded.
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- **Impact advisory on approval (REQ-566)** — a successful A-SPEC `spec_approve` may now carry
|
|
89
|
+
`impactAdvisory`: files whose symbols **call into** the declared Files-to-Touch but sit outside
|
|
90
|
+
the declaration (1-hop, capped at 10, repo-relative allow-list). Advisory, never verdict: it
|
|
91
|
+
rides the response *after* the seal is committed, degrades to absence on any failure, and every
|
|
92
|
+
emission lands in an observation ledger (`.ax/ledger/impact-advisories.<replica>.jsonl` — paths,
|
|
93
|
+
ids and integers only) so its false-positive rate is measured *before* anyone proposes a hard
|
|
94
|
+
gate. The RTM graph persists at `.ax/rtm.sqlite` and keeps itself fresh: `rtm_impact` rebuilds
|
|
95
|
+
on basis drift, and the Stop hook spawns a TTL-gated detached reindex (staleness was measured as
|
|
96
|
+
the advisory's quality factor: 7 findings on an 8-day-old graph, 17 after a fresh one).
|
|
97
|
+
- **SPEC intent summaries in the graph (REQ-568)** — graph nodes gain a `summary` column (schema
|
|
98
|
+
`rtm-graph/2` → `/3`; old stores rebuild automatically on first use, no migration step). Every
|
|
99
|
+
SPEC node stores `"<title> — <first sentence of its intent section>"`, extracted
|
|
100
|
+
deterministically (REQ=Problem / Need, H-SPEC=Intent, A-SPEC=Objective; 200-char cap; title-less
|
|
101
|
+
legacy specs — 20 measured in this very store — fall back to sentence, then id). Same store,
|
|
102
|
+
byte-identical graph; measured cost +6.4% build time / +4.2% file size. `RtmGraph.summaryOf(id)`
|
|
103
|
+
reads it back, and `rtm_impact` responses add `impactedSummaries: [{id, summary}]` beside the
|
|
104
|
+
unchanged `impacted` list.
|
|
105
|
+
- **Session-context observability (REQ-564)** — the ledger records which agent/model drove a
|
|
106
|
+
session and what the governance overhead cost, per replica
|
|
107
|
+
(`.ax/ledger/session-context.<replica>.jsonl`), grounding field reports in machine attribution.
|
|
108
|
+
|
|
109
|
+
### Changed
|
|
110
|
+
- **`impactAdvisory.files[].anchors` shape (REQ-568, code-interface)** — anchors are now
|
|
111
|
+
`[{id, summary}]` instead of `string[]`, so the reader sees *which intent* is at risk without a
|
|
112
|
+
spec-store round trip. Consumers parsing anchors must read `.id`. The summary is information
|
|
113
|
+
only: value tests pin that the advisory's file list, ordering and `more` count are independent
|
|
114
|
+
of the prose, and the observation ledger stays prose-free.
|
|
115
|
+
|
|
116
|
+
### Notes
|
|
117
|
+
- Two pre-registered negatives, kept as records rather than shipped: the judgement-axis
|
|
118
|
+
FACT/EVIDENCE bundle tied its bench (0.480 vs 0.478) and was discarded with its re-attempt
|
|
119
|
+
conditions sealed (REQ-567, 15th rejection); a spec-intent **vector assist** for uncited
|
|
120
|
+
requests was built, measured byte-identical to the lexical path over 441 held-out traceability
|
|
121
|
+
cases (long queries already match ~371 specs lexically; the lexical-zero segment is protected by
|
|
122
|
+
the reorder-not-admit rule), reverted, and its A/T-SPECs retired (REQ-568 S3, 16th rejection).
|
|
123
|
+
The S-491 embedding win belongs to direct query→file ranking and does not transport through the
|
|
124
|
+
spec-matching layer.
|
|
125
|
+
|
|
8
126
|
## [0.15.0] - 2026-09-06
|
|
9
127
|
|
|
10
128
|
The first field report on Holmes-Kit's own operational data (1,412 unique approval requests vs. 8
|
package/README.md
CHANGED
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
### 🛡️ Currently Supported Features (
|
|
17
|
+
### 🛡️ Currently Supported Features (Production Features)
|
|
18
|
+
|
|
19
|
+
- 📣 **Impact Advisory at sealing time** *(new in 0.16.0)*: approving an A-SPEC now returns what your Files-to-Touch declaration *missed* — files whose symbols **call into** the declared scope from outside it (1-hop, capped, repo-relative allow-list), computed from the persisted RTM graph at the moment of sealing. Advisory, never verdict: it rides the response *after* the seal commits, degrades to absence on any failure, and every emission lands in an observation ledger so its false-positive rate is **measured before** anyone proposes a hard gate. The graph keeps itself fresh — `rtm_impact` rebuilds on basis drift and the Stop hook spawns a TTL-gated detached reindex (staleness was measured as the advisory's quality factor: 7 findings on an 8-day-old graph, 17 after a fresh one). *(0.17.0 hardening)*: the advisory/impact graph is **approved-only** (a draft needs no approval to exist, so it can no longer reach these agent-visible channels), summary prose can't forge graph rows (structural characters fold at both the extraction and storage boundaries), and annotations are capped with explicit omission counts (a hub-grade response shrank 104.7KB → 18.7KB, −82%). Sealing also gains an **anchor-density advisory** (observation-only): an A-SPEC whose Files-to-Touch contains an anchor-dense file (live anchors ≥ max(8, p90)) is annotated with `anchorDensity: [{path, anchors, p90}]` and ledgered — grounded in the measured precision tax of anchor accumulation; a count *gate* was considered and refused.
|
|
20
|
+
- 🗣️ **The graph speaks intent** *(new in 0.16.0)*: every SPEC node stores a one-sentence intent summary (`"<title> — <first sentence of its intent section>"`, schema `rtm-graph/3`, old stores rebuild automatically) — extracted deterministically, **never generated** (same store, byte-identical graph; measured cost +6.4% build time / +4.2% file size). Advisory anchors arrive as `{id, summary}` and `rtm_impact` adds `impactedSummaries`, so the reader sees *which intent* is at risk without a spec-store round trip. Information only: value tests pin that no verdict, ranking or gate reads the prose.
|
|
21
|
+
- 📇 **Session-context observability** *(new in 0.16.0)*: the ledger records which agent/model drove a session and what the governance overhead cost, per replica (`session-context.<replica>.jsonl`), grounding field reports in machine attribution instead of guesswork.
|
|
18
22
|
|
|
19
23
|
- 📋 **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
24
|
- 🔴 **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.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20f5f64-mtrggtxw
|
|
@@ -897,7 +897,21 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
897
897
|
}
|
|
898
898
|
else {
|
|
899
899
|
const pin = (0, mcp_version_1.mcpLaunchVersion)({ command: entry.command, args: entry.args });
|
|
900
|
-
|
|
900
|
+
// @implements A-SPEC-569.4 — parity with the .mcp.json drift check (A-SPEC-251.2): a pin
|
|
901
|
+
// that RESOLVES is not enough, because a stale pin quietly keeps launching yesterday's
|
|
902
|
+
// gate. Same verdict function (non-semver pins like @latest judge 'unknown' → PASS, not
|
|
903
|
+
// an inverted "stale" WARN), same fix shape; an unreadable pin stays FAIL as before.
|
|
904
|
+
const cliVer = (0, mcp_launcher_1.readPackageVersion)(packageRoot); // same accessor as the .mcp.json check — one source of truth
|
|
905
|
+
const drift = cliVer ? (0, mcp_version_1.versionDriftVerdict)(pin, cliVer) : 'unknown';
|
|
906
|
+
if (pin === null) {
|
|
907
|
+
add('codex wiring', 'FAIL', `cannot read a launch version from the wiring: ${entry.command} ${entry.args.join(' ')}`, 'Rewire with holmes-kit init --target <dir> --agent codex.');
|
|
908
|
+
}
|
|
909
|
+
else if (drift === 'drift') {
|
|
910
|
+
add('codex wiring', 'WARN', `resolves via the npx pin ${pin}, but this install is ${cliVer} — a stale pin keeps launching the old server`, 'Rewire with holmes-kit init --target <dir> --agent codex --force to refresh the pin.');
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
add('codex wiring', 'PASS', `resolves via the npx pin ${pin}`);
|
|
914
|
+
}
|
|
901
915
|
}
|
|
902
916
|
// @implements A-SPEC-442 (was A-SPEC-423)
|
|
903
917
|
// Codex CAN hard-enforce, but only from an INSTALLED plugin: it loads plugins from
|
|
@@ -19,5 +19,8 @@ export interface McpEntryShape {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function mcpLaunchVersion(entry: McpEntryShape, readVersion?: (packageDir: string) => string | undefined): string | null;
|
|
21
21
|
export type DriftVerdict = 'match' | 'drift' | 'unknown';
|
|
22
|
-
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
22
|
+
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
23
|
+
* @implements A-SPEC-569.4 — 비-semver 런치 문자열(`latest`·`next`·`^0.16.0`)도 unknown:
|
|
24
|
+
* `@latest` 핀은 최신을 띄우는데 "구 서버를 계속 띄운다"는 drift 경고는 역진단이었다(고강도
|
|
25
|
+
* 리뷰 F5). 정확 semver 만 오프라인에서 비교 가능하다 — 판정 불능은 결함으로 둔갑시키지 않는다. */
|
|
23
26
|
export declare function versionDriftVerdict(launchVersion: string | null, cliVersion: string): DriftVerdict;
|
|
@@ -74,9 +74,12 @@ function mcpLaunchVersion(entry, readVersion) {
|
|
|
74
74
|
}
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
77
|
+
/** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift.
|
|
78
|
+
* @implements A-SPEC-569.4 — 비-semver 런치 문자열(`latest`·`next`·`^0.16.0`)도 unknown:
|
|
79
|
+
* `@latest` 핀은 최신을 띄우는데 "구 서버를 계속 띄운다"는 drift 경고는 역진단이었다(고강도
|
|
80
|
+
* 리뷰 F5). 정확 semver 만 오프라인에서 비교 가능하다 — 판정 불능은 결함으로 둔갑시키지 않는다. */
|
|
78
81
|
function versionDriftVerdict(launchVersion, cliVersion) {
|
|
79
|
-
if (launchVersion === null)
|
|
82
|
+
if (launchVersion === null || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(launchVersion))
|
|
80
83
|
return 'unknown';
|
|
81
84
|
return launchVersion === cliVersion ? 'match' : 'drift';
|
|
82
85
|
}
|
|
@@ -27,6 +27,9 @@ export interface RefusalRecord {
|
|
|
27
27
|
why: string;
|
|
28
28
|
ts: string;
|
|
29
29
|
replica?: string;
|
|
30
|
+
/** @implements A-SPEC-564.2 — the byte size of the deny feedback this refusal put back into the
|
|
31
|
+
* conversation (base reason + queue hint). Observation only; absent on legacy records. */
|
|
32
|
+
reasonBytes?: number;
|
|
30
33
|
}
|
|
31
34
|
export declare function refusalsFilename(replica: string): string;
|
|
32
35
|
/** Born split: there is no legacy single `refusals.jsonl` — the file did not exist before replicas did. */
|
|
@@ -121,6 +124,7 @@ export declare function enqueueApprovalRequest(root: string, req: {
|
|
|
121
124
|
kind: string;
|
|
122
125
|
target: string;
|
|
123
126
|
why: string;
|
|
127
|
+
reasonBytes?: number;
|
|
124
128
|
}): boolean;
|
|
125
129
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
126
130
|
export declare function readQueue(root: string, opts?: {
|
|
@@ -137,4 +141,6 @@ export declare function queueHint(root: string, req: {
|
|
|
137
141
|
kind: string;
|
|
138
142
|
target: string;
|
|
139
143
|
why: string;
|
|
144
|
+
}, opts?: {
|
|
145
|
+
baseReasonBytes?: number;
|
|
140
146
|
}): string;
|
|
@@ -281,6 +281,10 @@ function enqueueApprovalRequest(root, req) {
|
|
|
281
281
|
why: req.why,
|
|
282
282
|
ts: new Date().toISOString(),
|
|
283
283
|
...(replica !== undefined && replica !== '' ? { replica } : {}),
|
|
284
|
+
// @implements A-SPEC-564.2 — only the refusal log carries the feedback cost; inbox kinds have
|
|
285
|
+
// no deny text to measure.
|
|
286
|
+
...(!exports.DECISION_KINDS.has(req.kind) && typeof req.reasonBytes === 'number' && Number.isFinite(req.reasonBytes)
|
|
287
|
+
? { reasonBytes: Math.max(0, Math.round(req.reasonBytes)) } : {}),
|
|
284
288
|
};
|
|
285
289
|
// TYPE BEFORE WRITE, for the same reason as the read (round-7): `appendFileSync` on a FIFO with
|
|
286
290
|
// no reader blocks in open(2) forever. Guarding only the reader left the GATE wedged — measured:
|
|
@@ -344,7 +348,7 @@ function readQueue(root, opts) {
|
|
|
344
348
|
* Appended ONLY when the enqueue succeeded: a hint naming an id that was never written would send
|
|
345
349
|
* the operator to an empty list.
|
|
346
350
|
*/
|
|
347
|
-
function queueHint(root, req) {
|
|
351
|
+
function queueHint(root, req, opts) {
|
|
348
352
|
const id = approvalRequestId(req.kind, req.target);
|
|
349
353
|
// @implements A-SPEC-246
|
|
350
354
|
// Decisions are read BEFORE filing, so a human's answer reaches the agent in the very refusal
|
|
@@ -375,7 +379,11 @@ function queueHint(root, req) {
|
|
|
375
379
|
const denialLine = decided?.event === 'denied'
|
|
376
380
|
? (0, screen_safe_1.wrapColumns)(`\n[거부됨${decided.reason ? `: ${(0, screen_safe_1.flattenField)(decided.reason, 300)}` : ''}] 같은 요청을 반복하지 말고 거부 사유를 해소해 사용자와 상의하십시오.`, 76, ' \u2502 ')
|
|
377
381
|
: '';
|
|
378
|
-
|
|
379
|
-
|
|
382
|
+
// @implements A-SPEC-564.2 — the hint is composed BEFORE the enqueue so the refusal record can
|
|
383
|
+
// carry its own feedback cost: reasonBytes = the caller's base reason + this hint, i.e. the text
|
|
384
|
+
// that actually re-enters the conversation. Observation only — nothing reads it on a gate path.
|
|
385
|
+
const hint = `${denialLine}\n[승인 대기 ${id}] 운영자: ${(0, npx_bin_1.npxBin)()} holmes-kit approve`;
|
|
386
|
+
return enqueueApprovalRequest(root, { ...req, reasonBytes: (opts?.baseReasonBytes ?? 0) + hint.length })
|
|
387
|
+
? hint
|
|
380
388
|
: denialLine;
|
|
381
389
|
}
|
|
@@ -80,7 +80,22 @@ const TAINT_MARKERS = ['taint', 'dataflow-taint', 'flow-sensitive'];
|
|
|
80
80
|
// purpose, because a glob spanning the gate surface must never self-approve.
|
|
81
81
|
const GLOB_RISK_ROOTS = [...HIGH_RISK_PREFIXES, 'src/holmes/rtm/'];
|
|
82
82
|
function isHighRiskPath(p) {
|
|
83
|
-
|
|
83
|
+
// @implements A-SPEC-569.4 — '.' segments fold BEFORE the verdict: `src/holmes/./governance/x`
|
|
84
|
+
// dodged every prefix check while naming the gate surface exactly (REQ-556's remaining edge).
|
|
85
|
+
// Deliberately NOT `..`: folding that would impersonate path resolution this grader cannot do
|
|
86
|
+
// (shape is not location) — over-inclusion is allowed here, invented precision is not. The
|
|
87
|
+
// enforcer (matchesFtt) stays byte-literal, so a /./-token still admits nothing: only the
|
|
88
|
+
// grader's sight widened, never the gate's admission.
|
|
89
|
+
const unquoted = p.replace(/^["'`]|["'`]$/g, '');
|
|
90
|
+
// @implements A-SPEC-569.4 (revision, high-effort review F3) — a `..` segment is NOT folded
|
|
91
|
+
// (folding would impersonate path resolution this grader cannot do) but it IS flagged: the hook
|
|
92
|
+
// does not canonicalize relative paths, so `src/app/../holmes/governance/x.ts` stays admissible
|
|
93
|
+
// at the byte-literal enforcer under its own spelling while a fold-blind grader called it
|
|
94
|
+
// benign. An ascent token is ambiguous about where it lands, and ambiguity over the gate
|
|
95
|
+
// surface grades high-risk — over-inclusive on purpose, the rule this module already owns.
|
|
96
|
+
if (/(^|\/)\.\.(\/|$)/.test(unquoted))
|
|
97
|
+
return true;
|
|
98
|
+
const raw = unquoted.replace(/\/\.(?=\/|$)/g, '').replace(/^(\.\/)+/, '');
|
|
84
99
|
const s = raw.toLowerCase();
|
|
85
100
|
if (HIGH_RISK_PREFIXES.some((pre) => s.startsWith(pre)))
|
|
86
101
|
return true;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-context ledger: WHO attached (agent name@version) and — where a harness exposes it —
|
|
3
|
+
* WITH WHAT (model ids, token totals). REQ-564's whole point is that none of this existed anywhere
|
|
4
|
+
* in the ledgers (measured across provenance, outcomes and the queue), while the sources were one
|
|
5
|
+
* line away: MCP clientInfo carries {name, version} for ALL three harnesses (the server used the
|
|
6
|
+
* name and dropped the version), and Claude's transcript carries model + usage per message.
|
|
7
|
+
*
|
|
8
|
+
* HARNESS-NEUTRAL BY CONSTRUCTION: the client stamp is written by the MCP SERVER, which every
|
|
9
|
+
* harness talks to — the first design pinned it on the Stop hook and was caught as Claude-biased
|
|
10
|
+
* (the owner's catch that also produced REQ-565's gate). Enrichment beyond the stamp is
|
|
11
|
+
* capability-declared per harness, never faked.
|
|
12
|
+
*
|
|
13
|
+
* NUMBERS ONLY. Records carry names, versions, model ids and integers. The transcript's
|
|
14
|
+
* conversation text, commands and paths have no field to land in — the same structural privacy
|
|
15
|
+
* QueueEventLite uses. Observation NEVER feeds a verdict (Judgments must not be budgeted).
|
|
16
|
+
*/
|
|
17
|
+
export interface UsageTotals {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
cacheRead: number;
|
|
21
|
+
cacheCreation: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClientRecord {
|
|
24
|
+
kind: 'client';
|
|
25
|
+
sessionKey: string;
|
|
26
|
+
client: string;
|
|
27
|
+
clientVersion: string;
|
|
28
|
+
ts: string;
|
|
29
|
+
replica?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface UsageRecord {
|
|
32
|
+
kind: 'usage';
|
|
33
|
+
sessionKey: string;
|
|
34
|
+
models: string[];
|
|
35
|
+
usage: UsageTotals;
|
|
36
|
+
turns: number;
|
|
37
|
+
truncated?: boolean;
|
|
38
|
+
ts: string;
|
|
39
|
+
replica?: string;
|
|
40
|
+
}
|
|
41
|
+
export type SessionContextRecord = ClientRecord | UsageRecord;
|
|
42
|
+
export declare function sessionContextFilename(replica: string): string;
|
|
43
|
+
/** Born split: no legacy single file — this ledger never existed before replicas did. */
|
|
44
|
+
export declare function isSessionContextFilename(name: string): boolean;
|
|
45
|
+
/** Append to THIS machine's chain. Fail-open: observation must never break the observed. */
|
|
46
|
+
export declare function appendSessionContext(root: string, rec: SessionContextRecord): boolean;
|
|
47
|
+
/** Every replica's records, merged by ts (stable). Missing dir → []; corrupt lines skipped. */
|
|
48
|
+
export declare function readSessionContext(root: string): SessionContextRecord[];
|
|
49
|
+
/**
|
|
50
|
+
* @implements A-SPEC-564.1
|
|
51
|
+
* The lazy-once stamper the MCP server wires into its call path. Lazy because `getClientVersion()`
|
|
52
|
+
* has a value only AFTER initialize — the first tool call is the earliest honest moment — and
|
|
53
|
+
* because the ledger's location arrives with the first `root`-carrying call. Once, because a
|
|
54
|
+
* session has one identity; a root-less call defers rather than consumes the chance.
|
|
55
|
+
*/
|
|
56
|
+
export declare function makeSessionStamper(info: () => {
|
|
57
|
+
name?: string;
|
|
58
|
+
version?: string;
|
|
59
|
+
} | undefined, append?: (root: string, rec: ClientRecord) => boolean, sessionKey?: string): (root: unknown) => void;
|
|
60
|
+
/**
|
|
61
|
+
* @implements A-SPEC-564.2
|
|
62
|
+
* Numbers out of a Claude transcript — model ids, the four usage sums, a turn count. Streaming and
|
|
63
|
+
* capped: a giant transcript yields an honest partial sum with `truncated`, never a slow Stop (a
|
|
64
|
+
* huge response once blocked the very fix that would have shrunk it — observation must not repeat
|
|
65
|
+
* that shape). The return type has no field a conversation could leak through.
|
|
66
|
+
*/
|
|
67
|
+
export declare function summarizeTranscript(lines: Iterable<string>, opts?: {
|
|
68
|
+
maxBytes?: number;
|
|
69
|
+
}): {
|
|
70
|
+
models: string[];
|
|
71
|
+
usage: UsageTotals;
|
|
72
|
+
turns: number;
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.sessionContextFilename = sessionContextFilename;
|
|
37
|
+
exports.isSessionContextFilename = isSessionContextFilename;
|
|
38
|
+
exports.appendSessionContext = appendSessionContext;
|
|
39
|
+
exports.readSessionContext = readSessionContext;
|
|
40
|
+
exports.makeSessionStamper = makeSessionStamper;
|
|
41
|
+
exports.summarizeTranscript = summarizeTranscript;
|
|
42
|
+
// @implements A-SPEC-564.1
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
const replica_id_1 = require("./replica-id");
|
|
46
|
+
const FILE_RE = /^session-context\.([^.]+)\.jsonl$/;
|
|
47
|
+
function sessionContextFilename(replica) {
|
|
48
|
+
return `session-context.${replica}.jsonl`;
|
|
49
|
+
}
|
|
50
|
+
/** Born split: no legacy single file — this ledger never existed before replicas did. */
|
|
51
|
+
function isSessionContextFilename(name) {
|
|
52
|
+
return FILE_RE.test(name);
|
|
53
|
+
}
|
|
54
|
+
/** Append to THIS machine's chain. Fail-open: observation must never break the observed. */
|
|
55
|
+
function appendSessionContext(root, rec) {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
58
|
+
return false;
|
|
59
|
+
let replica = 'local';
|
|
60
|
+
try {
|
|
61
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
62
|
+
}
|
|
63
|
+
catch { /* keep the fallback */ }
|
|
64
|
+
const file = path.join(root, '.ax', 'ledger', sessionContextFilename(replica));
|
|
65
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
66
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Every replica's records, merged by ts (stable). Missing dir → []; corrupt lines skipped. */
|
|
74
|
+
function readSessionContext(root) {
|
|
75
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
76
|
+
let names;
|
|
77
|
+
try {
|
|
78
|
+
names = fs.readdirSync(dir).filter(isSessionContextFilename).sort();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const out = [];
|
|
84
|
+
let i = 0;
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
for (const line of text.split('\n')) {
|
|
94
|
+
const s = line.trim();
|
|
95
|
+
if (!s)
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
const r = JSON.parse(s);
|
|
99
|
+
if (r && typeof r === 'object' && (r.kind === 'client' || r.kind === 'usage')
|
|
100
|
+
&& typeof r.sessionKey === 'string' && typeof r.ts === 'string') {
|
|
101
|
+
out.push({ r: r, i: i++ });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out.sort((a, b) => (a.r.ts < b.r.ts ? -1 : a.r.ts > b.r.ts ? 1 : a.i - b.i)).map((e) => e.r);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* @implements A-SPEC-564.1
|
|
111
|
+
* The lazy-once stamper the MCP server wires into its call path. Lazy because `getClientVersion()`
|
|
112
|
+
* has a value only AFTER initialize — the first tool call is the earliest honest moment — and
|
|
113
|
+
* because the ledger's location arrives with the first `root`-carrying call. Once, because a
|
|
114
|
+
* session has one identity; a root-less call defers rather than consumes the chance.
|
|
115
|
+
*/
|
|
116
|
+
function makeSessionStamper(info, append = appendSessionContext, sessionKey = `mcp-${process.pid}`) {
|
|
117
|
+
let stamped = false;
|
|
118
|
+
return (root) => {
|
|
119
|
+
if (stamped || typeof root !== 'string' || root === '')
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
const v = (() => { try {
|
|
123
|
+
return info();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
} })();
|
|
128
|
+
stamped = append(root, {
|
|
129
|
+
kind: 'client',
|
|
130
|
+
sessionKey,
|
|
131
|
+
client: v?.name ?? 'unknown',
|
|
132
|
+
clientVersion: v?.version ?? 'unknown',
|
|
133
|
+
ts: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch { /* observation must never break the observed call */ }
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* @implements A-SPEC-564.2
|
|
141
|
+
* Numbers out of a Claude transcript — model ids, the four usage sums, a turn count. Streaming and
|
|
142
|
+
* capped: a giant transcript yields an honest partial sum with `truncated`, never a slow Stop (a
|
|
143
|
+
* huge response once blocked the very fix that would have shrunk it — observation must not repeat
|
|
144
|
+
* that shape). The return type has no field a conversation could leak through.
|
|
145
|
+
*/
|
|
146
|
+
function summarizeTranscript(lines, opts) {
|
|
147
|
+
const maxBytes = opts?.maxBytes ?? 64 * 1024 * 1024;
|
|
148
|
+
const models = [];
|
|
149
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
150
|
+
let turns = 0;
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
let truncated = false;
|
|
153
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
bytes += line.length + 1;
|
|
156
|
+
if (bytes > maxBytes) {
|
|
157
|
+
truncated = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
let m;
|
|
161
|
+
try {
|
|
162
|
+
const d = JSON.parse(line);
|
|
163
|
+
m = d && typeof d === 'object' ? d.message : undefined;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!m || typeof m !== 'object' || !m.usage || typeof m.usage !== 'object')
|
|
169
|
+
continue;
|
|
170
|
+
turns += 1;
|
|
171
|
+
if (typeof m.model === 'string' && m.model !== '' && !models.includes(m.model))
|
|
172
|
+
models.push(m.model);
|
|
173
|
+
usage.input += num(m.usage.input_tokens);
|
|
174
|
+
usage.output += num(m.usage.output_tokens);
|
|
175
|
+
usage.cacheRead += num(m.usage.cache_read_input_tokens);
|
|
176
|
+
usage.cacheCreation += num(m.usage.cache_creation_input_tokens);
|
|
177
|
+
}
|
|
178
|
+
return { models, usage, turns, truncated };
|
|
179
|
+
}
|
|
@@ -461,8 +461,11 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
461
461
|
// @implements A-SPEC-244 — the refusal itself files the review request. The queue kind is the
|
|
462
462
|
// approval SCOPE kind ('shell'), so the reviewing CLI can mint a covering grant mechanically.
|
|
463
463
|
if (!covers) {
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
// @implements A-SPEC-564.2 — the base reason's length rides along so the refusal record
|
|
465
|
+
// knows the FULL feedback cost (base + hint), not just the hint's half.
|
|
466
|
+
const base = 'requires an approval that covers this command';
|
|
467
|
+
return deny(base
|
|
468
|
+
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'shell', target: command, why: assessment.reasons.join('; ') }, { baseReasonBytes: base.length }));
|
|
466
469
|
}
|
|
467
470
|
// @implements A-SPEC-141
|
|
468
471
|
// Check and spend in ONE atomic operation. The previous shape asked `isNonceConsumed(...)`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
// @implements A-SPEC-566.4
|
|
37
|
+
/**
|
|
38
|
+
* The detached refresh child — the ONLY place this slice touches the scan/build machinery. Spawned
|
|
39
|
+
* by `maybeSpawnRtmRefresh` (rtm-refresh.ts) outside any turn; nothing in the hook path imports
|
|
40
|
+
* this file, which is what keeps A-SPEC-510.2 honest (no wasm rides into a gate process).
|
|
41
|
+
* rtm_impact's warm path IS the refresh: hash-cached scan, reusable-graph rebuild when stale.
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
if (require.main === module) {
|
|
46
|
+
try {
|
|
47
|
+
const root = process.argv[2];
|
|
48
|
+
if (root && fs.existsSync(path.join(root, '.ax'))) {
|
|
49
|
+
const { makeHandlers } = require('../mcp/handlers');
|
|
50
|
+
const { LocalMarkdownRepository } = require('../spec/spec-store');
|
|
51
|
+
const h = makeHandlers(new LocalMarkdownRepository(path.join(root, '.ax', 'specs')));
|
|
52
|
+
void h.rtm_impact({ root, changed: [] }).catch(() => undefined);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* fail-soft: a failed refresh leaves the old graph, which the advisory tolerates */ }
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const RTM_REFRESH_TTL_MS: number;
|
|
2
|
+
export declare const RTM_REFRESH_MARKER: string;
|
|
3
|
+
export interface RtmRefreshOpts {
|
|
4
|
+
root: string;
|
|
5
|
+
now: number;
|
|
6
|
+
execPath: string;
|
|
7
|
+
scriptPath: string;
|
|
8
|
+
spawn: (cmd: string, args: string[], opts: object) => {
|
|
9
|
+
unref?: () => void;
|
|
10
|
+
};
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function maybeSpawnRtmRefresh(opts: RtmRefreshOpts): void;
|