vigiles 5.2.0 → 7.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 (78) hide show
  1. package/README.md +99 -48
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +64 -4
  4. package/dist/adapters/claude-code/agent-runtime.js +131 -17
  5. package/dist/adapters/claude-code/dialect.d.ts +34 -0
  6. package/dist/adapters/claude-code/dialect.js +46 -33
  7. package/dist/adapters/claude-code/effect-region.js +1 -1
  8. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  9. package/dist/adapters/claude-code/skill-runtime.js +1 -9
  10. package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
  11. package/dist/adapters/claude-code/typed-spec.js +55 -0
  12. package/dist/adapters/codex/hook-protocol.js +3 -0
  13. package/dist/adapters/codex/mock-model.js +1 -1
  14. package/dist/claude-code.d.ts +1 -0
  15. package/dist/claude-code.js +8 -1
  16. package/dist/cli-commands.d.ts +19 -0
  17. package/dist/cli-commands.js +51 -0
  18. package/dist/cli.js +735 -76
  19. package/dist/core/bash-effects.d.ts +12 -0
  20. package/dist/core/bash-effects.js +31 -0
  21. package/dist/core/capability-diff.d.ts +46 -0
  22. package/dist/core/capability-diff.js +97 -0
  23. package/dist/core/compile.d.ts +1 -1
  24. package/dist/core/compile.js +14 -0
  25. package/dist/core/generate-harness.d.ts +187 -0
  26. package/dist/core/generate-harness.js +337 -0
  27. package/dist/core/guards.d.ts +126 -0
  28. package/dist/core/guards.js +309 -0
  29. package/dist/core/harness-driver.d.ts +1 -1
  30. package/dist/core/hook-program.d.ts +459 -0
  31. package/dist/core/hook-program.js +468 -0
  32. package/dist/core/hook-protocol.d.ts +7 -0
  33. package/dist/core/hook-providers.d.ts +138 -0
  34. package/dist/core/hook-providers.js +155 -0
  35. package/dist/core/hook-spec.d.ts +74 -0
  36. package/dist/core/hook-spec.js +130 -0
  37. package/dist/core/inline.js +1 -1
  38. package/dist/core/mcp-tool.d.ts +12 -0
  39. package/dist/core/mcp-tool.js +20 -0
  40. package/dist/core/mcp.d.ts +13 -0
  41. package/dist/core/mcp.js +67 -0
  42. package/dist/core/spec.d.ts +290 -8
  43. package/dist/core/spec.js +118 -3
  44. package/dist/core/types.d.ts +8 -0
  45. package/dist/dialect-drift.d.ts +65 -0
  46. package/dist/dialect-drift.js +216 -0
  47. package/dist/eval.d.ts +40 -5
  48. package/dist/eval.js +59 -5
  49. package/dist/guardrail-check.d.ts +85 -0
  50. package/dist/guardrail-check.js +152 -0
  51. package/dist/harness-assert.d.ts +10 -0
  52. package/dist/harness-assert.js +30 -0
  53. package/dist/hook-install.d.ts +43 -0
  54. package/dist/hook-install.js +91 -0
  55. package/dist/hook.d.ts +52 -0
  56. package/dist/hook.js +98 -0
  57. package/dist/leaderboard.d.ts +6 -0
  58. package/dist/leaderboard.js +43 -1
  59. package/dist/linting.d.ts +9 -5
  60. package/dist/linting.js +17 -5
  61. package/dist/optimize.js +1 -1
  62. package/dist/scaffold-test.d.ts +28 -0
  63. package/dist/scaffold-test.js +134 -15
  64. package/dist/scan-behavioral.d.ts +60 -0
  65. package/dist/scan-behavioral.js +239 -1
  66. package/dist/scan.d.ts +14 -0
  67. package/dist/scan.js +33 -1
  68. package/dist/score-explainer.js +1 -1
  69. package/dist/self-command-refs.d.ts +21 -0
  70. package/dist/self-command-refs.js +125 -0
  71. package/dist/testing.d.ts +5 -3
  72. package/dist/testing.js +37 -23
  73. package/dist/tool-intercept.d.ts +4 -4
  74. package/dist/tool-intercept.js +5 -5
  75. package/dist/unit.d.ts +2 -0
  76. package/dist/unit.js +8 -1
  77. package/hooks/refs-nudge.sh +1 -1
  78. package/package.json +5 -3
