@selesai/code 0.8.6 → 0.8.7
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 +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { visibleWidth, type MarkdownTheme } from "@earendil-works/pi-tui";
|
|
7
|
+
import { EXTERNAL_RUN_REGISTRY_KEY, EXTERNAL_RUN_REGISTRY_VERSION, registerExternalRun } from "../../src/api/external-runs.ts";
|
|
7
8
|
import { collectFleetSnapshot, openSubagentFleet, SubagentFleetComponent } from "../../src/tui/fleet.ts";
|
|
8
9
|
import { persistForegroundRunHistory, restoreForegroundRunHistory } from "../../src/runs/foreground/foreground-history.ts";
|
|
9
10
|
import { FLEET_STATUS_WIDGET_KEY } from "../../src/tui/fleet-status.ts";
|
|
@@ -13,6 +14,10 @@ import type { SubagentState } from "../../src/shared/types.ts";
|
|
|
13
14
|
import { fauxAssistantMessage } from "@earendil-works/pi-ai/providers/faux";
|
|
14
15
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
15
16
|
|
|
17
|
+
function clearExternalRuns(): void {
|
|
18
|
+
delete (globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)];
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
function stateForTest(): SubagentState {
|
|
17
22
|
return {
|
|
18
23
|
baseCwd: process.cwd(),
|
|
@@ -168,6 +173,138 @@ describe("native subagent fleet", () => {
|
|
|
168
173
|
}
|
|
169
174
|
});
|
|
170
175
|
|
|
176
|
+
it("shows cached external jobs as display-only without reading paths or invoking controls", async () => {
|
|
177
|
+
clearExternalRuns();
|
|
178
|
+
registerExternalRun({
|
|
179
|
+
id: "external-review",
|
|
180
|
+
sessionId: "session-current",
|
|
181
|
+
source: "interactive-shell",
|
|
182
|
+
label: "Dependency review",
|
|
183
|
+
state: "completed",
|
|
184
|
+
startedAt: 100,
|
|
185
|
+
updatedAt: 200,
|
|
186
|
+
currentAction: "Inspecting package metadata",
|
|
187
|
+
preview: "No dependency blockers found.",
|
|
188
|
+
reportPath: "/outside/trusted/roots/report.md",
|
|
189
|
+
transcriptPath: "/outside/trusted/roots/transcript.jsonl",
|
|
190
|
+
});
|
|
191
|
+
const state = stateForTest();
|
|
192
|
+
const snapshot = collectFleetSnapshot(state);
|
|
193
|
+
assert.equal(snapshot.error, undefined);
|
|
194
|
+
assert.deepEqual(snapshot.items.map((item) => ({ key: item.key, kind: item.kind, agent: item.agent })), [{
|
|
195
|
+
key: "external:external-review",
|
|
196
|
+
kind: "external",
|
|
197
|
+
agent: "Dependency review",
|
|
198
|
+
}]);
|
|
199
|
+
let steerCalls = 0;
|
|
200
|
+
let stopCalls = 0;
|
|
201
|
+
let inspectCalls = 0;
|
|
202
|
+
const component = new SubagentFleetComponent(
|
|
203
|
+
{ terminal: { rows: 32, columns: 120 }, requestRender() {} } as never,
|
|
204
|
+
theme as never,
|
|
205
|
+
state,
|
|
206
|
+
() => {},
|
|
207
|
+
{
|
|
208
|
+
refreshMs: 60_000,
|
|
209
|
+
markdownTheme,
|
|
210
|
+
actions: {
|
|
211
|
+
async steer() { steerCalls++; return { text: "unexpected" }; },
|
|
212
|
+
stop() { stopCalls++; return { text: "unexpected" }; },
|
|
213
|
+
async inspect() { inspectCalls++; return { text: "unexpected" }; },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
try {
|
|
218
|
+
const initial = component.render(120).join("\n");
|
|
219
|
+
assert.match(initial, /Fleet inspector.*external display-only/);
|
|
220
|
+
assert.match(initial, /Source: external · display-only/);
|
|
221
|
+
assert.match(initial, /Owner: interactive-shell/);
|
|
222
|
+
assert.match(initial, /Inspecting package metadata/);
|
|
223
|
+
assert.match(initial, /Elapsed: 100ms/);
|
|
224
|
+
assert.match(initial, /No dependency blockers found/);
|
|
225
|
+
assert.match(initial, /Report path: \/outside\/trusted\/roots\/report\.md/);
|
|
226
|
+
assert.match(initial, /Transcript path: \/outside\/trusted\/roots\/transcript\.jsonl/);
|
|
227
|
+
assert.doesNotMatch(initial, /Herdr ·|steer ·|stop ·/);
|
|
228
|
+
component.handleInput("H");
|
|
229
|
+
assert.match(component.render(120).join("\n"), /display-only and have no Herdr controls/);
|
|
230
|
+
component.handleInput("s");
|
|
231
|
+
assert.match(component.render(120).join("\n"), /display-only and remain controlled/);
|
|
232
|
+
component.handleInput("D");
|
|
233
|
+
assert.match(component.render(120).join("\n"), /display-only and remain controlled/);
|
|
234
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
235
|
+
assert.deepEqual({ steerCalls, stopCalls, inspectCalls }, { steerCalls: 0, stopCalls: 0, inspectCalls: 0 });
|
|
236
|
+
} finally {
|
|
237
|
+
component.dispose();
|
|
238
|
+
clearExternalRuns();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("keeps running external elapsed time live when endedAt is present", () => {
|
|
243
|
+
clearExternalRuns();
|
|
244
|
+
try {
|
|
245
|
+
const startedAt = Date.now() - 5_000;
|
|
246
|
+
registerExternalRun({
|
|
247
|
+
id: "external-running",
|
|
248
|
+
sessionId: "session-current",
|
|
249
|
+
source: "interactive-shell",
|
|
250
|
+
label: "Running external job",
|
|
251
|
+
state: "running",
|
|
252
|
+
startedAt,
|
|
253
|
+
updatedAt: startedAt + 100,
|
|
254
|
+
endedAt: startedAt + 100,
|
|
255
|
+
});
|
|
256
|
+
const component = new SubagentFleetComponent(
|
|
257
|
+
{ terminal: { rows: 32, columns: 120 }, requestRender() {} } as never,
|
|
258
|
+
theme as never,
|
|
259
|
+
stateForTest(),
|
|
260
|
+
() => {},
|
|
261
|
+
{ refreshMs: 60_000, markdownTheme },
|
|
262
|
+
);
|
|
263
|
+
try {
|
|
264
|
+
const rendered = component.render(120).join("\n");
|
|
265
|
+
assert.match(rendered, /Elapsed: 5\.0s/);
|
|
266
|
+
assert.doesNotMatch(rendered, /Elapsed: 100ms/);
|
|
267
|
+
} finally {
|
|
268
|
+
component.dispose();
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
clearExternalRuns();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("ignores malformed cached external jobs while rendering Fleet", () => {
|
|
276
|
+
clearExternalRuns();
|
|
277
|
+
const warnings: string[] = [];
|
|
278
|
+
const originalWarn = console.warn;
|
|
279
|
+
console.warn = (message?: unknown) => warnings.push(String(message));
|
|
280
|
+
(globalThis as Record<PropertyKey, unknown>)[Symbol.for(EXTERNAL_RUN_REGISTRY_KEY)] = {
|
|
281
|
+
version: EXTERNAL_RUN_REGISTRY_VERSION,
|
|
282
|
+
runs: new Map<string, unknown>([[
|
|
283
|
+
"session-current\0bad",
|
|
284
|
+
{ id: "bad", sessionId: "session-current", source: "tool", label: "Bad external", state: "running", startedAt: Number.NaN },
|
|
285
|
+
]]),
|
|
286
|
+
};
|
|
287
|
+
const state = stateForTest();
|
|
288
|
+
const component = new SubagentFleetComponent(
|
|
289
|
+
{ terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
|
|
290
|
+
theme as never,
|
|
291
|
+
state,
|
|
292
|
+
() => {},
|
|
293
|
+
{ refreshMs: 60_000, markdownTheme },
|
|
294
|
+
);
|
|
295
|
+
try {
|
|
296
|
+
const snapshot = collectFleetSnapshot(state);
|
|
297
|
+
assert.equal(snapshot.error, undefined);
|
|
298
|
+
assert.deepEqual(snapshot.items, []);
|
|
299
|
+
assert.match(warnings[0]!, /Removed Malformed cached external run/);
|
|
300
|
+
assert.match(component.render(100).join("\n"), /No current-session Fleet jobs/);
|
|
301
|
+
} finally {
|
|
302
|
+
console.warn = originalWarn;
|
|
303
|
+
component.dispose();
|
|
304
|
+
clearExternalRuns();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
171
308
|
it("shows live prompt summaries and opens Prompt Audit with the authored prompt visible", async () => {
|
|
172
309
|
const sentinel = "PROMPT_AUDIT_SENTINEL_1021";
|
|
173
310
|
const secondSentinel = "PROMPT_AUDIT_SECOND_CHILD";
|
|
@@ -367,16 +504,26 @@ describe("native subagent fleet", () => {
|
|
|
367
504
|
assert.equal(snapshot.items[0]?.agent, "workflow");
|
|
368
505
|
});
|
|
369
506
|
|
|
370
|
-
it("
|
|
507
|
+
it("hides workflow-owned foreground duplicates and opens the workflow parent inspector", async () => {
|
|
371
508
|
const state = stateForTest();
|
|
372
509
|
state.asyncJobs.set("workflow-1", {
|
|
373
510
|
asyncId: "workflow-1",
|
|
374
511
|
asyncDir: "/tmp/workflow-1",
|
|
512
|
+
sessionId: "session-current",
|
|
375
513
|
status: "running",
|
|
376
514
|
mode: "workflow",
|
|
377
515
|
startedAt: 10,
|
|
378
516
|
updatedAt: 20,
|
|
379
517
|
});
|
|
518
|
+
state.asyncJobs.set("unrelated", {
|
|
519
|
+
asyncId: "unrelated",
|
|
520
|
+
asyncDir: "/tmp/unrelated",
|
|
521
|
+
sessionId: "session-current",
|
|
522
|
+
status: "running",
|
|
523
|
+
mode: "single",
|
|
524
|
+
startedAt: 5,
|
|
525
|
+
updatedAt: 30,
|
|
526
|
+
});
|
|
380
527
|
state.foregroundControls.set("child-1", {
|
|
381
528
|
runId: "child-1",
|
|
382
529
|
parentWorkflowRunId: "workflow-1",
|
|
@@ -387,8 +534,7 @@ describe("native subagent fleet", () => {
|
|
|
387
534
|
activeChildren: new Map([[0, { index: 0, agent: "reviewer", startedAt: 10, updatedAt: 20 }]]),
|
|
388
535
|
});
|
|
389
536
|
const snapshot = collectFleetSnapshot(state);
|
|
390
|
-
|
|
391
|
-
assert.equal(child?.kind, "foreground-active");
|
|
537
|
+
assert.deepEqual(snapshot.items.map((item) => item.key), ["async:unrelated", "async:workflow-1"]);
|
|
392
538
|
const calls: Array<{ runId: string; asyncDir: string; index?: number }> = [];
|
|
393
539
|
const component = new SubagentFleetComponent(
|
|
394
540
|
{ terminal: { rows: 28, columns: 100 }, requestRender() {} } as never,
|
|
@@ -406,7 +552,7 @@ describe("native subagent fleet", () => {
|
|
|
406
552
|
},
|
|
407
553
|
);
|
|
408
554
|
try {
|
|
409
|
-
assert.ok(component.render(100).some((line) => line.includes("
|
|
555
|
+
assert.ok(component.render(100).some((line) => line.includes("workflow")));
|
|
410
556
|
component.handleInput("H");
|
|
411
557
|
await new Promise((resolve) => setImmediate(resolve));
|
|
412
558
|
assert.deepEqual(calls, [{ runId: "workflow-1", asyncDir: "/tmp/workflow-1" }]);
|
|
@@ -536,6 +682,7 @@ describe("native subagent fleet", () => {
|
|
|
536
682
|
fs.writeFileSync(outputPath, "restored foreground output", "utf-8");
|
|
537
683
|
|
|
538
684
|
const state = stateForTest();
|
|
685
|
+
state.artifactDirPreference = "project";
|
|
539
686
|
state.foregroundRuns!.set("restored", {
|
|
540
687
|
runId: "restored",
|
|
541
688
|
mode: "single",
|
|
@@ -556,6 +703,7 @@ describe("native subagent fleet", () => {
|
|
|
556
703
|
|
|
557
704
|
const restored = stateForTest();
|
|
558
705
|
restored.baseCwd = cwd;
|
|
706
|
+
restored.artifactDirPreference = "project";
|
|
559
707
|
assert.equal(restoreForegroundRunHistory(restored, { resultsDir }), 1);
|
|
560
708
|
const snapshot = collectFleetSnapshot(restored);
|
|
561
709
|
assert.deepEqual(snapshot.items.map((item) => item.key), ["foreground-recent:restored:0"]);
|
|
@@ -636,6 +784,7 @@ describe("native subagent fleet", () => {
|
|
|
636
784
|
const cwd = path.join(root, "project");
|
|
637
785
|
const artifactsRoot = getProjectArtifactsDir(cwd);
|
|
638
786
|
const state = stateForTest();
|
|
787
|
+
state.artifactDirPreference = "project";
|
|
639
788
|
for (let index = 0; index < 3; index++) {
|
|
640
789
|
const outputPath = path.join(artifactsRoot, "outputs", `run-${index}`, "output.md");
|
|
641
790
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
@@ -654,6 +803,7 @@ describe("native subagent fleet", () => {
|
|
|
654
803
|
|
|
655
804
|
const restored = stateForTest();
|
|
656
805
|
restored.baseCwd = cwd;
|
|
806
|
+
restored.artifactDirPreference = "project";
|
|
657
807
|
assert.equal(restoreForegroundRunHistory(restored, { resultsDir, limit: 2 }), 2);
|
|
658
808
|
assert.deepEqual([...restored.foregroundRuns!.keys()], ["run-2", "run-1"]);
|
|
659
809
|
|
|
@@ -802,6 +952,7 @@ describe("native subagent fleet", () => {
|
|
|
802
952
|
fs.writeFileSync(path.join(asyncDir, "status.json"), "{in-flight status", "utf-8");
|
|
803
953
|
const state = stateForTest();
|
|
804
954
|
state.baseCwd = path.join(root, "parent-cwd");
|
|
955
|
+
state.artifactDirPreference = "project";
|
|
805
956
|
state.asyncJobs.set("async-custom-cwd", {
|
|
806
957
|
asyncId: "async-custom-cwd",
|
|
807
958
|
asyncDir,
|
|
@@ -883,6 +1034,7 @@ describe("native subagent fleet", () => {
|
|
|
883
1034
|
try {
|
|
884
1035
|
const state = stateForTest();
|
|
885
1036
|
state.baseCwd = path.join(root, "parent-cwd");
|
|
1037
|
+
state.artifactDirPreference = "project";
|
|
886
1038
|
const effectiveCwd = path.join(root, "effective-cwd");
|
|
887
1039
|
const now = Date.now();
|
|
888
1040
|
state.foregroundControls.set("foreground-live", {
|
|
@@ -1282,6 +1434,40 @@ describe("native subagent fleet", () => {
|
|
|
1282
1434
|
}
|
|
1283
1435
|
});
|
|
1284
1436
|
|
|
1437
|
+
it("periodically redraws only while the overlay remains open", (t) => {
|
|
1438
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
1439
|
+
const state = stateForTest();
|
|
1440
|
+
let renderRequests = 0;
|
|
1441
|
+
let closed = false;
|
|
1442
|
+
const tui = { terminal: { rows: 28, columns: 90 }, requestRender: () => { renderRequests++; } };
|
|
1443
|
+
const component = new SubagentFleetComponent(
|
|
1444
|
+
tui as never,
|
|
1445
|
+
theme as never,
|
|
1446
|
+
state,
|
|
1447
|
+
() => { closed = true; },
|
|
1448
|
+
{ refreshMs: 10 },
|
|
1449
|
+
);
|
|
1450
|
+
t.mock.timers.tick(249);
|
|
1451
|
+
assert.equal(renderRequests, 0, "refresh cadence is bounded away from a hot loop");
|
|
1452
|
+
t.mock.timers.tick(1);
|
|
1453
|
+
assert.equal(renderRequests, 1);
|
|
1454
|
+
component.handleInput("\x1b");
|
|
1455
|
+
assert.equal(closed, true);
|
|
1456
|
+
t.mock.timers.tick(1_000);
|
|
1457
|
+
assert.equal(renderRequests, 1, "closing cancels periodic redraws");
|
|
1458
|
+
|
|
1459
|
+
const disposed = new SubagentFleetComponent(
|
|
1460
|
+
tui as never,
|
|
1461
|
+
theme as never,
|
|
1462
|
+
state,
|
|
1463
|
+
() => {},
|
|
1464
|
+
{ refreshMs: 250 },
|
|
1465
|
+
);
|
|
1466
|
+
disposed.dispose();
|
|
1467
|
+
t.mock.timers.tick(1_000);
|
|
1468
|
+
assert.equal(renderRequests, 1, "disposing cancels periodic redraws");
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1285
1471
|
it("refreshes the roster while the overlay remains open", async () => {
|
|
1286
1472
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-refresh-"));
|
|
1287
1473
|
try {
|
|
@@ -1293,7 +1479,7 @@ describe("native subagent fleet", () => {
|
|
|
1293
1479
|
theme as never,
|
|
1294
1480
|
state,
|
|
1295
1481
|
() => {},
|
|
1296
|
-
{ asyncDirRoot: root, resultsDir: path.join(root, "results"), refreshMs:
|
|
1482
|
+
{ asyncDirRoot: root, resultsDir: path.join(root, "results"), refreshMs: 250 },
|
|
1297
1483
|
);
|
|
1298
1484
|
let invalidations = 0;
|
|
1299
1485
|
const originalInvalidate = component.invalidate.bind(component);
|
|
@@ -1305,7 +1491,7 @@ describe("native subagent fleet", () => {
|
|
|
1305
1491
|
assert.ok(component.render(90).some((line) => line.includes("No tracked children")));
|
|
1306
1492
|
const initialOutput = Array.from({ length: 40 }, (_, index) => `output line ${index}`).join("\n");
|
|
1307
1493
|
const asyncDir = writeAsyncRun(root, { id: "appeared-live", output: initialOutput });
|
|
1308
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1494
|
+
await new Promise((resolve) => setTimeout(resolve, 275));
|
|
1309
1495
|
let lines = component.render(90);
|
|
1310
1496
|
assert.ok(lines.some((line) => line.includes("appeared")));
|
|
1311
1497
|
assert.ok(lines.some((line) => line.includes("output line 39")));
|
|
@@ -54,6 +54,38 @@ describe("Herdr inspector bootstrap", () => {
|
|
|
54
54
|
}
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
it("uses PATH node when Pi is a standalone executable", async () => {
|
|
58
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-bootstrap-standalone-"));
|
|
59
|
+
const originalExecPath = process.execPath;
|
|
60
|
+
try {
|
|
61
|
+
writeCompletedRun(root);
|
|
62
|
+
process.execPath = path.join(root, process.platform === "win32" ? "pi.exe" : "pi");
|
|
63
|
+
const calls: string[][] = [];
|
|
64
|
+
const client: HerdrClient = {
|
|
65
|
+
run: async <T>(args: string[]) => {
|
|
66
|
+
calls.push(args);
|
|
67
|
+
if (args[0] === "--version") return { ok: true, data: "herdr 0.7.5" as T };
|
|
68
|
+
if (args[0] === "pane" && args[1] === "split") return { ok: true, data: { pane: { pane_id: "w1:p9" } } as T };
|
|
69
|
+
return { ok: true, data: {} as T };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const opened = await handleHerdrInspectorAction("inspector.open", { id: "run-123" }, {
|
|
73
|
+
cwd: root,
|
|
74
|
+
asyncDirRoot: root,
|
|
75
|
+
resultsDir: path.join(root, "results"),
|
|
76
|
+
client,
|
|
77
|
+
});
|
|
78
|
+
assert.equal(opened.isError, undefined);
|
|
79
|
+
const command = calls.find((args) => args[0] === "pane" && args[1] === "run")?.[3] ?? "";
|
|
80
|
+
assert.equal(command.startsWith("& "), process.platform === "win32");
|
|
81
|
+
assert.match(command, process.platform === "win32" ? /^& "node\.exe" / : /^node /);
|
|
82
|
+
assert.doesNotMatch(command, /(?:^|[\\/])pi(?:\.exe)?'/);
|
|
83
|
+
} finally {
|
|
84
|
+
process.execPath = originalExecPath;
|
|
85
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
57
89
|
it("starts the packaged bootstrap with ordinary Node", () => {
|
|
58
90
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-herdr-bootstrap-run-"));
|
|
59
91
|
try {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import { formatShellCommand } from "../../src/inspectors/herdr/shell-command.ts";
|
|
8
|
+
|
|
9
|
+
describe("formatShellCommand on POSIX shells", () => {
|
|
10
|
+
const platform: NodeJS.Platform = "linux";
|
|
11
|
+
|
|
12
|
+
it("keeps a shell-safe executable bare so Nushell parses it as a command", () => {
|
|
13
|
+
assert.equal(
|
|
14
|
+
formatShellCommand("/usr/bin/node", ["/home/u/.pi/inspector-runner.mjs", "--async-dir", "/tmp/run-1|fc_abc"], platform),
|
|
15
|
+
"/usr/bin/node '/home/u/.pi/inspector-runner.mjs' '--async-dir' '/tmp/run-1|fc_abc'",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps bare command names bare", () => {
|
|
20
|
+
assert.equal(formatShellCommand("pi", ["review the diff"], platform), "pi 'review the diff'");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("uses a bare sh invoker for an executable containing spaces and equals", () => {
|
|
24
|
+
assert.equal(
|
|
25
|
+
formatShellCommand("/opt/My Tools/node=bin", ["--flag"], platform),
|
|
26
|
+
`sh -c 'exec "$0" "$@"' '/opt/My Tools/node=bin' '--flag'`,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("quotes executable paths and arguments containing quotes", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
formatShellCommand("/opt/My Tools/it's node", ["it's", "a|b"], platform),
|
|
33
|
+
"sh -c 'exec \"$0\" \"$@\"' '/opt/My Tools/it'\\''s node' 'it'\\''s' 'a|b'",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("executes an equals-sign path through the shell wrapper", { skip: process.platform === "win32" }, () => {
|
|
38
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi herdr shell="));
|
|
39
|
+
try {
|
|
40
|
+
const executable = path.join(root, "tool=' value");
|
|
41
|
+
fs.writeFileSync(executable, "#!/bin/sh\nprintf '%s' \"$1\"\n", "utf-8");
|
|
42
|
+
fs.chmodSync(executable, 0o755);
|
|
43
|
+
const launched = spawnSync("/bin/sh", ["-c", formatShellCommand(executable, ["arg=' value"], platform)], { encoding: "utf-8" });
|
|
44
|
+
assert.equal(launched.status, 0, launched.stderr);
|
|
45
|
+
assert.equal(launched.stdout, "arg=' value");
|
|
46
|
+
} finally {
|
|
47
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("formatShellCommand on Windows PowerShell", () => {
|
|
53
|
+
it("prefixes the call operator and preserves argument quoting", () => {
|
|
54
|
+
assert.equal(
|
|
55
|
+
formatShellCommand("C:\\Program Files\\nodejs\\node.exe", ["two words", 'say "hi"'], "win32"),
|
|
56
|
+
'& "C:\\Program Files\\nodejs\\node.exe" "two words" "say \\"hi\\""',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -368,6 +368,75 @@ describe("subagent extension child mode", () => {
|
|
|
368
368
|
}
|
|
369
369
|
});
|
|
370
370
|
|
|
371
|
+
it("uses configured main-window renderer spacing for call rows", () => {
|
|
372
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-renderer-density-config-"));
|
|
373
|
+
try {
|
|
374
|
+
const configDir = path.join(agentDir, "extensions", "subagent");
|
|
375
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
376
|
+
fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0 } }), "utf-8");
|
|
377
|
+
|
|
378
|
+
const script = String.raw`
|
|
379
|
+
import registerSubagentExtension from "./index.ts";
|
|
380
|
+
const events = { on() { return () => {}; }, emit() {} };
|
|
381
|
+
let registeredTool;
|
|
382
|
+
const fakePi = new Proxy({
|
|
383
|
+
events,
|
|
384
|
+
registerTool(tool) { if (tool.name === "subagent") registeredTool = tool; },
|
|
385
|
+
registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {}, sendMessage() {}, getSessionName() {},
|
|
386
|
+
}, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
|
|
387
|
+
registerSubagentExtension(fakePi);
|
|
388
|
+
if (!registeredTool) throw new Error("tool not registered");
|
|
389
|
+
const theme = { fg(_name, text) { return text; }, bold(text) { return text; } };
|
|
390
|
+
const call = registeredTool.renderCall({ agent: "worker", async: true }, theme).render(120).map((line) => line.trimEnd());
|
|
391
|
+
if (call.length !== 1 || call[0] !== "subagentworker[async]") throw new Error("unexpected call row: " + JSON.stringify(call));
|
|
392
|
+
`;
|
|
393
|
+
const env = parentToolEnv();
|
|
394
|
+
env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
395
|
+
execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
|
|
396
|
+
} finally {
|
|
397
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("uses configured main-window renderer density for slash results", () => {
|
|
402
|
+
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-slash-renderer-density-config-"));
|
|
403
|
+
try {
|
|
404
|
+
const configDir = path.join(agentDir, "extensions", "subagent");
|
|
405
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
406
|
+
fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({ mainWindowRenderer: { horizontalSpacing: 0, compactResultMaxLines: 3 } }), "utf-8");
|
|
407
|
+
|
|
408
|
+
const script = String.raw`
|
|
409
|
+
import registerSubagentExtension from "./index.ts";
|
|
410
|
+
const events = { on() { return () => {}; }, emit() {} };
|
|
411
|
+
let slashRenderer;
|
|
412
|
+
const fakePi = new Proxy({
|
|
413
|
+
events,
|
|
414
|
+
registerTool() {}, registerCommand() {}, registerShortcut() {}, sendMessage() {}, getSessionName() {},
|
|
415
|
+
registerMessageRenderer(type, renderer) { if (type === "subagent-slash-result") slashRenderer = renderer; },
|
|
416
|
+
}, { get(target, prop) { return prop in target ? target[prop] : () => undefined; } });
|
|
417
|
+
registerSubagentExtension(fakePi);
|
|
418
|
+
if (!slashRenderer) throw new Error("slash renderer not registered");
|
|
419
|
+
const result = slashRenderer({ details: {
|
|
420
|
+
requestId: "slash-density",
|
|
421
|
+
result: { content: [{ type: "text", text: "done" }], details: { mode: "parallel", results: [
|
|
422
|
+
{ agent: "scout", task: "a", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
|
|
423
|
+
{ agent: "reviewer", task: "b", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
|
|
424
|
+
{ agent: "writer", task: "c", exitCode: 0, messages: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 } },
|
|
425
|
+
] } },
|
|
426
|
+
} }, { expanded: false }, { fg(_name, text) { return text; }, bg(_name, text) { return text; }, bold(text) { return text; } });
|
|
427
|
+
const lines = result.render(120);
|
|
428
|
+
if (lines.length !== 6) throw new Error("expected outer spacer, box rows, and three capped result rows: " + JSON.stringify(lines));
|
|
429
|
+
if (!lines[4].includes("rows hidden")) throw new Error("compact cap was not applied: " + JSON.stringify(lines));
|
|
430
|
+
if (!lines[3].includes(" ✓ Agent 1/3: scout")) throw new Error("zero spacing was not applied: " + JSON.stringify(lines));
|
|
431
|
+
`;
|
|
432
|
+
const env = parentToolEnv();
|
|
433
|
+
env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
434
|
+
execFileSync(process.execPath, ["--experimental-strip-types", "--import", "./test/support/register-loader.mjs", "--input-type=module", "--eval", script], { cwd: projectRoot, env, stdio: "pipe" });
|
|
435
|
+
} finally {
|
|
436
|
+
fs.rmSync(agentDir, { recursive: true, force: true });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
371
440
|
it("registers only subagent_wait and honors waitTool disabled config", () => {
|
|
372
441
|
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-wait-tool-config-"));
|
|
373
442
|
try {
|
|
@@ -104,6 +104,19 @@ describe("resolveIntercomBridge", () => {
|
|
|
104
104
|
assert.equal(bridge.extensionDir, NATIVE_INTERCOM_EXTENSION_DIR);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it("uses a per-run override instead of the global config", () => {
|
|
108
|
+
const bridge = resolveIntercomBridge({
|
|
109
|
+
config: { mode: "always", resultDelivery: true },
|
|
110
|
+
override: { mode: "off" },
|
|
111
|
+
context: "fresh",
|
|
112
|
+
orchestratorTarget: "main",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
assert.equal(bridge.active, false);
|
|
116
|
+
assert.equal(bridge.mode, "off");
|
|
117
|
+
assert.equal(bridge.resultDelivery, false);
|
|
118
|
+
});
|
|
119
|
+
|
|
107
120
|
it("can disable external grouped-result delivery without disabling supervisor coordination", () => {
|
|
108
121
|
const bridge = resolveIntercomBridge({
|
|
109
122
|
config: { mode: "always", resultDelivery: false },
|
|
@@ -166,17 +179,18 @@ describe("applyIntercomBridgeToAgent", () => {
|
|
|
166
179
|
instruction: "Intercom orchestration channel:\n- Need a decision or blocked: contact_supervisor({ reason: \"need_decision\", message: \"<question>\" })\n- Blocked/update: contact_supervisor({ reason: \"progress_update\", message: \"UPDATE: <summary>\" })",
|
|
167
180
|
};
|
|
168
181
|
|
|
169
|
-
it("injects
|
|
182
|
+
it("injects contact_supervisor and prompt instructions", () => {
|
|
170
183
|
const updated = applyIntercomBridgeToAgent(makeAgent({ tools: ["read", "bash"] }), activeBridge);
|
|
171
|
-
assert.deepEqual(updated.tools, ["read", "bash", "
|
|
184
|
+
assert.deepEqual(updated.tools, ["read", "bash", "contact_supervisor"]);
|
|
172
185
|
assert.match(updated.systemPrompt, /Intercom orchestration channel:/);
|
|
173
186
|
assert.match(updated.systemPrompt, /contact_supervisor/);
|
|
187
|
+
assert.doesNotMatch(updated.systemPrompt ?? "", /Generic intercom/);
|
|
174
188
|
});
|
|
175
189
|
|
|
176
190
|
it("is idempotent", () => {
|
|
177
191
|
const first = applyIntercomBridgeToAgent(makeAgent({ tools: ["read"] }), activeBridge);
|
|
178
192
|
const second = applyIntercomBridgeToAgent(first, activeBridge);
|
|
179
|
-
assert.equal(second.tools?.filter((tool) => tool === "intercom").length,
|
|
193
|
+
assert.equal(second.tools?.filter((tool) => tool === "intercom").length, 0);
|
|
180
194
|
assert.equal(second.tools?.filter((tool) => tool === "contact_supervisor").length, 1);
|
|
181
195
|
assert.equal(second.systemPrompt, first.systemPrompt);
|
|
182
196
|
});
|
|
@@ -184,10 +198,15 @@ describe("applyIntercomBridgeToAgent", () => {
|
|
|
184
198
|
it("does not block native supervisor tools for agents with explicit extension allowlists", () => {
|
|
185
199
|
const agent = makeAgent({ tools: ["read"], extensions: ["/tmp/other-extension/index.ts"] });
|
|
186
200
|
const updated = applyIntercomBridgeToAgent(agent, activeBridge);
|
|
187
|
-
assert.deepEqual(updated.tools, ["read", "
|
|
201
|
+
assert.deepEqual(updated.tools, ["read", "contact_supervisor"]);
|
|
188
202
|
assert.match(updated.systemPrompt, /contact_supervisor/);
|
|
189
203
|
});
|
|
190
204
|
|
|
205
|
+
it("preserves explicitly requested external intercom tools", () => {
|
|
206
|
+
const updated = applyIntercomBridgeToAgent(makeAgent({ tools: ["read", "intercom"] }), activeBridge);
|
|
207
|
+
assert.deepEqual(updated.tools, ["read", "intercom", "contact_supervisor"]);
|
|
208
|
+
});
|
|
209
|
+
|
|
191
210
|
it("does not widen explicit empty or MCP-only builtin allowlists", () => {
|
|
192
211
|
const emptyTools = applyIntercomBridgeToAgent(makeAgent({ tools: [] }), activeBridge);
|
|
193
212
|
assert.deepEqual(emptyTools.tools, []);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
arbitrateCompletionGuardRescue,
|
|
5
5
|
createTaskMutationArbiter,
|
|
6
6
|
isCompletionGuardFailure,
|
|
7
|
+
mapArbiterDecision,
|
|
7
8
|
maybeRescueCompletionGuardFailure,
|
|
8
9
|
type TaskMutationArbiter,
|
|
9
10
|
} from "../../src/runs/shared/llm-intent-arbiter.ts";
|
|
@@ -30,7 +31,7 @@ function stubArbiter(verdict: "read-only" | "implementation" | "unavailable" | "
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
describe("arbitrateCompletionGuardRescue", () => {
|
|
33
|
-
it("rescues
|
|
34
|
+
it("rescues on a read-only verdict, keeps the failure otherwise", async () => {
|
|
34
35
|
const rescued = await arbitrateCompletionGuardRescue({
|
|
35
36
|
guardTriggered: true,
|
|
36
37
|
task: "t",
|
|
@@ -47,6 +48,18 @@ describe("arbitrateCompletionGuardRescue", () => {
|
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
it("never arbitrates from partial evidence on tasks over 8000 chars", async () => {
|
|
52
|
+
const middle = "And now apply the fix in the middle of this long task. ";
|
|
53
|
+
const longTask = "Review the setup. ".repeat(400) + middle + "Review more. ".repeat(400);
|
|
54
|
+
assert.ok(longTask.length > 8000);
|
|
55
|
+
const kept = await arbitrateCompletionGuardRescue({
|
|
56
|
+
guardTriggered: true,
|
|
57
|
+
task: longTask,
|
|
58
|
+
arbiter: stubArbiter("read-only"),
|
|
59
|
+
});
|
|
60
|
+
assert.deepEqual(kept, { triggered: true, rescued: false });
|
|
61
|
+
});
|
|
62
|
+
|
|
50
63
|
it("keeps the guard verdict without an arbiter or on arbiter failure", async () => {
|
|
51
64
|
assert.deepEqual(
|
|
52
65
|
await arbitrateCompletionGuardRescue({ guardTriggered: true, task: "t" }),
|
|
@@ -63,6 +76,48 @@ describe("arbitrateCompletionGuardRescue", () => {
|
|
|
63
76
|
});
|
|
64
77
|
});
|
|
65
78
|
|
|
79
|
+
describe("mapArbiterDecision", () => {
|
|
80
|
+
it("only a high-confidence read_only rescues", () => {
|
|
81
|
+
assert.equal(mapArbiterDecision({ classification: "read_only", confidence: "high" }), "read-only");
|
|
82
|
+
for (const confidence of ["low", "medium", undefined] as const) {
|
|
83
|
+
assert.equal(
|
|
84
|
+
mapArbiterDecision({ classification: "read_only", confidence }),
|
|
85
|
+
"implementation",
|
|
86
|
+
String(confidence),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
assert.equal(mapArbiterDecision({ classification: "implementation", confidence: "high" }), "implementation");
|
|
90
|
+
assert.equal(mapArbiterDecision(undefined), "unavailable");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("arbiter auth receiver", () => {
|
|
95
|
+
it("resolves registry credentials through the registry receiver (class instance state)", async () => {
|
|
96
|
+
// Prototype method reading instance state: a detached call would lose
|
|
97
|
+
// `this` and fail auth. The fix must call it as a method on the registry.
|
|
98
|
+
class FakeRegistry {
|
|
99
|
+
readonly key = "instance-key";
|
|
100
|
+
async getApiKeyAndHeaders(_model: unknown) {
|
|
101
|
+
return { ok: true, apiKey: this.key };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let captured: Record<string, unknown> | undefined;
|
|
105
|
+
const ctx = {
|
|
106
|
+
model: { provider: "test", id: "model-1", api: "test-api" },
|
|
107
|
+
modelRegistry: new FakeRegistry(),
|
|
108
|
+
} as never;
|
|
109
|
+
const arbiter = createTaskMutationArbiter(ctx, {
|
|
110
|
+
streamFn: async (_model: unknown, _context: unknown, opts: Record<string, unknown>) => {
|
|
111
|
+
captured = opts;
|
|
112
|
+
throw new Error("stream fail");
|
|
113
|
+
},
|
|
114
|
+
})!;
|
|
115
|
+
const verdict = await arbiter("t");
|
|
116
|
+
assert.equal(verdict, "unavailable"); // stream deliberately fails
|
|
117
|
+
assert.equal(captured?.apiKey, "instance-key", "registry credential must reach the stream function");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
66
121
|
describe("isCompletionGuardFailure", () => {
|
|
67
122
|
it("recognizes the guard error prefix", () => {
|
|
68
123
|
assert.equal(isCompletionGuardFailure({ error: GUARD_ERROR }), true);
|