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.
- package/README.md +59 -18
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +45 -6
- package/dist/adapters/claude-code/agent-runtime.js +94 -8
- package/dist/adapters/claude-code/dialect.d.ts +34 -0
- package/dist/adapters/claude-code/dialect.js +51 -19
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +40 -0
- package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
- package/dist/adapters/claude-code/typed-spec.js +55 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/claude-code.d.ts +1 -0
- package/dist/claude-code.js +8 -1
- package/dist/cli.js +469 -88
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +176 -39
- package/dist/core/dialect.d.ts +10 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/generate-harness.d.ts +187 -0
- package/dist/core/generate-harness.js +337 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-tool.d.ts +1 -1
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +432 -11
- package/dist/core/spec.js +166 -3
- package/dist/core/tool-contract.d.ts +1 -1
- package/dist/core/types.d.ts +6 -6
- package/dist/core/validate.js +4 -4
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +2 -0
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/scaffold-test.d.ts +58 -0
- package/dist/scaffold-test.js +263 -0
- package/dist/scan.d.ts +40 -0
- package/dist/scan.js +91 -43
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -4
- package/skills/edit-spec/SKILL.md +1 -1
package/dist/core/spec.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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:
|
|
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:
|
|
399
|
-
readonly err:
|
|
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:
|
|
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
|
-
/**
|
|
422
|
-
|
|
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. */
|