@@ -38,4 +38,16 @@ export declare function classifyBashCommand(command: string): BashEffect;
38
38
  * caller uses to decide "this Bash is provably an observation."
39
39
  */
40
40
  export declare function isReadOnlyBash(command: string): boolean;
41
+ /**
42
+ * Extract the static argv of every simple command (CallExpr) in `command`, each
43
+ * as an array of literal words (dynamic / quoted-interpolated words are dropped).
44
+ * AST-backed, so a leaf nested in a pipeline, `&&`/`;`/`|`, a subshell, or a
45
+ * compound command is still found — the structural query a robust matcher needs
46
+ * (a regex over the raw string misses `cd x && git push`). Parse failure → [].
47
+ *
48
+ * This is the matching primitive a typed hook's `command.runs("git push")` is
49
+ * built on: it sees the real `git push` leaf however it's wrapped, which the
50
+ * native `Bash(git:*)` glob (issue #30519) and a hand-written `grep` both miss.
51
+ */
52
+ export declare function leafCommands(command: string): string[][];
41
53
  //# sourceMappingURL=bash-effects.d.ts.map
@@ -26,6 +26,7 @@
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
27
  exports.classifyBashCommand = classifyBashCommand;
28
28
  exports.isReadOnlyBash = isReadOnlyBash;
29
+ exports.leafCommands = leafCommands;
29
30
  // mvdan-sh is a CJS package (GopherJS build) with no bundled TypeScript types.
30
31
  // The project compiles to CommonJS (Node16, no "type":"module"), so plain
31
32
  // require() works and is the idiomatic pattern here (see linters.ts).
