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
@@ -50,6 +50,42 @@ export type StrictLinterRule = [keyof KnownLinterRules] extends [never] ? Linter
50
50
  */
51
51
  export type StrictFile = [keyof KnownProjectFiles] extends [never] ? string : KnownProjectFiles[keyof KnownProjectFiles] & string;
52
52
  export type StrictCmd = [keyof KnownNpmScripts] extends [never] ? string : `npm run ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | `npm ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | (string & {});
53
+ /**
54
+ * A harness's tool vocabulary, split by the purity floor that admits it. The
55
+ * read-only/side-effecting split is harness-specific (it comes from the
56
+ * dialect's `builtinAgentTools` − `sideEffectingTools`), so the concrete unions
57
+ * are supplied by the adapter; core only describes the SHAPE.
58
+ *
59
+ * - `readOnly`: tools a `pure` unit may declare (observation only).
60
+ * - `bounded`: tools a `bounded` unit may declare — the read-only set PLUS the
61
+ * decidable side-effecting tools (Write/Edit/NotebookEdit) AND `Bash` (its
62
+ * command is decided at RUNTIME by the gate; the floor admits the tool). Bars
63
+ * MCP / unknown / wildcard.
64
+ *
65
+ * The default (`string` at both) imposes no constraint — any tool is accepted at
66
+ * every level, the historical behaviour of an untyped `agent()`/`skill()`.
67
+ */
68
+ export interface ToolVocabulary {
69
+ /** Union of tool names allowed under `purity: "pure"`. */
70
+ readonly readOnly: string;
71
+ /** Union of tool names allowed under `purity: "bounded"`. */
72
+ readonly bounded: string;
73
+ }
74
+ /** The fully-open default vocabulary — no constraint at any purity level. */
75
+ export interface OpenToolVocabulary extends ToolVocabulary {
76
+ readonly readOnly: string;
77
+ readonly bounded: string;
78
+ }
79
+ /**
80
+ * The tool names ALLOWED at a declared `purity`, given a vocabulary `V`:
81
+ * - `"pure"` → only `V["readOnly"]`.
82
+ * - `"bounded"` → `V["bounded"]` (read-only ∪ decidable side-effecting ∪ `Bash`).
83
+ * - `"dangerously-unrestricted"` (or no purity) → `string` (anything).
84
+ *
85
+ * With the open default vocabulary every branch widens to `string`, so an
86
+ * untyped surface accepts any tools.
87
+ */
88
+ export type AllowedAt<P extends AuthoredPurity | undefined, V extends ToolVocabulary> = P extends "pure" ? V["readOnly"] : P extends "bounded" ? V["bounded"] : string;
53
89
  export type ClaudeTool = "Read" | "Write" | "Edit" | "Bash" | "Grep" | "Glob" | "Agent" | "TodoWrite" | "WebSearch" | "WebFetch" | "NotebookEdit";
54
90
  export type HookEvent = "PreToolUse" | "PostToolUse" | "PreSession" | "PostSession" | "Notification";
55
91
  /** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.) or to a vigiles-internal check. */
