vigiles 5.2.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.
package/dist/cli.js CHANGED
@@ -14,12 +14,14 @@ const node_fs_1 = require("node:fs");
14
14
  const node_path_1 = require("node:path");
15
15
  const glob_1 = require("glob");
16
16
  const generate_types_js_1 = require("./core/generate-types.js");
17
+ const generate_harness_js_1 = require("./core/generate-harness.js");
17
18
  const validate_js_1 = require("./core/validate.js");
18
19
  const cli_flags_js_1 = require("./cli-flags.js");
19
20
  const setup_plan_js_1 = require("./setup-plan.js");
20
21
  const types_js_1 = require("./core/types.js");
21
22
  const test_coverage_js_1 = require("./test-coverage.js");
22
23
  const scaffold_test_js_1 = require("./scaffold-test.js");
24
+ const effects_js_1 = require("./core/effects.js");
23
25
  const scan_js_1 = require("./scan.js");
24
26
  const score_explainer_js_1 = require("./score-explainer.js");
25
27
  const scan_behavioral_js_1 = require("./scan-behavioral.js");
@@ -2573,6 +2575,78 @@ function handleGenerateSchema(args, restArgs) {
2573
2575
  console.log(" Add to your markdown frontmatter:\n" +
2574
2576
  ` # yaml-language-server: $schema=./${outPath}`);
2575
2577
  }
2578
+ /**
2579
+ * `vigiles generate-harness [dir] [out]` — emit one typed registry over every
2580
+ * `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
2581
+ * harness (dangling delegates → a tsc error; duplicate names → this command
2582
+ * exits non-zero; the capability lattice → a computed export). The third
2583
+ * generated artifact beside `generate-types` / `generate-schema`. See
2584
+ * docs/cli.md and research/whole-harness-codegen.md.
2585
+ */
2586
+ async function handleGenerateHarness(args, restArgs) {
2587
+ const checkOnly = args.includes("--check");
2588
+ const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
2589
+ const outPath = restArgs[1] ?? (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
2590
+ const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
2591
+ const specImport = args
2592
+ .filter((a) => a.startsWith("--spec-import="))
2593
+ .map((a) => a.split("=")[1])
2594
+ .filter(Boolean)[0] ?? undefined;
2595
+ // Resolve the harness ONCE (honour --harness / config / auto-detect) so the
2596
+ // capability lattice is computed against the right dialect — never defaulting
2597
+ // to Claude Code in core. The dialect is INJECTED into the generator.
2598
+ const harnessFlag = harnessFlagFrom(args);
2599
+ const adapter = harnessFlag
2600
+ ? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
2601
+ : (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
2602
+ console.log(`Scanning ${(0, generate_harness_js_1.labelFor)(process.cwd(), dir)} for *.spec.ts...\n`);
2603
+ const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
2604
+ const result = (0, generate_harness_js_1.generateHarness)(model, {
2605
+ dialect: adapter.dialect,
2606
+ outDir: (0, node_path_1.dirname)(fullOut),
2607
+ specImport,
2608
+ });
2609
+ console.log(` ${String(result.agentCount)} agent(s), ${String(result.edgeCount)} delegate edge(s)` +
2610
+ (result.handoffCount > 0
2611
+ ? `, ${String(result.handoffCount)} handoff check(s)`
2612
+ : ""));
2613
+ console.log(` capabilities: ${result.capabilities.purity} (` +
2614
+ `${String(result.capabilities.sideEffecting.length)} side-effecting, ` +
2615
+ `${String(result.capabilities.unknown.length)} unknown)`);
2616
+ // DUPLICATE NAME — the O(N) JS check (never a type). Exit non-zero, no write.
2617
+ if (result.duplicate) {
2618
+ console.log(`\n✗ ${result.duplicate.message}`);
2619
+ console.log(`::error::${result.duplicate.message}`);
2620
+ process.exit(2);
2621
+ }
2622
+ if (checkOnly) {
2623
+ if (!(0, node_fs_1.existsSync)(fullOut)) {
2624
+ console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-harness\` to create it.`);
2625
+ process.exit(1);
2626
+ }
2627
+ const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
2628
+ const normalize = (s) => s
2629
+ .split("\n")
2630
+ .map((l) => l.trimEnd())
2631
+ .join("\n")
2632
+ .replace(/\n{3,}/g, "\n\n")
2633
+ .trim();
2634
+ if (normalize(existing) === normalize(result.gen)) {
2635
+ console.log(`\n✓ ${outPath} is up to date`);
2636
+ }
2637
+ else {
2638
+ console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-harness\` to update.`);
2639
+ process.exit(1);
2640
+ }
2641
+ return;
2642
+ }
2643
+ const outDir = (0, node_path_1.dirname)(fullOut);
2644
+ if (!(0, node_fs_1.existsSync)(outDir))
2645
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
2646
+ (0, node_fs_1.writeFileSync)(fullOut, result.gen);
2647
+ console.log(`\n✓ Generated ${(0, generate_harness_js_1.labelFor)(process.cwd(), fullOut)}`);
2648
+ console.log(" `tsc --noEmit` over this file now checks every delegate target resolves.");
2649
+ }
2576
2650
  /**
2577
2651
  * `vigiles test` / `vigiles eval` — discover and run the two-tier harness
2578
2652
  * scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
@@ -2681,7 +2755,34 @@ function pluginNameFor(dir, manifestPath) {
2681
2755
  return (0, node_path_1.basename)(dir);
2682
2756
  }
2683
2757
  /** Enrich an untested Surface with the metadata the right template needs. */
