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.
@@ -100,18 +100,68 @@ console.log(formatTriggerRateReport(report));
100
100
  assertTriggerRate(report, { min: 0.8, maxFalsePositive: 0.3 });
101
101
  `;
102
102
  }
103
- /** A subagent the deterministic harness tier; point at the railway/Result path. */
104
- function agentScaffold(input) {
103
+ /** A JSON value placeholder for an `OutputFieldType`, for the `vigiles:ok` block. */
104
+ function placeholderFor(type) {
105
+ switch (type) {
106
+ case "number":
107
+ return 1;
108
+ case "boolean":
109
+ return true;
110
+ case "string[]":
111
+ return ["example"];
112
+ default:
113
+ return "example";
114
+ }
115
+ }
116
+ /** Render a `result(ok, err)` builder call reconstructed from the parsed contract. */
117
+ function renderContractBuilder(contract) {
118
+ const shape = (fields) => `{ ${fields.map((f) => `${f.name}: ${JSON.stringify(f.type)}`).join(", ")} }`;
119
+ return `result(\n ${shape(contract.ok)},\n ${shape(contract.err)},\n)`;
120
+ }
121
+ /**
122
+ * The OUTCOME test, GENERATED FROM the subagent's `result()` contract: reconstruct
123
+ * the contract, build a matching `vigiles:ok` block, and `assertAgentOk` it —
124
+ * deterministic, no LLM judge. This is the typed-spec payoff a markdown
125
+ * `description:` cannot give you: a parseable, typed outcome a test reads directly.
126
+ */
127
+ function outcomeSection(input, contract) {
128
+ const okValue = Object.fromEntries(contract.ok.map((f) => [f.name, placeholderFor(f.type)]));
129
+ const firstField = contract.ok[0]?.name;
130
+ const fieldAssertion = firstField
131
+ ? `// TODO: assert the VALUES you expect (the shape is already validated above), e.g.:\n// assert.ok(value.${firstField}, "expected a ${firstField}");`
132
+ : "";
133
+ return `import assert from "node:assert/strict";
134
+ import { result } from "vigiles/spec";
135
+ import { assertAgentOk } from "vigiles/testing";
136
+
137
+ // Reconstructed from ${input.name}'s ## Output contract (its compiled .md) — the
138
+ // typed result() the spec wrote. assertAgentOk parses + validates the outcome
139
+ // with NO model judge; swap \`okOutput\` for a real \`runHarness\` turn (Part B in
140
+ // examples/harness/railway-result.harness.mjs) to assert REAL behaviour.
141
+ const contract = ${renderContractBuilder(contract)};
142
+
143
+ const okOutput = [
144
+ "${input.name} finished its task.",
145
+ "\`\`\`vigiles:ok",
146
+ ${JSON.stringify(JSON.stringify(okValue))},
147
+ "\`\`\`",
148
+ ].join("\\n");
149
+
150
+ const value = assertAgentOk(okOutput, contract); // deterministic — no LLM judge
151
+ ${fieldAssertion}
152
+ console.log("✓ ${input.name}: result() outcome parses + validates against its typed contract");
153
+ `;
154
+ }
155
+ /** The fallback when the subagent has no `result()` contract — assert a tool use. */
156
+ function fallbackSection(input) {
105
157
  const toolHint = input.tools && input.tools.length > 0
106
158
  ? `assertToolUsed(r, ${JSON.stringify(input.tools[0])}); // its declared contract: ${input.tools.join(", ")}`
107
159
  : `assertToolUsed(r, "Task"); // TODO: assert what the subagent should do`;
108
- return `${header(`Starter harness test for the \`${input.name}\` subagent.`, `npx vigiles test ${suggestedPath(input)}`)}
109
- import { runHarnessTest, assertToolUsed } from "vigiles/testing";
110
-
111
- // A subagent's OUTCOME is best asserted via a result() contract — deterministic,
112
- // no LLM judge. If ${input.name} has one, use assertAgentOk(r.output, contract)
113
- // instead; see the railway-result example in the vigiles docs.
160
+ return `import { runHarnessTest, assertToolUsed } from "vigiles/testing";
114
161
 
162
+ // ${input.name} has no result() contract, so its outcome can't be asserted
163
+ // deterministically — add one (result() on its agent() spec) for a no-judge
164
+ // outcome test. For now, assert it reaches for the right tool.
115
165
  const r = await runHarnessTest({
116
166
  plugin: ".", // TODO: the plugin dir holding this subagent
117
167
  // TODO: a prompt that dispatches ${input.name} (via the Task tool).
@@ -124,6 +174,61 @@ ${toolHint}
124
174
  console.log("✓ ${input.name}: subagent test ran");
125
175
  `;