@@ -120,6 +156,12 @@ export type VerifiedCmd = string & {
120
156
  export type VerifiedRef = string & {
121
157
  readonly [__brand]: "VerifiedRef";
122
158
  };
159
+ export type VerifiedDir = string & {
160
+ readonly [__brand]: "VerifiedDir";
161
+ };
162
+ export type VerifiedGlob = string & {
163
+ readonly [__brand]: "VerifiedGlob";
164
+ };
123
165
  /** A typed file reference — verified at compile time. */
124
166
  export interface FileRef {
125
167
  readonly _ref: "file";
@@ -141,7 +183,17 @@ export interface SymbolRef {
141
183
  readonly file: VerifiedPath;
142
184
  readonly symbol: string;
143
185
  }
144
- export type Ref = FileRef | CmdRef | SkillRef | SymbolRef;
186
+ /** A typed directory reference — verified to exist AND be a directory. */
187
+ export interface DirRef {
188
+ readonly _ref: "dir";
189
+ readonly path: VerifiedDir;
190
+ }
191
+ /** A typed glob reference — verified to match at least one path. */
192
+ export interface GlobRef {
193
+ readonly _ref: "glob";
194
+ readonly pattern: VerifiedGlob;
195
+ }
196
+ export type Ref = FileRef | CmdRef | SkillRef | SymbolRef | DirRef | GlobRef;
145
197
  /**
146
198
  * Reference a file path — verified to exist at compile time.
147
199
  * When generated types are present, narrowed to known project files.
@@ -164,7 +216,33 @@ export declare function symbol(file: NoInfer<StrictFile>, name: string): SymbolR
164
216
  * Compiles to a markdown link: [skill name](path)
165
217
  */
166
218
  export declare function ref(path: string): SkillRef;
167
- export type InstructionFragment = string | Ref;
219
+ /**
220
+ * Reference a directory — verified at compile time to exist AND be a directory
221
+ * (not a file). The "architecture floats free" fix: a spec that names `src/core/`
222
+ * proves the directory is really there, where a plain string in prose rots
223
+ * silently. Compiles to the inline form `` `path` ``.
224
+ */
225
+ export declare function dir(path: string): DirRef;
226
+ /**
227
+ * Reference a glob pattern — verified at compile time to match at least one path,
228
+ * so `glob("src/*.test.ts")` proves tests actually exist where the instructions
229
+ * claim (the pattern supports the usual `*` / `**` syntax). Compiles to the
230
+ * inline form `` `pattern` ``.
231
+ */
232
+ export declare function glob(pattern: string): GlobRef;
233
+ /**
234
+ * A marked side-effect BOUNDARY inside a skill/agent body — "side effects are
235
+ * allowed ONLY inside this block." Compiles to `<!-- vigiles:effect -->` …
236
+ * `<!-- /vigiles:effect -->` markers the runtime PreToolUse gate keys on: outside
237
+ * the region the unit is treated as read-only (the `"pure"` effective floor),
238
+ * inside it the declared purity floor applies. The position-aware companion to
239
+ * the per-call `purity` floor. See `research/effect-boundary-design.md`.
240
+ */
241
+ export interface EffectRegion {
242
+ readonly _ref: "effect";
243
+ readonly body: InstructionFragment[];
244
+ }
245
+ export type InstructionFragment = string | Ref | EffectRegion;
168
246
  /**
169
247
  * Tagged template literal for skill instructions with typed references.
170
248
  *
@@ -175,6 +253,39 @@ export type InstructionFragment = string | Ref;
175
253
  * `
176
254
  */
177
255
  export declare function instructions(strings: TemplateStringsArray, ...values: InstructionFragment[]): InstructionFragment[];
256
+ /**
257
+ * Tagged template literal marking a side-effect boundary — usable as an
258
+ * interpolated fragment inside a body / `instructions\`\``:
259
+ *
260
+ * instructions`
261
+ * ## Apply
262
+ * ${effect`
263
+ * Side effects are allowed ONLY here:
264
+ * - write ${file("CHANGELOG.md")}
265
+ * - ${cmd("npm publish")}
266
+ * `}
267
+ * `
268
+ *
269
+ * Returns an `EffectRegion` fragment; `compile` wraps its rendered body in
270
+ * `<!-- vigiles:effect -->` markers. Independent of the `doc()` authoring
271
+ * surface — it does not block on it.
272
+ */
273
+ export declare function effect(strings: TemplateStringsArray, ...values: InstructionFragment[]): EffectRegion;
274
+ /**
275
+ * The purity an author DECLARES for a skill/agent — the floor `compile`
276
+ * enforces against the tool contract (see `purityViolations` in
277
+ * `core/effects.ts`). Mirrors the analysis `PurityLevel` for the two meaningful
278
+ * rungs, so what you DECLARE and what `scan` REPORTS share one vocabulary:
279
+ * - `"pure"`: only read-only tools — no side effects at all.
280
+ * - `"bounded"`: decidable side-effecting tools (Write, Edit, …) are allowed,
281
+ * but not `Bash` / unknown-effect / inherits-all (the unbounded cells).
282
+ * - `"dangerously-unrestricted"`: the explicit escape hatch — no enforcement.
283
+ * Deliberately loud (cf. React's `dangerouslySetInnerHTML`) so opting OUT of
284
+ * the guardrail stands out in review. Omitting `purity` is the same
285
+ * (unenforced) default WITHOUT typing the loud word — you write it only when
286
+ * you mean to override a stricter level.
287
+ */
288
+ export type AuthoredPurity = "pure" | "bounded" | "dangerously-unrestricted";
178
289
  /** Known markdown instruction file targets. */
179
290
  export type InstructionTarget = "CLAUDE.md" | "AGENTS.md" | (string & {});
180
291
  export interface ClaudeSpec {
@@ -190,7 +301,12 @@ export interface ClaudeSpec {
190
301
  readonly keyFiles?: Record<string, string>;
191
302
  /** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */
192
303
  readonly sections?: Record<string, string | InstructionFragment[]>;
193
- /** Maximum lines per prose section (per-spec override). */
304
+ /**
305
+ * Maximum lines for a single named prose section. Overrides the generous
306
+ * compile-time default (200 lines) that guards every section + agent section
307
+ * against an egregious content dump — set a tighter number to enforce your own
308
+ * house limit, or a larger one for an intentionally long section.
309
+ */
194
310
  readonly maxSectionLines?: number;
195
311
  /**
196
312
  * Maximum estimated tokens for the compiled output (~4 chars per token).
@@ -293,6 +409,31 @@ export interface SkillSpec {
293
409
  readonly inputs?: readonly SkillInput[];
294
410
  /** Whether to disable model invocation (frontmatter flag). */
295
411
  readonly disableModelInvocation?: boolean;
412
+ /**
413
+ * Execution context. `"fork"` runs the skill's body as the task inside a
414
+ * forked SUBAGENT (its own context window) instead of inline in the main
415
+ * conversation (Anthropic's `context: fork` frontmatter). This is the ONLY
416
+ * setting under which a skill gains a real call→return boundary — so it's the
417
+ * prerequisite for declaring an `output` Result contract (see `output`). Omit
418
+ * for the default inline execution.
419
+ */
420
+ readonly context?: "fork";
421
+ /**
422
+ * The allowed-tools contract for this skill. Each entry must be a known
423
+ * built-in tool or an MCP tool (`mcp__server__tool`). Omit to inherit all
424
+ * tools. When `purity` is `"pure"`/`"bounded"`, the declared tools are checked
425
+ * against that floor — compile rejects a tool looser than the declared level.
426
+ */
427
+ readonly tools?: readonly string[];
428
+ /**
429
+ * Declare this skill's purity floor — compile rejects a tool contract looser
430
+ * than it. `"pure"` allows only read-only tools; `"bounded"` also allows
431
+ * decidable side-effecting tools (Write, Edit, …) but bars `Bash` /
432
+ * unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it)
433
+ * enforces nothing. NOTE: `"pure"`/`"bounded"` require an explicit read-only
434
+ * `tools` list — an absent list inherits ALL tools and is a violation.
435
+ */
436
+ readonly purity?: AuthoredPurity;
296
437
  /**
297
438
  * Gated pipeline steps. When set, the skill compiles to a `## Steps`
298
439
  * checklist with a deterministic gate per step. Use this OR `body`.
@@ -303,6 +444,18 @@ export interface SkillSpec {
303
444
  * Compiles to a `## Result` section + a `vigiles:result` marker.
304
445
  */
305
446
  readonly result?: Gate;
447
+ /**
448
+ * The skill's typed railway outcome — the SAME `Result<ok, err>` contract a
449
+ * subagent declares with `result(okShape, errShape)`. Valid ONLY with
450
+ * `context: "fork"`: a forked skill runs as a subagent, so it has the
451
+ * call→return boundary a typed outcome needs (compile errors if `output` is set
452
+ * without `context: "fork"`). When valid, compiles to a `## Output contract`
453
+ * with a `vigiles:ok` / `vigiles:err` block — parseable (`parseAgentResult`) and
454
+ * testable (`assertAgentOk`) via the existing subagent rail. An INLINE skill has
455
+ * no return, so a typed outcome there is a category error — hence the gate. See
456
+ * `research/spec-syntax-and-railway-scope.md`.
457
+ */
458
+ readonly output?: OutputContract;
306
459
  /** Freeform instruction body (linear/unstructured skills). Use this OR `steps`. */
307
460
  readonly body?: string | InstructionFragment[];
308
461
  /**
@@ -313,13 +466,27 @@ export interface SkillSpec {
313
466
  */
314
467
  readonly maxInlineCodeLines?: number;
315
468
  }
469
+ /**
470
+ * The input to `skill()` — `SkillSpec` minus `_specType`, with `tools`
471
+ * constrained by the declared `purity` and vocabulary `V` (same mechanism as
472
+ * `AgentSpecInput`). The open default vocabulary leaves it unconstrained.
473
+ */
474
+ export type SkillSpecInput<P extends AuthoredPurity | undefined, V extends ToolVocabulary> = Omit<SkillSpec, "_specType" | "tools" | "purity"> & {
475
+ readonly purity?: P;
476
+ readonly tools?: readonly AllowedAt<P, V>[];
477
+ };
316
478
  /**
317
479
  * Define a SKILL.md specification.
318
480
  *
319
481
  * // skills/my-skill/SKILL.md.spec.ts
320
482
  * export default skill({ name: "my-skill", description: "...", body: "..." });
483
+ *
484
+ * Generic over a tool `Vocabulary` (default `OpenToolVocabulary` — no
485
+ * constraint), exactly like `agent()`: a vocabulary-bound `skill` (e.g.
486
+ * `vigiles/claude-code`) makes `purity: "pure"` + a side-effecting tool a `tsc`
487
+ * error; the bare core `skill()` accepts any tools, as before.
321
488
  */
322
- export declare function skill(spec: Omit<SkillSpec, "_specType">): SkillSpec;
489
+ export declare function skill<const P extends AuthoredPurity | undefined = undefined, V extends ToolVocabulary = OpenToolVocabulary>(spec: SkillSpecInput<P, V>): SkillSpec;
323
490
  /**
324
491
  * A subagent definition (compiles to `agents/<name>.md`). Unlike a skill —
325
492
  * reference material the model reads on activation — a subagent is a *delegated
@@ -338,6 +505,8 @@ export interface AgentSpec {
338
505
  readonly description: string;
339
506
  /** Model alias (e.g. "sonnet", "opus", "haiku", "inherit"). Optional. */
340
507
  readonly model?: string;
508
+ /** Subagent UI colour (Claude Code frontmatter, e.g. "pink", "blue"). Optional. */
509
+ readonly color?: string;
341
510
  /**
342
511
  * The allowed-tools contract — the rails the worker runs on. Each entry must be
343
512
  * a known built-in tool (Read/Write/Edit/Bash/Grep/Glob/WebSearch/WebFetch/
@@ -345,6 +514,17 @@ export interface AgentSpec {
345
514
  * Omit to inherit all tools. Verified at compile time.
346
515
  */
347
516
  readonly tools?: readonly string[];
517
+ /**
518
+ * The DENY-side contract — tools the worker may NOT use. Use this INSTEAD OF
519
+ * `tools`, not with it: `tools` is an allowlist (only these), so a tool not
520
+ * listed is already unavailable and a `disallowedTools` entry would be
521
+ * redundant. `disallowedTools` earns its place only when there's NO allowlist
522
+ * (the agent inherits ALL tools) and you want to subtract a few — e.g.
523
+ * `disallowedTools: ["Bash"]` on an otherwise-unrestricted worker. Rendered to
524
+ * the `disallowedTools:` frontmatter; close-typos are flagged (a typo'd entry
525
+ * blocks nothing). For a read-only floor prefer a tight `tools` list + `purity`.
526
+ */
527
+ readonly disallowedTools?: readonly string[];
348
528
  /**
349
529
  * The lead/intro prose of the system prompt (the "You are…" opener), before any
350
530
  * sections. Carries verified `file()`/`cmd()`/`symbol()`/`ref()` marks. No
@@ -367,7 +547,56 @@ export interface AgentSpec {
367
547
  * and testable (see `result()`, `parseAgentResult`, `assertAgentOk`).
368
548
  */
369
549
  readonly output?: OutputContract;
550
+ /**
551
+ * Declare this agent's purity floor — compile rejects a tool contract looser
552
+ * than it. `"pure"` allows only read-only tools; `"bounded"` also allows
553
+ * decidable side-effecting tools (Write, Edit, …) but bars `Bash` /
554
+ * unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it)
555
+ * enforces nothing. `"pure"`/`"bounded"` require an explicit `tools` list — a
556
+ * wildcard or absent-tools (inherits-all) is always a violation.
557
+ */
558
+ readonly purity?: AuthoredPurity;
370
559
  }
560
+ /**
561
+ * The input to `agent()` — `AgentSpec` minus the internal `_specType`, with the
562
+ * `tools` list constrained by the declared `purity` and the tool vocabulary `V`.
563
+ * `P` is inferred from the literal `purity` field (`const` inference), and
564
+ * `tools` is then typed `AllowedAt<P, V>[]`:
565
+ * - `purity: "pure"` → `tools` may list only `V["readOnly"]` tools.
566
+ * - `purity: "bounded"`→ `tools` may list `V["bounded"]` tools (admits `Bash`).
567
+ * - no `purity` / `"dangerously-unrestricted"` → `tools` is `string[]` (open).
568
+ *
569
+ * With the open default vocabulary (core `agent()`) every level widens to
570
+ * `string`, so any tools compile — backwards-compatible.
571
+ */
572
+ export type AgentSpecInput<P extends AuthoredPurity | undefined, V extends ToolVocabulary, Ok extends Shape = Shape, Err extends Shape = Shape> = Omit<AgentSpec, "_specType" | "tools" | "purity" | "output"> & {
573
+ readonly purity?: P;
574
+ readonly tools?: readonly AllowedAt<P, V>[];
575
+ /** The typed result contract — `result(ok, err)`. Its literal field shapes are
576
+ * captured into the returned `TypedAgentSpec` so a typed `pipe` can check them. */
577
+ readonly output?: OutputContract<Ok, Err>;
578
+ };
579
+ declare const __outcome: unique symbol;
580
+ /** Phantom carrier of an agent's success/error result shapes (type-level only). */
581
+ export interface TypedOutcome<Ok extends Shape, Err extends Shape> {
582
+ readonly [__outcome]: {
583
+ readonly ok: Ok;
584
+ readonly err: Err;
585
+ };
586
+ }
587
+ /** An `AgentSpec` that REMEMBERS its `result()` ok/err shapes at the type level
588
+ * (via a phantom field). Still an `AgentSpec`, so it flows everywhere one does.
589
+ * The input to a typed `pipe` / `then`. */
590
+ export type TypedAgentSpec<Ok extends Shape, Err extends Shape> = AgentSpec & TypedOutcome<Ok, Err>;
591
+ /**
592
+ * Extract a typed agent's success (`result().ok`) SHAPE at the type level. The
593
+ * phantom `__outcome` symbol is module-private, so this is the exported reader
594
+ * the whole-harness registry uses: `OkOf<typeof registry["planner"]>` is the
595
+ * literal `ok` shape `planner` produces, the producer side of a cross-file
596
+ * `Handoff<>` check. A plain `AgentSpec` (no `result()` contract) carries no
597
+ * phantom, so `OkOf` widens to the erased `Shape` — a no-op handoff, additive.
598
+ */
599
+ export type OkOf<T> = T extends TypedOutcome<infer Ok, Shape> ? Ok : Shape;
371
600
  /**
372
601
  * Define a subagent specification (compiles to `agents/<name>.md`).
373
602
  *
@@ -382,21 +611,41 @@ export interface AgentSpec {
382
611
  * "no-floating": enforce("@typescript-eslint/no-floating-promises", "Await promises."),
383
612
  * },
384
613
  * });
614
+ *
615
+ * Generic over a tool `Vocabulary` (default `OpenToolVocabulary` — no
616
+ * constraint). A harness adapter re-exports a vocabulary-bound `agent` (e.g.
617
+ * `vigiles/claude-code`) so `purity: "pure"` + a side-effecting tool is a `tsc`
618
+ * error at edit time; the bare core `agent()` accepts any tools, as before.
619
+ *
620
+ * Also generic over the result's `Ok`/`Err` shapes, inferred from `output:
621
+ * result(...)`. The returned value is a `TypedAgentSpec<Ok, Err>` — an
622
+ * `AgentSpec` that carries those shapes at the type level, so a typed `pipe`
623
+ * can cross-reference the handoff. With no `output` the shapes default to the
624
+ * erased `Shape`, and the value is still a plain `AgentSpec` — backwards-compatible.
385
625
  */
386
- export declare function agent(spec: Omit<AgentSpec, "_specType">): AgentSpec;
626
+ export declare function agent<const P extends AuthoredPurity | undefined = undefined, V extends ToolVocabulary = OpenToolVocabulary, Ok extends Shape = Shape, Err extends Shape = Shape>(spec: AgentSpecInput<P, V, Ok, Err>): TypedAgentSpec<Ok, Err>;
387
627
  /** The field types a result contract can declare (kept tiny + dependency-free). */
388
628
  export type OutputFieldType = "string" | "number" | "boolean" | "string[]";
629
+ /** A field SHAPE — a record of field-name → field-type, kept in the TYPE so a
630
+ * typed pipeline can cross-reference one agent's `ok` against the next agent's
631
+ * `needs`. The erased runtime form is `Record<string, OutputFieldType>`. */
632
+ export type Shape = Readonly<Record<string, OutputFieldType>>;
389
633
  /**
390
634
  * A subagent's typed result contract: the shape it must return on success
391
635
  * (`ok`) and on failure (`err`). Rich on both tracks — an error is structured
392
636
  * detail, not a bare pass/fail bit. Compiles into the worker's system prompt
393
637
  * (the `vigiles:ok` / `vigiles:err` block it must emit) and is the schema the
394
638
  * `parseAgentResult` parser + the `assertAgentOk/Err` test helpers validate.
639
+ *
640
+ * Generic over its `ok`/`err` field shapes so a typed value REMEMBERS them at
641
+ * the type level (the basis of typed composition — see `pipe`). The default
642
+ * type parameters widen to the historical erased `Shape`, so an `OutputContract`
643
+ * named with no arguments behaves exactly as before — backwards-compatible.
395
644
  */
396
- export interface OutputContract {
645
+ export interface OutputContract<Ok extends Shape = Shape, Err extends Shape = Shape> {
397
646
  readonly _ref: "output";
398
- readonly ok: Readonly<Record<string, OutputFieldType>>;
399
- readonly err: Readonly<Record<string, OutputFieldType>>;
647
+ readonly ok: Ok;
648
+ readonly err: Err;
400
649
  }
401
650
  /**
402
651
  * Declare a subagent's success/error result contract.
@@ -408,8 +657,14 @@ export interface OutputContract {
408
657
  *
409
658
  * (Distinct from a skill's `result:` postcondition gate — this types a
410
659
  * subagent's *return value*, the success/error tracks of the railway.)
660
+ *
661
+ * The literal field shapes are PRESERVED in the return type (`const` inference),
662
+ * not erased to `Record<string, OutputFieldType>` — this is what lets `pipe`
663
+ * cross-reference one agent's `ok` against the next agent's needs at `tsc` time.
664
+ * The return is still an `OutputContract`, so every existing consumer (the
665
+ * `output:` field, `renderOutputContract`, `parseAgentResult`) is unchanged.
411
666
  */
412
- export declare function result(ok: Record<string, OutputFieldType>, err: Record<string, OutputFieldType>): OutputContract;
667
+ export declare function result<const Ok extends Shape, const Err extends Shape>(ok: Ok, err: Err): OutputContract<Ok, Err>;
413
668
  /** One step on a railway: dispatch a flat subagent (the "activity"). */
414
669
  export interface RailwayStep {
415
670
  readonly _step: "delegate";
@@ -417,9 +672,31 @@ export interface RailwayStep {
417
672
  readonly agent: string;
418
673
  /** Optional task hint passed to the worker. */
419
674
  readonly task?: string;
675
+ /**
676
+ * Optional input contract the step reads from its predecessor's `result().ok`.
677
+ * When present, the whole-harness registry (`generate-harness`) emits a
678
+ * per-edge `Handoff<>` assertion so a CROSS-FILE handoff mismatch (a missing
679
+ * field or wrong type vs the prior step's `ok`) is a `tsc` error naming the
680
+ * field. Absent `needs` = no handoff check (today's behavior, the string-path
681
+ * backstop). Built by `needs(...)`, the same builder a typed `pipeStep` uses.
682
+ */
683
+ readonly needs?: Shape;
420
684
  }
421
- /** Build a railway step that dispatches `agent` (optionally with a task hint). */
422
- export declare function delegate(agent: string, task?: string): RailwayStep;
685
+ /**
686
+ * Build a railway step that dispatches `agent`.
687
+ *
688
+ * delegate("planner") // no task, no handoff
689
+ * delegate("implementer", "implement the plan") // task hint only
690
+ * delegate("reviewer", undefined, needs({ diff: "string" })) // + handoff check
691
+ *
692
+ * The optional 3rd argument carries the step's input `needs` (built by
693
+ * `needs(...)`). When present, the whole-harness registry asserts that the
694
+ * PREVIOUS success-track step's `result().ok` SUPPLIES it — a cross-file
695
+ * handoff that doesn't line up is a `tsc` error naming the offending field.
696
+ * Omitting it (the historical 1-/2-arg call) keeps the exact string-path
697
+ * behavior — fully backwards-compatible.
698
+ */
699
+ export declare function delegate(agent: string, task?: string, needsContract?: Shape): RailwayStep;
423
700
  /**
424
701
  * A railway over flat subagents. `steps` run in order on the success track; the
425
702
  * first step that returns an error short-circuits to `onError`. `recover`
@@ -450,6 +727,150 @@ export interface Railway {
450
727
  * })
451
728
  */
452
729
  export declare function railway(spec: Omit<Railway, "_specType">): Railway;
730
+ /**
731
+ * A subagent's INPUT contract — the fields it reads from the prior step's `ok`.
732
+ * Declared via `needs(...)` and threaded into the typed agent so `pipe` can
733
+ * cross-reference it. Independent of `result()` (the output) — an agent both
734
+ * `needs` an input shape and produces an `ok`/`err` output shape.
735
+ */
736
+ export type NeedsContract<N extends Shape> = N;
737
+ /**
738
+ * Declare the input fields a step reads from its predecessor's success payload.
739
+ * Pass it as `needs:` on a typed pipeline step. `needs({})` (the default) is a
740
+ * step with no upstream requirement — valid as the FIRST step of a pipeline.
741
+ *
742
+ * needs({ plan: "string", files: "string[]" })
743
+ */
744
+ export declare function needs<const N extends Shape>(shape: N): NeedsContract<N>;
745
+ /**
746
+ * A typed pipeline step: a `TypedAgentSpec` paired with the input `needs` it
747
+ * reads from the prior step's `ok`. `step()` builds one; `pipe` checks that the
748
+ * prior step's `ok` shape supplies this step's `needs`.
749
+ */
750
+ export interface PipeStep<Needs extends Shape, Ok extends Shape, Err extends Shape> {
751
+ readonly _step: "typed-delegate";
752
+ readonly agent: TypedAgentSpec<Ok, Err>;
753
+ readonly needs: Needs;
754
+ }
755
+ /**
756
+ * Pair a typed agent with the input it `needs` from the previous step. The first
757
+ * argument is an `agent()` VALUE (which carries its `result()` shape); the
758
+ * second is the `needs(...)` input contract.
759
+ *
760
+ * pipeStep(implementer, needs({ plan: "string", files: "string[]" }))
761
+ */
762
+ export declare function pipeStep<Needs extends Shape, Ok extends Shape, Err extends Shape>(a: TypedAgentSpec<Ok, Err>, needsContract?: Needs): PipeStep<Needs, Ok, Err>;
763
+ /**
764
+ * True iff `Producer` provides EVERY field `Consumer` needs, with matching
765
+ * field types. When satisfiable it is `true`; otherwise it collapses to a
766
+ * descriptive error object naming the offending field (`__missing` /
767
+ * `__mismatch`), which surfaces at the mismatched call. Shallow (a per-field
768
+ * mapped type, not a recursion) to avoid TS2589.
769
+ */
770
+ export type Supplies<Producer extends Shape, Consumer extends Shape> = {
771
+ [K in keyof Consumer]: K extends keyof Producer ? Producer[K] extends Consumer[K] ? true : {
772
+ readonly __mismatch: K;
773
+ readonly expected: Consumer[K];
774
+ readonly got: Producer[K];
775
+ } : {
776
+ readonly __missing: K;
777
+ readonly required: Consumer[K];
778
+ };
779
+ }[keyof Consumer];
780
+ /**
781
+ * Per-edge CROSS-FILE handoff check — the registry-scale form of `Supplies<>`,
782
+ * mirroring `KnownAgentName` (the dangling-delegate per-edge check). `Producer`
783
+ * is the prior success-track step's `result().ok` shape (read off the registry
784
+ * via `OkOf`); `Consumer` is THIS step's `needs(...)` input contract. Collapses
785
+ * to `true` when the producer supplies every field the consumer needs (matching
786
+ * types), else to a descriptive error object naming the offending field
787
+ * (`__handoff_error` wrapping `Supplies`'s `__missing`/`__mismatch`), so
788
+ * assigning `true` to it is a `tsc` error at edit time. Shallow (one wrap over
789
+ * the per-field `Supplies` mapped type, no recursion); the generator emits one
790
+ * assertion per consecutive step pair (O(N)), keeping clear of TS2589.
791
+ */
792
+ export type Handoff<Producer extends Shape, Consumer extends Shape> = Supplies<Producer, Consumer> extends true ? true : {
793
+ readonly __handoff_error: Supplies<Producer, Consumer>;
794
+ };
795
+ /** A typed pipeline value — carries the LAST step's `ok` and the UNION of every
796
+ * step's `err` (any step can short-circuit to the error track). */
797
+ export interface Pipeline<Ok extends Shape, Err extends Shape> {
798
+ readonly _specType: "pipeline";
799
+ /** Ordered agent names — the resolved compose order. */
800
+ readonly agents: readonly string[];
801
+ /** The final step's success shape. */
802
+ readonly ok: Ok;
803
+ /** The union of every step's error shape. */
804
+ readonly err: Err;
805
+ /** The underlying string-path railway, for `compileRailway` reuse. */
806
+ readonly railway: Railway;
807
+ }
808
+ /**
809
+ * Begin a typed pipeline from its first step. The first step has no upstream, so
810
+ * its `needs` must be empty (`needs({})` or omitted). Returns a `Pipeline`
811
+ * carrying that step's `ok`/`err` forward.
812
+ */
813
+ export declare function start<Ok extends Shape, Err extends Shape>(first: PipeStep<Record<string, never>, Ok, Err> | TypedAgentSpec<Ok, Err>): Pipeline<Ok, Err>;
814
+ /**
815
+ * Append a step to a typed pipeline. The handoff is CHECKED: the constraint
816
+ * `Supplies<PriorOk, Needs>` must be `true`, else the `next` parameter's type
817
+ * collapses to a `__HANDOFF_ERROR` object and `tsc` rejects the call, naming the
818
+ * missing/mismatched field. Carries the new step's `ok` forward and accumulates
819
+ * the error track. Shallow per-call check — no recursive chain type.
820
+ *
821
+ * Named `andThen` (Wlaschin's railway `bind`/`andThen`), NOT `then`: a module
822
+ * exporting a function called `then` becomes a thenable, so `await import()` of
823
+ * any barrel re-exporting it would invoke it — a footgun the rename avoids.
824
+ */
825
+ export declare function andThen<PriorOk extends Shape, PriorErr extends Shape, Needs extends Shape, Ok extends Shape, Err extends Shape>(prior: Pipeline<PriorOk, PriorErr>, next: Supplies<PriorOk, Needs> extends true ? PipeStep<Needs, Ok, Err> : {
826
+ readonly __HANDOFF_ERROR: Supplies<PriorOk, Needs>;
827
+ }): Pipeline<Ok, PriorErr | Err>;
828
+ /**
829
+ * Compose a typed pipeline in one call — the ergonomic form of
830
+ * `andThen(andThen(start(a), b), c)`. Each adjacent handoff is checked
831
+ * left-to-right: the FIRST step is the producer, the rest are
832
+ * `pipeStep(agent, needs(...))` consumers, and the compiler rejects the whole
833
+ * expression if ANY handoff's producer `ok` does not supply the consumer's
834
+ * `needs` (variadic chains hit TS2589 quickly, so `pipe` is a fixed set of
835
+ * overloads over the shallow `start`/`andThen` fold rather than a recursive
836
+ * variadic type — keep chains to a handful of steps; for longer ones, fold
837
+ * `andThen` explicitly).
838
+ *
839
+ * pipe(
840
+ * planner, // produces ok
841
+ * pipeStep(implementer, needs({ plan: "string", files: "string[]" })),
842
+ * pipeStep(reviewer, needs({ diff: "string" })),
843
+ * ) // ← won't compile if a handoff doesn't line up
844
+ */
845
+ export declare function pipe<A extends Shape, AE extends Shape>(a: TypedAgentSpec<A, AE>): Pipeline<A, AE>;
846
+ export declare function pipe<A extends Shape, AE extends Shape, BN extends Shape, B extends Shape, BE extends Shape>(a: TypedAgentSpec<A, AE>, b: Supplies<A, BN> extends true ? PipeStep<BN, B, BE> : {
847
+ readonly __HANDOFF_ERROR: Supplies<A, BN>;
848
+ }): Pipeline<B, AE | BE>;
849
+ export declare function pipe<A extends Shape, AE extends Shape, BN extends Shape, B extends Shape, BE extends Shape, CN extends Shape, C extends Shape, CE extends Shape>(a: TypedAgentSpec<A, AE>, b: Supplies<A, BN> extends true ? PipeStep<BN, B, BE> : {
850
+ readonly __HANDOFF_ERROR: Supplies<A, BN>;
851
+ }, c: Supplies<B, CN> extends true ? PipeStep<CN, C, CE> : {
852
+ readonly __HANDOFF_ERROR: Supplies<B, CN>;
853
+ }): Pipeline<C, AE | BE | CE>;
854
+ export declare function pipe<A extends Shape, AE extends Shape, BN extends Shape, B extends Shape, BE extends Shape, CN extends Shape, C extends Shape, CE extends Shape, DN extends Shape, D extends Shape, DE extends Shape>(a: TypedAgentSpec<A, AE>, b: Supplies<A, BN> extends true ? PipeStep<BN, B, BE> : {
855
+ readonly __HANDOFF_ERROR: Supplies<A, BN>;
856
+ }, c: Supplies<B, CN> extends true ? PipeStep<CN, C, CE> : {
857
+ readonly __HANDOFF_ERROR: Supplies<B, CN>;
858
+ }, d: Supplies<C, DN> extends true ? PipeStep<DN, D, DE> : {
859
+ readonly __HANDOFF_ERROR: Supplies<C, DN>;
860
+ }): Pipeline<D, AE | BE | CE | DE>;
861
+ /**
862
+ * Per-edge dangling-`delegate` check. `Target` is a delegate target NAME (a
863
+ * string literal the generator reads off a `railway()` value); `Names` is the
864
+ * literal union of every agent name in the harness (emitted by the generator).
865
+ * Collapses to `true` when the target resolves, else to a descriptive error
866
+ * object naming the dangling target + the railway it came from — so assigning
867
+ * `true` to it is a `tsc` error at edit time. Shallow (one conditional, no
868
+ * recursion); the generator emits one assertion per edge (O(N)).
869
+ */
870
+ export type KnownAgentName<Target extends string, Names extends string, From extends string = string> = [Target] extends [Names] ? true : {
871
+ readonly __dangling_delegate: Target;
872
+ readonly from: From;
873
+ };
453
874
  /** Derive the spec filename from an output filename. */
454
875
  export type SpecPath<Output extends `${string}.md`> = `${Output}.spec.ts`;
455
876
  /** Extract the output filename from a spec filename. */