@selesai/code 0.13.12 → 0.13.14
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 +9 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
- package/dist/extensions/pi-subagents/VISION.md +12 -1
- package/dist/extensions/pi-subagents/docs/agents.md +13 -6
- package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
- package/dist/extensions/pi-subagents/docs/missions.md +1 -1
- package/dist/extensions/pi-subagents/docs/models.md +6 -6
- package/dist/extensions/pi-subagents/docs/observability.md +6 -3
- package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
- package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/install.mjs +1 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
- package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
- package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
- package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
- package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
- package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
- package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
- package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
- package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
- package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
- package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
- package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
- package/dist/extensions/pi-zentui/README.md +19 -5
- package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
- package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
- package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
- package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
- package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
- package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
- package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
- package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
- package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
- package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
- package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
- package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
- package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
- package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
- package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
- package/dist/skills/brandkit/SKILL.md +0 -2
- package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
- package/dist/skills/gpt-taste/SKILL.md +0 -2
- package/dist/skills/image-to-code/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
- package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
- package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
- package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
- package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
- package/dist/skills/stitch/DESIGN.md +121 -0
- package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
- package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
makeAgent,
|
|
25
25
|
makeMinimalCtx,
|
|
26
26
|
events,
|
|
27
|
+
resolveMockPiCallArgs,
|
|
27
28
|
tryImport,
|
|
28
29
|
} from "../support/helpers.ts";
|
|
29
30
|
import registerSubagentExtension from "../../src/extension/index.ts";
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
type SubagentDelegationResponse,
|
|
38
39
|
type SubagentDelegationStarted,
|
|
39
40
|
} from "../../src/api/delegation.ts";
|
|
40
|
-
import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
|
|
41
|
+
import { CHAIN_RUNS_DIR, DIRS, INTERCOM_DETACH_REQUEST_EVENT, INTERCOM_DETACH_RESPONSE_EVENT, SUBAGENT_CONTROL_EVENT, TEMP_ARTIFACTS_DIR, type AsyncStatus, type ChildWatchdogProgress, type ControlEvent, type SubagentState } from "../../src/shared/types.ts";
|
|
41
42
|
import { ACTIVE_RUN_INDEX_DIR } from "../../src/runs/background/active-run-index.ts";
|
|
42
43
|
import { listAsyncRuns } from "../../src/runs/background/async-status.ts";
|
|
43
44
|
import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
|
|
@@ -159,12 +160,14 @@ interface RunSyncResult {
|
|
|
159
160
|
verifyRuns?: Array<{ status?: string }>;
|
|
160
161
|
runtimeChecks?: Array<{ id?: string; status?: string; message?: string }>;
|
|
161
162
|
};
|
|
163
|
+
watchdog?: ChildWatchdogProgress;
|
|
162
164
|
launchResolvedExtensions?: LaunchResolvedExtensions;
|
|
163
165
|
runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
interface MockPiCallRecord {
|
|
167
169
|
args?: string[];
|
|
170
|
+
effectiveArgs?: string[];
|
|
168
171
|
cwd?: string;
|
|
169
172
|
systemPrompts?: Array<{ mode?: string; path?: string; text?: string; error?: string }>;
|
|
170
173
|
}
|
|
@@ -205,7 +208,7 @@ async function withIsolatedWatchdogSettings<T>(projectDir: string, run: () => Pr
|
|
|
205
208
|
}
|
|
206
209
|
}
|
|
207
210
|
|
|
208
|
-
function childWatchdogStatus(phase: "idle" | "reviewing" | "
|
|
211
|
+
function childWatchdogStatus(phase: "idle" | "reviewing" | "stale" | "failed", seq: number) {
|
|
209
212
|
return {
|
|
210
213
|
type: CHILD_WATCHDOG_STATUS_EVENT,
|
|
211
214
|
runId: "watchdog-child-run",
|
|
@@ -215,7 +218,6 @@ function childWatchdogStatus(phase: "idle" | "reviewing" | "autofollow" | "settl
|
|
|
215
218
|
seq,
|
|
216
219
|
phase,
|
|
217
220
|
ts: Date.now() + seq,
|
|
218
|
-
followUpPending,
|
|
219
221
|
};
|
|
220
222
|
}
|
|
221
223
|
|
|
@@ -339,7 +341,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
339
341
|
removeTempDir(tempDir);
|
|
340
342
|
});
|
|
341
343
|
|
|
342
|
-
function readCall(): { args: string[]; cwd?: string; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
|
|
344
|
+
function readCall(): { args: string[]; effectiveArgs?: string[]; cwd?: string; systemPrompts: NonNullable<MockPiCallRecord["systemPrompts"]> } {
|
|
343
345
|
const callFile = fs.readdirSync(mockPi.dir)
|
|
344
346
|
.filter((name) => name.startsWith("call-") && name.endsWith(".json"))
|
|
345
347
|
.sort()
|
|
@@ -347,18 +349,22 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
347
349
|
assert.ok(callFile, "expected a recorded mock pi call");
|
|
348
350
|
const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord;
|
|
349
351
|
assert.ok(Array.isArray(payload.args), "expected recorded args");
|
|
350
|
-
return { args: payload.args, cwd: payload.cwd, systemPrompts: payload.systemPrompts ?? [] };
|
|
352
|
+
return { args: payload.args, effectiveArgs: payload.effectiveArgs, cwd: payload.cwd, systemPrompts: payload.systemPrompts ?? [] };
|
|
351
353
|
}
|
|
352
354
|
|
|
353
355
|
function readCallArgs(): string[] {
|
|
354
|
-
|
|
356
|
+
const call = readCall();
|
|
357
|
+
return resolveMockPiCallArgs(call);
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
function readAllCallArgs(): string[][] {
|
|
360
|
+
function readAllCallArgs(effective = false): string[][] {
|
|
358
361
|
return fs.readdirSync(mockPi.dir)
|
|
359
362
|
.filter((name) => name.startsWith("call-") && name.endsWith(".json"))
|
|
360
363
|
.sort()
|
|
361
|
-
.map((name) =>
|
|
364
|
+
.map((name) => {
|
|
365
|
+
const call = JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord;
|
|
366
|
+
return effective ? resolveMockPiCallArgs(call) : call.args ?? [];
|
|
367
|
+
});
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
function makeExecutor(
|
|
@@ -623,6 +629,38 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
623
629
|
assert.doesNotMatch(readCallArgs().join("\n"), /This path is authoritative for this run/);
|
|
624
630
|
});
|
|
625
631
|
|
|
632
|
+
it("keeps escaped read-only delegate tasks from triggering the completion guard", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
633
|
+
mockPi.onCall({ output: "The exact user-facing response" });
|
|
634
|
+
const task = [
|
|
635
|
+
"This is a read-only skill compliance scenario, not an implementation assignment.",
|
|
636
|
+
"Read the supplied skill and write the exact user-facing response.",
|
|
637
|
+
"Do not edit files.",
|
|
638
|
+
"Use a scenario that discusses selection for an implementation task or closeout of an implementation assignment.",
|
|
639
|
+
].join("\\n");
|
|
640
|
+
const result = await makeExecutor([makeAgent("delegate", {
|
|
641
|
+
tools: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
|
|
642
|
+
inheritProjectContext: true,
|
|
643
|
+
systemPromptMode: "append",
|
|
644
|
+
})]).execute(
|
|
645
|
+
"workflow-read-only-delegate",
|
|
646
|
+
{
|
|
647
|
+
async: false,
|
|
648
|
+
acceptance: false,
|
|
649
|
+
preflight: { version: 1, coverage: "complete", lanes: [{ key: "main", mode: "review" }] },
|
|
650
|
+
workflowScript: `return runs.all([{ key: "main", agent: "delegate", task: ${JSON.stringify(task)} }]);`,
|
|
651
|
+
},
|
|
652
|
+
new AbortController().signal,
|
|
653
|
+
undefined,
|
|
654
|
+
makeMinimalCtx(tempDir),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "workflow failed");
|
|
658
|
+
const child = (result.details as { results?: Array<{ exitCode?: number; error?: string; output?: string }> } | undefined)?.results?.[0];
|
|
659
|
+
assert.equal(child?.exitCode, 0);
|
|
660
|
+
assert.equal(child?.error, undefined);
|
|
661
|
+
assert.match(result.content[0]?.text ?? "", /The exact user-facing response/);
|
|
662
|
+
});
|
|
663
|
+
|
|
626
664
|
it("consumes one exact host-only workflow child permit before spawn", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
627
665
|
const executor = makeExecutor([makeAgent("echo"), makeAgent("other"), makeAgent("external", { runner: { type: "external-cli", command: "external" } })]);
|
|
628
666
|
const ctx = makeMinimalCtx(tempDir);
|
|
@@ -1848,7 +1886,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
1848
1886
|
it("notifies the parent when an async workflow child needs attention", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
1849
1887
|
mockPi.onCall({
|
|
1850
1888
|
steps: [
|
|
1851
|
-
{ jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents")] },
|
|
1889
|
+
{ jsonl: [events.toolStart("read", { path: "src/example.ts" }), events.toolEnd("read"), events.toolResult("read", "contents"), events.assistantMessage("Started")] },
|
|
1852
1890
|
{ delay: 2_500, jsonl: [events.assistantMessage("Done")] },
|
|
1853
1891
|
],
|
|
1854
1892
|
});
|
|
@@ -2368,6 +2406,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
2368
2406
|
assert.match(result.content[0]?.text ?? "", /reviewed auth/);
|
|
2369
2407
|
assert.equal(result.details.mode, "workflow");
|
|
2370
2408
|
assert.equal(result.details.results.length, 2);
|
|
2409
|
+
assert.deepEqual(result.details.results.map((entry) => entry.workflowKey), ["scan", "review"]);
|
|
2371
2410
|
assert.equal(result.details.workflow?.value && (result.details.workflow.value as { stateType?: unknown }).stateType, "object");
|
|
2372
2411
|
assert.ok(result.details.missionId);
|
|
2373
2412
|
const missionFiles = fs.readdirSync(path.join(agentDir, "missions", "projects"), { recursive: true })
|
|
@@ -4031,6 +4070,51 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
4031
4070
|
assert.equal(mockPi.callCount(), 1);
|
|
4032
4071
|
});
|
|
4033
4072
|
|
|
4073
|
+
it("passes an agent-level tool budget to an async single child", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4074
|
+
const toolBudget = { soft: 100, hard: 150, block: "*" as const };
|
|
4075
|
+
mockPi.onCall({ echoEnv: [TOOL_BUDGET_ENV] });
|
|
4076
|
+
const executor = makeExecutor([makeAgent("echo", { toolBudget })]);
|
|
4077
|
+
let asyncDir: string | undefined;
|
|
4078
|
+
let resultPath: string | undefined;
|
|
4079
|
+
|
|
4080
|
+
try {
|
|
4081
|
+
const result = await executor.execute(
|
|
4082
|
+
"agent-tool-budget-async-single",
|
|
4083
|
+
{ agent: "echo", task: "Run the async budget probe", async: true },
|
|
4084
|
+
new AbortController().signal,
|
|
4085
|
+
undefined,
|
|
4086
|
+
makeMinimalCtx(tempDir),
|
|
4087
|
+
);
|
|
4088
|
+
|
|
4089
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "async launch failed");
|
|
4090
|
+
assert.ok(result.details.asyncId);
|
|
4091
|
+
asyncDir = result.details.asyncDir;
|
|
4092
|
+
resultPath = path.join(DIRS.results, `${result.details.asyncId}.json`);
|
|
4093
|
+
|
|
4094
|
+
let persisted: { state?: string; results?: Array<{ output?: string }> } = {};
|
|
4095
|
+
for (let attempt = 0; attempt < 200; attempt++) {
|
|
4096
|
+
if (fs.existsSync(resultPath)) persisted = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
4097
|
+
if (persisted.state === "complete" || persisted.state === "failed") break;
|
|
4098
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
4099
|
+
}
|
|
4100
|
+
assert.equal(persisted.state, "complete");
|
|
4101
|
+
const childEnv = JSON.parse(persisted.results?.[0]?.output ?? "{}") as Record<string, string | null>;
|
|
4102
|
+
assert.deepEqual(JSON.parse(childEnv[TOOL_BUDGET_ENV] ?? "null"), toolBudget);
|
|
4103
|
+
assert.equal(mockPi.callCount(), 1);
|
|
4104
|
+
const processTerminalPath = path.join(asyncDir!, "process-terminal.json");
|
|
4105
|
+
let processTerminal: { state?: string } = {};
|
|
4106
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
4107
|
+
if (fs.existsSync(processTerminalPath)) processTerminal = JSON.parse(fs.readFileSync(processTerminalPath, "utf-8"));
|
|
4108
|
+
if (processTerminal.state && processTerminal.state !== "pending") break;
|
|
4109
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
4110
|
+
}
|
|
4111
|
+
assert.match(processTerminal.state ?? "", /^(observed|unknown)$/);
|
|
4112
|
+
} finally {
|
|
4113
|
+
if (asyncDir) fs.rmSync(asyncDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
|
|
4114
|
+
if (resultPath) fs.rmSync(resultPath, { force: true });
|
|
4115
|
+
}
|
|
4116
|
+
});
|
|
4117
|
+
|
|
4034
4118
|
it("keeps delegated agent and config tool budgets at a minimum of one", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4035
4119
|
const ctx = makeMinimalCtx(tempDir);
|
|
4036
4120
|
const cases = [
|
|
@@ -4067,6 +4151,52 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
4067
4151
|
assert.equal(mockPi.callCount(), 0);
|
|
4068
4152
|
});
|
|
4069
4153
|
|
|
4154
|
+
it("accepts JSON-encoded acceptance objects and diagnoses malformed strings", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4155
|
+
const acceptance = {
|
|
4156
|
+
level: "checked" as const,
|
|
4157
|
+
criteria: [{ id: "criterion-1", must: "Return required evidence" }],
|
|
4158
|
+
};
|
|
4159
|
+
const acceptedOutput = [
|
|
4160
|
+
"done",
|
|
4161
|
+
"```acceptance-report",
|
|
4162
|
+
JSON.stringify({
|
|
4163
|
+
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }],
|
|
4164
|
+
changedFiles: ["src/file.ts"],
|
|
4165
|
+
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
4166
|
+
commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
|
|
4167
|
+
validationOutput: ["tests passed"],
|
|
4168
|
+
residualRisks: [],
|
|
4169
|
+
noStagedFiles: true,
|
|
4170
|
+
}),
|
|
4171
|
+
"```",
|
|
4172
|
+
].join("\n");
|
|
4173
|
+
const executor = makeExecutor([makeAgent("echo")]);
|
|
4174
|
+
for (const [index, input] of [acceptance, JSON.stringify(acceptance)].entries()) {
|
|
4175
|
+
mockPi.onCall({ output: acceptedOutput });
|
|
4176
|
+
const result = await executor.execute(
|
|
4177
|
+
`acceptance-object-string-${index}`,
|
|
4178
|
+
{ async: false, agent: "echo", task: "Return the required evidence", acceptance: input as never },
|
|
4179
|
+
new AbortController().signal,
|
|
4180
|
+
undefined,
|
|
4181
|
+
makeMinimalCtx(tempDir),
|
|
4182
|
+
);
|
|
4183
|
+
|
|
4184
|
+
assert.equal(result.isError, undefined, result.content[0]?.text ?? "acceptance run failed");
|
|
4185
|
+
assert.equal(result.details.results[0]?.acceptance?.status, "checked");
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
const malformed = await executor.execute(
|
|
4189
|
+
"malformed-acceptance-object-string",
|
|
4190
|
+
{ async: false, agent: "echo", task: "Do not spawn", acceptance: '{"level":"checked"' as never },
|
|
4191
|
+
new AbortController().signal,
|
|
4192
|
+
undefined,
|
|
4193
|
+
makeMinimalCtx(tempDir),
|
|
4194
|
+
);
|
|
4195
|
+
assert.equal(malformed.isError, true);
|
|
4196
|
+
assert.match(malformed.content[0]?.text ?? "", /acceptance JSON string must encode a valid acceptance object/i);
|
|
4197
|
+
assert.equal(mockPi.callCount(), 2);
|
|
4198
|
+
});
|
|
4199
|
+
|
|
4070
4200
|
it("rejects invalid verified acceptance before spawning", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
4071
4201
|
const executor = makeExecutor([makeAgent("echo")]);
|
|
4072
4202
|
const invalidPolicies = [
|
|
@@ -4995,6 +5125,16 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
4995
5125
|
assert.doesNotMatch(call.args.join("\n"), /## Acceptance Contract/);
|
|
4996
5126
|
});
|
|
4997
5127
|
|
|
5128
|
+
it("does not inject inferred acceptance into reviewer prompts", async () => {
|
|
5129
|
+
mockPi.onCall({ output: "VERDICT: PASS" });
|
|
5130
|
+
const result = await runSync(tempDir, [makeAgent("reviewer", { tools: ["read"], completionGuard: false })], "reviewer", "Review the diff and return findings only.", {
|
|
5131
|
+
runId: "reviewer-inferred-acceptance",
|
|
5132
|
+
});
|
|
5133
|
+
|
|
5134
|
+
assert.equal(result.exitCode, 0);
|
|
5135
|
+
assert.doesNotMatch(readCall().args.join("\n"), /## Acceptance Contract/);
|
|
5136
|
+
});
|
|
5137
|
+
|
|
4998
5138
|
it("agent contract v1 keeps acceptance rejection out of execution status", async () => {
|
|
4999
5139
|
mockPi.onCall({ output: "Done\n```acceptance-report\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"not-satisfied\",\"evidence\":\"no proof\"}]}\n```" });
|
|
5000
5140
|
const agents = [makeAgent("worker", { tools: ["read"], completionGuard: false })];
|
|
@@ -5288,7 +5428,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5288
5428
|
"workflow-schema-acceptance-sidecar",
|
|
5289
5429
|
{
|
|
5290
5430
|
async: false,
|
|
5291
|
-
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5431
|
+
acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5292
5432
|
workflowScript: `
|
|
5293
5433
|
const child = await runs.run("schema", {
|
|
5294
5434
|
agent: "echo",
|
|
@@ -5326,7 +5466,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5326
5466
|
"workflow-schema-acceptance-missing-sidecar",
|
|
5327
5467
|
{
|
|
5328
5468
|
async: false,
|
|
5329
|
-
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5469
|
+
acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5330
5470
|
workflowScript: `
|
|
5331
5471
|
const child = await runs.run("schema", {
|
|
5332
5472
|
agent: "echo",
|
|
@@ -5350,6 +5490,38 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5350
5490
|
assert.match(result.content[0]?.text ?? "", /acceptance/i);
|
|
5351
5491
|
});
|
|
5352
5492
|
|
|
5493
|
+
it("uses fenced acceptance reports when outputSchema acceptance report capture is off", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
5494
|
+
const acceptanceReport = {
|
|
5495
|
+
criteriaSatisfied: [{ id: "proof", status: "satisfied", evidence: "fenced proof" }],
|
|
5496
|
+
changedFiles: [], testsAddedOrUpdated: [],
|
|
5497
|
+
commandsRun: [{ command: "mock", result: "passed", summary: "passed" }],
|
|
5498
|
+
validationOutput: ["validated"], residualRisks: [], noStagedFiles: true,
|
|
5499
|
+
};
|
|
5500
|
+
mockPi.onCall({
|
|
5501
|
+
matchArgIncludes: "Finish with a fenced JSON block tagged `acceptance-report`",
|
|
5502
|
+
stdoutRaw: [
|
|
5503
|
+
events.assistantMessage(`done\n\`\`\`acceptance-report\n${JSON.stringify(acceptanceReport)}\n\`\`\``),
|
|
5504
|
+
{ type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
|
|
5505
|
+
{ type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
|
|
5506
|
+
{ type: "tool_execution_end", toolName: "structured_output" },
|
|
5507
|
+
].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
|
|
5508
|
+
structuredOutputCapture: { ok: true },
|
|
5509
|
+
});
|
|
5510
|
+
|
|
5511
|
+
const result = await makeExecutor([makeAgent("echo")]).execute(
|
|
5512
|
+
"single-schema-fenced-acceptance",
|
|
5513
|
+
{
|
|
5514
|
+
agent: "echo", task: "Return structured data",
|
|
5515
|
+
outputSchema: { type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } },
|
|
5516
|
+
acceptance: { level: "checked", report: "off", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5517
|
+
},
|
|
5518
|
+
new AbortController().signal, undefined, makeMinimalCtx(tempDir),
|
|
5519
|
+
);
|
|
5520
|
+
|
|
5521
|
+
assert.equal(result.isError, undefined, result.content[0]?.text);
|
|
5522
|
+
assert.equal(result.details.results[0]?.acceptance?.status, "checked");
|
|
5523
|
+
});
|
|
5524
|
+
|
|
5353
5525
|
it("surfaces corrupt outputSchema acceptance sidecar read errors", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
|
|
5354
5526
|
mockPi.onCall({
|
|
5355
5527
|
stdoutRaw: [
|
|
@@ -5366,7 +5538,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5366
5538
|
"workflow-schema-acceptance-corrupt-sidecar",
|
|
5367
5539
|
{
|
|
5368
5540
|
async: false,
|
|
5369
|
-
acceptance: { level: "checked", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5541
|
+
acceptance: { level: "checked", report: "on", criteria: [{ id: "proof", must: "Return required proof" }] },
|
|
5370
5542
|
workflowScript: `
|
|
5371
5543
|
const child = await runs.run("schema", {
|
|
5372
5544
|
agent: "echo",
|
|
@@ -5798,7 +5970,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
5798
5970
|
assert.equal(result.finalOutput, "Recovered via file delivery");
|
|
5799
5971
|
assert.equal(mockPi.callCount(), 2);
|
|
5800
5972
|
const [firstArgs, retryArgs] = readAllCallArgs();
|
|
5801
|
-
|
|
5973
|
+
const firstTaskArg = firstArgs?.find((arg) => arg === "Task: Do work" || arg.endsWith("task.md"));
|
|
5974
|
+
if (process.platform === "darwin") {
|
|
5975
|
+
assert.ok(firstTaskArg?.startsWith("@"), "first attempt should deliver the task through a file on macOS");
|
|
5976
|
+
} else {
|
|
5977
|
+
assert.equal(firstTaskArg, "Task: Do work", "first attempt should deliver the task inline");
|
|
5978
|
+
}
|
|
5802
5979
|
const retryTaskArg = retryArgs?.at(-1) ?? "";
|
|
5803
5980
|
assert.ok(
|
|
5804
5981
|
retryTaskArg.startsWith("@") && retryTaskArg.endsWith("task.md"),
|
|
@@ -6616,7 +6793,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
6616
6793
|
const [firstArgs, resumedArgs] = readAllCallArgs();
|
|
6617
6794
|
assert.equal(firstArgs?.[firstArgs.indexOf("--session") + 1], sessionFile);
|
|
6618
6795
|
assert.equal(resumedArgs?.[resumedArgs.indexOf("--session") + 1], sessionFile);
|
|
6619
|
-
assert.match(
|
|
6796
|
+
assert.match(readAllCallArgs(true)[1]?.at(-1) ?? "", /Continue from the current files and transcript/);
|
|
6620
6797
|
assert.equal(fs.readFileSync(path.join(tempDir, "side-effect.txt"), "utf-8"), "done");
|
|
6621
6798
|
});
|
|
6622
6799
|
|
|
@@ -6689,7 +6866,12 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
6689
6866
|
it("delivers delegated activity-state transitions despite heartbeat suppression", async () => {
|
|
6690
6867
|
const attentionUpdates: Array<{ details?: { progress?: ProgressSummary[] } }> = [];
|
|
6691
6868
|
const attentionReleasePath = path.join(tempDir, "release-delegated-attention");
|
|
6692
|
-
mockPi.onCall({
|
|
6869
|
+
mockPi.onCall({
|
|
6870
|
+
steps: [
|
|
6871
|
+
{ jsonl: [events.assistantMessage("Started")] },
|
|
6872
|
+
{ waitForPath: attentionReleasePath, jsonl: [events.assistantMessage("Done")] },
|
|
6873
|
+
],
|
|
6874
|
+
});
|
|
6693
6875
|
const attentionRun = runSync!(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
6694
6876
|
suppressUnchangedDelegationUpdates: true,
|
|
6695
6877
|
controlConfig: {
|
|
@@ -6704,7 +6886,13 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
6704
6886
|
onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => attentionUpdates.push(update),
|
|
6705
6887
|
});
|
|
6706
6888
|
try {
|
|
6707
|
-
const
|
|
6889
|
+
const turnDeadline = Date.now() + 15_000;
|
|
6890
|
+
while (!attentionUpdates.some((update) => (update.details?.progress?.[0]?.turnCount ?? 0) > 0) && Date.now() < turnDeadline) {
|
|
6891
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6892
|
+
}
|
|
6893
|
+
assert.ok(attentionUpdates.some((update) => (update.details?.progress?.[0]?.turnCount ?? 0) > 0), "test fixture should complete one assistant turn before waiting for idle attention");
|
|
6894
|
+
|
|
6895
|
+
const deadline = Date.now() + 15_000;
|
|
6708
6896
|
while (!attentionUpdates.some((update) => update.details?.progress?.[0]?.activityState === "needs_attention") && Date.now() < deadline) {
|
|
6709
6897
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
6710
6898
|
}
|
|
@@ -6748,6 +6936,31 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
6748
6936
|
}
|
|
6749
6937
|
});
|
|
6750
6938
|
|
|
6939
|
+
it("does not deliver idle attention before a child completes its first assistant turn", async () => {
|
|
6940
|
+
const updates: Array<{ details?: { progress?: ProgressSummary[] } }> = [];
|
|
6941
|
+
const releasePath = path.join(tempDir, "release-zero-turn-attention");
|
|
6942
|
+
mockPi.onCall({ output: "Done", waitForPath: releasePath });
|
|
6943
|
+
|
|
6944
|
+
const runPromise = runSync!(tempDir, makeAgentConfigs(["echo"]), "echo", "Task", {
|
|
6945
|
+
controlConfig: {
|
|
6946
|
+
enabled: true,
|
|
6947
|
+
needsAttentionAfterMs: 200,
|
|
6948
|
+
activeNoticeAfterMs: 999_999,
|
|
6949
|
+
notifyOn: ["needs_attention"],
|
|
6950
|
+
notifyChannels: ["event"],
|
|
6951
|
+
},
|
|
6952
|
+
onUpdate: (update: { details?: { progress?: ProgressSummary[] } }) => updates.push(update),
|
|
6953
|
+
});
|
|
6954
|
+
try {
|
|
6955
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
6956
|
+
assert.equal(updates.some((update) => update.details?.progress?.[0]?.activityState === "needs_attention"), false);
|
|
6957
|
+
} finally {
|
|
6958
|
+
fs.writeFileSync(releasePath, "release", "utf-8");
|
|
6959
|
+
const result = await runPromise;
|
|
6960
|
+
assert.equal(result.exitCode, 0);
|
|
6961
|
+
}
|
|
6962
|
+
});
|
|
6963
|
+
|
|
6751
6964
|
it("reports foreground context window usage without changing cumulative spend", async () => {
|
|
6752
6965
|
mockPi.onCall({
|
|
6753
6966
|
jsonl: [{
|
|
@@ -7161,7 +7374,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7161
7374
|
);
|
|
7162
7375
|
|
|
7163
7376
|
const call = readCall();
|
|
7164
|
-
const taskArg = call.
|
|
7377
|
+
const taskArg = resolveMockPiCallArgs(call).at(-1) ?? "";
|
|
7165
7378
|
const systemPrompt = call.systemPrompts[0]?.text ?? "";
|
|
7166
7379
|
assert.equal(result.isError, undefined);
|
|
7167
7380
|
assert.match(taskArg, new RegExp(`Write your findings to exactly this path: ${escapeRegExp(overridePath)}`));
|
|
@@ -7188,7 +7401,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7188
7401
|
);
|
|
7189
7402
|
|
|
7190
7403
|
const call = readCall();
|
|
7191
|
-
const taskArg = call.
|
|
7404
|
+
const taskArg = resolveMockPiCallArgs(call).at(-1) ?? "";
|
|
7192
7405
|
const systemPrompt = call.systemPrompts[0]?.text ?? "";
|
|
7193
7406
|
assert.equal(result.isError, undefined);
|
|
7194
7407
|
assert.equal(fs.readFileSync(outputPath, "utf-8"), "complete read-only analysis");
|
|
@@ -7838,7 +8051,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7838
8051
|
assert.ok(elapsed < 5000, `unconfigured watchdog status should not delay final drain, took ${elapsed}ms`);
|
|
7839
8052
|
assert.equal(result.exitCode, 0);
|
|
7840
8053
|
assert.equal(result.finalOutput, "done-without-watchdog-config");
|
|
7841
|
-
assert.equal(
|
|
8054
|
+
assert.equal(result.watchdog, undefined);
|
|
7842
8055
|
});
|
|
7843
8056
|
});
|
|
7844
8057
|
|
|
@@ -7862,7 +8075,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7862
8075
|
assert.ok(elapsed < 6000, `settled watchdog should still allow cleanup, took ${elapsed}ms`);
|
|
7863
8076
|
assert.equal(result.exitCode, 0);
|
|
7864
8077
|
assert.equal(result.finalOutput, "done-before-watchdog");
|
|
7865
|
-
assert.equal(
|
|
8078
|
+
assert.equal(result.watchdog?.phase, "idle");
|
|
7866
8079
|
});
|
|
7867
8080
|
});
|
|
7868
8081
|
|
|
@@ -7882,12 +8095,84 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
7882
8095
|
assert.ok(elapsed < 5000, `watchdog tail fallback should not hang, took ${elapsed}ms`);
|
|
7883
8096
|
assert.equal(result.exitCode, 0);
|
|
7884
8097
|
assert.equal(result.finalOutput, "done-before-watchdog-timeout");
|
|
7885
|
-
const watchdog =
|
|
8098
|
+
const watchdog = result.watchdog;
|
|
7886
8099
|
assert.equal(watchdog?.phase, "stale");
|
|
7887
8100
|
assert.equal(watchdog?.timedOut, true);
|
|
7888
8101
|
});
|
|
7889
8102
|
});
|
|
7890
8103
|
|
|
8104
|
+
it("blocks or warns on launches that violate configured watchdog rules", async () => {
|
|
8105
|
+
await withIsolatedWatchdogSettings(tempDir, async () => {
|
|
8106
|
+
const settingsPath = path.join(tempDir, ".selesai", "settings.json");
|
|
8107
|
+
const writeRules = (action: "warn" | "block") => {
|
|
8108
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
8109
|
+
fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { watchdog: { rules: { action, roleModels: { echo: { deny: ["mock/*"], note: "echo must not use mock models" } } } } } }, null, 2), "utf-8");
|
|
8110
|
+
};
|
|
8111
|
+
const sent: unknown[] = [];
|
|
8112
|
+
const watchdog = new MainWatchdogRuntime({ cwd: tempDir, displayWarning: (details) => { sent.push(details); } });
|
|
8113
|
+
const executor = createSubagentExecutor!({
|
|
8114
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
8115
|
+
state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
|
|
8116
|
+
config: {},
|
|
8117
|
+
asyncByDefault: false,
|
|
8118
|
+
watchdog,
|
|
8119
|
+
tempArtifactsDir: tempDir,
|
|
8120
|
+
getSubagentSessionRoot: () => path.join(tempDir, ".pi-subagents", "sessions"),
|
|
8121
|
+
expandTilde: (value: string) => value,
|
|
8122
|
+
discoverAgents: () => ({ agents: [makeAgent("echo")] }),
|
|
8123
|
+
allowMutatingManagementActions: true,
|
|
8124
|
+
} as never);
|
|
8125
|
+
const callsBefore = mockPi.callCount();
|
|
8126
|
+
|
|
8127
|
+
writeRules("block");
|
|
8128
|
+
const blocked = await executor.execute("rules-block", { async: false, agent: "echo", task: "Do work", model: "mock/test-model" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
8129
|
+
assert.equal(blocked.isError, true);
|
|
8130
|
+
assert.match(blocked.content[0]?.text ?? "", /Launch blocked by subagents\.watchdog\.rules: Agent 'echo' was launched with denied model 'mock\/test-model'/);
|
|
8131
|
+
assert.equal(mockPi.callCount(), callsBefore, "a blocked launch never starts the child");
|
|
8132
|
+
assert.equal(sent.length, 0);
|
|
8133
|
+
|
|
8134
|
+
const nestedSettings = path.join(tempDir, "packages", "app", ".selesai", "settings.json");
|
|
8135
|
+
fs.mkdirSync(path.dirname(nestedSettings), { recursive: true });
|
|
8136
|
+
fs.writeFileSync(nestedSettings, JSON.stringify({ subagents: { watchdog: { rules: { action: "block", roleModels: { echo: { deny: ["mock/*"] } } } } } }, null, 2), "utf-8");
|
|
8137
|
+
writeRules("warn");
|
|
8138
|
+
const nestedBlocked = await executor.execute("rules-workflow-cwd", { async: false, cwd: "packages/app", workflowScript: `return runs.run("one", { agent: "echo", task: "Do work", model: "mock/test-model" });` }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
8139
|
+
assert.equal(nestedBlocked.isError, true, "rules load from the resolved workflow cwd");
|
|
8140
|
+
assert.equal(mockPi.callCount(), callsBefore);
|
|
8141
|
+
|
|
8142
|
+
mockPi.onCall({ output: "warned but ran" });
|
|
8143
|
+
const warned = await executor.execute("rules-warn", { async: false, agent: "echo", task: "Do work", model: "mock/test-model" }, new AbortController().signal, undefined, makeMinimalCtx(tempDir));
|
|
8144
|
+
assert.equal(warned.isError, undefined, warned.content[0]?.text);
|
|
8145
|
+
assert.equal(mockPi.callCount(), callsBefore + 1);
|
|
8146
|
+
assert.equal(sent.length, 1);
|
|
8147
|
+
assert.match((sent[0] as { summary?: string }).summary ?? "", /denied model 'mock\/test-model'/);
|
|
8148
|
+
assert.match((sent[0] as { evidence?: string }).evidence ?? "", /echo must not use mock models/);
|
|
8149
|
+
});
|
|
8150
|
+
});
|
|
8151
|
+
|
|
8152
|
+
it("fails explicit acceptance on an unaddressed child watchdog blocker and passes once a turn follows it", async () => {
|
|
8153
|
+
await withIsolatedWatchdogSettings(tempDir, async () => {
|
|
8154
|
+
writeWatchdogSettings(tempDir);
|
|
8155
|
+
const agents = makeAgentConfigs(["echo"]);
|
|
8156
|
+
const acceptance = { level: "checked" as const, criteria: ["Ship it"] };
|
|
8157
|
+
const blockerCheck = (result: RunSyncResult) => result.acceptance?.runtimeChecks?.find((entry) => entry.id === "watchdog-blocker");
|
|
8158
|
+
|
|
8159
|
+
mockPi.onCall({ jsonl: [events.watchdogWarning("concern", "Minor naming concern"), events.acceptanceReport(), events.watchdogWarning("blocker", "Claims tests passed without running them")] });
|
|
8160
|
+
const unaddressed = await runSync(tempDir, agents, "echo", "Task", { runId: "watchdog-child-run", acceptance });
|
|
8161
|
+
assert.deepEqual(unaddressed.watchdog?.warnings?.map((warning) => [warning.severity, warning.addressed]), [["concern", true], ["blocker", false]]);
|
|
8162
|
+
assert.equal(blockerCheck(unaddressed)?.status, "failed");
|
|
8163
|
+
assert.match(blockerCheck(unaddressed)?.message ?? "", /Unresolved watchdog blocker: Claims tests passed without running them/);
|
|
8164
|
+
assert.equal(unaddressed.acceptance?.status, "rejected");
|
|
8165
|
+
assert.equal(unaddressed.exitCode, 1);
|
|
8166
|
+
assert.match(unaddressed.error ?? "", /Unresolved watchdog blocker/);
|
|
8167
|
+
|
|
8168
|
+
mockPi.onCall({ jsonl: [events.assistantMessage("first pass"), events.watchdogWarning("blocker", "Claims tests passed without running them"), events.acceptanceReport()] });
|
|
8169
|
+
const addressed = await runSync(tempDir, agents, "echo", "Task", { runId: "watchdog-child-run-2", acceptance });
|
|
8170
|
+
assert.equal(addressed.watchdog?.warnings?.[0]?.addressed, true);
|
|
8171
|
+
assert.equal(blockerCheck(addressed)?.status, "passed");
|
|
8172
|
+
assert.equal(addressed.exitCode, 0, addressed.error);
|
|
8173
|
+
});
|
|
8174
|
+
});
|
|
8175
|
+
|
|
7891
8176
|
it("treats forced drain after final assistant output as cleanup success", async () => {
|
|
7892
8177
|
mockPi.onCall({
|
|
7893
8178
|
jsonl: [events.assistantMessage("done-before-drain")],
|
|
@@ -8012,7 +8297,7 @@ describe("single sync execution", { skip: !available ? "pi packages not availabl
|
|
|
8012
8297
|
});
|
|
8013
8298
|
|
|
8014
8299
|
const result = await runSync(tempDir, makeAgentConfigs(["slow"]), "slow", "Slow task", {
|
|
8015
|
-
timeoutMs:
|
|
8300
|
+
timeoutMs: 1000,
|
|
8016
8301
|
outputPath: reportPath,
|
|
8017
8302
|
outputMode: "file-only",
|
|
8018
8303
|
acceptance: false,
|
|
@@ -292,6 +292,7 @@ describe("subagents watchdog slash command", { skip: !available ? "watchdog comm
|
|
|
292
292
|
assert.match(content, /Subagent watchdog/);
|
|
293
293
|
assert.match(content, /Main: off \(default off\)/);
|
|
294
294
|
assert.match(content, /Runtime: idle/);
|
|
295
|
+
assert.match(content, /Rules: none/);
|
|
295
296
|
assert.match(content, /Review model call: real model review/);
|
|
296
297
|
assert.match(content, /Sources:/);
|
|
297
298
|
});
|
|
@@ -13,6 +13,10 @@ export function createMockPi(): MockPi {
|
|
|
13
13
|
return _createMockPi();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export function resolveMockPiCallArgs(call: { args?: readonly string[]; effectiveArgs?: readonly string[] }): string[] {
|
|
17
|
+
return [...(call.effectiveArgs ?? call.args ?? [])];
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export function createTempDir(prefix = "pi-subagent-test-"): string {
|
|
17
21
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
18
22
|
}
|
|
@@ -176,6 +180,27 @@ export const events = {
|
|
|
176
180
|
};
|
|
177
181
|
},
|
|
178
182
|
|
|
183
|
+
/** Final assistant turn carrying a satisfied acceptance report. */
|
|
184
|
+
acceptanceReport(): object {
|
|
185
|
+
const report = { criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "implemented" }], changedFiles: ["src/file.ts"], testsAddedOrUpdated: ["test/file.test.ts"], commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }], validationOutput: ["tests passed"], residualRisks: [], noStagedFiles: true };
|
|
186
|
+
return events.assistantMessage(["done", "```acceptance-report", JSON.stringify(report), "```"].join("\n"));
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/** A child watchdog warning custom message as the child watchdog writes it into the child JSONL. */
|
|
190
|
+
watchdogWarning(severity: "concern" | "blocker", summary: string, overrides: Record<string, unknown> = {}): object {
|
|
191
|
+
return {
|
|
192
|
+
type: "message_end",
|
|
193
|
+
message: {
|
|
194
|
+
role: "custom",
|
|
195
|
+
customType: "subagent_watchdog_warning",
|
|
196
|
+
content: `<subagent_watchdog severity="${severity}">${summary}</subagent_watchdog>`,
|
|
197
|
+
display: true,
|
|
198
|
+
details: { severity, category: "test-gap", source: "child", agent: "worker", summary, evidence: "The transcript claims tests passed but no test command ran.", recommendedAction: "Run the focused test before finishing.", state: "displayed", displayedAt: new Date().toISOString(), ...overrides },
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
|
|
179
204
|
/** Assistant write tool call plus its successful tool result, as one completed write. */
|
|
180
205
|
completedWrite(filePath: string, content: string, model = "mock/test-model"): object[] {
|
|
181
206
|
const id = `write-${++writeCallSeq}`;
|
|
@@ -2,8 +2,19 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const configuredTempRoot = process.env.SELESAI_SUBAGENTS_TEMP_ROOT?.trim();
|
|
6
|
+
const tempRoot = configuredTempRoot
|
|
7
|
+
? path.resolve(configuredTempRoot)
|
|
8
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-test-root-"));
|
|
9
|
+
process.env.SELESAI_SUBAGENTS_TEMP_ROOT = tempRoot;
|
|
10
|
+
|
|
11
|
+
const nestedTestProcess = process.env.SELESAI_SUBAGENTS_TEST_LOADER === "1";
|
|
12
|
+
const isolatedHome = path.join(tempRoot, "home");
|
|
13
|
+
process.env.HOME = isolatedHome;
|
|
14
|
+
process.env.USERPROFILE = isolatedHome;
|
|
15
|
+
if (!nestedTestProcess) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
16
|
+
process.env.SELESAI_SUBAGENTS_TEST_LOADER = "1";
|
|
17
|
+
|
|
18
|
+
if (!configuredTempRoot) {
|
|
8
19
|
process.on("exit", () => fs.rmSync(tempRoot, { recursive: true, force: true }));
|
|
9
|
-
}
|
|
20
|
+
}
|
|
@@ -56,12 +56,26 @@ function hasArgMatcher(response) {
|
|
|
56
56
|
return Object.prototype.hasOwnProperty.call(response ?? {}, "matchArgIncludes");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function resolveTaskFileArg(arg) {
|
|
60
|
+
if (typeof arg !== "string" || !arg.startsWith("@") || !arg.endsWith("task.md")) return arg;
|
|
61
|
+
try {
|
|
62
|
+
return fs.readFileSync(arg.slice(1), "utf-8");
|
|
63
|
+
} catch {
|
|
64
|
+
return arg;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveTaskFileArgs(args) {
|
|
69
|
+
return args.map(resolveTaskFileArg);
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
function responseMatchesArgs(response, args) {
|
|
60
73
|
const matcher = response?.matchArgIncludes;
|
|
61
74
|
if (matcher === undefined) return true;
|
|
62
75
|
const needles = Array.isArray(matcher) ? matcher : [matcher];
|
|
63
76
|
if (needles.length === 0) return true;
|
|
64
|
-
const
|
|
77
|
+
const effectiveArgs = resolveTaskFileArgs(args);
|
|
78
|
+
const haystack = `${args.join("\n")}\n${effectiveArgs.join("\n")}`;
|
|
65
79
|
return needles.every((needle) => typeof needle === "string" && haystack.includes(needle));
|
|
66
80
|
}
|
|
67
81
|
|
|
@@ -360,9 +374,10 @@ async function main() {
|
|
|
360
374
|
}
|
|
361
375
|
writeSessionFile(args);
|
|
362
376
|
writeToolDiagnostic(response);
|
|
377
|
+
const effectiveArgs = resolveTaskFileArgs(args);
|
|
363
378
|
const callPath = path.join(queueDir, `call-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}.json`);
|
|
364
379
|
const callTempPath = `${callPath}.tmp-${process.pid}-${Date.now()}`;
|
|
365
|
-
fs.writeFileSync(callTempPath, JSON.stringify({ args, cwd: process.cwd(), systemPrompts: readSystemPromptRecords(args), requiredChildTools: readRequiredToolsEnv() }), "utf-8");
|
|
380
|
+
fs.writeFileSync(callTempPath, JSON.stringify({ args, effectiveArgs, cwd: process.cwd(), systemPrompts: readSystemPromptRecords(args), requiredChildTools: readRequiredToolsEnv() }), "utf-8");
|
|
366
381
|
fs.renameSync(callTempPath, callPath);
|
|
367
382
|
|
|
368
383
|
if (typeof response.delay === "number" && response.delay > 0) {
|