126
176
  }
177
+ /**
178
+ * The SAFETY check, GENERATED FROM the subagent's side-effecting `tools`: assert it
179
+ * stays inside its declared write surface and never reaches for a destructive op.
180
+ * The `tools` allowlist + effectSurface identify the "hole"; the check asserts it —
181
+ * a test the typed contract writes for you (markdown can declare the tools, not test them).
182
+ */
183
+ function safetySection(input, sideEffecting) {
184
+ const checks = [];
185
+ if (sideEffecting.includes("Bash")) {
186
+ checks.push(` notTool("Bash", { command: /git push|rm -rf/ }), // never a destructive op`);
187
+ }
188
+ if (sideEffecting.some((t) => t === "Write" || t === "Edit")) {
189
+ checks.push(` didNotWrite("secrets.env"), // TODO: the path(s) it must NOT write outside its surface`);
190
+ }
191
+ if (checks.length === 0) {
192
+ checks.push(` // TODO: a notTool()/didNotWrite() per side-effecting tool: ${sideEffecting.join(", ")}`);
193
+ }
194
+ return `
195
+ // --- Safety (deterministic) — generated from ${input.name}'s side-effecting tools: ${sideEffecting.join(", ")} ---
196
+ // In a real run, replace this constructed Trace with a real \`runHarness\` /
197
+ // \`measure\` turn (use interceptTools so a real model's attempt is DENIED, never
198
+ // executed — see docs/eval-architecture.md). The checks below are derived from the
199
+ // declared tools contract — the agent's "hole" asserted to stay in its lane.
200
+ {
201
+ const trace = {
202
+ output: "done",
203
+ turns: 1,
204
+ hooks: [],
205
+ toolCalls: [
206
+ // TODO: the tool calls a benign run of ${input.name} makes.
207
+ { name: "Bash", input: { command: "git status" } },
208
+ ],
209
+ file: () => null,
210
+ };
211
+ assertChecks(trace, [
212
+ ${checks.join("\n")}
213
+ ]);
214
+ console.log("✓ ${input.name}: stayed inside its declared side-effect surface");
215
+ }
216
+ `;
217
+ }
218
+ /** A subagent → deterministic outcome + safety tests, generated from its typed contract. */
219
+ function agentScaffold(input) {
220
+ const head = header(`Starter harness test for the \`${input.name}\` subagent.`, `npx vigiles test ${suggestedPath(input)}`);
221
+ const safetyImport = input.sideEffectingTools && input.sideEffectingTools.length > 0
222
+ ? `import { notTool, didNotWrite, assertChecks } from "vigiles/testing";\n`
223
+ : "";
224
+ const body = input.resultContract
225
+ ? outcomeSection(input, input.resultContract)
226
+ : fallbackSection(input);
227
+ const safety = input.sideEffectingTools && input.sideEffectingTools.length > 0
228
+ ? safetySection(input, input.sideEffectingTools)
229
+ : "";
230
+ return `${head}\n${safetyImport}${body}${safety}`;
231
+ }
127
232
  const TIER = {
128
233
  hook: "unit",
129
234
  skill: "eval",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "5.2.0",
3
+ "version": "6.0.0",
4
4
  "description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
5
5
  "keywords": [
6
6
  "claude-code",