@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
|
@@ -6,6 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
import { ACTIVE_RUN_INDEX_DIR, updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
|
|
7
7
|
import {
|
|
8
8
|
finalizeProcessTerminal,
|
|
9
|
+
initializeProcessTerminal,
|
|
9
10
|
processTerminalPath,
|
|
10
11
|
readProcessTerminal,
|
|
11
12
|
sanitizeProcessTerminal,
|
|
@@ -59,6 +60,27 @@ test("process-terminal proof requires the matching runner instance and writer cl
|
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
test("startup process-terminal candidate does not certify unobserved writers", () => {
|
|
64
|
+
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-startup-"));
|
|
65
|
+
try {
|
|
66
|
+
initializeProcessTerminal(asyncDir, "run-startup", "runner-startup");
|
|
67
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ runId: "run-startup", state: "running", lifecycleArtifactVersion: 3, steps: [{ agent: "worker", status: "running" }] }));
|
|
68
|
+
fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "");
|
|
69
|
+
|
|
70
|
+
const proof = finalizeProcessTerminal(asyncDir, "run-startup", {
|
|
71
|
+
processInstanceId: "runner-startup",
|
|
72
|
+
closeObservedAt: 20,
|
|
73
|
+
exitCode: 1,
|
|
74
|
+
signal: null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(proof.state, "unknown");
|
|
78
|
+
assert.equal(proof.reason, "writer-close-unverified");
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(asyncDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
62
84
|
test("process-terminal rejects missing writer close evidence", () => {
|
|
63
85
|
const asyncDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-process-terminal-"));
|
|
64
86
|
try {
|
|
@@ -37,6 +37,7 @@ describe("profiles helpers", () => {
|
|
|
37
37
|
homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subprofiles-home-"));
|
|
38
38
|
process.env.HOME = homeDir;
|
|
39
39
|
process.env.USERPROFILE = homeDir;
|
|
40
|
+
process.env[SELESAI_SUBAGENT_SELESAI_BINARY_ENV] = path.join(homeDir, "pi-test-bin");
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
afterEach(() => {
|
|
@@ -30,6 +30,8 @@ describe("public project-panes package export", () => {
|
|
|
30
30
|
|
|
31
31
|
it("runs the structured lifecycle and verifies pane ownership before idle close", async () => {
|
|
32
32
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-public-project-pane-"));
|
|
33
|
+
const previousPiBinary = process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
|
|
34
|
+
process.env.SELESAI_SUBAGENT_SELESAI_BINARY = path.join(root, "pi-bin");
|
|
33
35
|
try {
|
|
34
36
|
const projectRoot = fs.realpathSync(root);
|
|
35
37
|
const calls: string[][] = [];
|
|
@@ -76,6 +78,8 @@ describe("public project-panes package export", () => {
|
|
|
76
78
|
assert.ok(calls.some((args) => args.join(" ") === "tab focus tab-20"));
|
|
77
79
|
assert.ok(calls.some((args) => args.join(" ") === "pane close w1:p20"));
|
|
78
80
|
} finally {
|
|
81
|
+
if (previousPiBinary === undefined) delete process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
|
|
82
|
+
else process.env.SELESAI_SUBAGENT_SELESAI_BINARY = previousPiBinary;
|
|
79
83
|
fs.rmSync(root, { recursive: true, force: true });
|
|
80
84
|
}
|
|
81
85
|
});
|
|
@@ -131,6 +135,8 @@ describe("public project-panes package export", () => {
|
|
|
131
135
|
|
|
132
136
|
it("returns a structured error and closes the pane when binding persistence fails", async () => {
|
|
133
137
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-public-project-pane-write-failure-"));
|
|
138
|
+
const previousPiBinary = process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
|
|
139
|
+
process.env.SELESAI_SUBAGENT_SELESAI_BINARY = path.join(root, "pi-bin");
|
|
134
140
|
try {
|
|
135
141
|
fs.mkdirSync(path.join(root, ".selesai"));
|
|
136
142
|
fs.writeFileSync(path.join(root, ".pi-subagents"), "directory collision");
|
|
@@ -152,6 +158,8 @@ describe("public project-panes package export", () => {
|
|
|
152
158
|
}
|
|
153
159
|
assert.ok(calls.some((args) => args.join(" ") === "pane close w1:p24"));
|
|
154
160
|
} finally {
|
|
161
|
+
if (previousPiBinary === undefined) delete process.env.SELESAI_SUBAGENT_SELESAI_BINARY;
|
|
162
|
+
else process.env.SELESAI_SUBAGENT_SELESAI_BINARY = previousPiBinary;
|
|
155
163
|
fs.rmSync(root, { recursive: true, force: true });
|
|
156
164
|
}
|
|
157
165
|
});
|
|
@@ -33,37 +33,6 @@ function result(agent: string, output: string) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function countedWorkflowJob(onNodesRead: () => void): AsyncJobState {
|
|
37
|
-
const nodes = [
|
|
38
|
-
{ id: "step-0", kind: "step", agent: "scout", label: "Scout", status: "completed", flatIndex: 0, stepIndex: 0 },
|
|
39
|
-
{ id: "step-1", kind: "step", agent: "reviewer", label: "Review", status: "running", flatIndex: 1, stepIndex: 1 },
|
|
40
|
-
] as const;
|
|
41
|
-
return {
|
|
42
|
-
asyncId: "workflow-run",
|
|
43
|
-
asyncDir: "/tmp/workflow-run",
|
|
44
|
-
status: "running",
|
|
45
|
-
mode: "workflow",
|
|
46
|
-
agents: ["scout", "reviewer"],
|
|
47
|
-
currentStep: 1,
|
|
48
|
-
startedAt: 0,
|
|
49
|
-
updatedAt: 1000,
|
|
50
|
-
steps: [
|
|
51
|
-
{ index: 0, agent: "scout", status: "completed", workflowKey: "step-0" },
|
|
52
|
-
{ index: 1, agent: "reviewer", status: "running", workflowKey: "step-1" },
|
|
53
|
-
],
|
|
54
|
-
workflowGraph: {
|
|
55
|
-
runId: "workflow-run",
|
|
56
|
-
mode: "workflow",
|
|
57
|
-
phases: [],
|
|
58
|
-
currentNodeId: "step-1",
|
|
59
|
-
get nodes() {
|
|
60
|
-
onNodesRead();
|
|
61
|
-
return nodes;
|
|
62
|
-
},
|
|
63
|
-
} as never,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
36
|
test("row clips content to the available width", () => {
|
|
68
37
|
const rendered = row("abcdef", 6, theme as any);
|
|
69
38
|
assert.equal(visibleWidth(rendered), 6);
|
|
@@ -317,6 +286,133 @@ test("compact multi-result cards prefer bounded workflow labels over raw tasks",
|
|
|
317
286
|
assert.match(text, /Ctrl\+Alt\+F Fleet/);
|
|
318
287
|
});
|
|
319
288
|
|
|
289
|
+
test("workflow checklist does not map child-local result indexes onto graph nodes", () => {
|
|
290
|
+
const text = componentText(renderSubagentResult({
|
|
291
|
+
content: [{ type: "text", text: "failed" }],
|
|
292
|
+
details: {
|
|
293
|
+
mode: "workflow",
|
|
294
|
+
results: [
|
|
295
|
+
{ ...result("scout", "ok"), index: 0 },
|
|
296
|
+
{ ...result("writer", "failed"), index: 0, exitCode: 1 },
|
|
297
|
+
],
|
|
298
|
+
workflowGraph: {
|
|
299
|
+
runId: "workflow-local-indexes",
|
|
300
|
+
mode: "workflow",
|
|
301
|
+
phases: [{ title: "inventory", nodeIds: ["inventory"] }, { title: "write", nodeIds: ["writer"] }],
|
|
302
|
+
nodes: [
|
|
303
|
+
{ id: "inventory", kind: "agent", agent: "scout", label: "Inventory", status: "completed", flatIndex: 0 },
|
|
304
|
+
{ id: "writer", kind: "agent", agent: "writer", label: "Writer", status: "failed", flatIndex: 1 },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
}, { expanded: true }, theme as any));
|
|
309
|
+
|
|
310
|
+
assert.match(text, /Checklist 1\/2 done · 1 failed/);
|
|
311
|
+
assert.match(text, /✗ write 1 failed/);
|
|
312
|
+
assert.match(text, /✗ Writer · writer · failed/);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("collapsed workflow widgets lead with checklist phases while expanded widgets keep child detail", () => {
|
|
316
|
+
const now = 50_000;
|
|
317
|
+
const workflowGraph = {
|
|
318
|
+
runId: "workflow-collapsed",
|
|
319
|
+
mode: "workflow" as const,
|
|
320
|
+
phases: [
|
|
321
|
+
{ title: "inventory", nodeIds: ["inventory"] },
|
|
322
|
+
{ title: "writers", nodeIds: ["writer-a", "writer-b"] },
|
|
323
|
+
{ title: "reviews", nodeIds: ["review"] },
|
|
324
|
+
{ title: "gates", nodeIds: ["gate"] },
|
|
325
|
+
],
|
|
326
|
+
nodes: [
|
|
327
|
+
{ id: "inventory", kind: "step" as const, label: "inventory", agent: "scout", status: "completed" as const, flatIndex: 0 },
|
|
328
|
+
{ id: "writer-a", kind: "agent" as const, label: "writer-a", agent: "writer", status: "running" as const, flatIndex: 1 },
|
|
329
|
+
{ id: "writer-b", kind: "agent" as const, label: "writer-b", agent: "writer", status: "pending" as const, flatIndex: 2 },
|
|
330
|
+
{ id: "review", kind: "step" as const, label: "review", agent: "reviewer", status: "pending" as const, flatIndex: 3 },
|
|
331
|
+
{ id: "gate", kind: "step" as const, label: "gate", agent: "reviewer", status: "pending" as const, flatIndex: 4 },
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
const job: AsyncJobState = {
|
|
335
|
+
asyncId: "workflow-collapsed",
|
|
336
|
+
asyncDir: "/tmp/workflow-collapsed",
|
|
337
|
+
cwd: "/tmp/workflow",
|
|
338
|
+
status: "running",
|
|
339
|
+
mode: "workflow",
|
|
340
|
+
agents: ["scout", "writer", "reviewer"],
|
|
341
|
+
stepsTotal: 5,
|
|
342
|
+
currentStep: 1,
|
|
343
|
+
toolCount: 38,
|
|
344
|
+
startedAt: 1_000,
|
|
345
|
+
updatedAt: now,
|
|
346
|
+
workflowGraph,
|
|
347
|
+
steps: [
|
|
348
|
+
{ index: 0, workflowKey: "inventory", agent: "scout", status: "complete", description: "Inspect the repository" },
|
|
349
|
+
{ index: 1, workflowKey: "writer-a", agent: "writer", status: "running", description: "Review auth flow", currentTool: "edit", currentToolStartedAt: now - 2_000, toolCount: 19, outputName: "review-a.md" },
|
|
350
|
+
{ index: 2, workflowKey: "writer-b", agent: "writer", status: "pending", description: "Review billing flow", outputName: "review-b.md" },
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const collapsed = buildWidgetLines([job], theme, 240).join("\n");
|
|
355
|
+
assert.match(collapsed, /1\/5 done · 1 active · 3 queued/);
|
|
356
|
+
assert.match(collapsed, /✓ inventory/);
|
|
357
|
+
assert.match(collapsed, /writers 1 active · 1 queued/);
|
|
358
|
+
assert.match(collapsed, /◦ reviews 1 queued/);
|
|
359
|
+
assert.match(collapsed, /◦ gates 1 queued/);
|
|
360
|
+
assert.match(collapsed, /bottleneck/);
|
|
361
|
+
assert.match(collapsed, /Press configured-expand-key for live detail · Ctrl\+Alt\+F Fleet/);
|
|
362
|
+
assert.equal((collapsed.match(/1\/5 done · 1 active · 3 queued/g) ?? []).length, 1);
|
|
363
|
+
assert.doesNotMatch(collapsed, /Step \d\/\d|task:|workspace:|ref:|out(?:put)?:/i);
|
|
364
|
+
|
|
365
|
+
const expanded = buildWidgetLines([job], theme, 240, true).join("\n");
|
|
366
|
+
assert.match(expanded, /Stage 2\/5/);
|
|
367
|
+
assert.match(expanded, /task: .*Review auth flow/);
|
|
368
|
+
assert.match(expanded, /workspace:\/tmp\/workflow/);
|
|
369
|
+
assert.match(expanded, /out:review-a\.md/);
|
|
370
|
+
assert.match(expanded, /output: [\\/]tmp[\\/]workflow-collapsed[\\/]output-1\.log/);
|
|
371
|
+
|
|
372
|
+
const generic = buildWidgetLines([{
|
|
373
|
+
asyncId: "workflow-generic",
|
|
374
|
+
asyncDir: "/tmp/workflow-generic",
|
|
375
|
+
status: "running",
|
|
376
|
+
mode: "workflow",
|
|
377
|
+
agents: ["reviewer", "scout"],
|
|
378
|
+
stepsTotal: 2,
|
|
379
|
+
steps: [
|
|
380
|
+
{ index: 0, agent: "reviewer", status: "running" },
|
|
381
|
+
{ index: 1, agent: "scout", status: "running" },
|
|
382
|
+
],
|
|
383
|
+
} as AsyncJobState], theme, 240).join("\n");
|
|
384
|
+
assert.match(generic, /Workflow 2 active/);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("compact foreground workflow results use checklist phases instead of child rows", () => {
|
|
388
|
+
const workflowGraph = {
|
|
389
|
+
runId: "foreground-workflow",
|
|
390
|
+
mode: "workflow" as const,
|
|
391
|
+
phases: [{ title: "inventory", nodeIds: ["inventory"] }, { title: "writers", nodeIds: ["writer"] }],
|
|
392
|
+
nodes: [
|
|
393
|
+
{ id: "inventory", kind: "step" as const, label: "inventory", agent: "scout", status: "completed" as const, flatIndex: 0 },
|
|
394
|
+
{ id: "writer", kind: "agent" as const, label: "writer", agent: "writer", status: "running" as const, flatIndex: 1 },
|
|
395
|
+
],
|
|
396
|
+
};
|
|
397
|
+
const text = componentText(renderSubagentResult({
|
|
398
|
+
content: [{ type: "text", text: "running" }],
|
|
399
|
+
details: {
|
|
400
|
+
mode: "workflow",
|
|
401
|
+
results: [
|
|
402
|
+
{ ...result("scout", "done"), workflowKey: "inventory" },
|
|
403
|
+
{ ...result("writer", ""), workflowKey: "writer", task: "raw task that should stay in expanded detail", progress: { status: "running", index: 1, agent: "writer", toolCount: 7, tokens: 0, durationMs: 2_000 } },
|
|
404
|
+
],
|
|
405
|
+
workflowGraph,
|
|
406
|
+
},
|
|
407
|
+
} as never, { expanded: false }, theme as any));
|
|
408
|
+
|
|
409
|
+
assert.match(text, /1\/2 done · 1 active/);
|
|
410
|
+
assert.match(text, /✓ inventory/);
|
|
411
|
+
assert.match(text, /writers 1 active/);
|
|
412
|
+
assert.match(text, /Press configured-expand-key for live detail · Ctrl\+Alt\+F Fleet/);
|
|
413
|
+
assert.doesNotMatch(text, /Step \d\/\d|task:|workspace:|ref:|out(?:put)?:/i);
|
|
414
|
+
});
|
|
415
|
+
|
|
320
416
|
test("compact chain rendering uses workflow graph labels and parallel groups", () => {
|
|
321
417
|
const component = renderSubagentResult({
|
|
322
418
|
content: [{ type: "text", text: "done" }],
|
|
@@ -357,23 +453,6 @@ test("compact chain rendering uses workflow graph labels and parallel groups", (
|
|
|
357
453
|
assert.match(text, /Step 3\/3: Writer/);
|
|
358
454
|
});
|
|
359
455
|
|
|
360
|
-
test("widget rendering reuses one staged workflow projection", () => {
|
|
361
|
-
let nodeReads = 0;
|
|
362
|
-
const lines = buildWidgetLines([countedWorkflowJob(() => nodeReads++)], theme as any, 120, false, 0);
|
|
363
|
-
|
|
364
|
-
assert.equal(nodeReads, 1);
|
|
365
|
-
assert.match(lines.join("\n"), /staged lane · stage 2\/2 · Review · reviewer · running/);
|
|
366
|
-
assert.match(lines.join("\n"), /Stage 2\/2: Review/);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
test("widget render keys reuse one staged workflow projection", () => {
|
|
370
|
-
let nodeReads = 0;
|
|
371
|
-
|
|
372
|
-
widgetRenderKey(countedWorkflowJob(() => nodeReads++));
|
|
373
|
-
|
|
374
|
-
assert.equal(nodeReads, 1);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
456
|
test("compact chain rendering shows failed zero-child dynamic fanout groups", () => {
|
|
378
457
|
const component = renderSubagentResult({
|
|
379
458
|
content: [{ type: "text", text: "failed" }],
|
|
@@ -360,6 +360,70 @@ describe("async run status inspection", () => {
|
|
|
360
360
|
}
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
+
it("shows host steps in exact workflow status checklist", () => {
|
|
364
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-host-checklist-"));
|
|
365
|
+
try {
|
|
366
|
+
const asyncRoot = path.join(root, "runs");
|
|
367
|
+
const asyncDir = path.join(asyncRoot, "run-workflow-host");
|
|
368
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
369
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
370
|
+
runId: "run-workflow-host",
|
|
371
|
+
mode: "workflow",
|
|
372
|
+
state: "running",
|
|
373
|
+
startedAt: 100,
|
|
374
|
+
lastUpdate: 200,
|
|
375
|
+
workflowGraph: { runId: "run-workflow-host", mode: "workflow", phases: [], nodes: [{ id: "ci", kind: "host-step", label: "CI", status: "running", hostStep: { version: 1, kind: "host-step", monitorKind: "ci", id: "ci", label: "CI", state: "running", updatedAt: 200 } }] },
|
|
376
|
+
}, null, 2), "utf-8");
|
|
377
|
+
|
|
378
|
+
const result = inspectSubagentStatus({ id: "run-workflow-host" }, {
|
|
379
|
+
asyncDirRoot: asyncRoot,
|
|
380
|
+
resultsDir: path.join(root, "results"),
|
|
381
|
+
kill: () => true,
|
|
382
|
+
now: () => 250,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const text = textContent(result);
|
|
386
|
+
assert.equal(result.isError, undefined);
|
|
387
|
+
assert.match(text, /Workflow checklist: 0\/1 done · 1 active/);
|
|
388
|
+
assert.match(text, /CI 1 active/);
|
|
389
|
+
} finally {
|
|
390
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("keeps preflight as a plan hint outside the runtime checklist", () => {
|
|
395
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-workflow-preflight-"));
|
|
396
|
+
try {
|
|
397
|
+
const asyncRoot = path.join(root, "runs");
|
|
398
|
+
const asyncDir = path.join(asyncRoot, "run-workflow-preflight");
|
|
399
|
+
fs.mkdirSync(asyncDir, { recursive: true });
|
|
400
|
+
fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
|
|
401
|
+
runId: "run-workflow-preflight",
|
|
402
|
+
mode: "workflow",
|
|
403
|
+
state: "running",
|
|
404
|
+
startedAt: 100,
|
|
405
|
+
lastUpdate: 200,
|
|
406
|
+
preflight: { version: 1, lanes: [{ key: "pr14", mode: "review" }] },
|
|
407
|
+
steps: [{ agent: "reviewer", workflowKey: "pr14-quality", status: "running" }],
|
|
408
|
+
}, null, 2), "utf-8");
|
|
409
|
+
|
|
410
|
+
const result = inspectSubagentStatus({ id: "run-workflow-preflight" }, {
|
|
411
|
+
asyncDirRoot: asyncRoot,
|
|
412
|
+
resultsDir: path.join(root, "results"),
|
|
413
|
+
kill: () => true,
|
|
414
|
+
now: () => 250,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const text = textContent(result);
|
|
418
|
+
assert.equal(result.isError, undefined);
|
|
419
|
+
assert.match(text, /Plan: 1 lane · pr14/);
|
|
420
|
+
assert.match(text, /Workflow checklist: 0\/1 done · 1 active/);
|
|
421
|
+
assert.doesNotMatch(text, /1 queued/);
|
|
422
|
+
} finally {
|
|
423
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
363
427
|
it("refuses to tail status outputFile paths outside the async directory", () => {
|
|
364
428
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-run-status-transcript-escape-"));
|
|
365
429
|
try {
|
|
@@ -1446,6 +1510,8 @@ describe("async run status inspection", () => {
|
|
|
1446
1510
|
results: [{
|
|
1447
1511
|
agent: "worker",
|
|
1448
1512
|
success: false,
|
|
1513
|
+
structuredOutput: { payload: { ok: true } },
|
|
1514
|
+
structuredOutputPath: "/runs/structured-output/output.json",
|
|
1449
1515
|
timeoutRecovery: {
|
|
1450
1516
|
termination: "timed-out",
|
|
1451
1517
|
changedFiles: ["input.md"],
|
|
@@ -1470,6 +1536,8 @@ describe("async run status inspection", () => {
|
|
|
1470
1536
|
assert.match(text, /Recovery needed: review the diff and artifacts before resuming or launching dependent stages\./);
|
|
1471
1537
|
assert.match(text, /requested report: missing/);
|
|
1472
1538
|
assert.match(text, /changed tracked files: input\.md/);
|
|
1539
|
+
assert.match(text, /Structured output: \{"payload":\{"ok":true\}\}/);
|
|
1540
|
+
assert.match(text, /Structured output path: \/runs\/structured-output\/output\.json/);
|
|
1473
1541
|
} finally {
|
|
1474
1542
|
fs.rmSync(root, { recursive: true, force: true });
|
|
1475
1543
|
}
|
|
@@ -123,6 +123,23 @@ describe("runtime agent registration", () => {
|
|
|
123
123
|
registration.dispose();
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it("preserves excludeTools on runtime agents", () => {
|
|
127
|
+
const registration = registerAgent({
|
|
128
|
+
pi,
|
|
129
|
+
name: "runtime-exclude-helper",
|
|
130
|
+
definition: {
|
|
131
|
+
description: "Runtime exclude helper",
|
|
132
|
+
systemPrompt: "Help at runtime.",
|
|
133
|
+
tools: ["read", "write"],
|
|
134
|
+
excludeTools: ["write", "unknown_tool"],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const agent = mergeRuntimeAgents(pi, discoverAgents(tempProject, "both")).agents.find((candidate) => candidate.name === "runtime-exclude-helper");
|
|
139
|
+
assert.deepEqual(agent?.excludeTools, ["write", "unknown_tool"]);
|
|
140
|
+
registration.dispose();
|
|
141
|
+
});
|
|
142
|
+
|
|
126
143
|
it("registers through the owner runtime when consumer and owner API objects differ", () => {
|
|
127
144
|
const events = makeEventBus();
|
|
128
145
|
const ownerPi = makePiWithEvents(events);
|
|
@@ -97,6 +97,10 @@ function text(result: Awaited<ReturnType<ScheduledRunManager["handleToolCall"]>>
|
|
|
97
97
|
return result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function detailRecords(result: Awaited<ReturnType<ScheduledRunManager["handleToolCall"]>>): Array<Record<string, unknown>> {
|
|
101
|
+
return result.details?.schedules?.records as Array<Record<string, unknown>> ?? [];
|
|
102
|
+
}
|
|
103
|
+
|
|
100
104
|
async function flush(): Promise<void> {
|
|
101
105
|
for (let i = 0; i < 8; i++) await Promise.resolve();
|
|
102
106
|
}
|
|
@@ -158,6 +162,85 @@ describe("project schedule management", () => {
|
|
|
158
162
|
assert.match(text(shown), /Night review/);
|
|
159
163
|
});
|
|
160
164
|
|
|
165
|
+
it("restores session-only schedules only in the creating session", async () => {
|
|
166
|
+
const owner = harness({ sessionId: "owner-session" });
|
|
167
|
+
const created = await owner.manager.handleToolCall({
|
|
168
|
+
action: "schedule.create",
|
|
169
|
+
id: "owner-only",
|
|
170
|
+
name: "Owner only",
|
|
171
|
+
every: "1h",
|
|
172
|
+
cwd: owner.ctx.cwd,
|
|
173
|
+
sessionOnly: true,
|
|
174
|
+
workflowScript: "return 1",
|
|
175
|
+
}, owner.ctx);
|
|
176
|
+
assert.equal(created.isError, undefined);
|
|
177
|
+
assert.equal(detailRecords(created)[0]?.sessionOnly, true);
|
|
178
|
+
assert.equal("ownerSessionFile" in detailRecords(created)[0]!, false);
|
|
179
|
+
|
|
180
|
+
const root = scheduledRunStorePath(owner.ctx.cwd, undefined, path.join(owner.root, "stores"));
|
|
181
|
+
const [record] = listScheduledRunSummaries(owner.ctx.cwd, path.join(owner.root, "stores"));
|
|
182
|
+
assert.equal(record?.sessionOnly, true);
|
|
183
|
+
assert.equal(record?.ownerSessionFile, owner.ctx.sessionManager.getSessionFile());
|
|
184
|
+
|
|
185
|
+
owner.manager.stop();
|
|
186
|
+
const otherTimers = new FakeTimers();
|
|
187
|
+
const otherLaunches: Launch[] = [];
|
|
188
|
+
const other = createScheduledRunManager({
|
|
189
|
+
config: { scheduledRuns: { enabled: true } },
|
|
190
|
+
storeRoot: path.join(owner.root, "stores"),
|
|
191
|
+
now: () => owner.clock.now,
|
|
192
|
+
timers: otherTimers,
|
|
193
|
+
launch: (params, launchCtx) => new Promise((resolve) => otherLaunches.push({ params: params as Record<string, unknown>, ctx: launchCtx, resolve: resolve as Launch["resolve"] })) as never,
|
|
194
|
+
});
|
|
195
|
+
const otherContext = context(owner.ctx.cwd, "other-session");
|
|
196
|
+
other.bindSession(otherContext);
|
|
197
|
+
assert.equal(otherTimers.values.size, 0, "non-owner sessions must not arm session-only schedules");
|
|
198
|
+
|
|
199
|
+
const manual = await other.handleToolCall({ action: "schedule.run", id: "owner-only" }, otherContext);
|
|
200
|
+
assert.match(text(manual), /not its owner/);
|
|
201
|
+
assert.equal("ownerSessionFile" in detailRecords(manual)[0]!, false);
|
|
202
|
+
const shownToOther = await other.handleToolCall({ action: "schedule.show", id: "owner-only" }, otherContext);
|
|
203
|
+
assert.equal("ownerSessionFile" in detailRecords(shownToOther)[0]!, false);
|
|
204
|
+
const listedToOther = await other.handleToolCall({ action: "schedule.list" }, otherContext);
|
|
205
|
+
assert.equal("ownerSessionFile" in detailRecords(listedToOther)[0]!, false);
|
|
206
|
+
assert.equal(otherLaunches.length, 0);
|
|
207
|
+
assert.equal(listScheduledRunSummaries(owner.ctx.cwd, path.join(owner.root, "stores"))[0]?.ownerSessionFile, owner.ctx.sessionManager.getSessionFile());
|
|
208
|
+
assert.equal(fs.existsSync(path.join(root, "owner-only", "active.lock")), false);
|
|
209
|
+
|
|
210
|
+
const ownerTimers = new FakeTimers();
|
|
211
|
+
const ownerAgain = createScheduledRunManager({
|
|
212
|
+
config: { scheduledRuns: { enabled: true } },
|
|
213
|
+
storeRoot: path.join(owner.root, "stores"),
|
|
214
|
+
now: () => owner.clock.now,
|
|
215
|
+
timers: ownerTimers,
|
|
216
|
+
launch: async () => ({ content: [{ type: "text", text: "unused" }], details: { mode: "management", results: [] } }),
|
|
217
|
+
});
|
|
218
|
+
ownerAgain.bindSession(context(owner.ctx.cwd, "owner-session"));
|
|
219
|
+
assert.equal(ownerTimers.values.size, 1, "the creating session must restore its schedule");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("re-arms an owner timer after a non-owner context consumes it", async () => {
|
|
223
|
+
const h = harness({ sessionId: "owner-session" });
|
|
224
|
+
await h.manager.handleToolCall({ action: "schedule.create", id: "owner-only", cwd: h.ctx.cwd, every: "1h", sessionOnly: true, workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
|
|
225
|
+
const nextRunAt = listScheduledRunSummaries(h.ctx.cwd, path.join(h.root, "stores"))[0]?.trigger.nextRunAt;
|
|
226
|
+
h.manager.bindSession(context(h.ctx.cwd, "other-session"));
|
|
227
|
+
h.clock.now += 3_600_000;
|
|
228
|
+
h.timers.fireAll();
|
|
229
|
+
await flush();
|
|
230
|
+
assert.equal(h.launches.length, 0, "a non-owner context must not launch the schedule");
|
|
231
|
+
assert.equal(h.timers.values.size, 0, "the consumed non-owner timer is not left spinning");
|
|
232
|
+
assert.equal(listScheduledRunSummaries(h.ctx.cwd, path.join(h.root, "stores"))[0]?.trigger.nextRunAt, nextRunAt, "a non-owner context must not advance the schedule");
|
|
233
|
+
|
|
234
|
+
h.manager.bindSession(h.ctx);
|
|
235
|
+
assert.equal(h.timers.values.size, 1, "rebinding the owner must restore the timer");
|
|
236
|
+
h.timers.fireAll();
|
|
237
|
+
await flush();
|
|
238
|
+
assert.equal(h.launches.length, 1);
|
|
239
|
+
assert.equal(h.launches[0]!.ctx.sessionManager.getSessionId(), "owner-session");
|
|
240
|
+
h.launches[0]!.resolve({ content: [{ type: "text", text: "Async" }], details: { mode: "single", results: [], asyncId: "owner-async" } });
|
|
241
|
+
await flush();
|
|
242
|
+
});
|
|
243
|
+
|
|
161
244
|
it("does not let completed one-shot schedules consume maxPending capacity", async () => {
|
|
162
245
|
const h = harness({ config: { scheduledRuns: { enabled: true, maxPending: 1 } } });
|
|
163
246
|
await h.manager.handleToolCall({ action: "schedule.create", id: "completed", at: "+1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
|
|
@@ -668,6 +751,11 @@ describe("recurring schedule execution", () => {
|
|
|
668
751
|
const targetCtx = context(target, "target-session");
|
|
669
752
|
h.manager.bindSession(targetCtx);
|
|
670
753
|
h.manager.bindSession(h.ctx);
|
|
754
|
+
const sessionOnly = await h.manager.handleToolCall({ action: "schedule.create", id: "session-only-targeted", cwd: target, every: "1h", sessionOnly: true, workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
|
|
755
|
+
assert.equal(sessionOnly.isError, true);
|
|
756
|
+
assert.match(text(sessionOnly), /sessionOnly schedules cannot use an explicit cross-project cwd/);
|
|
757
|
+
assert.deepEqual(listScheduledRunSummaries(target, path.join(h.root, "stores")), [], "rejected session-only schedules must not dead-end in the target project");
|
|
758
|
+
|
|
671
759
|
await h.manager.handleToolCall({ action: "schedule.create", id: "targeted", cwd: target, every: "1h", workflowScript: "return runs.run('main', { agent: 'worker' })" }, h.ctx);
|
|
672
760
|
h.clock.now += 3_600_000;
|
|
673
761
|
h.timers.fireAll();
|
|
@@ -596,7 +596,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
596
596
|
const reviewedRecoveryBranch = acceptanceStringBranches.find((branch) => Array.isArray(branch.enum) && branch.enum.includes("reviewed"));
|
|
597
597
|
assert.deepEqual(reviewedRecoveryBranch?.enum, ["reviewed"]);
|
|
598
598
|
assert.equal(reviewedRecoveryBranch?.deprecated, true);
|
|
599
|
+
assert.equal(acceptanceStringBranches.some((branch) => branch.enum === undefined), true, "acceptance should tolerate JSON-encoded object strings");
|
|
599
600
|
assert.match(String(acceptanceSchema.description ?? ""), /reviewer\/read-only calls, omit acceptance/i);
|
|
601
|
+
assert.match(String(acceptanceSchema.description ?? ""), /prefer an inline JSON object/i);
|
|
602
|
+
assert.match(String(acceptanceSchema.description ?? ""), /JSON-encoded object strings are tolerated only during input normalization/i);
|
|
600
603
|
assert.match(String(acceptanceSchema.description ?? ""), /acceptance\.review\.required/);
|
|
601
604
|
const acceptanceObjectBranch = anyOfBranches(acceptanceSchema).find((branch) => branch.type === "object");
|
|
602
605
|
assert.ok(acceptanceObjectBranch, "acceptance should support object config");
|
|
@@ -621,11 +624,10 @@ describe("SubagentParams schema", { skip: !schemasAvailable ? "typebox not avail
|
|
|
621
624
|
{ action: "not-a-real-action" },
|
|
622
625
|
{ config: { name: "reviewer", description: "Review things" } },
|
|
623
626
|
{ config: JSON.stringify({ name: "reviewer", description: "Review things" }) },
|
|
627
|
+
{ agent: "worker", task: "Fix", acceptance: JSON.stringify({ level: "checked", evidence: ["commands-run"] }) },
|
|
624
628
|
];
|
|
625
629
|
const invalidValues = [
|
|
626
630
|
{ skill: 123 },
|
|
627
|
-
{ agent: "worker", task: "Fix", acceptance: "none" },
|
|
628
|
-
{ agent: "worker", task: "Fix", acceptance: "verified" },
|
|
629
631
|
{ skill: [123] },
|
|
630
632
|
{ output: 123 },
|
|
631
633
|
{ timeoutMs: 0 },
|
|
@@ -1429,28 +1429,28 @@ describe("scripted workflow runtime", () => {
|
|
|
1429
1429
|
);
|
|
1430
1430
|
});
|
|
1431
1431
|
|
|
1432
|
-
it("passes per-child
|
|
1433
|
-
const launches: Array<{ key: string; worktree: unknown }> = [];
|
|
1432
|
+
it("passes per-child workflow controls through runs.run and runs.all", async () => {
|
|
1433
|
+
const launches: Array<{ key: string; worktree: unknown; control: unknown }> = [];
|
|
1434
1434
|
await runWorkflowScript({
|
|
1435
1435
|
script: `
|
|
1436
|
-
const one = await runs.run("one", { agent: "worker", task: "one", worktree: true });
|
|
1436
|
+
const one = await runs.run("one", { agent: "worker", task: "one", worktree: true, control: { needsAttentionAfterMs: 111 } });
|
|
1437
1437
|
const rest = await runs.all([
|
|
1438
|
-
{ key: "two", agent: "worker", task: "two", worktree: true },
|
|
1439
|
-
{ key: "three", agent: "reviewer", task: "three", worktree: false }
|
|
1438
|
+
{ key: "two", agent: "worker", task: "two", worktree: true, control: { activeNoticeAfterMs: 222 } },
|
|
1439
|
+
{ key: "three", agent: "reviewer", task: "three", worktree: false, control: { enabled: false } }
|
|
1440
1440
|
]);
|
|
1441
1441
|
return [one.key, ...rest.map((entry) => entry.key)];
|
|
1442
1442
|
`,
|
|
1443
1443
|
timeoutMs: 2_000,
|
|
1444
1444
|
async launch(key, params) {
|
|
1445
|
-
launches.push({ key, worktree: params.worktree });
|
|
1445
|
+
launches.push({ key, worktree: params.worktree, control: params.control });
|
|
1446
1446
|
return { key, ok: true, output: key, artifactPaths: [], results: [] };
|
|
1447
1447
|
},
|
|
1448
1448
|
async status(key) { return { key, ok: true, output: "ok", artifactPaths: [] }; },
|
|
1449
1449
|
});
|
|
1450
1450
|
assert.deepEqual(launches, [
|
|
1451
|
-
{ key: "one", worktree: true },
|
|
1452
|
-
{ key: "two", worktree: true },
|
|
1453
|
-
{ key: "three", worktree: false },
|
|
1451
|
+
{ key: "one", worktree: true, control: { needsAttentionAfterMs: 111 } },
|
|
1452
|
+
{ key: "two", worktree: true, control: { activeNoticeAfterMs: 222 } },
|
|
1453
|
+
{ key: "three", worktree: false, control: { enabled: false } },
|
|
1454
1454
|
]);
|
|
1455
1455
|
});
|
|
1456
1456
|
|
|
@@ -139,7 +139,7 @@ describe("acknowledged steering action", () => {
|
|
|
139
139
|
assert.equal(result.isError, undefined);
|
|
140
140
|
assert.equal(result.details.steering?.state, "delivered");
|
|
141
141
|
assert.match(result.content[0]!.text, /Steering delivered/);
|
|
142
|
-
assert.match(result.content[0]!.text, /Message
|
|
142
|
+
assert.match(result.content[0]!.text, /Message sent:\n```text\ncorrect course\n```/);
|
|
143
143
|
} finally {
|
|
144
144
|
removeAsyncDir(asyncDir);
|
|
145
145
|
}
|
|
@@ -330,7 +330,7 @@ describe("acknowledged steering action", () => {
|
|
|
330
330
|
const result = await action;
|
|
331
331
|
assert.equal(result.isError, undefined);
|
|
332
332
|
assert.equal(result.details.steering?.state, "recovered");
|
|
333
|
-
assert.match(result.content[0]!.text, /Message
|
|
333
|
+
assert.match(result.content[0]!.text, /Message sent:\n```text\ncorrect course\n```/);
|
|
334
334
|
assert.equal(result.details.steering?.replacementRunId, "replacement");
|
|
335
335
|
assert.ok(result.details.steering?.targets[0]?.lateDeliveredAt);
|
|
336
336
|
const limits = receivedLimits as { timeoutMs: number; absoluteDeadlineAt: number; toolBudget: unknown };
|
|
@@ -3,17 +3,23 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
|
-
import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, steeringMessagePreview, terminalSteeringNoticeState, updateSteeringTarget } from "../../src/runs/background/steering.ts";
|
|
6
|
+
import { actionResultFromSteeringStatus, claimSteeringRecovery, createSteeringStatus, recordSteeringRequest, remainingSteeringRecoveryLimits, steeringMessagePreview, steeringReceipt, terminalSteeringNoticeState, updateSteeringTarget } from "../../src/runs/background/steering.ts";
|
|
7
7
|
import { applySteeringRecoveryAgentConfig } from "../../src/runs/background/async-resume.ts";
|
|
8
8
|
import type { AgentConfig } from "../../src/agents/agents.ts";
|
|
9
9
|
|
|
10
10
|
describe("steering lifecycle ledger", () => {
|
|
11
|
-
it("redacts and
|
|
11
|
+
it("redacts, bounds, and separates steering message previews", () => {
|
|
12
12
|
const secret = "ghp_1234567890abcdef";
|
|
13
|
-
const
|
|
13
|
+
const message = `Use ${secret}\n${"x".repeat(200)}`;
|
|
14
|
+
const preview = steeringMessagePreview(message);
|
|
15
|
+
const receipt = steeringReceipt(message, "Steering delivered.");
|
|
14
16
|
assert.ok(preview.length <= 160);
|
|
15
17
|
assert.match(preview, /\[redacted\]/);
|
|
16
18
|
assert.doesNotMatch(preview, new RegExp(secret));
|
|
19
|
+
assert.match(receipt, /\[redacted\]/);
|
|
20
|
+
assert.doesNotMatch(receipt, new RegExp(secret));
|
|
21
|
+
assert.match(receipt, /^Steering delivered\.\n\nMessage sent:\n```text\n[\s\S]+\n```$/);
|
|
22
|
+
assert.match(steeringReceipt("review this:\n```\ntext\n```", "Steering queued."), /^Steering queued\.\n\nMessage sent:\n````text\n[\s\S]+\n````$/);
|
|
17
23
|
});
|
|
18
24
|
|
|
19
25
|
it("retains 20 recent requests while aggregate totals remain monotonic", () => {
|