@holmes-lab/holmes-kit 0.23.2 → 0.23.3
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 +45 -0
- package/README.md +1 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.js +2 -0
- package/dist/holmes/hooks/pre-tool-use.js +2 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +48 -1
- package/dist/holmes/mcp/tool-schemas.js +2 -0
- package/dist/holmes/project/ci-lock.d.ts +20 -0
- package/dist/holmes/project/ci-lock.js +111 -0
- package/dist/holmes/project/dist-freshness.d.ts +30 -0
- package/dist/holmes/project/dist-freshness.js +114 -0
- package/dist/holmes/semantic/credentials.js +2 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,51 @@ 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.23.3] - 2026-09-18
|
|
9
|
+
|
|
10
|
+
A suite that takes half as long, a build that admits when it is stale, and a CI matrix that stopped
|
|
11
|
+
reporting its own noise as a verdict.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **The Files-to-Touch declarations that were anchored, and the ones that were not** (A-SPEC-670,
|
|
15
|
+
A-SPEC-665, A-SPEC-672, A-SPEC-674). Five trace gaps that REQ-670's parser fix uncovered are
|
|
16
|
+
closed: `A-SPEC-495`, `A-SPEC-549.3` and `A-SPEC-549.4` declared scanned production files and
|
|
17
|
+
carried no anchor in any of them. Every one of the six places was checked before it was claimed —
|
|
18
|
+
an anchor is an assertion, not a metric — and all six were true. Unlinked approved specs 52 → 49,
|
|
19
|
+
`codeLinkedPct` 91.95 → 92.41, trace gaps 5 → 0.
|
|
20
|
+
- **The CI matrix was reporting its own noise as a verdict** (A-SPEC-664, A-SPEC-674). Measured over
|
|
21
|
+
22 runs: **every one was red**, and the reds were dominated by two suites this repository knows as
|
|
22
|
+
load gauges (`dashboard.test` 20/22, `entity-store-boundaries` 12/22). The cause was contention —
|
|
23
|
+
the Linux VM takes 11 of the host's 12 cores and ran jest with default workers. A signal that is
|
|
24
|
+
always red cannot separate a regression from itself. With the worker cap below, both canaries went
|
|
25
|
+
green on Linux for the first time in 23 runs, and the matrix immediately produced its first real
|
|
26
|
+
finding.
|
|
27
|
+
- **A dead lock holder could stop the matrix for ever, silently** (A-SPEC-674). The runner's lock was
|
|
28
|
+
released by `trap ... EXIT`, which SIGKILL and a process-group teardown skip; one such lock stood
|
|
29
|
+
for 95 minutes while nine commits went unjudged. The lock now names its holder and liveness is
|
|
30
|
+
judged by PID, never by elapsed time — calling a slow run dead would start a second run on the
|
|
31
|
+
same machine. A skipped run also leaves a `skipped` row now: REQ-664's rule is that no row means
|
|
32
|
+
"not run", so a silent skip erased the skip itself.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **The Stop hook says when `dist/` no longer represents the source** (A-SPEC-673). Thirty suites
|
|
36
|
+
load `dist/` while they run and nothing asserted it was current; only the release gate compared the
|
|
37
|
+
build id to HEAD, and only at publish time. A stale build does not go red — it verifies old code
|
|
38
|
+
and returns green. The judgement is built on the build id and **not** on mtime, measured: this
|
|
39
|
+
tree's `.build-id` had a newer mtime than every source while naming a commit nine behind, and two
|
|
40
|
+
changed sources were missing from `dist` entirely. Five states, of which `fresh` and `absent` say
|
|
41
|
+
nothing at all, and the two unjudgeable ones say **the check could not run** rather than passing
|
|
42
|
+
quietly. Non-blocking, on the `tracked` channel, and silent in a workspace that does not build.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- **The test suite runs in 353s instead of 505s** (A-SPEC-672). The wall clock equalled the longest
|
|
46
|
+
single file to the decimal, so more workers could not help. Splitting the two heaviest files alone
|
|
47
|
+
moved it 505s → 493s while the serial total rose 2,836s → 5,109s: four heavy suites running at once
|
|
48
|
+
contended hard enough to swallow the win. Measured 2x2, the split and a worker cap **interact** —
|
|
49
|
+
alone they are worth 12s and 44s, together 132s. `jest.maxWorkers` is now `"50%"`, a proportion
|
|
50
|
+
rather than a number, because a fixed count means something else on a machine with a different core
|
|
51
|
+
count. Cases are preserved exactly, per-spec execution counts included.
|
|
52
|
+
|
|
8
53
|
## [0.23.2] - 2026-09-18
|
|
9
54
|
|
|
10
55
|
Three rules that existed only in prose, and the one number a broken parser had published.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
18
|
|
|
19
|
+
- 🧱 **A stale build is told, not discovered** *(new in 0.23.3)*: thirty suites in this project load `dist/` while they run, and nothing asserted that it still represented the source — only the release gate compared the build id to HEAD, and only at publish time. A stale build does not go red; it verifies old code and returns green. The Stop hook now reports it on the non-blocking `tracked` channel, judged by the **build id and never by mtime**: measured here, `.build-id` had a newer mtime than every source file while naming a commit nine behind HEAD, with two changed sources missing from `dist` entirely. A workspace that does not build hears nothing, `fresh` says nothing, and the two states that cannot be judged say **that** rather than passing quietly.
|
|
19
20
|
- 📐 **Declarations are read as written** *(new in 0.23.2)*: `Files to Touch` is where a spec declares the files it will touch, and three things read it — fulfilment advisories, the declaration census and the approval impact note. The parser took only the **first word of a list item**, so measured over 678 approved specs here, **88 specs and 172 paths were declared and never read**; 24 of them parsed to zero while naming files plainly. Several paths on one line, an indented continuation, a Korean first word, a prose paragraph — all invisible. They are read now, wherever they sit, and a bare `name.ext` keeps its old position rule so a property access like `module.exports` is still not a file. A declared path that does not exist but is the suffix of exactly one repository file is reported as an abbreviation rather than a defect; two candidates stays an ambiguity and nothing is guessed. Cost, measured across the whole corpus: **zero** new `missing` findings.
|
|
20
21
|
- 📄 **The publish gate reads the docs** *(new in 0.23.2)*: the publish playbook has demanded "bring README and CHANGELOG up to this release" since 0.16.0 and only prose enforced it, so it failed four times — including 0.21.0, 0.22.0 and 0.23.0, which each shipped with a feature list frozen at 0.20.0. The release gate now refuses two things it can decide: a missing CHANGELOG entry for the version being published, and an entry with `### Added` while `README.md` has not changed since the previous release. Replayed over eight releases it refuses exactly the three that were stale and passes the other five. What needs judgement — is the old wording still true? — stays with the person and is **reported**, never faked; a check that could not run says so instead of reading as a pass.
|
|
21
22
|
- 🧩 **Your config files survive a re-wire** *(new in 0.23.0)*: `init --agent antigravity` and `init --agent codex` used to replace `.agents/mcp_config.json`, `.agents/hooks.json` and `marketplace.json` **whole**. Measured with a real `--dry-run` before the fix: a neighbour MCP server, a neighbour hook namespace, a neighbour plugin, a marketplace's own name and an operator's `disabled` flag all survived a re-wire at a rate of **zero**. They now survive — only the holmes-kit entry is refreshed, and `init` names what it kept. A `disabled` you set stays set (and `init` says the gate will not run while it stands, rather than switching it back on silently); an existing file that is not readable JSON is refused with a reason instead of being replaced. The Claude wiring already merged; the other two harnesses now have the same discipline.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
970ee6cf-mu6tt9oo
|
|
@@ -48,6 +48,8 @@ exports.renderRefusals = renderRefusals;
|
|
|
48
48
|
exports.renderNonTtyHint = renderNonTtyHint;
|
|
49
49
|
exports.runInteractive = runInteractive;
|
|
50
50
|
// @implements A-SPEC-246, A-SPEC-626
|
|
51
|
+
// @implements A-SPEC-549.4 — renderPending and renderNonTtyHint in this file are the English CLI
|
|
52
|
+
// surface the Hangul-absence guard reads.
|
|
51
53
|
const execution_context_1 = require("../project/execution-context");
|
|
52
54
|
const root_1 = require("../project/root");
|
|
53
55
|
const fs = __importStar(require("node:fs"));
|
|
@@ -46,6 +46,8 @@ exports.wiredSpecsDir = wiredSpecsDir;
|
|
|
46
46
|
exports.evaluateHook = evaluateHook;
|
|
47
47
|
exports.readSpecsSync = readSpecsSync;
|
|
48
48
|
// @implements A-SPEC-278
|
|
49
|
+
// @implements A-SPEC-549.3 — the hook's deny reasons are English here; hooks-english.test.ts drives
|
|
50
|
+
// evaluateHook in this file to prove it.
|
|
49
51
|
// @implements A-SPEC-643
|
|
50
52
|
// @implements A-SPEC-642
|
|
51
53
|
// @implements A-SPEC-194, A-SPEC-195
|
|
@@ -3,7 +3,9 @@ import { Spec } from '../spec/spec-parser';
|
|
|
3
3
|
import type { TestOutcome } from '../review/test-runner';
|
|
4
4
|
import { type KnownDefectJudgement } from '../rtm/known-defects';
|
|
5
5
|
import { type CiVerdict } from '../project/ci-runs';
|
|
6
|
+
import { type DistVerdict } from '../project/dist-freshness';
|
|
6
7
|
export declare function collectKnownDefects(root: string, now: Date): KnownDefectJudgement | undefined;
|
|
8
|
+
export declare function collectDistFreshness(root: string): DistVerdict | undefined;
|
|
7
9
|
export declare function collectCiVerdicts(root: string, now?: Date): CiVerdict[];
|
|
8
10
|
/**
|
|
9
11
|
* @implements A-SPEC-100.2
|
|
@@ -38,6 +40,11 @@ export interface StopEvidence {
|
|
|
38
40
|
* hook could not look; a `not-run` verdict when it looked and found no row (never silence).
|
|
39
41
|
*/
|
|
40
42
|
ci?: CiVerdict[];
|
|
43
|
+
/**
|
|
44
|
+
* @implements A-SPEC-673 — whether `dist/` still represents the source. Absent when the workspace
|
|
45
|
+
* does not build at all; a `no-build-id`/`unknown` verdict when it looked and could not judge.
|
|
46
|
+
*/
|
|
47
|
+
dist?: DistVerdict;
|
|
41
48
|
/** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
|
|
42
49
|
provenance?: {
|
|
43
50
|
ok: boolean;
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
37
|
exports.collectKnownDefects = collectKnownDefects;
|
|
38
|
+
exports.collectDistFreshness = collectDistFreshness;
|
|
38
39
|
exports.collectCiVerdicts = collectCiVerdicts;
|
|
39
40
|
exports.changedAnchoredAspecs = changedAnchoredAspecs;
|
|
40
41
|
exports.unanchoredChangedSources = unanchoredChangedSources;
|
|
@@ -55,6 +56,8 @@ exports.guardCountOrZero = guardCountOrZero;
|
|
|
55
56
|
exports.readGuardCount = readGuardCount;
|
|
56
57
|
exports.writeGuardCount = writeGuardCount;
|
|
57
58
|
const fs = __importStar(require("node:fs"));
|
|
59
|
+
// @implements A-SPEC-549.3 — evaluateStop's messages are English here; the Hangul-absence guard
|
|
60
|
+
// calls into this file.
|
|
58
61
|
// @implements A-SPEC-645
|
|
59
62
|
// @implements A-SPEC-642
|
|
60
63
|
const npx_bin_1 = require("../project/npx-bin");
|
|
@@ -81,6 +84,7 @@ const root_1 = require("../project/root");
|
|
|
81
84
|
const known_defects_1 = require("../rtm/known-defects");
|
|
82
85
|
const test_files_1 = require("../cpg/test-files");
|
|
83
86
|
const ci_runs_1 = require("../project/ci-runs");
|
|
87
|
+
const dist_freshness_1 = require("../project/dist-freshness");
|
|
84
88
|
// @implements A-SPEC-660 — the I/O half of the known-defect marker: walk the workspace's test files
|
|
85
89
|
// (the same directory rule and test predicate ART-4's anchor scan uses), parse each for markers,
|
|
86
90
|
// judge them against the injected clock. A walk that cannot START is NO SIGNAL (undefined) — never
|
|
@@ -128,6 +132,32 @@ function collectKnownDefects(root, now) {
|
|
|
128
132
|
return undefined;
|
|
129
133
|
}
|
|
130
134
|
}
|
|
135
|
+
// @implements A-SPEC-673 — the I/O half of the build-freshness line: read `dist/.build-id` and ask
|
|
136
|
+
// git how far that commit sits behind HEAD. A workspace that does not build hears nothing; a build
|
|
137
|
+
// that cannot be judged says so rather than passing quietly.
|
|
138
|
+
function collectDistFreshness(root) {
|
|
139
|
+
if (!(0, dist_freshness_1.hasDist)(root))
|
|
140
|
+
return undefined;
|
|
141
|
+
const buildId = (0, dist_freshness_1.distBuildId)(root);
|
|
142
|
+
let head = null;
|
|
143
|
+
try {
|
|
144
|
+
head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
head = null;
|
|
148
|
+
}
|
|
149
|
+
let behind = null;
|
|
150
|
+
if (buildId && head) {
|
|
151
|
+
try {
|
|
152
|
+
const n = Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${buildId}..HEAD`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim());
|
|
153
|
+
behind = Number.isFinite(n) ? n : null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
behind = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return (0, dist_freshness_1.distFreshness)({ distExists: true, buildId, head, behind });
|
|
160
|
+
}
|
|
131
161
|
// @implements A-SPEC-664 — the I/O half of the CI line: read the ci-runs ledger, judge Linux (always)
|
|
132
162
|
// and any other OS that has a row, with git measuring how far each judged commit sits behind HEAD.
|
|
133
163
|
// A tree without a ledger yields a `not-run` verdict — a line, not an absence.
|
|
@@ -529,6 +559,7 @@ const TRACK_LABELS = {
|
|
|
529
559
|
'ART-2': 'code-graph cycles',
|
|
530
560
|
'ART-9': 'known-defect debt',
|
|
531
561
|
'CI': 'matrix',
|
|
562
|
+
'DIST': 'build freshness',
|
|
532
563
|
};
|
|
533
564
|
/**
|
|
534
565
|
* One line per ARTICLE, each under its own name.
|
|
@@ -602,6 +633,14 @@ function evaluateStop(specs, evidence) {
|
|
|
602
633
|
const t = evidence.ci.map((v) => ({ article: 'CI', detail: (0, ci_runs_1.ciStatusLine)(v) }));
|
|
603
634
|
tracked = [...(tracked ?? []), ...t];
|
|
604
635
|
}
|
|
636
|
+
// @implements A-SPEC-673 — a build that no longer represents the source. Thirty suites load `dist/`
|
|
637
|
+
// while they run, so a stale one does not go red: it verifies old code and returns green. `fresh`
|
|
638
|
+
// and `absent` produce no line, because a line is spent only on something the reader can act on.
|
|
639
|
+
if (evidence?.dist) {
|
|
640
|
+
const detail = (0, dist_freshness_1.distStatusLine)(evidence.dist);
|
|
641
|
+
if (detail)
|
|
642
|
+
tracked = [...(tracked ?? []), { article: 'DIST', detail }];
|
|
643
|
+
}
|
|
605
644
|
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
606
645
|
// @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
|
|
607
646
|
// are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
|
|
@@ -1194,7 +1233,15 @@ if (require.main === module) {
|
|
|
1194
1233
|
catch {
|
|
1195
1234
|
ci = undefined;
|
|
1196
1235
|
}
|
|
1197
|
-
|
|
1236
|
+
// @implements A-SPEC-673 — the build-freshness line rides the same non-blocking channel.
|
|
1237
|
+
let dist;
|
|
1238
|
+
try {
|
|
1239
|
+
dist = collectDistFreshness(stopProjectRoot());
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
dist = undefined;
|
|
1243
|
+
}
|
|
1244
|
+
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}), ...(dist ? { dist } : {}) });
|
|
1198
1245
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
1199
1246
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
1200
1247
|
// @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TOOL_SCHEMAS = exports.HOOK_ENFORCED_TOOLS = void 0;
|
|
4
4
|
// @implements A-SPEC-629, A-SPEC-628, A-SPEC-623, A-SPEC-277, A-SPEC-624, A-SPEC-627
|
|
5
|
+
// @implements A-SPEC-495 — the maintenance_analyze description carries the semantic-evidence
|
|
6
|
+
// consumer contract (sem-verify / semCos / semanticAlternates) beside the anti-prediction warning.
|
|
5
7
|
// @implements A-SPEC-642
|
|
6
8
|
// @implements A-SPEC-641
|
|
7
9
|
// @implements A-SPEC-639
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface CiLockHolder {
|
|
2
|
+
pid: number;
|
|
3
|
+
host: string;
|
|
4
|
+
rev: string;
|
|
5
|
+
at: string;
|
|
6
|
+
}
|
|
7
|
+
export type CiLockState = 'free' | 'held' | 'dead' | 'unknown';
|
|
8
|
+
export interface CiLockInput {
|
|
9
|
+
holder: CiLockHolder | null;
|
|
10
|
+
thisHost: string;
|
|
11
|
+
alive: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Null for every shape we cannot trust — including the legacy empty directory. */
|
|
14
|
+
export declare function readCiLock(lockDir: string): CiLockHolder | null;
|
|
15
|
+
/**
|
|
16
|
+
* A holder written by ANOTHER host is `unknown`, not dead: we cannot ask that machine about its
|
|
17
|
+
* pids, and guessing would hand two runners the same VM. Unknown is respected like `held`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ciLockState(input: CiLockInput): CiLockState;
|
|
20
|
+
export declare function ciLockLine(state: CiLockState, holder: CiLockHolder | null, now?: Date): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
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.readCiLock = readCiLock;
|
|
37
|
+
exports.ciLockState = ciLockState;
|
|
38
|
+
exports.ciLockLine = ciLockLine;
|
|
39
|
+
// @implements A-SPEC-674
|
|
40
|
+
/**
|
|
41
|
+
* Who holds the CI runner's lock, and is that holder still alive.
|
|
42
|
+
*
|
|
43
|
+
* The runner serialises itself with a `mkdir` lock — atomic, and that atomicity is the whole reason
|
|
44
|
+
* it works — released by `trap ... EXIT`. SIGKILL and launchd tearing down the process group skip
|
|
45
|
+
* that trap. Measured 2026-09-18: one such lock survived 95 minutes while the matrix judged nothing
|
|
46
|
+
* and nine commits went unjudged, with zero jest processes in the VM and a load of 0.57.
|
|
47
|
+
*
|
|
48
|
+
* The lock used to be an EMPTY directory, so it could not say who held it — and a lock that cannot
|
|
49
|
+
* name its holder cannot be asked whether that holder is alive. It now carries a holder file, and
|
|
50
|
+
* the judgement below is about liveness, never about elapsed time: calling a slow run dead would
|
|
51
|
+
* start a second run on the same VM, which is a worse failure than waiting.
|
|
52
|
+
*/
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
/** Null for every shape we cannot trust — including the legacy empty directory. */
|
|
56
|
+
function readCiLock(lockDir) {
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = fs.readFileSync(path.join(lockDir, 'holder.json'), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const h = parsed;
|
|
72
|
+
if (typeof h?.pid !== 'number' || !Number.isFinite(h.pid))
|
|
73
|
+
return null;
|
|
74
|
+
if (typeof h.host !== 'string' || typeof h.rev !== 'string' || typeof h.at !== 'string')
|
|
75
|
+
return null;
|
|
76
|
+
return { pid: h.pid, host: h.host, rev: h.rev, at: h.at };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A holder written by ANOTHER host is `unknown`, not dead: we cannot ask that machine about its
|
|
80
|
+
* pids, and guessing would hand two runners the same VM. Unknown is respected like `held`.
|
|
81
|
+
*/
|
|
82
|
+
function ciLockState(input) {
|
|
83
|
+
const { holder, thisHost, alive } = input;
|
|
84
|
+
if (!holder)
|
|
85
|
+
return 'free';
|
|
86
|
+
if (holder.host !== thisHost)
|
|
87
|
+
return 'unknown';
|
|
88
|
+
return alive ? 'held' : 'dead';
|
|
89
|
+
}
|
|
90
|
+
const elapsed = (at, now) => {
|
|
91
|
+
const t = Date.parse(at);
|
|
92
|
+
if (!Number.isFinite(t))
|
|
93
|
+
return null;
|
|
94
|
+
const mins = Math.floor((now.getTime() - t) / 60000);
|
|
95
|
+
// A clock that runs backwards is a fact about the clock, not about the holder; never report it as
|
|
96
|
+
// negative time.
|
|
97
|
+
return mins < 0 ? null : `${mins} min`;
|
|
98
|
+
};
|
|
99
|
+
function ciLockLine(state, holder, now = new Date()) {
|
|
100
|
+
if (state === 'free' || !holder)
|
|
101
|
+
return '';
|
|
102
|
+
const held = elapsed(holder.at, now);
|
|
103
|
+
const forHow = held ? ` for ${held}` : '';
|
|
104
|
+
if (state === 'dead') {
|
|
105
|
+
return `ci lock holder pid ${holder.pid} is gone (held${forHow}, rev ${holder.rev}) — reclaiming it; the matrix judges nothing while a dead holder stands`;
|
|
106
|
+
}
|
|
107
|
+
if (state === 'unknown') {
|
|
108
|
+
return `ci lock was taken by ${holder.host} (rev ${holder.rev}${forHow}) — this host cannot judge another machine's pids, so it is respected`;
|
|
109
|
+
}
|
|
110
|
+
return `ci lock is held by pid ${holder.pid} running rev ${holder.rev}${forHow}`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type DistState = 'fresh' | 'behind' | 'absent' | 'no-build-id' | 'unknown';
|
|
2
|
+
export interface DistVerdict {
|
|
3
|
+
state: DistState;
|
|
4
|
+
buildId: string | null;
|
|
5
|
+
head: string | null;
|
|
6
|
+
behind: number | null;
|
|
7
|
+
}
|
|
8
|
+
export interface DistInput {
|
|
9
|
+
distExists: boolean;
|
|
10
|
+
buildId: string | null;
|
|
11
|
+
head: string | null;
|
|
12
|
+
/** Commits from the build id to HEAD, or null when it could not be obtained. */
|
|
13
|
+
behind: number | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The adoption predicate: existence, never contents. A workspace that does not build has no build
|
|
17
|
+
* freshness to report, and telling it about one would be noise it can do nothing with (the same
|
|
18
|
+
* reasoning REQ-664 used for the CI ledger).
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasDist(root: string): boolean;
|
|
21
|
+
/** The commit the artefact came from — the part before the dash `npm run build` writes. */
|
|
22
|
+
export declare function distBuildId(root: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Five states because each calls for something different: `behind` means build, `absent` means
|
|
25
|
+
* nothing at all to this workspace, and the two unjudgeable ones mean the check did not run — which
|
|
26
|
+
* must never be reported as a check that passed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function distFreshness(input: DistInput): DistVerdict;
|
|
29
|
+
/** Empty when there is nothing to say. A line is spent only on a fact the reader can act on. */
|
|
30
|
+
export declare function distStatusLine(v: DistVerdict): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
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.hasDist = hasDist;
|
|
37
|
+
exports.distBuildId = distBuildId;
|
|
38
|
+
exports.distFreshness = distFreshness;
|
|
39
|
+
exports.distStatusLine = distStatusLine;
|
|
40
|
+
// @implements A-SPEC-673
|
|
41
|
+
/**
|
|
42
|
+
* Whether the built artefact still represents the source.
|
|
43
|
+
*
|
|
44
|
+
* Thirty suites in this repository load `dist/` while they run — they spawn it or require it — and
|
|
45
|
+
* nothing asserted that it was current. `scripts/verify-release.js` compares the build id to HEAD,
|
|
46
|
+
* but only at publish time, so during development a stale build does not go red: it quietly verifies
|
|
47
|
+
* old code and returns green. This repository has already paid for that once.
|
|
48
|
+
*
|
|
49
|
+
* The judgement is built on the BUILD ID, never on mtime, for a measured reason. On 2026-09-18 this
|
|
50
|
+
* tree's `dist` was nine commits behind HEAD and two changed sources were missing from it entirely,
|
|
51
|
+
* while `find src -newer dist/.build-id` answered zero — the marker's mtime had been touched without
|
|
52
|
+
* a rebuild. mtime lies. The build id says which commit the artefact actually came from.
|
|
53
|
+
*
|
|
54
|
+
* This module only JUDGES. It never rebuilds: a hook does not change the operator's tree in silence.
|
|
55
|
+
*/
|
|
56
|
+
const fs = __importStar(require("node:fs"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
/**
|
|
59
|
+
* The adoption predicate: existence, never contents. A workspace that does not build has no build
|
|
60
|
+
* freshness to report, and telling it about one would be noise it can do nothing with (the same
|
|
61
|
+
* reasoning REQ-664 used for the CI ledger).
|
|
62
|
+
*/
|
|
63
|
+
function hasDist(root) {
|
|
64
|
+
try {
|
|
65
|
+
return fs.statSync(path.join(root, 'dist')).isDirectory();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** The commit the artefact came from — the part before the dash `npm run build` writes. */
|
|
72
|
+
function distBuildId(root) {
|
|
73
|
+
let raw;
|
|
74
|
+
try {
|
|
75
|
+
raw = fs.readFileSync(path.join(root, 'dist', '.build-id'), 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const id = raw.trim().split('-')[0];
|
|
81
|
+
// A shape we do not recognise falls through to a judgement we cannot make, never to a pass.
|
|
82
|
+
return /^[0-9a-f]{7,40}$/.test(id) ? id : null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Five states because each calls for something different: `behind` means build, `absent` means
|
|
86
|
+
* nothing at all to this workspace, and the two unjudgeable ones mean the check did not run — which
|
|
87
|
+
* must never be reported as a check that passed.
|
|
88
|
+
*/
|
|
89
|
+
function distFreshness(input) {
|
|
90
|
+
const { distExists, buildId, head, behind } = input;
|
|
91
|
+
const base = { buildId: buildId ?? null, head: head ?? null, behind: typeof behind === 'number' ? behind : null };
|
|
92
|
+
if (!distExists)
|
|
93
|
+
return { state: 'absent', ...base };
|
|
94
|
+
if (!buildId)
|
|
95
|
+
return { state: 'no-build-id', ...base };
|
|
96
|
+
if (!head || typeof behind !== 'number' || !Number.isFinite(behind) || behind < 0)
|
|
97
|
+
return { state: 'unknown', ...base };
|
|
98
|
+
return { state: behind === 0 ? 'fresh' : 'behind', ...base };
|
|
99
|
+
}
|
|
100
|
+
/** Empty when there is nothing to say. A line is spent only on a fact the reader can act on. */
|
|
101
|
+
function distStatusLine(v) {
|
|
102
|
+
if (v.state === 'fresh' || v.state === 'absent')
|
|
103
|
+
return '';
|
|
104
|
+
if (v.state === 'no-build-id') {
|
|
105
|
+
return 'dist/.build-id is missing or unreadable — the build could not be judged against the source; `npm run build` writes it';
|
|
106
|
+
}
|
|
107
|
+
if (v.state === 'unknown') {
|
|
108
|
+
return `dist was built from ${v.buildId ?? 'an unknown commit'} and the distance to HEAD could not be obtained — the build could not be judged`;
|
|
109
|
+
}
|
|
110
|
+
// No invented threshold: one commit is reported like nine, because which commits matter is a
|
|
111
|
+
// question the reader can answer and this function cannot.
|
|
112
|
+
const n = v.behind ?? 0;
|
|
113
|
+
return `dist is ${n} commit${n === 1 ? '' : 's'} behind HEAD (built from ${v.buildId}) — suites that load dist are verifying that build, not this tree; \`npm run build\``;
|
|
114
|
+
}
|
|
@@ -37,6 +37,8 @@ exports.resolveSemanticKey = resolveSemanticKey;
|
|
|
37
37
|
exports.storeSemanticKey = storeSemanticKey;
|
|
38
38
|
exports.removeSemanticKey = removeSemanticKey;
|
|
39
39
|
// @implements A-SPEC-592
|
|
40
|
+
// @implements A-SPEC-549.4 — declared by that spec's Files to Touch and carries no Hangul; note that
|
|
41
|
+
// the guard's own SOURCES list does not cover this file (reported, not fixed here).
|
|
40
42
|
// @implements A-SPEC-477
|
|
41
43
|
/**
|
|
42
44
|
* The cloud tier's credential: WHERE the consent lives, and in what order it is looked up.
|
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.23.
|
|
4
|
+
"version": "0.23.3",
|
|
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",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"testTimeout": 30000,
|
|
63
63
|
"setupFilesAfterEnv": [
|
|
64
64
|
"<rootDir>/src/holmes/test-support/jest-timeouts-setup.ts"
|
|
65
|
-
]
|
|
65
|
+
],
|
|
66
|
+
"maxWorkers": "50%"
|
|
66
67
|
},
|
|
67
68
|
"dependencies": {
|
|
68
69
|
"@modelcontextprotocol/sdk": "^1.29.0",
|