@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
|
@@ -20,11 +20,15 @@ const config = resolveControlConfig(undefined, {
|
|
|
20
20
|
describe("subagent control attention state", () => {
|
|
21
21
|
it("marks a run as needing attention only after the idle threshold", () => {
|
|
22
22
|
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, now: 50 }), undefined);
|
|
23
|
-
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, now: 400 }), "needs_attention");
|
|
23
|
+
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, turnCount: 1, now: 400 }), "needs_attention");
|
|
24
24
|
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, currentTool: "bash", now: 400 }), undefined);
|
|
25
|
-
assert.equal(deriveActivityState({ config, startedAt: 0, now: 400 }), "needs_attention");
|
|
25
|
+
assert.equal(deriveActivityState({ config, startedAt: 0, turnCount: 1, now: 400 }), "needs_attention");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
it("does not mark a zero-turn run as needing attention", () => {
|
|
29
|
+
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, turnCount: 0, now: 400 }), undefined);
|
|
30
|
+
assert.equal(deriveActivityState({ config, startedAt: 0, lastActivityAt: 0, now: 400 }), undefined);
|
|
31
|
+
});
|
|
28
32
|
|
|
29
33
|
it("builds compact needs-attention control events", () => {
|
|
30
34
|
const event = buildControlEvent({
|
|
@@ -103,25 +107,34 @@ describe("subagent control attention state", () => {
|
|
|
103
107
|
it("scales the default idle threshold for higher thinking levels", () => {
|
|
104
108
|
const defaults = resolveControlConfig();
|
|
105
109
|
|
|
106
|
-
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, now: 60_001 }), "needs_attention");
|
|
107
|
-
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, thinking: "low", now: 60_001 }), "needs_attention");
|
|
108
|
-
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, thinking: "minimal", now: 60_001 }), "needs_attention");
|
|
109
|
-
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, thinking: "high", now: 60_001 }), undefined);
|
|
110
|
-
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, thinking: "high", now: 300_001 }), "needs_attention");
|
|
110
|
+
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, turnCount: 1, now: 60_001 }), "needs_attention");
|
|
111
|
+
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, turnCount: 1, thinking: "low", now: 60_001 }), "needs_attention");
|
|
112
|
+
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, turnCount: 1, thinking: "minimal", now: 60_001 }), "needs_attention");
|
|
113
|
+
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, turnCount: 1, thinking: "high", now: 60_001 }), undefined);
|
|
114
|
+
assert.equal(deriveActivityState({ config: defaults, startedAt: 0, turnCount: 1, thinking: "high", now: 300_001 }), "needs_attention");
|
|
111
115
|
});
|
|
112
116
|
|
|
113
117
|
it("keeps explicit idle threshold overrides higher priority than thinking scale", () => {
|
|
114
118
|
const explicit = resolveControlConfig(undefined, { needsAttentionAfterMs: 90_000 });
|
|
115
119
|
|
|
116
120
|
assert.equal(explicit.needsAttentionAfterMsIsExplicit, true);
|
|
117
|
-
assert.equal(deriveActivityState({ config: explicit, startedAt: 0, thinking: "high", now: 90_001 }), "needs_attention");
|
|
121
|
+
assert.equal(deriveActivityState({ config: explicit, startedAt: 0, turnCount: 1, thinking: "high", now: 90_001 }), "needs_attention");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("preserves inherited default idle scaling when merging a resolved control config", () => {
|
|
125
|
+
const inherited = resolveControlConfig();
|
|
126
|
+
const merged = resolveControlConfig(inherited, { activeNoticeAfterMs: 123_000 });
|
|
127
|
+
|
|
128
|
+
assert.equal(merged.needsAttentionAfterMsIsExplicit, false);
|
|
129
|
+
assert.equal(merged.activeNoticeAfterMs, 123_000);
|
|
130
|
+
assert.equal(deriveActivityState({ config: merged, startedAt: 0, turnCount: 1, thinking: "high", now: 60_001 }), undefined);
|
|
118
131
|
});
|
|
119
132
|
|
|
120
133
|
it("treats recovered resolved configs without explicitness metadata as fixed thresholds", () => {
|
|
121
134
|
const recovered = { ...resolveControlConfig(undefined, { needsAttentionAfterMs: 90_000 }) };
|
|
122
135
|
delete recovered.needsAttentionAfterMsIsExplicit;
|
|
123
136
|
|
|
124
|
-
assert.equal(deriveActivityState({ config: recovered, startedAt: 0, thinking: "high", now: 90_001 }), "needs_attention");
|
|
137
|
+
assert.equal(deriveActivityState({ config: recovered, startedAt: 0, turnCount: 1, thinking: "high", now: 90_001 }), "needs_attention");
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
it("marks non-exempt open tools for attention at the active threshold", () => {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
|
|
18
18
|
} from "../../src/runs/shared/pi-args.ts";
|
|
19
19
|
import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV } from "../../src/runs/shared/runtime-acknowledged-extensions.ts";
|
|
20
|
-
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "../../src/runs/shared/structured-output.ts";
|
|
20
|
+
import { clearStructuredOutputCaptures, STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV, STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "../../src/runs/shared/structured-output.ts";
|
|
21
21
|
import { TOOL_BUDGET_ENV } from "../../src/runs/shared/tool-budget.ts";
|
|
22
22
|
import { getAgentDir } from "../../src/shared/utils.ts";
|
|
23
23
|
import { PERMISSION_POLICY_ENV } from "../../src/runs/shared/permissions.ts";
|
|
@@ -49,6 +49,8 @@ const envSnapshot = {
|
|
|
49
49
|
SELESAI_SUBAGENT_STEER_ACK_DIR: process.env.SELESAI_SUBAGENT_STEER_ACK_DIR,
|
|
50
50
|
SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE,
|
|
51
51
|
SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA,
|
|
52
|
+
SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE,
|
|
53
|
+
SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED,
|
|
52
54
|
SELESAI_SUBAGENT_RUNTIME_ACKNOWLEDGED_EXTENSIONS: process.env.SELESAI_SUBAGENT_RUNTIME_ACKNOWLEDGED_EXTENSIONS,
|
|
53
55
|
SELESAI_SUBAGENT_TOOL_BUDGET: process.env.SELESAI_SUBAGENT_TOOL_BUDGET,
|
|
54
56
|
SELESAI_SUBAGENT_PERMISSION_POLICY: process.env.SELESAI_SUBAGENT_PERMISSION_POLICY,
|
|
@@ -62,7 +64,7 @@ const envSnapshot = {
|
|
|
62
64
|
SELESAI_SUBAGENT_CHILD_AGENT: process.env.SELESAI_SUBAGENT_CHILD_AGENT,
|
|
63
65
|
SELESAI_SUBAGENT_CHILD_INDEX: process.env.SELESAI_SUBAGENT_CHILD_INDEX,
|
|
64
66
|
SELESAI_SUBAGENT_WATCHDOG_CHILD_CONFIG: process.env.SELESAI_SUBAGENT_WATCHDOG_CHILD_CONFIG,
|
|
65
|
-
|
|
67
|
+
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
const SKILLS_SECTION = "\n\nThe following skills provide specialized instructions for specific tasks.\nUse the read tool to load a skill's file when the task matches its description.\nWhen a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\n\n<available_skills>\n <skill>\n <name>safe-bash</name>\n <description>desc</description>\n <location>/tmp/SKILL.md</location>\n </skill>\n <skill>\n <name>pi-subagents</name>\n <description>delegate to subagents</description>\n <location>/tmp/pi-subagents/SKILL.md</location>\n </skill>\n</available_skills>";
|
|
@@ -105,6 +107,10 @@ afterEach(() => {
|
|
|
105
107
|
else process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE;
|
|
106
108
|
if (envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA === undefined) delete process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
107
109
|
else process.env[STRUCTURED_OUTPUT_SCHEMA_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA;
|
|
110
|
+
if (envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE === undefined) delete process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV];
|
|
111
|
+
else process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE;
|
|
112
|
+
if (envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED === undefined) delete process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV];
|
|
113
|
+
else process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED;
|
|
108
114
|
if (envSnapshot.SELESAI_SUBAGENT_RUNTIME_ACKNOWLEDGED_EXTENSIONS === undefined) delete process.env[RUNTIME_EXTENSION_ACK_PATH_ENV];
|
|
109
115
|
else process.env[RUNTIME_EXTENSION_ACK_PATH_ENV] = envSnapshot.SELESAI_SUBAGENT_RUNTIME_ACKNOWLEDGED_EXTENSIONS;
|
|
110
116
|
if (envSnapshot.SELESAI_SUBAGENT_TOOL_BUDGET === undefined) delete process.env[TOOL_BUDGET_ENV];
|
|
@@ -131,8 +137,7 @@ afterEach(() => {
|
|
|
131
137
|
else process.env[SUBAGENT_CHILD_INDEX_ENV] = envSnapshot.SELESAI_SUBAGENT_CHILD_INDEX;
|
|
132
138
|
if (envSnapshot.SELESAI_SUBAGENT_WATCHDOG_CHILD_CONFIG === undefined) delete process.env[CHILD_WATCHDOG_CONFIG_ENV];
|
|
133
139
|
else process.env[CHILD_WATCHDOG_CONFIG_ENV] = envSnapshot.SELESAI_SUBAGENT_WATCHDOG_CHILD_CONFIG;
|
|
134
|
-
|
|
135
|
-
else process.env.SELESAI_SUBAGENT_SESSION_NAME = envSnapshot.SELESAI_SUBAGENT_SESSION_NAME;
|
|
140
|
+
|
|
136
141
|
});
|
|
137
142
|
|
|
138
143
|
function setSupervisorEnv(): void {
|
|
@@ -182,9 +187,8 @@ describe("subagent prompt runtime", () => {
|
|
|
182
187
|
agentEndTimeoutMs: 5,
|
|
183
188
|
maxWarnings: null,
|
|
184
189
|
lsp: { enabled: false, timeoutMs: 100, maxFiles: 1, maxDiagnostics: 1 },
|
|
185
|
-
autoFollowBlockers: false,
|
|
186
|
-
autoFollowMaxAttempts: null,
|
|
187
190
|
stalemateRepeats: 2,
|
|
191
|
+
cadence: { everyNTools: null },
|
|
188
192
|
});
|
|
189
193
|
const handlers: Array<(event: { toolName?: string; input?: unknown }, ctx: { signal?: AbortSignal }) => unknown> = [];
|
|
190
194
|
registerPermissionGate({ on(event: string, handler: (event: { toolName?: string; input?: unknown }, ctx: { signal?: AbortSignal }) => unknown) { if (event === "tool_call") handlers.push(handler); } } as never, async () => new Promise(() => undefined));
|
|
@@ -696,9 +700,8 @@ describe("subagent prompt runtime", () => {
|
|
|
696
700
|
agentEndTimeoutMs: 500,
|
|
697
701
|
maxWarnings: null,
|
|
698
702
|
lsp: { enabled: false, timeoutMs: 3000, maxFiles: 20, maxDiagnostics: 50 },
|
|
699
|
-
autoFollowBlockers: false,
|
|
700
|
-
autoFollowMaxAttempts: 3,
|
|
701
703
|
stalemateRepeats: 2,
|
|
704
|
+
cadence: { everyNTools: null },
|
|
702
705
|
});
|
|
703
706
|
const handlersWith = new Map<string, unknown[]>();
|
|
704
707
|
registerSubagentPromptRuntime({
|
|
@@ -752,6 +755,86 @@ describe("subagent prompt runtime", () => {
|
|
|
752
755
|
}
|
|
753
756
|
});
|
|
754
757
|
|
|
758
|
+
it("requires and validates acceptanceReport when structured capture is required", async () => {
|
|
759
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-structured-acceptance-"));
|
|
760
|
+
try {
|
|
761
|
+
const schemaPath = path.join(dir, "schema.json");
|
|
762
|
+
const outputPath = path.join(dir, "output.json");
|
|
763
|
+
const acceptancePath = path.join(dir, "acceptance.json");
|
|
764
|
+
fs.writeFileSync(schemaPath, JSON.stringify({ type: "object" }), "utf-8");
|
|
765
|
+
process.env[STRUCTURED_OUTPUT_SCHEMA_ENV] = schemaPath;
|
|
766
|
+
process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] = outputPath;
|
|
767
|
+
process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = acceptancePath;
|
|
768
|
+
process.env[STRUCTURED_OUTPUT_ACCEPTANCE_REQUIRED_ENV] = "1";
|
|
769
|
+
let execute: ((_id: string, params: { value: unknown; acceptanceReport?: unknown }) => Promise<unknown>) | undefined;
|
|
770
|
+
let parameters: { required?: string[] } | undefined;
|
|
771
|
+
|
|
772
|
+
registerSubagentPromptRuntime({
|
|
773
|
+
registerTool(tool: { name: string; parameters: unknown; execute: typeof execute }) {
|
|
774
|
+
if (tool.name === "structured_output") {
|
|
775
|
+
execute = tool.execute;
|
|
776
|
+
parameters = tool.parameters as { required?: string[] };
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
on() {},
|
|
780
|
+
} as { registerTool(tool: { name: string; parameters: unknown; execute: typeof execute }): void; on(): void });
|
|
781
|
+
|
|
782
|
+
assert.deepEqual(parameters?.required, ["value", "acceptanceReport"]);
|
|
783
|
+
await assert.rejects(execute!("missing", { value: {} }), /Missing acceptanceReport/);
|
|
784
|
+
await assert.rejects(execute!("empty", { value: {}, acceptanceReport: {} }), /expected at least one acceptance report field/);
|
|
785
|
+
await execute!("valid", { value: {}, acceptanceReport: { manualNotes: "validated evidence" } });
|
|
786
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(acceptancePath, "utf-8")), { manualNotes: "validated evidence" });
|
|
787
|
+
} finally {
|
|
788
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it("clears stale optional acceptance reports when structured output omits them", async () => {
|
|
793
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-structured-acceptance-stale-"));
|
|
794
|
+
try {
|
|
795
|
+
const schemaPath = path.join(dir, "schema.json");
|
|
796
|
+
const outputPath = path.join(dir, "output.json");
|
|
797
|
+
const acceptancePath = path.join(dir, "acceptance.json");
|
|
798
|
+
fs.writeFileSync(schemaPath, JSON.stringify({ type: "object" }), "utf-8");
|
|
799
|
+
fs.writeFileSync(acceptancePath, JSON.stringify({ manualNotes: "stale" }), "utf-8");
|
|
800
|
+
process.env[STRUCTURED_OUTPUT_SCHEMA_ENV] = schemaPath;
|
|
801
|
+
process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] = outputPath;
|
|
802
|
+
process.env[STRUCTURED_OUTPUT_ACCEPTANCE_CAPTURE_ENV] = acceptancePath;
|
|
803
|
+
let execute: ((_id: string, params: { value: unknown }) => Promise<unknown>) | undefined;
|
|
804
|
+
|
|
805
|
+
registerSubagentPromptRuntime({
|
|
806
|
+
registerTool(tool: { name: string; execute: typeof execute }) {
|
|
807
|
+
if (tool.name === "structured_output") execute = tool.execute;
|
|
808
|
+
},
|
|
809
|
+
on() {},
|
|
810
|
+
} as { registerTool(tool: { name: string; execute: typeof execute }): void; on(): void });
|
|
811
|
+
|
|
812
|
+
await execute!("without-report", { value: {} });
|
|
813
|
+
assert.equal(fs.existsSync(acceptancePath), false);
|
|
814
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf-8")), {});
|
|
815
|
+
} finally {
|
|
816
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it("reports capture cleanup failures after clearing every stale file it can", () => {
|
|
821
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-structured-cleanup-"));
|
|
822
|
+
try {
|
|
823
|
+
const outputPath = path.join(dir, "output.json");
|
|
824
|
+
const acceptancePath = path.join(dir, "acceptance.json");
|
|
825
|
+
fs.mkdirSync(outputPath);
|
|
826
|
+
fs.writeFileSync(acceptancePath, JSON.stringify({ manualNotes: "stale" }), "utf-8");
|
|
827
|
+
|
|
828
|
+
const error = clearStructuredOutputCaptures({ schema: { type: "object" }, schemaPath: path.join(dir, "schema.json"), outputPath, acceptanceReportPath: acceptancePath });
|
|
829
|
+
|
|
830
|
+
assert.match(error ?? "", /Failed to clear stale structured output capture/);
|
|
831
|
+
assert.equal(fs.existsSync(outputPath), true);
|
|
832
|
+
assert.equal(fs.existsSync(acceptancePath), false);
|
|
833
|
+
} finally {
|
|
834
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
755
838
|
it("scopes local structured_output schema refs under the value parameter", () => {
|
|
756
839
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-structured-refs-"));
|
|
757
840
|
try {
|
|
@@ -16,6 +16,13 @@ describe("classifyTaskMutationIntent", () => {
|
|
|
16
16
|
assert.equal(classifyTaskMutationIntent("worker", "Do not modify tests; implement the fix").kind, "implementation");
|
|
17
17
|
assert.equal(classifyTaskMutationIntent("worker", "Fix the bug. Do not edit files outside src/.").kind, "implementation");
|
|
18
18
|
assert.equal(classifyTaskMutationIntent("worker", "Must not touch the production database; implement the fix locally").kind, "implementation");
|
|
19
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify tests\\nImplement the fix").kind, "implementation");
|
|
20
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify tests\\r\\nImplement the fix").kind, "implementation");
|
|
21
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nin src; implement the fix").kind, "implementation");
|
|
22
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nin output/; implement the fix").kind, "implementation");
|
|
23
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nin report/; implement the fix").kind, "implementation");
|
|
24
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\r\\noutside docs; implement the fix").kind, "implementation");
|
|
25
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\nin src; implement the fix").kind, "implementation");
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
it("stops the prohibition object before a following implementation clause", () => {
|
|
@@ -33,9 +40,29 @@ describe("classifyTaskMutationIntent", () => {
|
|
|
33
40
|
assert.equal(classifyTaskMutationIntent("worker", "Do not modify tests and fixtures").kind, "read-only");
|
|
34
41
|
});
|
|
35
42
|
|
|
43
|
+
it("recognizes coordinated read-only prohibitions", () => {
|
|
44
|
+
for (const task of [
|
|
45
|
+
"Do not read more files, run commands, or edit anything.",
|
|
46
|
+
"Do not read more files, run commands, and edit anything.",
|
|
47
|
+
"Do not read files or modify anything.",
|
|
48
|
+
]) {
|
|
49
|
+
assert.notEqual(classifyTaskMutationIntent("worker", task).kind, "implementation", task);
|
|
50
|
+
assert.equal(expectsImplementationMutation("worker", task), false, task);
|
|
51
|
+
assert.equal(taskMayMutate(task), false, task);
|
|
52
|
+
}
|
|
53
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not read more files, run commands, or edit anything; implement the fix").kind, "implementation");
|
|
54
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not read more files, run commands, or edit anything\\r\\nImplement the fix").kind, "implementation");
|
|
55
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not read more files\\r\\nImplement or edit the fix").kind, "implementation");
|
|
56
|
+
assert.equal(taskMayMutate("Do not read more files\\r\\nImplement or edit the fix"), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
36
59
|
it("lets blanket no-edit prohibitions win over write verbs", () => {
|
|
37
60
|
assert.equal(classifyTaskMutationIntent("worker", "Implement this. Do not edit files.").kind, "read-only");
|
|
38
61
|
assert.equal(classifyTaskMutationIntent("worker", "Do not edit files. Tell me how to fix the bug.").kind, "read-only");
|
|
62
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\nIn your final response, explain how to implement the fix.").kind, "read-only");
|
|
63
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nIn your final output, implement the fix.").kind, "read-only");
|
|
64
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nIn your final output/report/response, implement the fix.").kind, "read-only");
|
|
65
|
+
assert.equal(classifyTaskMutationIntent("worker", "Do not modify files\\nIn your report, explain how to fix the bug.").kind, "read-only");
|
|
39
66
|
assert.equal(classifyTaskMutationIntent("worker", "Report on the extraction pipeline. Do not modify project/source files.").kind, "read-only");
|
|
40
67
|
assert.equal(classifyTaskMutationIntent("reviewer", "Final correctness review after prior fixes. Inspect all changed files and tests. Do not modify project/source files. Report findings.").kind, "read-only");
|
|
41
68
|
assert.equal(classifyTaskMutationIntent("worker", "Verification-only task. Do not edit product/source/config files.\n Run a disposable check, delete its temporary harness, and retain only\n a sanitized report at an explicitly named artifact path.").kind, "read-only");
|
|
@@ -78,6 +78,56 @@ describe("shared temp paths", () => {
|
|
|
78
78
|
}
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
it("isolates agent-dir profile writes from an inherited SELESAI_CODING_AGENT_DIR", () => {
|
|
82
|
+
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-agent-isolation-"));
|
|
83
|
+
const tempRoot = path.join(fixture, "temp-root");
|
|
84
|
+
const callerAgentDir = path.join(fixture, "caller-agent");
|
|
85
|
+
try {
|
|
86
|
+
const loaderUrl = new URL("../support/isolated-temp-root.mjs", import.meta.url).href;
|
|
87
|
+
const profilesUrl = new URL("../../src/profiles/profiles.ts", import.meta.url).href;
|
|
88
|
+
const utilsUrl = new URL("../../src/shared/utils.ts", import.meta.url).href;
|
|
89
|
+
const script = `
|
|
90
|
+
import * as fs from "node:fs";
|
|
91
|
+
import * as path from "node:path";
|
|
92
|
+
import { applySubagentProfile, getSubagentProfilesDir } from ${JSON.stringify(profilesUrl)};
|
|
93
|
+
import { getAgentDir } from ${JSON.stringify(utilsUrl)};
|
|
94
|
+
|
|
95
|
+
const profilesDir = getSubagentProfilesDir();
|
|
96
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
97
|
+
fs.writeFileSync(path.join(profilesDir, "isolated.json"), JSON.stringify({ subagents: { agentOverrides: { worker: { thinking: "high" } } } }));
|
|
98
|
+
const result = applySubagentProfile("isolated");
|
|
99
|
+
console.log(JSON.stringify({ agentDir: getAgentDir(), profilePath: path.join(profilesDir, "isolated.json"), settingsPath: result.settingsPath }));
|
|
100
|
+
`;
|
|
101
|
+
const env = {
|
|
102
|
+
...process.env,
|
|
103
|
+
SELESAI_CODING_AGENT_DIR: callerAgentDir,
|
|
104
|
+
SELESAI_SUBAGENTS_TEMP_ROOT: tempRoot,
|
|
105
|
+
};
|
|
106
|
+
delete env.SELESAI_SUBAGENTS_TEST_LOADER;
|
|
107
|
+
const result = spawnSync(process.execPath, [
|
|
108
|
+
"--experimental-strip-types",
|
|
109
|
+
"--import", loaderUrl,
|
|
110
|
+
"--input-type=module",
|
|
111
|
+
"--eval", script,
|
|
112
|
+
], {
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
env,
|
|
116
|
+
});
|
|
117
|
+
assert.equal(result.status, 0, result.stderr);
|
|
118
|
+
const output = JSON.parse(result.stdout.trim()) as { agentDir: string; profilePath: string; settingsPath: string };
|
|
119
|
+
const isolatedAgentDir = path.join(tempRoot, "home", ".selesai", "agent");
|
|
120
|
+
assert.equal(output.agentDir, isolatedAgentDir);
|
|
121
|
+
assert.equal(output.profilePath, path.join(isolatedAgentDir, "profiles", "pi-subagents", "isolated.json"));
|
|
122
|
+
assert.equal(output.settingsPath, path.join(isolatedAgentDir, "settings.json"));
|
|
123
|
+
assert.equal(fs.existsSync(output.profilePath), true);
|
|
124
|
+
assert.equal(fs.existsSync(output.settingsPath), true);
|
|
125
|
+
assert.equal(fs.existsSync(callerAgentDir), false);
|
|
126
|
+
} finally {
|
|
127
|
+
fs.rmSync(fixture, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
81
131
|
it("anchors shared temp directories under one scoped root", () => {
|
|
82
132
|
assert.equal(path.dirname(RESULTS_DIR), TEMP_ROOT_DIR);
|
|
83
133
|
assert.equal(path.dirname(ASYNC_DIR), TEMP_ROOT_DIR);
|
|
@@ -94,7 +94,6 @@ describe("registered subagent tool description", () => {
|
|
|
94
94
|
assert.doesNotMatch(description, /tasks\[\]|chain\[\]/i);
|
|
95
95
|
assert.match(description, /ordinary async subagents notify this session natively.*return control.*do not call bg_wait/i);
|
|
96
96
|
assert.match(description, /bg_wait only for provider, detached, or other background work without a native notification/i);
|
|
97
|
-
assert.doesNotMatch(description, /subagent_wait/i);
|
|
98
97
|
assert.match(description, /continue independent work only until its next dependency barrier; consume the result before work that depends on it/i);
|
|
99
98
|
assert.match(description, /children\.list.*resume only rows reported resumable/i);
|
|
100
99
|
assert.match(description, /Each workflow key identifies one result lane.*new stable workflow key.*retained resume pass/i);
|
|
@@ -77,14 +77,14 @@ describe("effectiveToolTimeoutMs", () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
it("never applies a tool timeout to blocking coordination tools", () => {
|
|
80
|
-
for (const tool of ["contact_supervisor", "intercom", "bg_wait"
|
|
80
|
+
for (const tool of ["contact_supervisor", "intercom", "bg_wait"]) {
|
|
81
81
|
assert.equal(isToolTimeoutExempt(tool), true, `${tool} must be exempt`);
|
|
82
82
|
assert.equal(effectiveToolTimeoutMs(tool, 1234), undefined);
|
|
83
83
|
assert.equal(effectiveToolTimeoutMs(tool, undefined), undefined);
|
|
84
84
|
}
|
|
85
85
|
assert.equal(isToolTimeoutExempt("bash"), false);
|
|
86
86
|
assert.equal(isToolTimeoutExempt(undefined), false);
|
|
87
|
-
assert.deepEqual([...TOOL_TIMEOUT_ALLOWLIST].sort(), ["bg_wait", "contact_supervisor", "intercom"
|
|
87
|
+
assert.deepEqual([...TOOL_TIMEOUT_ALLOWLIST].sort(), ["bg_wait", "contact_supervisor", "intercom"]);
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
90
|
|
|
@@ -66,6 +66,40 @@ describe("workflow wait completion projection", () => {
|
|
|
66
66
|
assert.doesNotMatch(JSON.stringify(completion), /raw recovery message|settlementDiagnostic/);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it("retains captured structured output and its durable artifact path", () => {
|
|
70
|
+
const completion = toWaitCompletion({
|
|
71
|
+
success: true,
|
|
72
|
+
results: [{
|
|
73
|
+
agent: "delegate",
|
|
74
|
+
success: true,
|
|
75
|
+
output: "",
|
|
76
|
+
structuredOutput: { payload: { ok: true }, contract_checks: {} },
|
|
77
|
+
structuredOutputPath: "/runs/structured-output/output.json",
|
|
78
|
+
}],
|
|
79
|
+
}, "run-structured");
|
|
80
|
+
|
|
81
|
+
assert.deepEqual(completion.results?.[0]?.structuredOutput, { payload: { ok: true }, contract_checks: {} });
|
|
82
|
+
assert.equal(completion.results?.[0]?.structuredOutputPath, "/runs/structured-output/output.json");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("omits oversized structured output while retaining its artifact path", () => {
|
|
86
|
+
const completion = toWaitCompletion({
|
|
87
|
+
success: true,
|
|
88
|
+
results: [{
|
|
89
|
+
agent: "delegate",
|
|
90
|
+
structuredOutput: { payload: "x".repeat(8_000) },
|
|
91
|
+
structuredOutputPath: "/runs/structured-output/output.json",
|
|
92
|
+
}],
|
|
93
|
+
}, "run-large-structured");
|
|
94
|
+
|
|
95
|
+
assert.equal(completion.results?.[0]?.structuredOutput, undefined);
|
|
96
|
+
assert.equal(completion.results?.[0]?.structuredOutputPath, "/runs/structured-output/output.json");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rejects non-JSON structured output", () => {
|
|
100
|
+
assert.throws(() => toWaitCompletion({ success: true, results: [{ structuredOutput: 1n }] }, "run-invalid-structured"), /JSON-serializable|serialize a BigInt/);
|
|
101
|
+
});
|
|
102
|
+
|
|
69
103
|
it("rejects unbounded or unknown summary fields at the replay boundary", () => {
|
|
70
104
|
assert.throws(() => toWaitCompletion({ workflowChildren: { version: 1, parentToolCallId: "tool", workflowRunId: "run", inventoryComplete: true, workflowState: "completed", children: [], output: "secret" } }, "run"), /unsupported fields/);
|
|
71
105
|
});
|
|
@@ -107,7 +107,7 @@ describe("non-blocking wait subscriptions", () => {
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
it("rejects non-blocking subscriptions from headless tool calls", async () => {
|
|
110
|
+
it("registers bg_wait and rejects non-blocking subscriptions from headless tool calls", async () => {
|
|
111
111
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-subscribe-headless-"));
|
|
112
112
|
try {
|
|
113
113
|
const state = makeState();
|
|
@@ -127,7 +127,6 @@ describe("non-blocking wait subscriptions", () => {
|
|
|
127
127
|
arm() { throw new Error("headless calls must not arm subscriptions"); },
|
|
128
128
|
});
|
|
129
129
|
assert.deepEqual(registered.map((entry) => entry.name), ["bg_wait", "subagent_wait"]);
|
|
130
|
-
assert.match(registered[1]?.description ?? "", /deprecated compatibility alias for `bg_wait`/i);
|
|
131
130
|
await assert.rejects(
|
|
132
131
|
registered[0]!.execute("wait", { id: "run-headless", nonBlocking: true }, undefined, undefined, { hasUI: false }),
|
|
133
132
|
/long-lived interactive subagent runtime/,
|
|
@@ -2,15 +2,82 @@ import { describe, it } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import {
|
|
4
4
|
CHILD_WATCHDOG_STATUS_EVENT,
|
|
5
|
+
CHILD_WATCHDOG_WARNING_LIMIT,
|
|
5
6
|
acceptChildWatchdogEvent,
|
|
7
|
+
applyChildWatchdogMessage,
|
|
6
8
|
childWatchdogIsActive,
|
|
7
9
|
decodeChildWatchdogConfig,
|
|
8
10
|
isChildWatchdogStatusEvent,
|
|
9
11
|
resolveChildWatchdogConfig,
|
|
12
|
+
unresolvedChildWatchdogBlockers,
|
|
10
13
|
} from "../../src/watchdog/child-status.ts";
|
|
11
14
|
import { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
|
|
15
|
+
import { events } from "../support/helpers.ts";
|
|
16
|
+
|
|
17
|
+
function warningMessage(overrides: Record<string, unknown> = {}) {
|
|
18
|
+
return (events.watchdogWarning("blocker", "Claims tests passed without running them", { category: "correctness", evidence: "No test command appears in the transcript.", displayedAt: "2026-09-01T00:00:00.000Z", ...overrides }) as { message: object }).message;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("child watchdog warning envelope", () => {
|
|
22
|
+
it("lifts watchdog warning messages, ignores other messages, and marks warnings addressed on assistant turns", () => {
|
|
23
|
+
let snapshot = applyChildWatchdogMessage(undefined, warningMessage(), 5);
|
|
24
|
+
assert.deepEqual(snapshot, {
|
|
25
|
+
phase: "idle",
|
|
26
|
+
seq: 0,
|
|
27
|
+
lastUpdate: 5,
|
|
28
|
+
warnings: [{
|
|
29
|
+
severity: "blocker",
|
|
30
|
+
category: "correctness",
|
|
31
|
+
summary: "Claims tests passed without running them",
|
|
32
|
+
evidence: "No test command appears in the transcript.",
|
|
33
|
+
recommendedAction: "Run the focused test before finishing.",
|
|
34
|
+
displayedAt: "2026-09-01T00:00:00.000Z",
|
|
35
|
+
addressed: false,
|
|
36
|
+
stalemate: false,
|
|
37
|
+
}],
|
|
38
|
+
});
|
|
39
|
+
assert.equal(applyChildWatchdogMessage(undefined, warningMessage({ state: "stalemate" }))?.warnings?.[0]?.stalemate, true);
|
|
40
|
+
assert.equal(applyChildWatchdogMessage(undefined, { role: "assistant", content: [] }), undefined);
|
|
41
|
+
assert.equal(applyChildWatchdogMessage(undefined, { role: "custom", customType: "subagent-notify", details: {} }), undefined);
|
|
42
|
+
assert.equal(applyChildWatchdogMessage(undefined, warningMessage({ severity: "nit" })), undefined);
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < CHILD_WATCHDOG_WARNING_LIMIT + 5; index++) {
|
|
45
|
+
snapshot = applyChildWatchdogMessage(snapshot, warningMessage({ summary: `warning ${index}` }));
|
|
46
|
+
}
|
|
47
|
+
assert.equal(snapshot?.warnings?.length, CHILD_WATCHDOG_WARNING_LIMIT);
|
|
48
|
+
assert.equal(snapshot?.warnings?.at(-1)?.summary, `warning ${CHILD_WATCHDOG_WARNING_LIMIT + 4}`);
|
|
49
|
+
|
|
50
|
+
const status = acceptChildWatchdogEvent({ current: snapshot, event: { type: CHILD_WATCHDOG_STATUS_EVENT, seq: 1, phase: "reviewing", ts: 10 } });
|
|
51
|
+
assert.equal(status?.warnings?.length, CHILD_WATCHDOG_WARNING_LIMIT, "status events keep the warning envelope");
|
|
52
|
+
assert.equal(unresolvedChildWatchdogBlockers(status).length, CHILD_WATCHDOG_WARNING_LIMIT);
|
|
53
|
+
|
|
54
|
+
const addressed = applyChildWatchdogMessage(status, { role: "assistant" });
|
|
55
|
+
assert.equal(addressed?.warnings?.every((entry) => entry.addressed), true);
|
|
56
|
+
assert.equal(unresolvedChildWatchdogBlockers(addressed).length, 0);
|
|
57
|
+
assert.equal(applyChildWatchdogMessage(addressed, { role: "assistant" }), undefined, "nothing left to address");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("treats stalemate blockers as unresolved even when a turn followed, and never concerns", () => {
|
|
61
|
+
const withBlocker = applyChildWatchdogMessage(undefined, warningMessage({ state: "stalemate" }));
|
|
62
|
+
const withConcern = applyChildWatchdogMessage(withBlocker, warningMessage({ severity: "concern" }));
|
|
63
|
+
const snapshot = applyChildWatchdogMessage(withConcern, { role: "assistant" });
|
|
64
|
+
assert.deepEqual(unresolvedChildWatchdogBlockers(snapshot).map((entry) => entry.severity), ["blocker"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
12
67
|
|
|
13
68
|
describe("child watchdog status helpers", () => {
|
|
69
|
+
it("resolves child cadence from override, then children, then the top-level cadence", () => {
|
|
70
|
+
const base = { ...DEFAULT_WATCHDOG_CONFIG, enabled: true, children: { ...DEFAULT_WATCHDOG_CONFIG.children, enabled: true, overrides: {} } };
|
|
71
|
+
assert.deepEqual(resolveChildWatchdogConfig({ config: base, agent: "worker" })?.cadence, { everyNTools: null });
|
|
72
|
+
const root = { ...base, cadence: { everyNTools: 20 } };
|
|
73
|
+
assert.deepEqual(resolveChildWatchdogConfig({ config: root, agent: "worker" })?.cadence, { everyNTools: 20 });
|
|
74
|
+
const children = { ...root, children: { ...root.children, cadence: { everyNTools: 10 } } };
|
|
75
|
+
assert.deepEqual(resolveChildWatchdogConfig({ config: children, agent: "worker" })?.cadence, { everyNTools: 10 });
|
|
76
|
+
const override = { ...children, children: { ...children.children, overrides: { worker: { cadence: { everyNTools: 5 } } } } };
|
|
77
|
+
assert.deepEqual(resolveChildWatchdogConfig({ config: override, agent: "worker" })?.cadence, { everyNTools: 5 });
|
|
78
|
+
assert.deepEqual(resolveChildWatchdogConfig({ config: override, agent: "reviewer" })?.cadence, { everyNTools: 10 });
|
|
79
|
+
});
|
|
80
|
+
|
|
14
81
|
it("preserves child model and explicit override thinking when resolving config", () => {
|
|
15
82
|
const config = resolveChildWatchdogConfig({
|
|
16
83
|
config: {
|
|
@@ -41,7 +108,6 @@ describe("child watchdog status helpers", () => {
|
|
|
41
108
|
|
|
42
109
|
it("decodes child watchdog config and rejects malformed enabled payloads", () => {
|
|
43
110
|
const payload = {
|
|
44
|
-
enabled: true,
|
|
45
111
|
runId: "run-1",
|
|
46
112
|
agent: "worker",
|
|
47
113
|
childIndex: 1,
|
|
@@ -49,9 +115,8 @@ describe("child watchdog status helpers", () => {
|
|
|
49
115
|
agentEndTimeoutMs: 200,
|
|
50
116
|
maxWarnings: null,
|
|
51
117
|
lsp: { enabled: false, timeoutMs: 50, maxFiles: 2, maxDiagnostics: 3 },
|
|
52
|
-
autoFollowBlockers: true,
|
|
53
|
-
autoFollowMaxAttempts: 2,
|
|
54
118
|
stalemateRepeats: 3,
|
|
119
|
+
cadence: { everyNTools: 10 },
|
|
55
120
|
};
|
|
56
121
|
const config = decodeChildWatchdogConfig(JSON.stringify(payload));
|
|
57
122
|
|
|
@@ -61,18 +126,14 @@ describe("child watchdog status helpers", () => {
|
|
|
61
126
|
() => decodeChildWatchdogConfig(JSON.stringify({ ...payload, lsp: { ...payload.lsp, timeoutMs: 0 } })),
|
|
62
127
|
/lsp\.timeoutMs/,
|
|
63
128
|
);
|
|
64
|
-
assert.throws(
|
|
65
|
-
() => decodeChildWatchdogConfig(JSON.stringify({ ...payload, autoFollowBlockers: undefined })),
|
|
66
|
-
/autoFollowBlockers/,
|
|
67
|
-
);
|
|
68
|
-
assert.throws(
|
|
69
|
-
() => decodeChildWatchdogConfig(JSON.stringify({ ...payload, autoFollowBlockers: "yes" })),
|
|
70
|
-
/autoFollowBlockers/,
|
|
71
|
-
);
|
|
72
129
|
assert.throws(
|
|
73
130
|
() => decodeChildWatchdogConfig(JSON.stringify({ ...payload, stalemateRepeats: 0 })),
|
|
74
131
|
/stalemateRepeats/,
|
|
75
132
|
);
|
|
133
|
+
assert.throws(
|
|
134
|
+
() => decodeChildWatchdogConfig(JSON.stringify({ ...payload, cadence: { everyNTools: 3 } })),
|
|
135
|
+
/cadence\.everyNTools/,
|
|
136
|
+
);
|
|
76
137
|
});
|
|
77
138
|
|
|
78
139
|
it("accepts latest matching status events and drops stale or foreign events", () => {
|
|
@@ -84,12 +145,11 @@ describe("child watchdog status helpers", () => {
|
|
|
84
145
|
seq: 1,
|
|
85
146
|
phase: "reviewing",
|
|
86
147
|
ts: 10,
|
|
87
|
-
followUpPending: false,
|
|
88
148
|
} as const;
|
|
89
149
|
|
|
90
150
|
assert.equal(isChildWatchdogStatusEvent(firstEvent), true);
|
|
91
151
|
const first = acceptChildWatchdogEvent({ event: firstEvent, runId: "run-1", agent: "worker", childIndex: 0, current: undefined });
|
|
92
|
-
assert.deepEqual(first, { phase: "reviewing", seq: 1, lastUpdate: 10
|
|
152
|
+
assert.deepEqual(first, { phase: "reviewing", seq: 1, lastUpdate: 10 });
|
|
93
153
|
assert.equal(childWatchdogIsActive(first), true);
|
|
94
154
|
assert.equal(acceptChildWatchdogEvent({ event: firstEvent, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
|
|
95
155
|
assert.equal(acceptChildWatchdogEvent({ event: { ...firstEvent, seq: 2, runId: undefined }, current: first, runId: "run-1", agent: "worker", childIndex: 0 }), undefined);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } 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 { afterEach, beforeEach, describe, it } from "node:test";
|
|
7
|
+
import { captureWatchdogDiffBaseline, createWatchdogDiffTool, WATCHDOG_DIFF_MAX_CHARS } from "../../src/watchdog/diff-tool.ts";
|
|
8
|
+
|
|
9
|
+
let repo = "";
|
|
10
|
+
|
|
11
|
+
function git(...args: string[]): string {
|
|
12
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function run(tool: ReturnType<typeof createWatchdogDiffTool>, params: { path?: string; stat?: boolean } = {}): Promise<string> {
|
|
16
|
+
const result = await tool.execute("call", params, undefined as never, undefined as never, undefined as never);
|
|
17
|
+
return result.content.map((part) => (part as { text?: string }).text ?? "").join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("watchdog diff tool", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
repo = fs.mkdtempSync(path.join(os.tmpdir(), "watchdog-diff-"));
|
|
23
|
+
git("init", "-q");
|
|
24
|
+
git("config", "user.email", "test@example.com");
|
|
25
|
+
git("config", "user.name", "Test User");
|
|
26
|
+
fs.mkdirSync(path.join(repo, "src"), { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(repo, "src", "a.ts"), "export const a = 1;\n", "utf-8");
|
|
28
|
+
fs.writeFileSync(path.join(repo, "README.md"), "readme\n", "utf-8");
|
|
29
|
+
git("add", ".");
|
|
30
|
+
git("commit", "-q", "-m", "base");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("captures the session baseline and reports no changes when clean", async () => {
|
|
38
|
+
const baseline = captureWatchdogDiffBaseline(path.join(repo, "src"));
|
|
39
|
+
assert.ok(baseline);
|
|
40
|
+
assert.equal(fs.realpathSync.native(baseline.root), fs.realpathSync.native(repo), "git expands Windows 8.3 short names; compare native real paths");
|
|
41
|
+
assert.equal(baseline.ref, git("rev-parse", "HEAD").trim());
|
|
42
|
+
assert.match(await run(createWatchdogDiffTool(baseline)), /^No changes since baseline/);
|
|
43
|
+
assert.equal(captureWatchdogDiffBaseline(os.tmpdir()), undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("shows tracked changes since the baseline, including later commits, and lists untracked paths", async () => {
|
|
47
|
+
const baseline = captureWatchdogDiffBaseline(repo)!;
|
|
48
|
+
fs.writeFileSync(path.join(repo, "src", "a.ts"), "export const a = 2;\n", "utf-8");
|
|
49
|
+
git("commit", "-q", "-am", "child commit");
|
|
50
|
+
fs.writeFileSync(path.join(repo, "src", "b.ts"), "export const b = 1;\n", "utf-8");
|
|
51
|
+
const tool = createWatchdogDiffTool(baseline);
|
|
52
|
+
|
|
53
|
+
const full = await run(tool);
|
|
54
|
+
assert.match(full, /-export const a = 1;/);
|
|
55
|
+
assert.match(full, /\+export const a = 2;/);
|
|
56
|
+
assert.match(full, /Untracked files \(use read to inspect\):\n src\/b\.ts/);
|
|
57
|
+
assert.doesNotMatch(full, /export const b = 1;/);
|
|
58
|
+
|
|
59
|
+
const stat = await run(tool, { stat: true });
|
|
60
|
+
assert.match(stat, /src\/a\.ts \|/);
|
|
61
|
+
assert.match(stat, /Untracked files \(use read to inspect\):\n src\/b\.ts/);
|
|
62
|
+
|
|
63
|
+
const narrowed = await run(tool, { path: "README.md" });
|
|
64
|
+
assert.doesNotMatch(narrowed, /a\.ts|b\.ts/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rejects traversal and option-like paths and bounds large diffs", async () => {
|
|
68
|
+
const baseline = captureWatchdogDiffBaseline(repo)!;
|
|
69
|
+
const tool = createWatchdogDiffTool(baseline);
|
|
70
|
+
await assert.rejects(() => run(tool, { path: "../etc" }), /must not contain '\.\.'/);
|
|
71
|
+
await assert.rejects(() => run(tool, { path: "-R" }), /must not start with '-'/);
|
|
72
|
+
await assert.rejects(() => run(tool, { path: "/etc/hosts" }), /relative/);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(path.join(repo, "src", "a.ts"), `${"export const line = 1;\n".repeat(3_000)}`, "utf-8");
|
|
75
|
+
const bounded = await run(tool);
|
|
76
|
+
assert.ok(bounded.length <= WATCHDOG_DIFF_MAX_CHARS, `bounded output was ${bounded.length}`);
|
|
77
|
+
assert.match(bounded, /characters omitted; call again with a narrower path/);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -46,9 +46,8 @@ const childConfig = JSON.stringify({
|
|
|
46
46
|
agentEndTimeoutMs: 1_000,
|
|
47
47
|
maxWarnings: null,
|
|
48
48
|
lsp: { enabled: false, timeoutMs: 100, maxFiles: 1, maxDiagnostics: 1 },
|
|
49
|
-
autoFollowBlockers: false,
|
|
50
|
-
autoFollowMaxAttempts: null,
|
|
51
49
|
stalemateRepeats: 2,
|
|
50
|
+
cadence: { everyNTools: null },
|
|
52
51
|
});
|
|
53
52
|
|
|
54
53
|
describe("watchdog permission arbiter", () => {
|
|
@@ -97,9 +96,8 @@ describe("watchdog permission arbiter", () => {
|
|
|
97
96
|
agentEndTimeoutMs: 5,
|
|
98
97
|
maxWarnings: null,
|
|
99
98
|
lsp: { enabled: false, timeoutMs: 100, maxFiles: 1, maxDiagnostics: 1 },
|
|
100
|
-
autoFollowBlockers: false,
|
|
101
|
-
autoFollowMaxAttempts: null,
|
|
102
99
|
stalemateRepeats: 2,
|
|
100
|
+
cadence: { everyNTools: null },
|
|
103
101
|
}),
|
|
104
102
|
}),
|
|
105
103
|
new Promise((resolve) => setTimeout(() => resolve("hung"), 100)),
|