2684
- function scaffoldInputFor(s, report, pluginName) {
2758
+ /** Extract `"name": type` fields from one rendered `vigiles:ok`/`err` shape block. */
2759
+ function parseContractFields(block) {
2760
+ const fields = [];
2761
+ const re = /"([^"]+)"\s*:\s*(string\[\]|string|number|boolean)/g;
2762
+ let m;
2763
+ while ((m = re.exec(block)) !== null) {
2764
+ fields.push({ name: m[1], type: m[2] });
2765
+ }
2766
+ return fields;
2767
+ }
2768
+ /**
2769
+ * Parse a subagent's compiled `## Output contract` (the `vigiles:ok` / `vigiles:err`
2770
+ * blocks the compiler emits) back into a typed `ResultContract`, so the generator
2771
+ * can write an `assertAgentOk` test against the real fields. Returns null when the
2772
+ * agent has no result() contract.
2773
+ */
2774
+ function parseResultContract(md) {
2775
+ const ok = /```vigiles:ok\n([\s\S]*?)```/.exec(md);
2776
+ const err = /```vigiles:err\n([\s\S]*?)```/.exec(md);
2777
+ if (!ok && !err)
2778
+ return null;
2779
+ const okFields = ok ? parseContractFields(ok[1]) : [];
2780
+ const errFields = err ? parseContractFields(err[1]) : [];
2781
+ if (okFields.length === 0 && errFields.length === 0)
2782
+ return null;
2783
+ return { ok: okFields, err: errFields };
2784
+ }
2785
+ function scaffoldInputFor(s, report, pluginName, dir, dialect) {
2685
2786
  const base = { kind: s.kind, name: s.name, path: s.path };
2686
2787
  switch (s.kind) {
2687
2788
  case "skill": {
@@ -2690,7 +2791,18 @@ function scaffoldInputFor(s, report, pluginName) {
2690
2791
  }
2691
2792
  case "agent": {
2692
2793
  const ag = report.agents.find((x) => x.name === s.name);
2693
- return { ...base, tools: ag?.tools ?? null };
2794
+ const tools = ag?.tools ?? null;
2795
+ const sideEffectingTools = tools
2796
+ ? (0, effects_js_1.effectSurface)(tools, dialect).sideEffecting
2797
+ : undefined;
2798
+ let resultContract = null;
2799
+ try {
2800
+ resultContract = parseResultContract((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, s.path), "utf-8"));
2801
+ }
2802
+ catch {
2803
+ // agent .md unreadable → no contract to generate against
2804
+ }
2805
+ return { ...base, tools, sideEffectingTools, resultContract };
2694
2806
  }
2695
2807
  case "hook":
2696
2808
  return { ...base, hookCommand: `bash ${s.path}` };
@@ -2719,7 +2831,7 @@ function handleScaffoldTest(restArgs, args) {
2719
2831
  });
2720
2832
  const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
2721
2833
  const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
2722
- const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName)));
2834
+ const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName, dir, adapter.dialect)));
2723
2835
  if (json) {
2724
2836
  console.log(JSON.stringify(scaffolds, null, 2));
2725
2837
  return;
@@ -2777,6 +2889,8 @@ function printUsage(command) {
2777
2889
  console.log(" vigiles generate-types --check Verify .d.ts is up to date");
2778
2890
  console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
2779
2891
  console.log(" vigiles generate-schema --check Verify schema.json is up to date");
2892
+ console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
2893
+ console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
2780
2894
  console.log(" vigiles --version Print the version number");
2781
2895
  if (command && command !== "--help") {
2782
2896
  console.log(`\nUnknown command: "${command}"`);
@@ -2927,9 +3041,13 @@ function agentHookCommand() {
2927
3041
  }
2928
3042
  let tool = "";
2929
3043
  let command;
3044
+ let event = "";
3045
+ let toolInput;
2930
3046
  try {
2931
3047
  const parsed = JSON.parse(raw);
3048
+ event = parsed.hook_event_name ?? "";
2932
3049
  tool = parsed.tool_name ?? "";
3050
+ toolInput = parsed.tool_input;
2933
3051
  if (typeof parsed.tool_input?.command === "string") {
2934
3052
  command = parsed.tool_input.command;
2935
3053
  }
@@ -2937,9 +3055,34 @@ function agentHookCommand() {
2937
3055
  catch {
2938
3056
  /* malformed input → no tool, allow */
2939
3057
  }
3058
+ const cwd = process.cwd();
3059
+ // EXPERIMENTAL (parked P3, flat-only — do NOT auto-wire): the Task/SubagentStop
3060
+ // bracketing below assumes one active subagent at a time and is NOT nesting-safe
3061
+ // (CC v2.1.172 depth-5 nesting needs a stack + spawn-tool-name check). See
3062
+ // research/effect-boundary-design.md.
3063
+ // SubagentStop → CLOSE the window deterministically (no model `agent-done`):
3064
+ // the subagent returned, so its contract/purity no longer apply.
3065
+ if (event === "SubagentStop") {
3066
+ (0, agent_runtime_js_1.clearActiveAgent)(cwd);
3067
+ (0, effect_region_js_1.clearEffectActive)(cwd);
3068
+ return;
3069
+ }
3070
+ // PreToolUse(Task) → OPEN the window deterministically (no model `agent-start`
3071
+ // / `effect-enter`): the parent is dispatching a subagent, so activate that
3072
+ // subagent's compiled contract for the tool calls it is about to make. The
3073
+ // Task dispatch itself is the PARENT's action — don't gate it against the
3074
+ // subagent's contract; just open the window and allow.
3075
+ if (tool === "Task") {
3076
+ const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
3077
+ if (agentPath) {
3078
+ (0, agent_runtime_js_1.setActiveAgent)(cwd, agentPath);
3079
+ (0, effect_region_js_1.setEffectActive)(cwd);
3080
+ }
3081
+ return;
3082
+ }
2940
3083
  if (!tool)
2941
3084
  return;
2942
- const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool, command);
3085
+ const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(cwd, tool, command);
2943
3086
  if (!decision.allow) {
2944
3087
  console.error(decision.message);
2945
3088
  process.exit(2);
@@ -3298,6 +3441,9 @@ async function main() {
3298
3441
  case "generate-schema":
3299
3442
  handleGenerateSchema(args, restArgs);
3300
3443
  break;
3444
+ case "generate-harness":
3445
+ await handleGenerateHarness(args, restArgs);
3446
+ break;
3301
3447
  default:
3302
3448
  if (!handleSkillCommand(command, restArgs))
3303
3449
  printUsage(command);
@@ -25,7 +25,7 @@ export declare function verifyHash(content: string): {
25
25
  */
26
26
  /** @internal */ export declare function estimateTokens(text: string): number;
27
27
  export interface CompileError {
28
- type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway" | "purity-violation" | "output-without-fork";
28
+ type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway" | "purity-violation" | "output-without-fork" | "effect-in-skill";
29
29
  message: string;
30
30
  path?: string;
31
31
  }
@@ -730,6 +730,20 @@ function compileSkill(spec, options = {}) {
730
730
  "as a subagent, or drop `output`.",
731
731
  });
732
732
  }
733
+ // effect() is a SUBAGENT primitive. A deterministic effect REGION needs a
734
+ // structural call→return bracket to scope it; a default skill is spliced into
735
+ // the main conversation and has none (the dogfood that proved the model-emitted
736
+ // boundary is fragile — research/effect-boundary-design.md). A skill bounds its
737
+ // effects with a `purity` floor and promotes to `context:'fork'` (a subagent)
738
+ // when it must mutate.
739
+ if (collectSkillRefs(spec).some((f) => typeof f !== "string" && f._ref === "effect")) {
740
+ errors.push({
741
+ type: "effect-in-skill",
742
+ message: "effect() is a subagent primitive — a skill has no call→return boundary to " +
743
+ "scope an effect region. Declare a `purity` floor on the skill and use " +
744
+ "context:'fork' to run it as a subagent when it must mutate.",
745
+ });
746
+ }
733
747
  // purity floor check — the dialect is optional (callers that don't pass one
734
748
  // skip the check rather than crash; the CLI always passes it). An absent
735
749
  // tools list inherits ALL tools, so it's checked as the "*" wildcard (a
@@ -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