@maxiaochao/pi-toolkit 0.11.18 → 0.11.20
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/ARCHITECTURE.md +6 -2
- package/packages/herdr-subagents/CHANGELOG.md +8 -0
- package/packages/herdr-subagents/README.md +5 -1
- package/packages/herdr-subagents/TESTING.md +2 -0
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +13 -21
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/config.ts +7 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2026-10-14 - v0.11.20
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.5.3: allow optional Pi-native tool exposure while inheriting Pi defaults when unset
|
|
4
|
+
|
|
5
|
+
## 2026-10-14 - v0.11.19
|
|
6
|
+
|
|
7
|
+
- herdr-subagents v0.5.2: stop Pi startup from scanning historical runs or closing recorded Herdr tabs
|
|
8
|
+
|
|
1
9
|
## 2026-10-14 - v0.11.18
|
|
2
10
|
|
|
3
11
|
- simplify global worktree isolation guidance
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.20",
|
|
4
4
|
"description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -107,6 +107,10 @@ There is no fixed drain delay and no transcript parsing. Pi retries are internal
|
|
|
107
107
|
|
|
108
108
|
Writing the same report again is idempotent. A conflicting replacement is rejected.
|
|
109
109
|
|
|
110
|
-
## Cancellation and
|
|
110
|
+
## Cancellation and shutdown
|
|
111
111
|
|
|
112
|
-
Caller or parent-session cancellation aborts active waits, marks nonterminal tasks cancelled, and closes the owned
|
|
112
|
+
Caller or parent-session cancellation aborts active waits, marks nonterminal tasks cancelled, and closes the Herdr tab owned by that session. A new Pi session never scans, mutates, resumes, or closes historical runs during startup. If Pi is terminated before its shutdown handlers run, its visible Subagent tabs remain for explicit user inspection or cleanup.
|
|
113
|
+
|
|
114
|
+
## Tool exposure
|
|
115
|
+
|
|
116
|
+
The extension reads optional `exposure` once before registering its two tools. When omitted, the extension also omits the registration field and delegates to Pi's default. Global or project configuration can select Pi's `model-only` or `direct` exposure explicitly. Changing this registration-time setting requires `/reload` or a Pi restart; the extension does not re-register tools during a session.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 2026-10-14 - v0.5.3
|
|
2
|
+
|
|
3
|
+
- allow optional Pi-native tool exposure while inheriting Pi defaults when unset
|
|
4
|
+
|
|
5
|
+
## 2026-10-14 - v0.5.2
|
|
6
|
+
|
|
7
|
+
- stop Pi startup from scanning historical runs or closing recorded Herdr tabs
|
|
8
|
+
|
|
1
9
|
## 2026-10-14 - v0.5.1
|
|
2
10
|
|
|
3
11
|
- fix focus-safe run cleanup and compact report output
|
|
@@ -36,6 +36,8 @@ The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.we
|
|
|
36
36
|
|
|
37
37
|
Each task starts a single-task interactive Pi process in its own Herdr pane. Herdr first confirms the interactive Pi is ready, then the prompt is delivered exactly once. Prompt acknowledgement never decides task completion. Panes remain ordinary Herdr panes: they can be focused and inspected while work is running or after it settles. An early-finishing child stays at the Pi UI while sibling tasks continue. After the whole run settles, cleanup closes an unfocused run tab immediately or waits for the user to leave a focused run tab.
|
|
38
38
|
|
|
39
|
+
Cleanup belongs to the Pi session that created the run. Starting a new Pi session never scans historical runs or closes their recorded Herdr tabs. If Pi terminates before shutdown cleanup runs, any surviving Subagent tab remains visible for explicit inspection or cleanup.
|
|
40
|
+
|
|
39
41
|
## Result protocol
|
|
40
42
|
|
|
41
43
|
The child must call `subagent_report` once with a terminal status:
|
|
@@ -87,10 +89,12 @@ The orchestrator is the only writer of `run.json`, `task.json`, and promoted `re
|
|
|
87
89
|
|
|
88
90
|
`herdr_subagents_control` supports `list`, `status`, `history`, `cancel`, and `cleanup`. Follow-up work is a new task or run; settled tasks are never revived.
|
|
89
91
|
|
|
92
|
+
When `exposure` is absent, both tools omit the registration field and inherit Pi's default exposure. Set `"exposure": "model-only"` to keep explicit top-level model calls while excluding Codemode scripts and other nested tools, or `"exposure": "direct"` to request Pi's direct-and-nested behavior explicitly. Exposure is read when the extension loads, so use `/reload` or restart Pi after changing it.
|
|
93
|
+
|
|
90
94
|
Use `/herdr-subagents focus <task-number>` to focus a running pane. `/herdr-subagents active`, `history`, `agents`, `models`, and `settings` provide compact inspection.
|
|
91
95
|
|
|
92
96
|
## Personas and settings
|
|
93
97
|
|
|
94
98
|
Bundled personas are `worker`, `explorer`, `analyst`, and `reviewer`. Project personas can be added under `.pi/agents/` using the same frontmatter format as the bundled files. Access is declared as `read` or `write` and drives concurrency safety.
|
|
95
99
|
|
|
96
|
-
Settings are read from the
|
|
100
|
+
Settings are read from `~/.pi/agent/herdr-subagents.json` and the project-level `.pi/herdr-subagents.json`, with the project value taking precedence. Optional `exposure` accepts Pi's `model-only` or `direct`; omitting it delegates to Pi. `defaultConcurrency`, `maxConcurrency`, and `stalledWarningSeconds` remain supported.
|
|
@@ -16,6 +16,8 @@ It verifies:
|
|
|
16
16
|
- waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
|
|
17
17
|
- explicit failure when the process exits without a report;
|
|
18
18
|
- parent cancellation of running and queued tasks;
|
|
19
|
+
- startup isolation: a new Pi session neither mutates historical records nor closes their recorded Herdr tabs;
|
|
20
|
+
- omitted exposure delegates both tools to Pi's default, while project configuration can consistently select model-only exposure;
|
|
19
21
|
- immutable terminal task records;
|
|
20
22
|
- idempotent duplicate reports and rejection of conflicting replacements;
|
|
21
23
|
- keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI.
|
|
@@ -2,12 +2,11 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionCommandContext, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Container, Editor, Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
5
|
+
import { loadSubagentConfiguration, type ConfigurationPaths } from "../src/config.ts";
|
|
6
6
|
import { CliHerdrAutomation, type HerdrAutomation } from "../src/herdr.ts";
|
|
7
7
|
import { activeRunText } from "../src/monitor.ts";
|
|
8
8
|
import { historyText, listSubagentHistory } from "../src/history.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcile.ts";
|
|
9
|
+
import { cancelActiveSubagentRuns } from "../src/reconcile.ts";
|
|
11
10
|
import { executeHerdrSubagents, executeHerdrSubagentsControl, type HerdrSubagentsBatchDetails, type HerdrSubagentsControlToolParams, type HerdrSubagentsRunParams } from "../src/tool.ts";
|
|
12
11
|
import { HerdrSubagentsControlParams, HerdrSubagentsParams } from "../src/parameters.ts";
|
|
13
12
|
import { HERDR_ACTIONS } from "../src/validation.ts";
|
|
@@ -63,7 +62,7 @@ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRender
|
|
|
63
62
|
return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
|
|
65
|
+
async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController, registeredExposure: { value?: "direct" | "model-only"; source: string }): Promise<void> {
|
|
67
66
|
const section = args.trim() || "active";
|
|
68
67
|
if (section === "active") {
|
|
69
68
|
ctx.ui.notify(fleet.active ? activeRunText(fleet.active) : "No active Subagent run.", "info");
|
|
@@ -91,7 +90,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
91
90
|
if (section === "settings") {
|
|
92
91
|
const settings = configuration.settings;
|
|
93
92
|
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking };
|
|
94
|
-
const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`);
|
|
93
|
+
const lines = [`exposure=${registeredExposure.value ?? "Pi default"} (${registeredExposure.source}; loaded at extension registration)`, ...Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`)];
|
|
95
94
|
if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
|
|
96
95
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
97
96
|
return;
|
|
@@ -101,6 +100,8 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
101
100
|
|
|
102
101
|
export interface HerdrSubagentsExtensionDependencies {
|
|
103
102
|
herdr?: HerdrAutomation;
|
|
103
|
+
configurationCwd?: string;
|
|
104
|
+
configurationPaths?: ConfigurationPaths;
|
|
104
105
|
execute?: typeof executeHerdrSubagents;
|
|
105
106
|
executeControl?: typeof executeHerdrSubagentsControl;
|
|
106
107
|
isEditor?: (value: unknown) => boolean;
|
|
@@ -108,19 +109,6 @@ export interface HerdrSubagentsExtensionDependencies {
|
|
|
108
109
|
fleetWidgetRuntime?: FleetWidgetRuntime;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
async function recoverParentSession(cwd: string, herdr: HerdrAutomation, owner: { sessionId?: string; processId: number }, notify: (message: string) => void): Promise<void> {
|
|
112
|
-
try {
|
|
113
|
-
const queued = await loadQueuedRuns(cwd, notify);
|
|
114
|
-
await cancelQueuedRuns(cwd, queued.map((run) => run.runId), { reason: "Owning parent Pi session is no longer active.", owner, onlyOrphaned: true, onError: notify });
|
|
115
|
-
}
|
|
116
|
-
catch (error) { notify(`Subagent queue recovery failed: ${error instanceof Error ? error.message : String(error)}`); }
|
|
117
|
-
try {
|
|
118
|
-
const reconciliation = await reconcileSubagentRuns(cwd, herdr, owner);
|
|
119
|
-
for (const message of reconciliation.cleanupErrors) notify(message);
|
|
120
|
-
}
|
|
121
|
-
catch (error) { notify(`Subagent recovery could not inspect active runs: ${error instanceof Error ? error.message : String(error)}`); }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
112
|
async function shutdownParentSession(options: { cwd: string; herdr: HerdrAutomation; owner: { sessionId?: string; processId: number }; fleet: FleetController; sessionController: AbortController; runControllers: Map<string, AbortController>; inFlight: Set<Promise<unknown>>; notify: (message: string) => void }): Promise<void> {
|
|
125
113
|
options.sessionController.abort(new Error("Parent Pi session closed."));
|
|
126
114
|
for (const controller of options.runControllers.values()) controller.abort(new Error("Parent Pi session closed."));
|
|
@@ -135,6 +123,9 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
135
123
|
const herdr = dependencies.herdr ?? new CliHerdrAutomation();
|
|
136
124
|
const execute = dependencies.execute ?? executeHerdrSubagents;
|
|
137
125
|
const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
|
|
126
|
+
const registrationConfiguration = loadSubagentConfiguration(dependencies.configurationCwd ?? process.cwd(), packageAgentsDir, undefined, dependencies.configurationPaths);
|
|
127
|
+
const exposure = registrationConfiguration.settings.exposure;
|
|
128
|
+
const registeredExposure = { value: exposure, source: registrationConfiguration.settingSources.exposure ?? "Pi" };
|
|
138
129
|
const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
|
|
139
130
|
const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget, dependencies.fleetWidgetRuntime);
|
|
140
131
|
const runControllers = new Map<string, AbortController>();
|
|
@@ -146,16 +137,16 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
146
137
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
147
138
|
await shutdownParentSession({ cwd: ctx.cwd, herdr, owner: { sessionId: ownerSessionId, processId: process.pid }, fleet, sessionController, runControllers, inFlight, notify: (message) => ctx.ui.notify(message, "error") });
|
|
148
139
|
});
|
|
149
|
-
pi.on("session_start",
|
|
140
|
+
pi.on("session_start", (_event, ctx) => {
|
|
150
141
|
sessionController.abort(new Error("Parent Pi session changed."));
|
|
151
142
|
sessionController = new AbortController();
|
|
152
143
|
ownerSessionId = `${ctx.sessionManager.getSessionId()}:${randomUUID()}`;
|
|
153
144
|
fleet.bind(ctx.ui, ctx.mode === "tui");
|
|
154
|
-
await recoverParentSession(ctx.cwd, herdr, { sessionId: ownerSessionId, processId: process.pid }, (message) => ctx.ui.notify(message, "error"));
|
|
155
145
|
});
|
|
156
146
|
|
|
157
147
|
pi.registerTool({
|
|
158
148
|
name: "herdr_subagents",
|
|
149
|
+
...(exposure ? { exposure } : {}),
|
|
159
150
|
label: "Herdr Subagents",
|
|
160
151
|
description: "Run one or more ordered tasks in visible, focusable Herdr Pi panes. concurrency 1 is strict serial execution; higher values form a bounded pool for read-only tasks. Write-capable tasks require concurrency 1. The call returns only after every task has one terminal completed, failed, or cancelled result. Complete results and Pi sessions stay in project-local records.",
|
|
161
152
|
promptSnippet: "Run focused work in visible Herdr subagents",
|
|
@@ -196,6 +187,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
196
187
|
|
|
197
188
|
pi.registerTool({
|
|
198
189
|
name: "herdr_subagents_control",
|
|
190
|
+
...(exposure ? { exposure } : {}),
|
|
199
191
|
label: "Herdr Subagents Control",
|
|
200
192
|
description: "Inspect Herdr personas, query one run by ID, list run history, cancel active work, or remove an archived run. Use herdr_subagents to start new work.",
|
|
201
193
|
parameters: HerdrSubagentsControlParams,
|
|
@@ -246,7 +238,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
246
238
|
pi.registerCommand("herdr-subagents", {
|
|
247
239
|
description: "Inspect effective subagent agents, models, and settings",
|
|
248
240
|
getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
249
|
-
handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet),
|
|
241
|
+
handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet, registeredExposure),
|
|
250
242
|
});
|
|
251
243
|
}
|
|
252
244
|
|
|
@@ -4,8 +4,10 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { discoverPackageAgents, type AgentDefinition } from "./personas.ts";
|
|
5
5
|
|
|
6
6
|
export type PersonaAccess = "read" | "write";
|
|
7
|
+
export type SubagentToolExposure = "direct" | "model-only";
|
|
7
8
|
export interface PersonaConfig { model?: string; thinking?: string; skills?: string[]; extensions?: string[]; }
|
|
8
9
|
export interface SubagentConfig {
|
|
10
|
+
exposure?: SubagentToolExposure;
|
|
9
11
|
defaultConcurrency: number;
|
|
10
12
|
maxConcurrency: number;
|
|
11
13
|
stalledWarningSeconds: number;
|
|
@@ -32,6 +34,7 @@ export interface SubagentConfiguration {
|
|
|
32
34
|
diagnostics: string[];
|
|
33
35
|
}
|
|
34
36
|
interface ConfigFile {
|
|
37
|
+
exposure?: string;
|
|
35
38
|
defaultConcurrency?: number;
|
|
36
39
|
maxConcurrency?: number;
|
|
37
40
|
stalledWarningSeconds?: number;
|
|
@@ -61,6 +64,10 @@ function readConfig(path: string, source: string, diagnostics: string[]): Config
|
|
|
61
64
|
|
|
62
65
|
function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, sources: Record<string, string>, diagnostics: string[]): SubagentConfig {
|
|
63
66
|
const result: SubagentConfig = { ...base, personas: { ...base.personas } };
|
|
67
|
+
if (input.exposure !== undefined) {
|
|
68
|
+
if (input.exposure === "direct" || input.exposure === "model-only") { result.exposure = input.exposure; sources.exposure = source; }
|
|
69
|
+
else diagnostics.push(`${source} exposure must be 'direct' or 'model-only'`);
|
|
70
|
+
}
|
|
64
71
|
for (const key of ["defaultConcurrency", "maxConcurrency", "stalledWarningSeconds"] as const) {
|
|
65
72
|
const value = input[key];
|
|
66
73
|
if (value === undefined) continue;
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
|
|
6
6
|
import registerSubagentReport from "../extensions/subagent-report.ts";
|
|
7
|
+
import { registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
|
|
7
8
|
import { applyTaskStatus } from "../src/records.ts";
|
|
8
9
|
|
|
9
10
|
process.env.HERDR_ENV = "1";
|
|
@@ -150,6 +151,50 @@ await assert.rejects(() => run([task("writer", "edit", "write"), task("reader",
|
|
|
150
151
|
const terminal = { id: "task", runId: "run", order: 1, status: "completed" };
|
|
151
152
|
assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are immutable/);
|
|
152
153
|
|
|
154
|
+
{
|
|
155
|
+
const lifecycleRoot = join(root, "startup-history-isolation");
|
|
156
|
+
const runDirectory = join(lifecycleRoot, ".pi", "herdr-subagents", "runs", "historical-run");
|
|
157
|
+
const taskDirectory = join(runDirectory, "tasks", "01-task");
|
|
158
|
+
mkdirSync(taskDirectory, { recursive: true });
|
|
159
|
+
const historicalRun = { id: "historical-run", label: "historical", status: "completed", ownerSessionId: "old-session", ownerProcessId: 99999999, tabId: "w1:t-reused", taskIds: ["historical-task"], completedAt: "2026-10-01T00:00:00.000Z" };
|
|
160
|
+
writeFileSync(join(runDirectory, "run.json"), JSON.stringify(historicalRun));
|
|
161
|
+
writeFileSync(join(taskDirectory, "task.json"), JSON.stringify({ id: "historical-task", runId: "historical-run", order: 1, status: "completed", completedAt: historicalRun.completedAt }));
|
|
162
|
+
const events = new Map();
|
|
163
|
+
const registeredTools = new Map();
|
|
164
|
+
const herdrCalls = [];
|
|
165
|
+
registerHerdrSubagents({
|
|
166
|
+
on(name, handler) { events.set(name, handler); },
|
|
167
|
+
registerTool(tool) { registeredTools.set(tool.name, tool); },
|
|
168
|
+
registerCommand() {},
|
|
169
|
+
}, {
|
|
170
|
+
herdr: {
|
|
171
|
+
async isTabFocused(tabId) { herdrCalls.push(["isTabFocused", tabId]); return false; },
|
|
172
|
+
async closeTab(tabId) { herdrCalls.push(["closeTab", tabId]); },
|
|
173
|
+
},
|
|
174
|
+
isEditor: () => false,
|
|
175
|
+
});
|
|
176
|
+
await events.get("session_start")?.({}, {
|
|
177
|
+
cwd: lifecycleRoot,
|
|
178
|
+
mode: "rpc",
|
|
179
|
+
sessionManager: { getSessionId: () => "new-session" },
|
|
180
|
+
ui: { notify() {} },
|
|
181
|
+
});
|
|
182
|
+
assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
|
|
183
|
+
assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
|
|
184
|
+
assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
|
|
185
|
+
assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
|
|
186
|
+
mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
|
|
187
|
+
writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only" }));
|
|
188
|
+
const configuredTools = new Map();
|
|
189
|
+
registerHerdrSubagents({
|
|
190
|
+
on() {},
|
|
191
|
+
registerTool(tool) { configuredTools.set(tool.name, tool); },
|
|
192
|
+
registerCommand() {},
|
|
193
|
+
}, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot });
|
|
194
|
+
assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
|
|
195
|
+
assert.equal(configuredTools.get("herdr_subagents_control")?.exposure, "model-only", "Project configuration applies consistently to both Subagent tools");
|
|
196
|
+
}
|
|
197
|
+
|
|
153
198
|
{
|
|
154
199
|
const directory = join(root, "report-idempotency");
|
|
155
200
|
mkdirSync(directory, { recursive: true });
|