@@ -402,4 +403,34 @@ function classifyBashCommand(command) {
402
403
  function isReadOnlyBash(command) {
403
404
  return classifyBashCommand(command) === "read-only";
404
405
  }
406
+ /**
407
+ * Extract the static argv of every simple command (CallExpr) in `command`, each
408
+ * as an array of literal words (dynamic / quoted-interpolated words are dropped).
409
+ * AST-backed, so a leaf nested in a pipeline, `&&`/`;`/`|`, a subshell, or a
410
+ * compound command is still found — the structural query a robust matcher needs
411
+ * (a regex over the raw string misses `cd x && git push`). Parse failure → [].
412
+ *
413
+ * This is the matching primitive a typed hook's `command.runs("git push")` is
414
+ * built on: it sees the real `git push` leaf however it's wrapped, which the
415
+ * native `Bash(git:*)` glob (issue #30519) and a hand-written `grep` both miss.
416
+ */
417
+ function leafCommands(command) {
418
+ let file;
419
+ try {
420
+ file = sh.syntax.NewParser().Parse(command, "cmd.sh");
421
+ }
422
+ catch {
423
+ return [];
424
+ }
425
+ const out = [];
426
+ sh.syntax.Walk(file, (node) => {
427
+ if (sh.syntax.NodeType(node) === "CallExpr" && node.Args) {
428
+ const argv = node.Args.map((w) => getLiteral(w)).filter((s) => s !== null);
429
+ if (argv.length > 0)
430
+ out.push(argv);
431
+ }
432
+ return true;
433
+ });
434
+ return out;
435
+ }
405
436
  //# sourceMappingURL=bash-effects.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Capability-diff — "did this change widen the agent's blast radius?" (moat #2).
3
+ *
4
+ * The whole-harness capability lattice ({@link HarnessCapabilities}, computed by
5
+ * `computeHarnessCapabilities`) is the set of effects an agent/harness can reach:
6
+ * read-only tools, side-effecting tools, unknown/MCP tools, and the loosest purity.
7
+ * Diffing two lattices (a PR's base vs head) yields a deterministic, model-free
8
+ * verdict: a change WIDENED the blast radius iff it adds a side-effecting or
9
+ * unknown/MCP tool, or loosens the purity floor. New read-only tools and removals
10
+ * are reported but are NOT a widening.
11
+ *
12
+ * Honesty / don't-cry-wolf: a widening is INFORMATIONAL by default (a PR comment),
13
+ * not an automatic failure — widening the surface is often intended. The CLI gates
14
+ * a non-zero exit behind an explicit `--fail-on-widen`. Pure + harness-agnostic
15
+ * (operates on the lattice, no dialect needed). See research/typed-spec-moat.md (#2).
16
+ */
17
+ import type { HarnessCapabilities } from "./generate-harness.js";
18
+ import type { PurityLevel } from "./effects.js";
19
+ /** A purity move between two lattices (omitted when unchanged). */
20
+ export interface PurityChange {
21
+ readonly from: PurityLevel;
22
+ readonly to: PurityLevel;
23
+ /** `widened` = loosened (pure→bounded→unrestricted); `narrowed` = tightened. */
24
+ readonly direction: "widened" | "narrowed";
25
+ }
26
+ export interface CapabilityDiff {
27
+ /** Side-effecting tools reachable AFTER but not before — the core blast-radius growth. */
28
+ readonly addedSideEffecting: readonly string[];
29
+ /** Unknown-effect (MCP / unrecognized) tools newly reachable — also widened reach. */
30
+ readonly addedUnknown: readonly string[];
31
+ /** Read-only tools newly reachable — benign (reported, NOT a widening). */
32
+ readonly addedReadOnly: readonly string[];
33
+ /** Tools reachable before but not after — a NARROWING (good; informational). */
34
+ readonly removed: readonly string[];
35
+ /** The purity move, or null when unchanged. */
36
+ readonly purity: PurityChange | null;
37
+ /** The verdict: did the blast radius GROW (new side-effecting/unknown, or purity loosened)? */
38
+ readonly widened: boolean;
39
+ }
40
+ /** Diff two capability lattices → what changed + the widened verdict. Pure. */
41
+ export declare function diffCapabilities(before: HarnessCapabilities, after: HarnessCapabilities): CapabilityDiff;
42
+ /** True when the diff carries no change at all (the common, quiet case). */
43
+ export declare function isNoOpDiff(d: CapabilityDiff): boolean;
44
+ /** Render a capability-diff as a PR-comment-style report (Markdown-friendly). */
45
+ export declare function formatCapabilityDiff(d: CapabilityDiff): string;
46
+ //# sourceMappingURL=capability-diff.d.ts.map
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Capability-diff — "did this change widen the agent's blast radius?" (moat #2).
4
+ *
5
+ * The whole-harness capability lattice ({@link HarnessCapabilities}, computed by
6
+ * `computeHarnessCapabilities`) is the set of effects an agent/harness can reach:
7
+ * read-only tools, side-effecting tools, unknown/MCP tools, and the loosest purity.
8
+ * Diffing two lattices (a PR's base vs head) yields a deterministic, model-free
9
+ * verdict: a change WIDENED the blast radius iff it adds a side-effecting or
10
+ * unknown/MCP tool, or loosens the purity floor. New read-only tools and removals
11
+ * are reported but are NOT a widening.
12
+ *
13
+ * Honesty / don't-cry-wolf: a widening is INFORMATIONAL by default (a PR comment),
14
+ * not an automatic failure — widening the surface is often intended. The CLI gates
15
+ * a non-zero exit behind an explicit `--fail-on-widen`. Pure + harness-agnostic
16
+ * (operates on the lattice, no dialect needed). See research/typed-spec-moat.md (#2).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.diffCapabilities = diffCapabilities;
20
+ exports.isNoOpDiff = isNoOpDiff;
21
+ exports.formatCapabilityDiff = formatCapabilityDiff;
22
+ const PURITY_RANK = {
23
+ pure: 0,
24
+ bounded: 1,
25
+ unrestricted: 2,
26
+ };
27
+ const addedIn = (before, after) => after.filter((x) => !before.includes(x));
28
+ /** Diff two capability lattices → what changed + the widened verdict. Pure. */
29
+ function diffCapabilities(before, after) {
30
+ const addedSideEffecting = addedIn(before.sideEffecting, after.sideEffecting);
31
+ const addedUnknown = addedIn(before.unknown, after.unknown);
32
+ const addedReadOnly = addedIn(before.readOnly, after.readOnly);
33
+ // A tool is "removed" if it was reachable in ANY bucket before and in NONE after.
34
+ const afterAll = new Set([
35
+ ...after.readOnly,
36
+ ...after.sideEffecting,
37
+ ...after.unknown,
38
+ ]);
39
+ const removed = [
40
+ ...before.readOnly,
41
+ ...before.sideEffecting,
42
+ ...before.unknown,
43
+ ].filter((x) => !afterAll.has(x));
44
+ const fromRank = PURITY_RANK[before.purity];
45
+ const toRank = PURITY_RANK[after.purity];
46
+ const purity = fromRank === toRank
47
+ ? null
48
+ : {
49
+ from: before.purity,
50
+ to: after.purity,
51
+ direction: toRank > fromRank ? "widened" : "narrowed",
52
+ };
53
+ const widened = addedSideEffecting.length > 0 ||
54
+ addedUnknown.length > 0 ||
55
+ purity?.direction === "widened";
56
+ return {
57
+ addedSideEffecting,
58
+ addedUnknown,
59
+ addedReadOnly,
60
+ removed,
61
+ purity,
62
+ widened,
63
+ };
64
+ }
65
+ /** True when the diff carries no change at all (the common, quiet case). */
66
+ function isNoOpDiff(d) {
67
+ return (d.addedSideEffecting.length === 0 &&
68
+ d.addedUnknown.length === 0 &&
69
+ d.addedReadOnly.length === 0 &&
70
+ d.removed.length === 0 &&
71
+ d.purity === null);
72
+ }
73
+ /** Render a capability-diff as a PR-comment-style report (Markdown-friendly). */
74
+ function formatCapabilityDiff(d) {
75
+ if (isNoOpDiff(d)) {
76
+ return "Capability surface unchanged — no blast-radius change.";
77
+ }
78
+ const lines = [
79
+ d.widened
80
+ ? "⚠️ Capability surface **WIDENED** — this change grows the agent's blast radius:"
81
+ : "Capability surface changed (no widening — narrowing / read-only only):",
82
+ ];
83
+ if (d.addedSideEffecting.length > 0)
84
+ lines.push(` + side-effecting: ${d.addedSideEffecting.join(", ")}`);
85
+ if (d.addedUnknown.length > 0)
86
+ lines.push(` + unknown/MCP: ${d.addedUnknown.join(", ")}`);
87
+ if (d.purity?.direction === "widened")
88
+ lines.push(` + purity loosened: ${d.purity.from} → ${d.purity.to}`);
89
+ if (d.addedReadOnly.length > 0)
90
+ lines.push(` · read-only added (benign): ${d.addedReadOnly.join(", ")}`);
91
+ if (d.removed.length > 0)
92
+ lines.push(` − narrowed (removed): ${d.removed.join(", ")}`);
93
+ if (d.purity?.direction === "narrowed")
94
+ lines.push(` − purity tightened: ${d.purity.from} → ${d.purity.to}`);
95
+ return lines.join("\n");
96
+ }
97
+ //# sourceMappingURL=capability-diff.js.map
@@ -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