vigiles 5.0.1 → 5.2.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 +15 -9
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
- package/dist/adapters/claude-code/agent-runtime.js +66 -37
- package/dist/adapters/claude-code/dialect.js +37 -2
- 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 +48 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/eval.d.ts +94 -0
- package/dist/adapters/codex/eval.js +227 -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/cli.js +712 -21
- package/dist/codex.d.ts +1 -0
- package/dist/codex.js +3 -0
- 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 +169 -74
- package/dist/core/description-overlap.d.ts +27 -0
- package/dist/core/description-overlap.js +53 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/frontmatter-read.d.ts +25 -0
- package/dist/core/frontmatter-read.js +138 -0
- package/dist/core/hook-events.d.ts +34 -0
- package/dist/core/hook-events.js +48 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-config.d.ts +20 -0
- package/dist/core/mcp-config.js +40 -0
- package/dist/core/mcp-hook.d.ts +35 -0
- package/dist/core/mcp-hook.js +70 -0
- package/dist/core/mcp-tool.d.ts +50 -0
- package/dist/core/mcp-tool.js +61 -0
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +142 -3
- package/dist/core/spec.js +48 -0
- package/dist/core/tool-contract.d.ts +68 -0
- package/dist/core/tool-contract.js +113 -0
- package/dist/core/types.d.ts +91 -2
- package/dist/core/validate.js +23 -1
- package/dist/eval.d.ts +69 -13
- package/dist/eval.js +106 -51
- 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 +63 -3
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/plugin-loader.d.ts +1 -0
- package/dist/plugin-loader.js +71 -18
- package/dist/scaffold-test.d.ts +30 -0
- package/dist/scaffold-test.js +158 -0
- package/dist/scan-behavioral.d.ts +73 -0
- package/dist/scan-behavioral.js +150 -0
- package/dist/scan.d.ts +166 -1
- package/dist/scan.js +622 -55
- 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 -6
- package/skills/edit-spec/SKILL.md +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Codex EVAL-tier transport — the runner + trace parser that
|
|
4
|
+
* `measureTriggerRate`/`runEval` dispatch to via the `ModelOutputParser` seam.
|
|
5
|
+
*
|
|
6
|
+
* SCHEMA: CONFIRMED against real `codex exec --json` (codex-cli 0.139.0, ChatGPT
|
|
7
|
+
* auth). The stream is the thread/item model:
|
|
8
|
+
*
|
|
9
|
+
* {"type":"thread.started","thread_id":"…"}
|
|
10
|
+
* {"type":"turn.started"}
|
|
11
|
+
* {"type":"item.started","item":{"id":"item_0","type":"command_execution",…}} // mid-flight
|
|
12
|
+
* {"type":"item.completed","item":{"id":"item_0","type":"command_execution","command":"…","aggregated_output":"…","exit_code":0}}
|
|
13
|
+
* {"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"…"}}
|
|
14
|
+
* {"type":"turn.completed","usage":{"input_tokens":…,"cached_input_tokens":…,"output_tokens":…}}
|
|
15
|
+
*
|
|
16
|
+
* So: assistant text = `item.completed` with `item.type:"agent_message"` →
|
|
17
|
+
* `item.text`; a tool call = `item.type:"command_execution"` → `item.command`;
|
|
18
|
+
* usage rides `turn.completed`. We count `item.completed` ONLY (an `item.started`
|
|
19
|
+
* carries the same `id` mid-flight — counting both double-counts).
|
|
20
|
+
*
|
|
21
|
+
* THE SKILL FINDING: Codex has NO discrete "skill selected" event (its CLI has no
|
|
22
|
+
* Skill-tool concept). When a skill triggers, the model READS the skill's
|
|
23
|
+
* `SKILL.md` via a `command_execution` (`sed/cat … skills/<name>/SKILL.md`) and
|
|
24
|
+
* usually says so in an `agent_message`. So "did skill X fire" on Codex is not a
|
|
25
|
+
* clean trace event like Claude's `Skill` tool_use — it's detected by the
|
|
26
|
+
* SKILL.md read (`codexSkillFired`). Best-effort by nature (a cached skill might
|
|
27
|
+
* not be re-read); pair with a behavioral/judged check for certainty.
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.codexEvalDriver = void 0;
|
|
31
|
+
exports.parseCodexEvalRun = parseCodexEvalRun;
|
|
32
|
+
exports.codexRunError = codexRunError;
|
|
33
|
+
exports.codexSkillFired = codexSkillFired;
|
|
34
|
+
exports.installCodexSkills = installCodexSkills;
|
|
35
|
+
exports.codexEvalAgentRunner = codexEvalAgentRunner;
|
|
36
|
+
exports.codexEvalRunner = codexEvalRunner;
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const node_path_1 = require("node:path");
|
|
40
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
41
|
+
const num = (v) => (typeof v === "number" ? v : 0);
|
|
42
|
+
/** Parse the JSONL stream, skipping blank / non-JSON / malformed lines. */
|
|
43
|
+
function parseLines(stdout) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
46
|
+
const s = line.trim();
|
|
47
|
+
if (!s.startsWith("{"))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
out.push(JSON.parse(s));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* tolerate a partial / non-event line */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Is this completed item a tool/command call (vs an agent_message / error)? */
|
|
59
|
+
function isToolItem(itemType) {
|
|
60
|
+
return (itemType === "command_execution" || /function_call|tool/.test(itemType));
|
|
61
|
+
}
|
|
62
|
+
function buildCall(item, itemType) {
|
|
63
|
+
return {
|
|
64
|
+
// command_execution → the shell command; function-style → its name.
|
|
65
|
+
name: str(item.command) || str(item.name) || itemType,
|
|
66
|
+
input: item,
|
|
67
|
+
resultText: str(item.aggregated_output),
|
|
68
|
+
isError: num(item.exit_code) !== 0 && item.exit_code != null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Map a `turn.completed` usage block to the common EvalUsage. */
|
|
72
|
+
function usageFrom(u) {
|
|
73
|
+
return {
|
|
74
|
+
costUsd: 0, // codex on the ChatGPT sub reports no per-run USD
|
|
75
|
+
durationMs: 0,
|
|
76
|
+
inputTokens: num(u.input_tokens),
|
|
77
|
+
outputTokens: num(u.output_tokens),
|
|
78
|
+
cacheCreationTokens: 0,
|
|
79
|
+
cacheReadTokens: num(u.cached_input_tokens),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const ZERO_USAGE = {
|
|
83
|
+
costUsd: 0,
|
|
84
|
+
durationMs: 0,
|
|
85
|
+
inputTokens: 0,
|
|
86
|
+
outputTokens: 0,
|
|
87
|
+
cacheCreationTokens: 0,
|
|
88
|
+
cacheReadTokens: 0,
|
|
89
|
+
};
|
|
90
|
+
/** Parse `codex exec --json` stdout into the common trace fields (confirmed schema). */
|
|
91
|
+
function parseCodexEvalRun(out) {
|
|
92
|
+
const texts = [];
|
|
93
|
+
const toolCalls = [];
|
|
94
|
+
let usage = ZERO_USAGE;
|
|
95
|
+
for (const e of parseLines(out.stdout)) {
|
|
96
|
+
if (e.type === "turn.completed" && e.usage)
|
|
97
|
+
usage = usageFrom(e.usage);
|
|
98
|
+
// Count COMPLETED items only — item.started carries the same id mid-flight.
|
|
99
|
+
if (e.type !== "item.completed" || !e.item)
|
|
100
|
+
continue;
|
|
101
|
+
const itemType = str(e.item.type);
|
|
102
|
+
if (itemType === "agent_message") {
|
|
103
|
+
const t = str(e.item.text);
|
|
104
|
+
if (t)
|
|
105
|
+
texts.push(t);
|
|
106
|
+
}
|
|
107
|
+
else if (isToolItem(itemType)) {
|
|
108
|
+
toolCalls.push(buildCall(e.item, itemType));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
// Fallback to trimmed stdout if no agent_message was seen (keeps output non-empty).
|
|
113
|
+
output: texts.join("\n") || out.stdout.trim(),
|
|
114
|
+
turns: texts.length,
|
|
115
|
+
toolCalls,
|
|
116
|
+
hooks: [],
|
|
117
|
+
subagents: [],
|
|
118
|
+
usage,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The error message if the run errored or was rate-limited (an `error` /
|
|
123
|
+
* `turn.failed` event), else null. CRITICAL for the eval tier: an errored turn
|
|
124
|
+
* must NOT be scored as a clean "skill didn't fire" miss — dogfooding hit a Codex
|
|
125
|
+
* usage limit ("You've hit your usage limit…") whose `error` event left an empty
|
|
126
|
+
* trace that `codexSkillFired` read as recall 0. A caller should skip/retry an
|
|
127
|
+
* errored run, not count it. (The Claude path has `isRateLimited` + backoff; this
|
|
128
|
+
* is the Codex equivalent detector.)
|
|
129
|
+
*/
|
|
130
|
+
function codexRunError(out) {
|
|
131
|
+
for (const e of parseLines(out.stdout)) {
|
|
132
|
+
if (e.type === "error")
|
|
133
|
+
return str(e.message) || "codex error";
|
|
134
|
+
if (e.type === "turn.failed")
|
|
135
|
+
return str(e.error?.message) || "turn failed";
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Did Codex activate skill `name` on this run? Detected by the SKILL.md read —
|
|
141
|
+
* Codex has no discrete skill-selection event, so when a skill triggers the model
|
|
142
|
+
* reads its `…/<name>/SKILL.md` via a `command_execution`. Best-effort (a cached
|
|
143
|
+
* skill might not be re-read); for the trigger-rate `fired` predicate over Codex.
|
|
144
|
+
*/
|
|
145
|
+
function codexSkillFired(run, name) {
|
|
146
|
+
const needle = `${name}/SKILL.md`;
|
|
147
|
+
return run.toolCalls.some((c) => str(c.name).includes(needle));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Materialize a (Claude-shaped) plugin dir's skills into `<cwd>/.codex/skills/` —
|
|
151
|
+
* where Codex actually discovers them (validated live: codex reads
|
|
152
|
+
* `<cwd>/.codex/skills/<name>/SKILL.md`). This is the Codex analog of Claude's
|
|
153
|
+
* `--plugin-dir`: `measureTriggerRate` hands the runner a `pluginDir` (the
|
|
154
|
+
* stubbed/packaged skills), and the Codex runner installs them here before the
|
|
155
|
+
* turn. Pure fs — unit-testable without a binary.
|
|
156
|
+
*/
|
|
157
|
+
function installCodexSkills(pluginDir, cwd) {
|
|
158
|
+
const skillsRoot = (0, node_path_1.join)(pluginDir, "skills");
|
|
159
|
+
if (!(0, node_fs_1.existsSync)(skillsRoot))
|
|
160
|
+
return 0;
|
|
161
|
+
let n = 0;
|
|
162
|
+
for (const name of (0, node_fs_1.readdirSync)(skillsRoot)) {
|
|
163
|
+
const src = (0, node_path_1.join)(skillsRoot, name, "SKILL.md");
|
|
164
|
+
if (!(0, node_fs_1.existsSync)(src))
|
|
165
|
+
continue;
|
|
166
|
+
const dest = (0, node_path_1.join)(cwd, ".codex", "skills", name, "SKILL.md");
|
|
167
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
|
|
168
|
+
(0, node_fs_1.writeFileSync)(dest, (0, node_fs_1.readFileSync)(src, "utf-8"));
|
|
169
|
+
n += 1;
|
|
170
|
+
}
|
|
171
|
+
return n;
|
|
172
|
+
}
|
|
173
|
+
/* v8 ignore start -- real codex subprocess; validated against the binary, not the unit gate */
|
|
174
|
+
/**
|
|
175
|
+
* The Codex eval-tier `AgentRunner`: install the run's skills into `.codex/skills`
|
|
176
|
+
* (Codex's discovery path, vs Claude's `--plugin-dir`), then drive a real
|
|
177
|
+
* `codex exec --json` turn. The seam `measureTriggerRate(spec, { evalDriver:
|
|
178
|
+
* codexEvalDriver })` dispatches through.
|
|
179
|
+
*/
|
|
180
|
+
function codexEvalAgentRunner(args) {
|
|
181
|
+
if (args.pluginDir)
|
|
182
|
+
installCodexSkills(args.pluginDir, args.cwd);
|
|
183
|
+
return Promise.resolve(codexEvalRunner({
|
|
184
|
+
task: args.task,
|
|
185
|
+
cwd: args.cwd,
|
|
186
|
+
timeoutMs: args.timeoutMs,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The Codex eval driver — pass to `measureTriggerRate(spec, { evalDriver:
|
|
191
|
+
* codexEvalDriver })` to run a trigger-rate eval natively on `codex exec`. Pair
|
|
192
|
+
* the spec's `fired` with `codexSkillFired` (Codex has no Skill-tool event).
|
|
193
|
+
*/
|
|
194
|
+
exports.codexEvalDriver = {
|
|
195
|
+
runner: codexEvalAgentRunner,
|
|
196
|
+
parse: parseCodexEvalRun,
|
|
197
|
+
runError: codexRunError,
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Spawn real `codex exec --json` for the eval tier (real model, the user's codex
|
|
201
|
+
* auth — NOT the mock). CONFIRMED flags (codex 0.139.0): `--json` for the event
|
|
202
|
+
* stream, `--skip-git-repo-check` for a bare cwd, the approvals/sandbox bypass so
|
|
203
|
+
* the turn runs unattended, `-C <cwd>` for the working dir, prompt as the trailing
|
|
204
|
+
* positional, and stdin = /dev/null (`stdio: ["ignore",…]`) — codex otherwise
|
|
205
|
+
* blocks on "Reading additional input from stdin…". Needs ChatGPT/API auth +
|
|
206
|
+
* network egress to the model backend.
|
|
207
|
+
*/
|
|
208
|
+
function codexEvalRunner(args) {
|
|
209
|
+
const r = (0, node_child_process_1.spawnSync)("codex", [
|
|
210
|
+
"exec",
|
|
211
|
+
"--json",
|
|
212
|
+
"--skip-git-repo-check",
|
|
213
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
214
|
+
"-C",
|
|
215
|
+
args.cwd,
|
|
216
|
+
args.task,
|
|
217
|
+
], {
|
|
218
|
+
cwd: args.cwd,
|
|
219
|
+
encoding: "utf-8",
|
|
220
|
+
timeout: args.timeoutMs,
|
|
221
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
222
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
223
|
+
});
|
|
224
|
+
return { code: r.status ?? 1, stdout: r.stdout ?? "" };
|
|
225
|
+
}
|
|
226
|
+
/* v8 ignore stop */
|
|
227
|
+
//# sourceMappingURL=eval.js.map
|
|
@@ -9,6 +9,9 @@ exports.codexLayout = {
|
|
|
9
9
|
settingsFormat: "toml",
|
|
10
10
|
instructionFile: "AGENTS.md",
|
|
11
11
|
surfaceDirs: ["skills", "prompts"],
|
|
12
|
+
skillDir: "skills",
|
|
13
|
+
agentDir: "", // Codex `[agents]` is a TOML concurrency table, not a subagent dir
|
|
14
|
+
commandDir: "prompts",
|
|
12
15
|
materializeRoot: ".codex",
|
|
13
16
|
pluginRootToken: "${PLUGIN_ROOT}",
|
|
14
17
|
mcpConfigFile: ".mcp.json",
|
|
@@ -16,6 +16,9 @@ exports.opencodeLayout = {
|
|
|
16
16
|
// `.opencode/` segment. (Contrast Claude Code: root-level `skills/` surfaces
|
|
17
17
|
// relocated under `.claude`.)
|
|
18
18
|
surfaceDirs: [".opencode/agent", ".opencode/command"],
|
|
19
|
+
skillDir: ".opencode/skill",
|
|
20
|
+
agentDir: ".opencode/agent",
|
|
21
|
+
commandDir: ".opencode/command",
|
|
19
22
|
materializeRoot: "",
|
|
20
23
|
pluginRootToken: "${OPENCODE_PLUGIN_ROOT}",
|
|
21
24
|
mcpConfigFile: "opencode.json",
|
package/dist/check.d.ts
CHANGED
|
@@ -104,6 +104,14 @@ export declare function turns(opts: {
|
|
|
104
104
|
}): Check<Trace>;
|
|
105
105
|
/** The agent wrote (or left) a file at this path in the work dir. */
|
|
106
106
|
export declare function wrote(path: string): Check<Trace>;
|
|
107
|
+
/**
|
|
108
|
+
* The agent did NOT leave a file at this path — the **side-effect boundary**
|
|
109
|
+
* negative: a skill that declares it writes only `out.txt` should leave nothing
|
|
110
|
+
* at `secrets.env`. The symmetric sibling of `wrote()`; pairs with
|
|
111
|
+
* `notTool(...)` to assert a unit stayed inside its declared write surface
|
|
112
|
+
* deterministically (no model judge).
|
|
113
|
+
*/
|
|
114
|
+
export declare function didNotWrite(path: string): Check<Trace>;
|
|
107
115
|
/** The named subagent (`Task` `subagent_type`) ran and passed every nested check. */
|
|
108
116
|
export declare function subagent(name: string, checks: readonly Check<Trace>[]): Check<Trace>;
|
|
109
117
|
/** The hook blocked the event (exit 2 / deny / block). */
|
package/dist/check.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.hookFired = hookFired;
|
|
|
11
11
|
exports.received = received;
|
|
12
12
|
exports.turns = turns;
|
|
13
13
|
exports.wrote = wrote;
|
|
14
|
+
exports.didNotWrite = didNotWrite;
|
|
14
15
|
exports.subagent = subagent;
|
|
15
16
|
exports.blocked = blocked;
|
|
16
17
|
exports.allowed = allowed;
|
|
@@ -236,16 +237,34 @@ function wrote(path) {
|
|
|
236
237
|
toJSON: () => ({ kind: "wrote", path }),
|
|
237
238
|
};
|
|
238
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* The agent did NOT leave a file at this path — the **side-effect boundary**
|
|
242
|
+
* negative: a skill that declares it writes only `out.txt` should leave nothing
|
|
243
|
+
* at `secrets.env`. The symmetric sibling of `wrote()`; pairs with
|
|
244
|
+
* `notTool(...)` to assert a unit stayed inside its declared write surface
|
|
245
|
+
* deterministically (no model judge).
|
|
246
|
+
*/
|
|
247
|
+
function didNotWrite(path) {
|
|
248
|
+
return {
|
|
249
|
+
kind: "didNotWrite",
|
|
250
|
+
eval: (t) => t.file(path) === null
|
|
251
|
+
? ok(`file "${path}" was not created`)
|
|
252
|
+
: no(`expected the agent NOT to create "${path}", but it exists`),
|
|
253
|
+
toJSON: () => ({ kind: "didNotWrite", path }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
239
256
|
// ---------------------------------------------------------------------------
|
|
240
257
|
// Subagent — a `Task` run as a nested trace. Run checks over what the SUBAGENT
|
|
241
258
|
// did, not just that `Task` fired. Composes the whole vocabulary recursively.
|
|
242
259
|
// ---------------------------------------------------------------------------
|
|
243
|
-
/** Wrap a subagent's tool calls as a minimal `Trace` so checks
|
|
260
|
+
/** Wrap a subagent's tool calls + returned text as a minimal `Trace` so checks
|
|
261
|
+
* (incl. `output()` over the sub's RETURN — where a result() vigiles:ok/err block
|
|
262
|
+
* lands) run over it. */
|
|
244
263
|
function subTrace(sub) {
|
|
245
264
|
return {
|
|
246
265
|
toolCalls: sub.toolCalls,
|
|
247
266
|
hooks: [],
|
|
248
|
-
output:
|
|
267
|
+
output: sub.output,
|
|
249
268
|
modelRequests: [],
|
|
250
269
|
turns: 0,
|
|
251
270
|
subagents: [],
|
|
@@ -258,7 +277,12 @@ function subagent(name, checks) {
|
|
|
258
277
|
kind: "subagent",
|
|
259
278
|
eval: (t) => {
|
|
260
279
|
const subs = t.subagents ?? [];
|
|
261
|
-
|
|
280
|
+
// A `--plugin-dir` agent's `subagent_type` is namespaced `plugin:agent`
|
|
281
|
+
// (e.g. "reviewer-spec:code-reviewer"), but callers pass the bare agent name
|
|
282
|
+
// — so match the full id OR its last `:`-segment. Non-namespaced (harness
|
|
283
|
+
// mock) names match exactly as before.
|
|
284
|
+
const bare = (n) => n.includes(":") ? n.slice(n.lastIndexOf(":") + 1) : n;
|
|
285
|
+
const sub = subs.find((s) => s.name === name || bare(s.name) === name);
|
|
262
286
|
if (!sub) {
|
|
263
287
|
return no(`expected subagent "${name}" to run; subagents that ran: ${subs.length > 0 ? `[${subs.map((s) => s.name).join(", ")}]` : "none"}`);
|
|
264
288
|
}
|