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