vigiles 5.1.0 → 6.0.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.
Files changed (57) hide show
  1. package/README.md +59 -18
  2. package/dist/adapters/claude-code/adapter.js +1 -0
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +45 -6
  4. package/dist/adapters/claude-code/agent-runtime.js +94 -8
  5. package/dist/adapters/claude-code/dialect.d.ts +34 -0
  6. package/dist/adapters/claude-code/dialect.js +51 -19
  7. package/dist/adapters/claude-code/effect-region.d.ts +9 -0
  8. package/dist/adapters/claude-code/effect-region.js +45 -0
  9. package/dist/adapters/claude-code/layout.js +3 -0
  10. package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
  11. package/dist/adapters/claude-code/skill-runtime.js +40 -0
  12. package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
  13. package/dist/adapters/claude-code/typed-spec.js +55 -0
  14. package/dist/adapters/codex/adapter.js +3 -0
  15. package/dist/adapters/codex/layout.js +3 -0
  16. package/dist/adapters/opencode/adapter.js +1 -0
  17. package/dist/adapters/opencode/layout.js +3 -0
  18. package/dist/check.d.ts +8 -0
  19. package/dist/check.js +27 -3
  20. package/dist/claude-code.d.ts +1 -0
  21. package/dist/claude-code.js +8 -1
  22. package/dist/cli.js +469 -88
  23. package/dist/core/adapter.d.ts +10 -0
  24. package/dist/core/bash-effects.d.ts +41 -0
  25. package/dist/core/bash-effects.js +405 -0
  26. package/dist/core/compile.d.ts +3 -1
  27. package/dist/core/compile.js +176 -39
  28. package/dist/core/dialect.d.ts +10 -0
  29. package/dist/core/effects.d.ts +172 -0
  30. package/dist/core/effects.js +245 -0
  31. package/dist/core/generate-harness.d.ts +187 -0
  32. package/dist/core/generate-harness.js +337 -0
  33. package/dist/core/layout.d.ts +6 -0
  34. package/dist/core/mcp-tool.d.ts +1 -1
  35. package/dist/core/orphans.js +21 -0
  36. package/dist/core/spec.d.ts +432 -11
  37. package/dist/core/spec.js +166 -3
  38. package/dist/core/tool-contract.d.ts +1 -1
  39. package/dist/core/types.d.ts +6 -6
  40. package/dist/core/validate.js +4 -4
  41. package/dist/harness-test.d.ts +7 -0
  42. package/dist/harness-test.js +19 -7
  43. package/dist/leaderboard.d.ts +2 -0
  44. package/dist/leaderboard.js +2 -0
  45. package/dist/optimize.d.ts +74 -0
  46. package/dist/optimize.js +94 -0
  47. package/dist/scaffold-test.d.ts +58 -0
  48. package/dist/scaffold-test.js +263 -0
  49. package/dist/scan.d.ts +40 -0
  50. package/dist/scan.js +91 -43
  51. package/dist/score-explainer.d.ts +69 -0
  52. package/dist/score-explainer.js +169 -0
  53. package/dist/test-coverage.d.ts +7 -0
  54. package/dist/test-coverage.js +39 -24
  55. package/package.json +2 -1
  56. package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -4
  57. package/skills/edit-spec/SKILL.md +1 -1
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyToolEffect = classifyToolEffect;
4
+ exports.effectSurface = effectSurface;
5
+ exports.purityViolations = purityViolations;
6
+ exports.pureContractViolations = pureContractViolations;
7
+ exports.decidePurityGate = decidePurityGate;
8
+ const hash_js_1 = require("./hash.js");
9
+ const bash_effects_js_1 = require("./bash-effects.js");
10
+ // ---------------------------------------------------------------------------
11
+ // Internal helpers
12
+ // ---------------------------------------------------------------------------
13
+ /** Strips a `Tool(restriction)` suffix and returns the base tool name. */
14
+ function baseTool(raw) {
15
+ return raw.split("(")[0].trim();
16
+ }
17
+ /** Returns true for the wildcard sentinels that mean "inherits-all". */
18
+ function isWildcard(tool) {
19
+ return tool === "" || tool === "*";
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Public API
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Classify the effect of ONE tool name against a dialect's known catalogs.
26
+ *
27
+ * A `Tool(restriction)` suffix (e.g. `Bash(git:*)`) is stripped first — the
28
+ * restriction narrows what the tool can DO but doesn't change its effect class
29
+ * (Bash with any restriction is still conservatively side-effecting).
30
+ *
31
+ * Classification rules (in priority order):
32
+ * 1. In `dialect.sideEffectingTools` → `"side-effecting"`
33
+ * 2. In `dialect.builtinAgentTools` (and NOT side-effecting) → `"read-only"`
34
+ * 3. Matches `dialect.mcpToolPattern` → `"unknown"` (MCP tools are not
35
+ * classifiable from the name alone — treated as unknown-effect)
36
+ * 4. Otherwise → `"unknown"` (unrecognized tool; may be a plugin tool or a typo)
37
+ */
38
+ function classifyToolEffect(tool, dialect) {
39
+ const base = baseTool(tool);
40
+ const sideEffecting = dialect.sideEffectingTools ?? [];
41
+ if (sideEffecting.includes(base))
42
+ return "side-effecting";
43
+ if (dialect.builtinAgentTools.includes(base))
44
+ return "read-only";
45
+ if (dialect.mcpToolPattern.test(base))
46
+ return "unknown";
47
+ return "unknown";
48
+ }
49
+ /**
50
+ * Compute the static effect surface of a declared `tools:` contract.
51
+ *
52
+ * `"*"` / `""` (inherits-all) entries make purity `"unrestricted"` because the
53
+ * contract grants access to all tools including every side-effecting one — the
54
+ * full surface is unknowable statically. They are NOT listed in any bucket
55
+ * (they represent a wildcard, not a named tool).
56
+ *
57
+ * De-duplication: a tool name that appears more than once in `tools` is counted
58
+ * once in its bucket (base tool after restriction stripping).
59
+ */
60
+ function effectSurface(tools, dialect) {
61
+ const readOnly = new Set();
62
+ const sideEffecting = new Set();
63
+ const unknown = new Set();
64
+ let hasWildcard = false;
65
+ let hasBash = false;
66
+ for (const raw of tools) {
67
+ const base = baseTool(raw);
68
+ if (isWildcard(base)) {
69
+ hasWildcard = true;
70
+ continue; // wildcards don't go into any named bucket
71
+ }
72
+ const effect = classifyToolEffect(raw, dialect);
73
+ switch (effect) {
74
+ case "read-only":
75
+ readOnly.add(base);
76
+ break;
77
+ case "side-effecting":
78
+ sideEffecting.add(base);
79
+ if (base === "Bash")
80
+ hasBash = true;
81
+ break;
82
+ case "unknown":
83
+ unknown.add(base);
84
+ break;
85
+ default:
86
+ (0, hash_js_1.assertNever)(effect);
87
+ }
88
+ }
89
+ const purity = hasWildcard || hasBash || unknown.size > 0
90
+ ? "unrestricted"
91
+ : sideEffecting.size > 0
92
+ ? "bounded"
93
+ : "pure";
94
+ return {
95
+ readOnly: [...readOnly],
96
+ sideEffecting: [...sideEffecting],
97
+ unknown: [...unknown],
98
+ purity,
99
+ };
100
+ }
101
+ /**
102
+ * Returns the violations of a DECLARED purity floor — the tools that make the
103
+ * actual effect surface LOOSER than the declared level. Empty ⇒ the contract
104
+ * honours the declared level. The `message` on each is actionable (names the
105
+ * tool, the effect class, and what to do).
106
+ *
107
+ * What counts as a violation depends on `declared`:
108
+ * - `"pure"`: every side-effecting tool (incl. `Bash`), every
109
+ * unknown-effect tool, and any wildcard (a pure unit may
110
+ * only observe — no `Bash`, no effects, fully static).
111
+ * - `"bounded"`: only the truly UNBOUNDED tools — unknown-effect (MCP /
112
+ * unrecognized) and wildcards. Every decidable side-effecting
113
+ * tool is ALLOWED: Write/Edit confine to the boundary, and
114
+ * `Bash` is admitted because the RUNTIME gate
115
+ * (`decidePurityGate`) refines it by command (read-only Bash
116
+ * is an observation; a mutating command is denied).
117
+ * - `"unrestricted"`: never a violation (the rung carries no constraint).
118
+ *
119
+ * A wildcard (`"*"` / `""`) contract is a violation at every constrained level:
120
+ * "inherits-all" grants every tool, so neither `pure` nor `bounded` can hold.
121
+ */
122
+ function purityViolations(tools, dialect, declared) {
123
+ if (declared === "unrestricted")
124
+ return []; // no constraint to violate
125
+ const violations = [];
126
+ const seen = new Set();
127
+ for (const raw of tools) {
128
+ const base = baseTool(raw);
129
+ if (isWildcard(base)) {
130
+ const key = base === "" ? '""' : '"*"';
131
+ if (!seen.has(key)) {
132
+ seen.add(key);
133
+ violations.push({
134
+ tool: base,
135
+ effect: "side-effecting",
136
+ message: `${key} (inherits-all) is not allowed in a ${declared} contract — it grants access to every tool, including side-effecting ones. Declare explicit tools instead.`,
137
+ });
138
+ }
139
+ continue;
140
+ }
141
+ if (seen.has(base))
142
+ continue;
143
+ const effect = classifyToolEffect(raw, dialect);
144
+ switch (effect) {
145
+ case "read-only":
146
+ break; // allowed at every level
147
+ case "unknown":
148
+ seen.add(base);
149
+ violations.push({
150
+ tool: base,
151
+ effect,
152
+ message: `"${base}" has unknown effect class (MCP or unrecognized tool); a ${declared} contract cannot declare it — its effects are unbounded from static analysis. Remove it or declare the unit dangerously-unrestricted.`,
153
+ });
154
+ break;
155
+ case "side-effecting":
156
+ // In a BOUNDED unit every decidable side-effecting tool is allowed:
157
+ // Write/Edit confine to the boundary, and `Bash` is admitted because the
158
+ // RUNTIME gate (`decidePurityGate`) refines it by command — a read-only
159
+ // Bash is an observation, a mutating command is denied. Only `pure` bars
160
+ // them (a pure unit may only observe; no `Bash`, no effects).
161
+ if (declared === "bounded")
162
+ break;
163
+ seen.add(base);
164
+ violations.push({
165
+ tool: base,
166
+ effect,
167
+ message: base === "Bash"
168
+ ? `"Bash" is undecidable at the tool-name level; a pure unit cannot declare it. A read-only Bash belongs in a bounded unit (the runtime gate confines it by command) — declare the unit bounded or dangerously-unrestricted.`
169
+ : `"${base}" is side-effecting; a pure unit cannot declare it. Remove it or declare the unit bounded.`,
170
+ });
171
+ break;
172
+ default:
173
+ (0, hash_js_1.assertNever)(effect);
174
+ }
175
+ }
176
+ return violations;
177
+ }
178
+ /**
179
+ * The violations of a `purity: "pure"` contract — every side-effecting,
180
+ * unknown-effect, or wildcard tool. A thin alias for `purityViolations(…,
181
+ * "pure")` kept for the common pure case.
182
+ */
183
+ function pureContractViolations(tools, dialect) {
184
+ return purityViolations(tools, dialect, "pure");
185
+ }
186
+ /**
187
+ * The RUNTIME half of the purity contract: decide whether a single LIVE tool
188
+ * call is allowed under the active unit's declared purity floor.
189
+ *
190
+ * Unlike `purityViolations` (which checks the DECLARED tools contract
191
+ * statically), this sees the ACTUAL call — including the `Bash` command string —
192
+ * so it refines `Bash` by effect via `isReadOnlyBash`. That command is the whole
193
+ * reason the gate's home is the runtime hook: only here is the concrete command
194
+ * visible (the static surface sees a tool name + a `Bash(git:*)` pattern, never
195
+ * the command).
196
+ *
197
+ * Rules (the ladder, command-refined):
198
+ * - `unrestricted` → always allow (no constraint).
199
+ * - read-only tool → allow at every level.
200
+ * - `Bash` → allow iff the command is provably read-only (an observation);
201
+ * otherwise deny — a mutating/undecidable command's effect must move to a
202
+ * marked boundary. Same at `pure` and `bounded`.
203
+ * - other side-effecting tool (Write, Edit, …) → allow under `bounded`
204
+ * (a decidable, boundary-confined effect), deny under `pure` (observe-only).
205
+ * - unknown-effect (MCP / unrecognized) → deny under `pure`/`bounded`
206
+ * (unbounded from static analysis).
207
+ *
208
+ * Dialect injected (core ⊄ adapter). Reuses `classifyToolEffect` +
209
+ * `isReadOnlyBash` — one-detector-no-drift with compile + scan.
210
+ */
211
+ function decidePurityGate(declared, tool, command, dialect) {
212
+ if (declared === "unrestricted")
213
+ return { allow: true, message: "" };
214
+ const base = baseTool(tool);
215
+ const effect = classifyToolEffect(tool, dialect);
216
+ if (effect === "read-only")
217
+ return { allow: true, message: "" };
218
+ if (base === "Bash") {
219
+ if (command !== undefined && (0, bash_effects_js_1.isReadOnlyBash)(command)) {
220
+ return { allow: true, message: "" };
221
+ }
222
+ const shown = command ? `"${command}" ` : "";
223
+ return {
224
+ allow: false,
225
+ message: `Bash command ${shown}is not provably read-only; a ${declared} unit may run only read-only Bash ` +
226
+ `(observation). Move the side effect into a marked boundary, or declare the unit dangerously-unrestricted.`,
227
+ };
228
+ }
229
+ if (effect === "side-effecting") {
230
+ if (declared === "bounded")
231
+ return { allow: true, message: "" };
232
+ return {
233
+ allow: false,
234
+ message: `"${base}" is side-effecting; a pure unit may only observe. ` +
235
+ `Declare the unit bounded (or dangerously-unrestricted) to use it.`,
236
+ };
237
+ }
238
+ // unknown — MCP / unrecognized tool
239
+ return {
240
+ allow: false,
241
+ message: `"${base}" has unknown effect class; a ${declared} unit cannot use it — its effects are unbounded. ` +
242
+ `Declare the unit dangerously-unrestricted to allow it.`,
243
+ };
244
+ }
245
+ //# sourceMappingURL=effects.js.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * vigiles generate-harness — emit ONE typed registry over the whole harness.
3
+ *
4
+ * The third generated artifact beside `generate-types` (`.d.ts`) and
5
+ * `generate-schema` (JSON Schema): a `harness.gen.ts` that imports every
6
+ * `*.spec.ts` in a directory, folds the agents into a `registry`, and asserts
7
+ * the cross-spec invariants at the TYPE level — so a single `tsc --noEmit`
8
+ * checks the WHOLE harness as one program (think TanStack's `routeTree.gen.ts`).
9
+ * See research/whole-harness-codegen.md for the design + the measured perf.
10
+ *
11
+ * The shipped scope (the first increment):
12
+ * 1. DANGLING `delegate` → a `tsc` error. Each `railway()` delegate target is a
13
+ * name the generator reads at codegen time; the gen file emits one shallow
14
+ * per-edge assertion (`KnownAgentName<"target", AgentName>` — O(N), no
15
+ * recursion) that the target resolves to a real agent, else a `tsc` error
16
+ * naming the dangling target + its railway.
17
+ * 2. DUPLICATE agent/skill NAMES → a generator error (this module returns a
18
+ * `duplicate` diagnostic; the CLI exits non-zero). This is the O(N) JS check
19
+ * the encoding rule mandates — a set-uniqueness MAPPED TYPE is the TS2589
20
+ * wall (measured ≈ N=1000), so duplicates are NEVER a type.
21
+ * 3. The whole-harness CAPABILITY LATTICE: the UNION of every agent's
22
+ * `effectSurface(tools, dialect)` — a generator-computed value + type, the
23
+ * substrate the future repo-scale capability-diff reads.
24
+ * 4. CROSS-FILE TYPED COMPOSITION: when a `railway()` success-track step declares
25
+ * what it `needs()`, the gen file emits one shallow per-pair assertion
26
+ * (`Handoff<OkOf<typeof registry[producer]>, needs>` — O(N), no recursion)
27
+ * that the PRIOR step's `result().ok` SUPPLIES it, so a cross-file handoff
28
+ * mismatch (a missing field / wrong type) is a `tsc` error naming the field.
29
+ * The repo-scale generalization of the per-file `pipe`/`Supplies` composition.
30
+ * Scoped to the linear success track; recover/onError (which consume an `err`,
31
+ * not the prior `ok`) are a noted follow-up.
32
+ *
33
+ * Harness-agnostic: the `dialect` (for the capability lattice) is INJECTED by
34
+ * the composition root (the CLI), never hard-coded — mirroring `compileAgent` /
35
+ * `scanPlugin`. The core stays free of any Claude-Code literal.
36
+ */
37
+ import type { HarnessDialect } from "./dialect.js";
38
+ import { type PurityLevel } from "./effects.js";
39
+ /** One agent the harness defines (its registry-relevant facts). */
40
+ export interface HarnessAgentEntry {
41
+ /** The agent's dispatch name — also a registry key + the dangling-check union. */
42
+ readonly name: string;
43
+ /** The agent's declared tool contract (used to compute its effect surface). */
44
+ readonly tools?: readonly string[];
45
+ /** The spec file this agent came from (for import + duplicate diagnostics). */
46
+ readonly file: string;
47
+ }
48
+ /** One delegate edge: a railway dispatches `target` (resolved against agents). */
49
+ export interface HarnessDelegateEdge {
50
+ /** The railway / orchestrator the edge originates from (for the diagnostic). */
51
+ readonly from: string;
52
+ /** The delegate target name — must resolve to a known agent, else dangling. */
53
+ readonly target: string;
54
+ }
55
+ /**
56
+ * One CROSS-FILE handoff edge: a consecutive success-track pair where the
57
+ * CONSUMER (`to`) declares the input it `needs`, asserted against the PRODUCER
58
+ * (`from`, the prior step's agent) `result().ok` at the type level. The
59
+ * registry already imports each agent's `TypedAgentSpec`, so the generator reads
60
+ * the producer's `ok` shape off the registry (`OkOf<typeof registry[from]>`) and
61
+ * emits one shallow `Handoff<>` assertion per such pair (O(N), no recursion).
62
+ */
63
+ export interface HarnessHandoffEdge {
64
+ /** The railway the edge originates from (for the diagnostic). */
65
+ readonly railway: string;
66
+ /** The PRODUCER agent name (the prior success-track step) — registry key. */
67
+ readonly from: string;
68
+ /** The CONSUMER agent name (this step) — names the failing edge. */
69
+ readonly to: string;
70
+ /** The consumer's declared input shape (`needs(...)`) — the literal emitted. */
71
+ readonly needs: Readonly<Record<string, string>>;
72
+ }
73
+ /** Everything the generator needs, already loaded (the pure-core input). */
74
+ export interface HarnessModel {
75
+ readonly agents: readonly HarnessAgentEntry[];
76
+ readonly edges: readonly HarnessDelegateEdge[];
77
+ /**
78
+ * Cross-file handoff edges (consecutive success-track step pairs whose
79
+ * consumer declares `needs`). Optional + defaults to none, so an existing
80
+ * model with no handoffs generates exactly as before — backwards-compatible.
81
+ */
82
+ readonly handoffs?: readonly HarnessHandoffEdge[];
83
+ }
84
+ export interface GenerateHarnessOptions {
85
+ /** The dialect the capability lattice is computed against (injected). */
86
+ readonly dialect: HarnessDialect;
87
+ /**
88
+ * The module specifier the generated file imports `KnownAgentName` from.
89
+ * Defaults to the public package (`"vigiles/spec"`); the in-repo dogfood
90
+ * passes a relative path so the generated file resolves without the package.
91
+ */
92
+ readonly specImport?: string;
93
+ /** The directory the gen file will be written to (to relativize spec imports). */
94
+ readonly outDir: string;
95
+ }
96
+ /** A duplicate-name collision found at codegen time (the O(N) JS check). */
97
+ export interface DuplicateNameDiagnostic {
98
+ readonly name: string;
99
+ readonly first: string;
100
+ readonly second: string;
101
+ /** A ready-to-print message. */
102
+ readonly message: string;
103
+ }
104
+ /** The whole-harness capability lattice — the union of every agent's surface. */
105
+ export interface HarnessCapabilities {
106
+ /** Every read-only tool reachable anywhere in the harness (de-duped, sorted). */
107
+ readonly readOnly: readonly string[];
108
+ /** Every side-effecting tool reachable anywhere (de-duped, sorted). */
109
+ readonly sideEffecting: readonly string[];
110
+ /** Every unknown-effect tool (MCP / unrecognized) reachable anywhere. */
111
+ readonly unknown: readonly string[];
112
+ /** The harness-wide purity: the LOOSEST purity of any single agent. */
113
+ readonly purity: PurityLevel;
114
+ }
115
+ export interface GenerateHarnessResult {
116
+ /** The generated `harness.gen.ts` source (always produced — even on a dup, so
117
+ * the caller can decide; the CLI gates the WRITE on `duplicate` being absent). */
118
+ readonly gen: string;
119
+ /** The computed capability lattice (also embedded in `gen`). */
120
+ readonly capabilities: HarnessCapabilities;
121
+ /** Set iff two agents declare the same name — the caller exits non-zero. */
122
+ readonly duplicate?: DuplicateNameDiagnostic;
123
+ /** The number of agents + edges folded in (for the CLI summary). */
124
+ readonly agentCount: number;
125
+ readonly edgeCount: number;
126
+ /** The number of cross-file handoff assertions emitted (for the CLI summary). */
127
+ readonly handoffCount: number;
128
+ }
129
+ /**
130
+ * Fold every agent's `effectSurface` into one harness-wide lattice: the union of
131
+ * each bucket and the loosest purity. An agent with no `tools` inherits all (a
132
+ * wildcard), so its surface is `unrestricted` — handled by `effectSurface` when
133
+ * we pass `["*"]`. O(N) over the agents; the per-agent legs are fixed-arity.
134
+ */
135
+ export declare function computeHarnessCapabilities(agents: readonly HarnessAgentEntry[], dialect: HarnessDialect): HarnessCapabilities;
136
+ /**
137
+ * Find the FIRST pair of agents that declare the same `name`. O(N) over the
138
+ * agents — the set-cardinality check the encoding rule says must live in the JS
139
+ * generator, never as an N×N mapped type (the measured TS2589 wall). Returns
140
+ * `undefined` when names are unique.
141
+ */
142
+ export declare function findDuplicateName(agents: readonly HarnessAgentEntry[]): DuplicateNameDiagnostic | undefined;
143
+ /**
144
+ * Generate the `harness.gen.ts` source over an already-loaded `HarnessModel`.
145
+ *
146
+ * Pure: no filesystem read, no spec loading — just string emission + the two
147
+ * O(N) computations (capability lattice + duplicate check). The fs/scan wrapper
148
+ * (`loadHarnessModel`) feeds this.
149
+ */
150
+ export declare function generateHarness(model: HarnessModel, options: GenerateHarnessOptions): GenerateHarnessResult;
151
+ /** A minimal shape of a loaded spec value (the fields the model reads). */
152
+ interface LoadedSpecLike {
153
+ readonly _specType?: string;
154
+ readonly name?: string;
155
+ readonly tools?: readonly string[];
156
+ readonly steps?: readonly {
157
+ readonly agent?: string;
158
+ readonly needs?: Readonly<Record<string, string>>;
159
+ }[];
160
+ readonly onError?: {
161
+ readonly agent?: string;
162
+ };
163
+ readonly recover?: {
164
+ readonly step?: {
165
+ readonly agent?: string;
166
+ };
167
+ };
168
+ }
169
+ /** Discover every `*.spec.ts` directly under `dir` (non-recursive, sorted). */
170
+ export declare function findHarnessSpecFiles(dir: string): string[];
171
+ /**
172
+ * Build a `HarnessModel` from `dir`'s spec files using a caller-supplied
173
+ * `load(file) → value` (the CLI injects its `loadSpec`, so this stays
174
+ * fs/runtime-agnostic and unit-testable with fakes). Agents become registry
175
+ * entries; railways contribute delegate edges (steps + recover + onError).
176
+ */
177
+ export declare function loadHarnessModel(dir: string, load: (absFile: string) => Promise<LoadedSpecLike | null>): Promise<HarnessModel>;
178
+ /** Convenience: the gen file's basename, used by the CLI default out path. */
179
+ export declare const HARNESS_GEN_FILENAME = "harness.gen.ts";
180
+ /** Relative label for a path under cwd (CLI-only nicety; pure). */
181
+ export declare function labelFor(cwd: string, abs: string): string;
182
+ /** Read a spec file's raw text (helper exposed for callers that need the source). */
183
+ export declare function readSpecSource(absFile: string): string;
184
+ /** The directory a gen file at `outFile` lives in (helper for the CLI). */
185
+ export declare function genOutDir(outFile: string): string;
186
+ export {};
187
+ //# sourceMappingURL=generate-harness.d.ts.map