@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistText, projectWorkflowChecklist } from "../../src/workflows/workflow-checklist.ts";
|
|
5
|
+
import type { HostStepNodeV1, WorkflowGraphSnapshot } from "../../src/shared/types.ts";
|
|
6
|
+
|
|
7
|
+
function graph(): WorkflowGraphSnapshot {
|
|
8
|
+
return {
|
|
9
|
+
runId: "workflow-checklist",
|
|
10
|
+
mode: "workflow",
|
|
11
|
+
phases: [
|
|
12
|
+
{ title: "inventory", nodeIds: ["inventory"] },
|
|
13
|
+
{ title: "writers", nodeIds: ["writer-a", "writer-b"] },
|
|
14
|
+
{ title: "reviews", nodeIds: ["review"] },
|
|
15
|
+
{ title: "gate", nodeIds: ["gate"] },
|
|
16
|
+
],
|
|
17
|
+
nodes: [
|
|
18
|
+
{ id: "inventory", kind: "step", label: "inventory", agent: "scout", status: "completed", flatIndex: 0 },
|
|
19
|
+
{ id: "writer-a", kind: "agent", label: "writer-a", agent: "writer", status: "completed", flatIndex: 1 },
|
|
20
|
+
{ id: "writer-b", kind: "agent", label: "writer-b", agent: "writer", status: "running", flatIndex: 2 },
|
|
21
|
+
{ id: "review", kind: "step", label: "review", agent: "reviewer", status: "pending", flatIndex: 3 },
|
|
22
|
+
{ id: "gate", kind: "step", label: "gate", agent: "reviewer", status: "pending", acceptanceStatus: "rejected", flatIndex: 4 },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test("workflow checklist fuses graph phases with loaded child state without duplicate rows", () => {
|
|
28
|
+
const projection = projectWorkflowChecklist({
|
|
29
|
+
graph: graph(),
|
|
30
|
+
steps: [
|
|
31
|
+
{ workflowKey: "inventory", agent: "scout", status: "complete" },
|
|
32
|
+
{ workflowKey: "writer-a", agent: "writer", status: "complete" },
|
|
33
|
+
{ workflowKey: "writer-b", agent: "writer", status: "running", currentTool: "grep", toolCount: 18, startedAt: 1000 },
|
|
34
|
+
{ workflowKey: "review", agent: "reviewer", status: "pending", context: "fork" },
|
|
35
|
+
],
|
|
36
|
+
now: 5000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(projection.phases.map((phase) => [phase.label, phase.state, phase.total, phase.done]), [
|
|
40
|
+
["inventory", "complete", 1, 1],
|
|
41
|
+
["writers", "running", 2, 1],
|
|
42
|
+
["reviews", "queued", 1, 0],
|
|
43
|
+
["gate", "blocked", 1, 0],
|
|
44
|
+
]);
|
|
45
|
+
assert.deepEqual([projection.total, projection.done, projection.running, projection.queued, projection.blocked], [5, 2, 1, 1, 1]);
|
|
46
|
+
assert.equal(projection.phases.flatMap((phase) => phase.items).length, 5);
|
|
47
|
+
assert.equal(projection.phases[1]!.items[1]!.durationMs, 4000);
|
|
48
|
+
assert.equal(projection.phases[2]!.items[0]!.context, "fork");
|
|
49
|
+
assert.equal(projection.bottleneck?.key, "gate");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("workflow checklist preserves explicit host monitor verdicts and trace lane identity", () => {
|
|
53
|
+
const host: HostStepNodeV1 = {
|
|
54
|
+
version: 1,
|
|
55
|
+
kind: "host-step",
|
|
56
|
+
monitorKind: "ci",
|
|
57
|
+
id: "ci",
|
|
58
|
+
label: "CI",
|
|
59
|
+
provider: "buildkite",
|
|
60
|
+
state: "done",
|
|
61
|
+
verdict: "inconclusive",
|
|
62
|
+
target: "main",
|
|
63
|
+
freshness: { expectedRef: "main", stale: true },
|
|
64
|
+
updatedAt: 1000,
|
|
65
|
+
};
|
|
66
|
+
const projection = projectWorkflowChecklist({
|
|
67
|
+
hostSteps: [host],
|
|
68
|
+
preflight: { version: 1, coverage: "partial", lanes: [{ key: "reviewers", mode: "review" }] },
|
|
69
|
+
trace: [
|
|
70
|
+
{ operation: "run", key: "reviewers.a", generatedLaneKey: "reviewers", state: "started", agent: "reviewer" },
|
|
71
|
+
{ operation: "run", key: "reviewers.a", generatedLaneKey: "reviewers", state: "completed", agent: "reviewer" },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(projection.phases.map((phase) => phase.label), ["CI", "reviewers"]);
|
|
76
|
+
assert.equal(projection.phases[0]!.items[0]!.state, "blocked");
|
|
77
|
+
assert.equal(projection.phases[0]!.items[0]!.monitorKind, "ci");
|
|
78
|
+
assert.equal(projection.phases[0]!.items[0]!.verdict, "inconclusive");
|
|
79
|
+
assert.equal(projection.phases[1]!.items[0]!.state, "complete");
|
|
80
|
+
assert.equal(projection.phases[1]!.items[0]!.preflight?.mode, "review");
|
|
81
|
+
assert.match(formatWorkflowChecklistBottleneck(projection.bottleneck) ?? "", /CI/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("workflow checklist counts one host monitor when host status and trace share an id", () => {
|
|
85
|
+
const host: HostStepNodeV1 = {
|
|
86
|
+
version: 1,
|
|
87
|
+
kind: "host-step",
|
|
88
|
+
monitorKind: "ci",
|
|
89
|
+
id: "ci",
|
|
90
|
+
label: "CI",
|
|
91
|
+
state: "done",
|
|
92
|
+
verdict: "pass",
|
|
93
|
+
updatedAt: 1000,
|
|
94
|
+
};
|
|
95
|
+
const projection = projectWorkflowChecklist({
|
|
96
|
+
hostSteps: [host],
|
|
97
|
+
trace: [{ operation: "host", key: "ci", state: "completed", label: "CI" }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(projection.total, 1);
|
|
101
|
+
assert.deepEqual(projection.phases.flatMap((phase) => phase.items).map((item) => [item.key, item.kind, item.state]), [["ci", "host", "complete"]]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("workflow checklist keeps terminal child state newer than stale running trace", () => {
|
|
105
|
+
const projection = projectWorkflowChecklist({
|
|
106
|
+
graph: graph(),
|
|
107
|
+
steps: [{ workflowKey: "writer-b", agent: "writer", status: "complete" }],
|
|
108
|
+
trace: [{ operation: "run", key: "writer-b", state: "started", agent: "writer" }],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.equal(projection.running, 0);
|
|
112
|
+
assert.equal(projection.phases[1]!.items[1]!.state, "complete");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("workflow checklist keeps terminal graph state newer than stale running child state", () => {
|
|
116
|
+
const snapshot = graph();
|
|
117
|
+
snapshot.nodes[2] = { ...snapshot.nodes[2]!, status: "failed", error: "writer failed" };
|
|
118
|
+
const projection = projectWorkflowChecklist({
|
|
119
|
+
graph: snapshot,
|
|
120
|
+
steps: [{ workflowKey: "writer-b", agent: "writer", status: "running" }],
|
|
121
|
+
trace: [{ operation: "run", key: "writer-b", state: "started", agent: "writer" }],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
assert.equal(projection.running, 0);
|
|
125
|
+
assert.equal(projection.failed, 1);
|
|
126
|
+
assert.equal(projection.phases[1]!.state, "failed");
|
|
127
|
+
assert.equal(projection.phases[1]!.items[1]!.state, "failed");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("workflow checklist keeps terminal graph state newer than stale trace without loaded child state", () => {
|
|
131
|
+
const snapshot = graph();
|
|
132
|
+
snapshot.nodes[2] = { ...snapshot.nodes[2]!, status: "failed", error: "writer failed" };
|
|
133
|
+
const projection = projectWorkflowChecklist({
|
|
134
|
+
graph: snapshot,
|
|
135
|
+
trace: [{ operation: "run", key: "writer-b", state: "started", agent: "writer" }],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(projection.running, 0);
|
|
139
|
+
assert.equal(projection.failed, 1);
|
|
140
|
+
assert.equal(projection.phases[1]!.items[1]!.state, "failed");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("workflow checklist keeps graph acceptance blockers ahead of stale running child state", () => {
|
|
144
|
+
const snapshot = graph();
|
|
145
|
+
snapshot.nodes[2] = { ...snapshot.nodes[2]!, acceptanceStatus: "rejected" };
|
|
146
|
+
const projection = projectWorkflowChecklist({
|
|
147
|
+
graph: snapshot,
|
|
148
|
+
steps: [{ workflowKey: "writer-b", agent: "writer", status: "running" }],
|
|
149
|
+
trace: [{ operation: "run", key: "writer-b", state: "started", agent: "writer" }],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.equal(projection.running, 0);
|
|
153
|
+
assert.equal(projection.blocked, 2);
|
|
154
|
+
assert.equal(projection.phases[1]!.items[1]!.state, "blocked");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("workflow checklist uses preflight only to annotate authoritative work", () => {
|
|
158
|
+
const projection = projectWorkflowChecklist({
|
|
159
|
+
graph: graph(),
|
|
160
|
+
preflight: { version: 1, coverage: "partial", lanes: [{ key: "writer-b", mode: "mutation" }, { key: "writers", mode: "mutation" }, { key: "deploy", mode: "gate" }] },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(projection.total, 5);
|
|
164
|
+
assert.deepEqual(projection.phases.map((phase) => phase.label), ["inventory", "writers", "reviews", "gate"]);
|
|
165
|
+
assert.equal(projection.queued, 1);
|
|
166
|
+
assert.equal(projection.phases[2]?.items[0]?.key, "review");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("workflow checklist does not turn unmatched preflight metadata into queued work", () => {
|
|
170
|
+
const preflight = { version: 1 as const, coverage: "partial" as const, lanes: [{ key: "pr14", mode: "review" as const }] };
|
|
171
|
+
const declarationOnly = projectWorkflowChecklist({ preflight });
|
|
172
|
+
const mismatchedRuntime = projectWorkflowChecklist({
|
|
173
|
+
preflight,
|
|
174
|
+
trace: [{ operation: "run", key: "pr14-quality", state: "started", agent: "reviewer" }],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.deepEqual(declarationOnly, { phases: [], total: 0, done: 0, running: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0 });
|
|
178
|
+
assert.equal(mismatchedRuntime.total, 1);
|
|
179
|
+
assert.equal(mismatchedRuntime.running, 1);
|
|
180
|
+
assert.equal(mismatchedRuntime.queued, 0);
|
|
181
|
+
assert.deepEqual(mismatchedRuntime.phases.flatMap((phase) => phase.items).map((item) => item.key), ["pr14-quality"]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("workflow checklist prefers specific dotted preflight lanes over generated aliases", () => {
|
|
185
|
+
const projection = projectWorkflowChecklist({
|
|
186
|
+
preflight: {
|
|
187
|
+
version: 1,
|
|
188
|
+
coverage: "partial",
|
|
189
|
+
lanes: [
|
|
190
|
+
{ key: "writer", mode: "mutation" },
|
|
191
|
+
{ key: "writer.quality", mode: "review" },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
trace: [{ operation: "run", key: "writer.quality.deep", generatedLaneKey: "writer", state: "started", agent: "reviewer" }],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.equal(projection.total, 1);
|
|
198
|
+
assert.deepEqual(projection.phases.map((phase) => phase.label), ["writer.quality"]);
|
|
199
|
+
assert.equal(projection.phases[0]?.items[0]?.preflight?.mode, "review");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("workflow checklist text exposes aggregate, phase, and bottleneck signals with bounded errors", () => {
|
|
203
|
+
const projection = projectWorkflowChecklist({
|
|
204
|
+
trace: [{ operation: "run", key: "review", state: "failed", label: "reviewer", agent: "reviewer", error: "Output: secret details" }],
|
|
205
|
+
});
|
|
206
|
+
const text = formatWorkflowChecklistText(projection).join("\n");
|
|
207
|
+
assert.match(text, /Workflow checklist: 0\/1 done · 1 failed/);
|
|
208
|
+
assert.match(text, /reviewer/);
|
|
209
|
+
assert.match(text, /bottleneck/);
|
|
210
|
+
assert.doesNotMatch(text, /Output:/);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("workflow checklist text can suppress item rows for status surfaces", () => {
|
|
214
|
+
const projection = projectWorkflowChecklist({
|
|
215
|
+
graph: graph(),
|
|
216
|
+
steps: [{ workflowKey: "writer-b", agent: "writer", status: "running" }],
|
|
217
|
+
});
|
|
218
|
+
const text = formatWorkflowChecklistText(projection, "", { includeItems: false }).join("\n");
|
|
219
|
+
|
|
220
|
+
assert.match(text, /Workflow checklist:/);
|
|
221
|
+
assert.match(text, /writers 1 done · 1 active/);
|
|
222
|
+
assert.doesNotMatch(text, /writer-b · writer · active/);
|
|
223
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import { prepareWorkflowLaunchParams, promptAuditRedoParams, sanitizeRunPathSegment } from "../../src/runs/foreground/subagent-executor.ts";
|
|
3
|
+
import { prepareWorkflowLaunchParams, promptAuditRedoParams, resolveRevivalControlConfig, sanitizeRunPathSegment } from "../../src/runs/foreground/subagent-executor.ts";
|
|
4
|
+
import { resolveControlConfig } from "../../src/runs/shared/subagent-control.ts";
|
|
4
5
|
|
|
5
6
|
describe("workflow launch params", () => {
|
|
6
7
|
it("keeps omitted workflow child async foreground", () => {
|
|
@@ -32,6 +33,20 @@ describe("workflow launch params", () => {
|
|
|
32
33
|
assert.equal(params.maxSubagentSpawnsPerRun, undefined);
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
it("merges partial control overrides into workflow child defaults", () => {
|
|
37
|
+
const params = prepareWorkflowLaunchParams(
|
|
38
|
+
{ control: { needsAttentionAfterMs: 111, activeNoticeAfterMs: 222 } },
|
|
39
|
+
{ agent: "worker", task: "Run", control: { activeNoticeAfterMs: 333 } },
|
|
40
|
+
"workflow-run",
|
|
41
|
+
"run",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(params.control, {
|
|
45
|
+
needsAttentionAfterMs: 111,
|
|
46
|
+
activeNoticeAfterMs: 333,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
35
50
|
it("marks only new async workflow children to preserve live supervisor-detach awaits", () => {
|
|
36
51
|
// Retained workflow children already use the async result-file await path.
|
|
37
52
|
assert.equal(prepareWorkflowLaunchParams(
|
|
@@ -238,6 +253,40 @@ describe("workflow launch params", () => {
|
|
|
238
253
|
);
|
|
239
254
|
});
|
|
240
255
|
|
|
256
|
+
it("forwards control defaults and overrides to retained workflow children", () => {
|
|
257
|
+
assert.deepEqual(
|
|
258
|
+
prepareWorkflowLaunchParams(
|
|
259
|
+
{ control: { needsAttentionAfterMs: 111, activeNoticeAfterMs: 222 } },
|
|
260
|
+
{ resume: "retained-run", task: "Continue", control: { activeNoticeAfterMs: 333 } },
|
|
261
|
+
"workflow-run",
|
|
262
|
+
"continue",
|
|
263
|
+
),
|
|
264
|
+
{
|
|
265
|
+
action: "resume",
|
|
266
|
+
id: "retained-run",
|
|
267
|
+
message: "Continue",
|
|
268
|
+
workflowParentRunId: "workflow-run",
|
|
269
|
+
workflowKey: "continue",
|
|
270
|
+
control: {
|
|
271
|
+
needsAttentionAfterMs: 111,
|
|
272
|
+
activeNoticeAfterMs: 333,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("lets retained workflow child control overrides amend recovered control configs", () => {
|
|
279
|
+
const recovered = resolveControlConfig(undefined, { needsAttentionAfterMs: 111, activeNoticeAfterMs: 222 });
|
|
280
|
+
const control = resolveRevivalControlConfig({
|
|
281
|
+
recoveryControlConfig: recovered,
|
|
282
|
+
requestedControl: { activeNoticeAfterMs: 333, notifyChannels: [] },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
assert.equal(control.needsAttentionAfterMs, 111);
|
|
286
|
+
assert.equal(control.activeNoticeAfterMs, 333);
|
|
287
|
+
assert.deepEqual(control.notifyChannels, []);
|
|
288
|
+
});
|
|
289
|
+
|
|
241
290
|
it("does not inherit parent deadlines for retained workflow children", () => {
|
|
242
291
|
assert.deepEqual(
|
|
243
292
|
prepareWorkflowLaunchParams(
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatWorkflowPreflightWarningSummary,
|
|
14
14
|
normalizeWorkflowPreflight,
|
|
15
15
|
validateWorkflowPreflight,
|
|
16
|
+
workflowPreflightLaneForRuntimeKey,
|
|
16
17
|
workflowPreflightWarnings,
|
|
17
18
|
} from "../../src/workflows/workflow-preflight.ts";
|
|
18
19
|
|
|
@@ -121,6 +122,21 @@ describe("workflow preflight metadata", () => {
|
|
|
121
122
|
assert.ok(WORKFLOW_PREFLIGHT_MAX_CLAIMS > 0);
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
it("selects exact lanes before generated or dotted-root matches", () => {
|
|
126
|
+
const overlapping = {
|
|
127
|
+
version: 1 as const,
|
|
128
|
+
coverage: "partial" as const,
|
|
129
|
+
lanes: [
|
|
130
|
+
{ key: "writer", mode: "mutation" as const },
|
|
131
|
+
{ key: "writer.quality", mode: "review" as const },
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
assert.equal(workflowPreflightLaneForRuntimeKey(overlapping, "writer.quality")?.mode, "review");
|
|
136
|
+
assert.equal(workflowPreflightLaneForRuntimeKey(overlapping, "writer.quality.deep", ["writer"])?.mode, "review");
|
|
137
|
+
assert.equal(workflowPreflightLaneForRuntimeKey(overlapping, "generated", ["writer"])?.mode, "mutation");
|
|
138
|
+
});
|
|
139
|
+
|
|
124
140
|
it("treats exact and direct dotted keys as declared lane stages by convention", () => {
|
|
125
141
|
const lanes = normalizeWorkflowPreflight({
|
|
126
142
|
version: 1,
|
|
@@ -151,5 +167,11 @@ describe("workflow preflight metadata", () => {
|
|
|
151
167
|
|
|
152
168
|
const exactTrace = [{ operation: "run", key: "audit", state: "completed" as const }];
|
|
153
169
|
assert.deepEqual(workflowPreflightWarnings(lanes, exactTrace, { settled: true }), []);
|
|
170
|
+
|
|
171
|
+
const hyphenatedTrace = [{ operation: "run", key: "audit-quality", state: "completed" as const }];
|
|
172
|
+
assert.deepEqual(workflowPreflightWarnings(lanes, hyphenatedTrace, { settled: true }), [
|
|
173
|
+
"Preflight advisory: workflow key 'audit-quality' launched without a declared lane.",
|
|
174
|
+
"Preflight advisory: declared lane 'audit' was not launched.",
|
|
175
|
+
]);
|
|
154
176
|
});
|
|
155
177
|
});
|
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
diffWorktrees,
|
|
11
11
|
findWorktreeTaskCwdConflict,
|
|
12
12
|
formatWorktreeDiffSummary,
|
|
13
|
+
buildWorktreeNaming,
|
|
14
|
+
normalizeWorktreeBranchPrefix,
|
|
13
15
|
resolveExpectedWorktreeAgentCwd,
|
|
16
|
+
resolveWorktreeProvider,
|
|
17
|
+
sanitizeWorktreePathComponent,
|
|
18
|
+
shouldDeferWorktreeCwd,
|
|
14
19
|
type WorktreeSetup,
|
|
15
20
|
} from "../../src/runs/shared/worktree.ts";
|
|
16
21
|
|
|
@@ -50,18 +55,21 @@ function createHookScript(_repoDir: string, fileName: string, source: string): s
|
|
|
50
55
|
const hookScriptSkip = process.platform === "win32"
|
|
51
56
|
? "Hook script execution differs on Windows CI environments."
|
|
52
57
|
: undefined;
|
|
58
|
+
const worktrunkShimSkip = process.platform === "win32"
|
|
59
|
+
? "Windows Terminal installs a wt.exe app alias that can outrank test command shims."
|
|
60
|
+
: undefined;
|
|
53
61
|
|
|
54
62
|
describe("worktree", () => {
|
|
55
63
|
it("createWorktrees returns expected structure", () => {
|
|
56
64
|
const repoDir = createRepo("pi-worktree-structure-");
|
|
57
65
|
let setup: WorktreeSetup | undefined;
|
|
58
66
|
try {
|
|
59
|
-
setup = createWorktrees(repoDir, "structure", 2);
|
|
67
|
+
setup = createWorktrees(repoDir, "structure", 2, { provider: "native" });
|
|
60
68
|
assert.equal(setup.worktrees.length, 2);
|
|
61
69
|
assert.equal(setup.cwd, git(repoDir, ["rev-parse", "--show-toplevel"]));
|
|
62
70
|
for (let i = 0; i < setup.worktrees.length; i++) {
|
|
63
71
|
const worktree = setup.worktrees[i]!;
|
|
64
|
-
assert.equal(worktree.branch, `pi-
|
|
72
|
+
assert.equal(worktree.branch, `pi-subagents/task-structure-s0-t${i}`);
|
|
65
73
|
assert.equal(worktree.index, i);
|
|
66
74
|
assert.equal(worktree.agentCwd, worktree.path);
|
|
67
75
|
assert.equal(worktree.nodeModulesLinked, false);
|
|
@@ -80,10 +88,11 @@ describe("worktree", () => {
|
|
|
80
88
|
let planned: WorktreeSetup | undefined;
|
|
81
89
|
try {
|
|
82
90
|
setup = createWorktrees(repoDir, "before-create", 1, {
|
|
91
|
+
provider: "native",
|
|
83
92
|
beforeCreate: (candidate) => {
|
|
84
93
|
planned = candidate;
|
|
85
94
|
assert.equal(fs.existsSync(candidate.worktrees[0]!.path), false);
|
|
86
|
-
assert.equal(candidate.worktrees[0]!.branch, "pi-
|
|
95
|
+
assert.equal(candidate.worktrees[0]!.branch, "pi-subagents/task-before-creat-s0-t0");
|
|
87
96
|
},
|
|
88
97
|
});
|
|
89
98
|
assert.equal(planned?.baseCommit, setup.baseCommit);
|
|
@@ -95,6 +104,129 @@ describe("worktree", () => {
|
|
|
95
104
|
}
|
|
96
105
|
});
|
|
97
106
|
|
|
107
|
+
it("records Worktrunk ownership and uses its returned path", () => {
|
|
108
|
+
try { resolveWorktreeProvider("worktrunk"); } catch { return; }
|
|
109
|
+
const repoDir = createRepo("pi-worktree-worktrunk-");
|
|
110
|
+
let setup: WorktreeSetup | undefined;
|
|
111
|
+
try {
|
|
112
|
+
setup = createWorktrees(repoDir, "worktrunk-s0", 1, {
|
|
113
|
+
provider: "worktrunk",
|
|
114
|
+
agents: ["worker"],
|
|
115
|
+
labels: ["Review API"],
|
|
116
|
+
tasks: ["Review API behavior"],
|
|
117
|
+
branchPrefix: "pi-test/",
|
|
118
|
+
});
|
|
119
|
+
const worktree = setup.worktrees[0]!;
|
|
120
|
+
assert.equal(worktree.provider, "worktrunk");
|
|
121
|
+
assert.equal(worktree.branch, "pi-test/Review-API-1f1a55a7-worktrunk-s0-t0");
|
|
122
|
+
assert.equal(worktree.naming?.requestedBranch, worktree.branch);
|
|
123
|
+
assert.ok(fs.existsSync(worktree.path));
|
|
124
|
+
assert.notEqual(worktree.path, resolveExpectedWorktreeAgentCwd(repoDir, "worktrunk-s0", 0));
|
|
125
|
+
} finally {
|
|
126
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
127
|
+
cleanupRepo(repoDir);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("rejects Worktrunk responses that point at the source checkout", { skip: worktrunkShimSkip }, () => {
|
|
132
|
+
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-fake-source-wt-"));
|
|
133
|
+
const fakeScript = path.join(fakeBin, "wt.cjs");
|
|
134
|
+
const fakeWt = path.join(fakeBin, process.platform === "win32" ? "wt.cmd" : "wt");
|
|
135
|
+
fs.writeFileSync(fakeScript, `const { spawnSync } = require("node:child_process");
|
|
136
|
+
const args = process.argv.slice(2);
|
|
137
|
+
if (args[0] === "--version") { console.log("wt v0.75.0"); process.exit(0); }
|
|
138
|
+
if (args[0] === "switch" && args[1] === "--help") { console.log("--create --base --no-cd --no-hooks --format"); process.exit(0); }
|
|
139
|
+
const repo = args[args.indexOf("-C") + 1];
|
|
140
|
+
const branch = args[args.indexOf("--create") + 1];
|
|
141
|
+
const base = args[args.indexOf("--base") + 1];
|
|
142
|
+
const result = spawnSync("git", ["-C", repo, "checkout", "-b", branch], { encoding: "utf-8" });
|
|
143
|
+
if (result.status !== 0) { process.stderr.write(result.stderr || result.stdout); process.exit(result.status || 1); }
|
|
144
|
+
console.log(JSON.stringify({ action: "created", branch, path: repo, created_branch: true, base_branch: base }));
|
|
145
|
+
`, "utf-8");
|
|
146
|
+
if (process.platform === "win32") fs.writeFileSync(fakeWt, `@echo off\r\n"${process.execPath}" "%~dp0wt.cjs" %*\r\n`, "utf-8");
|
|
147
|
+
else {
|
|
148
|
+
fs.writeFileSync(fakeWt, `#!/bin/sh\nexec "${process.execPath}" "${fakeScript}" "$@"\n`, "utf-8");
|
|
149
|
+
fs.chmodSync(fakeWt, 0o755);
|
|
150
|
+
}
|
|
151
|
+
const previousPath = process.env.PATH;
|
|
152
|
+
const previousWindowsPath = process.env.Path;
|
|
153
|
+
const previousPathExt = process.env.PATHEXT;
|
|
154
|
+
const repoDir = createRepo("pi-worktree-source-wt-");
|
|
155
|
+
try {
|
|
156
|
+
const originalBranch = git(repoDir, ["branch", "--show-current"]);
|
|
157
|
+
process.env.PATH = `${fakeBin}${path.delimiter}${previousPath ?? ""}`;
|
|
158
|
+
process.env.Path = `${fakeBin}${path.delimiter}${previousWindowsPath ?? previousPath ?? ""}`;
|
|
159
|
+
process.env.PATHEXT = [".CMD", ".EXE", ".BAT", previousPathExt ?? ""].filter(Boolean).join(path.delimiter);
|
|
160
|
+
assert.throws(
|
|
161
|
+
() => createWorktrees(repoDir, "source-path", 1, { provider: "worktrunk" }),
|
|
162
|
+
/source checkout path/i,
|
|
163
|
+
);
|
|
164
|
+
assert.equal(git(repoDir, ["branch", "--show-current"]), originalBranch);
|
|
165
|
+
assert.equal(git(repoDir, ["branch", "--list", "pi-subagents/task-source-path-s0-t0"]), "");
|
|
166
|
+
} finally {
|
|
167
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
168
|
+
else process.env.PATH = previousPath;
|
|
169
|
+
if (previousWindowsPath === undefined) delete process.env.Path;
|
|
170
|
+
else process.env.Path = previousWindowsPath;
|
|
171
|
+
if (previousPathExt === undefined) delete process.env.PATHEXT;
|
|
172
|
+
else process.env.PATHEXT = previousPathExt;
|
|
173
|
+
cleanupRepo(repoDir);
|
|
174
|
+
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("falls back to native only when Worktrunk capability probing fails", () => {
|
|
179
|
+
const fakeBin = fs.mkdtempSync(path.join(os.tmpdir(), "pi-worktree-fake-wt-"));
|
|
180
|
+
const fakeWt = path.join(fakeBin, process.platform === "win32" ? "wt.cmd" : "wt");
|
|
181
|
+
fs.writeFileSync(fakeWt, process.platform === "win32" ? "@echo not-worktrunk\r\n" : "#!/bin/sh\nprintf 'not-worktrunk\\n'\n", "utf-8");
|
|
182
|
+
if (process.platform !== "win32") fs.chmodSync(fakeWt, 0o755);
|
|
183
|
+
const previousPath = process.env.PATH;
|
|
184
|
+
const previousWindowsPath = process.env.Path;
|
|
185
|
+
const previousPathExt = process.env.PATHEXT;
|
|
186
|
+
const repoDir = createRepo("pi-worktree-provider-fallback-");
|
|
187
|
+
let setup: WorktreeSetup | undefined;
|
|
188
|
+
try {
|
|
189
|
+
process.env.PATH = `${fakeBin}${path.delimiter}${previousPath ?? ""}`;
|
|
190
|
+
process.env.Path = `${fakeBin}${path.delimiter}${previousWindowsPath ?? previousPath ?? ""}`;
|
|
191
|
+
process.env.PATHEXT = [".CMD", ".EXE", ".BAT", previousPathExt ?? ""].filter(Boolean).join(path.delimiter);
|
|
192
|
+
assert.equal(resolveWorktreeProvider(undefined), "native");
|
|
193
|
+
assert.equal(shouldDeferWorktreeCwd(undefined), true);
|
|
194
|
+
assert.equal(resolveWorktreeProvider(undefined, " "), "native");
|
|
195
|
+
assert.equal(shouldDeferWorktreeCwd(undefined, " "), false);
|
|
196
|
+
assert.throws(() => createWorktrees(repoDir, "provider-fallback", 1, { baseDir: " " }), /cannot be empty/i);
|
|
197
|
+
setup = createWorktrees(repoDir, "provider-fallback", 1);
|
|
198
|
+
assert.equal(setup.worktrees[0]?.provider, "native");
|
|
199
|
+
assert.throws(() => resolveWorktreeProvider("worktrunk"), /Worktrunk provider is unavailable/i);
|
|
200
|
+
} finally {
|
|
201
|
+
if (setup) cleanupWorktrees(setup, { kind: "setup-rollback" });
|
|
202
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
203
|
+
else process.env.PATH = previousPath;
|
|
204
|
+
if (previousWindowsPath === undefined) delete process.env.Path;
|
|
205
|
+
else process.env.Path = previousWindowsPath;
|
|
206
|
+
if (previousPathExt === undefined) delete process.env.PATHEXT;
|
|
207
|
+
else process.env.PATHEXT = previousPathExt;
|
|
208
|
+
cleanupRepo(repoDir);
|
|
209
|
+
fs.rmSync(fakeBin, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("sanitizes readable names and validates provider branch namespaces", () => {
|
|
214
|
+
const naming = buildWorktreeNaming({
|
|
215
|
+
runId: "run-s3",
|
|
216
|
+
index: 4,
|
|
217
|
+
agent: "worker",
|
|
218
|
+
task: "Fix: API / paths",
|
|
219
|
+
branchPrefix: "pi-custom",
|
|
220
|
+
});
|
|
221
|
+
assert.equal(naming.requestedBranch, "pi-custom/worker-Fix-API-paths-17ad7139-run-s3-t4");
|
|
222
|
+
assert.equal(naming.branchPrefix, "pi-custom/");
|
|
223
|
+
assert.equal(sanitizeWorktreePathComponent("..."), "task");
|
|
224
|
+
assert.ok(Buffer.byteLength(sanitizeWorktreePathComponent("é".repeat(200)), "utf-8") <= 96);
|
|
225
|
+
assert.equal(buildWorktreeNaming({ runId: "run", index: 0, agent: "worker", task: "Fix\nAPI" }).label, "worker-Fix API");
|
|
226
|
+
assert.equal(normalizeWorktreeBranchPrefix("pi-custom/"), "pi-custom/");
|
|
227
|
+
assert.throws(() => normalizeWorktreeBranchPrefix("../unsafe"), /invalid/i);
|
|
228
|
+
});
|
|
229
|
+
|
|
98
230
|
it("createWorktrees maps subdirectory cwd to each agentCwd", () => {
|
|
99
231
|
const repoDir = createRepo("pi-worktree-subdir-");
|
|
100
232
|
const nestedDir = path.join(repoDir, "packages", "app");
|
|
@@ -629,7 +761,7 @@ process.stdout.write(JSON.stringify({ syntheticPaths: [] }));
|
|
|
629
761
|
() => createWorktrees(repoDir, runId, 2, { setupHook: { hookPath: path.relative(repoDir, hookPath) } }),
|
|
630
762
|
/worktree setup hook failed with exit code 1/i,
|
|
631
763
|
);
|
|
632
|
-
const branchList = git(repoDir, ["branch", "--list",
|
|
764
|
+
const branchList = git(repoDir, ["branch", "--list", "pi-subagents/task-hook-cleanup-s0-t*"]);
|
|
633
765
|
assert.equal(branchList.trim(), "", "temporary branches should be cleaned up after setup failure");
|
|
634
766
|
} finally {
|
|
635
767
|
cleanupRepo(repoDir);
|
|
@@ -14,14 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
## What is this?
|
|
16
16
|
|
|
17
|
-
Zentui gives
|
|
17
|
+
Zentui gives Pi surfaces independent, opt-in treatments:
|
|
18
18
|
|
|
19
19
|
- **Editor** — Opencode, Opencode copy-friendly, Accent Rail, and Minimalist input treatments
|
|
20
20
|
- **User messages** — framed, framed copy-friendly, compact, and labeled transcript messages
|
|
21
|
+
- **Thinking (Experimental)** — optional Rail, Tree, or Streaming private thinking renderers, without owning the Working line
|
|
21
22
|
- **Working line** — optional ownership of Pi's complete in-progress row and settled turn summary
|
|
22
23
|
- **Footer** — Pi's native Footer, a Starship-style statusline, or a hidden zero-row Footer
|
|
23
24
|
|
|
24
|
-
Editor, User messages, Working line, and selector borders have independent `enabled` fields. Footer uses one `style`: `native`, `starship`, or `hidden`. Use `/zentui` to configure each component without coupling it to the others.
|
|
25
|
+
Editor, User messages, Thinking (Experimental), Working line, and selector borders have independent `enabled` fields. Footer uses one `style`: `native`, `starship`, or `hidden`. Use `/zentui` to configure each component without coupling it to the others.
|
|
25
26
|
|
|
26
27
|
## Highlights
|
|
27
28
|
|
|
@@ -29,6 +30,7 @@ Editor, User messages, Working line, and selector borders have independent `enab
|
|
|
29
30
|
| --- | --- | --- |
|
|
30
31
|
| Editor | `opencode` | Opencode, copy-friendly, Accent Rail, Minimalist |
|
|
31
32
|
| User messages | `framed` | Framed, copy-friendly, Compact, Labeled |
|
|
33
|
+
| Thinking (Experimental) | disabled (`tree`) | Rail, Tree, Streaming |
|
|
32
34
|
| Working line | disabled | Five spinner presets, live tool/time/thinking/token segments, turn summary |
|
|
33
35
|
| Footer | `starship` | Native, Starship, Hidden |
|
|
34
36
|
| Selector borders | `zentui` | Independent enablement and color source |
|
|
@@ -87,7 +89,7 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
87
89
|
|
|
88
90
|
## Configure
|
|
89
91
|
|
|
90
|
-
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections.
|
|
92
|
+
Run `/zentui` inside Pi to configure Appearance, Editor, User messages, Thinking (Experimental), Working line, Footer, Segments, Git, and Extensions. Use `Tab` and `Shift+Tab` to switch sections. Most changes apply live. Active Streaming can switch live to Rail or Tree, and Rail and Tree can switch live between each other. Entering Streaming from a structural mode, first enable, and re-enable after a live disable require restarting Pi. Configuration is saved to:
|
|
91
93
|
|
|
92
94
|
```text
|
|
93
95
|
~/.pi/agent/zentui.json
|
|
@@ -106,6 +108,10 @@ A small starter config:
|
|
|
106
108
|
"enabled": true,
|
|
107
109
|
"style": "framed"
|
|
108
110
|
},
|
|
111
|
+
"thinkingSteps": {
|
|
112
|
+
"enabled": false,
|
|
113
|
+
"mode": "tree"
|
|
114
|
+
},
|
|
109
115
|
"workingLine": {
|
|
110
116
|
"enabled": false
|
|
111
117
|
},
|
|
@@ -138,7 +144,13 @@ Useful shortcuts:
|
|
|
138
144
|
/zentui format clear
|
|
139
145
|
```
|
|
140
146
|
|
|
141
|
-
Pi 0.
|
|
147
|
+
**Thinking (Experimental)** uses one private `AssistantMessageComponent` renderer for Rail, Tree, and Streaming, tested against exact Pi versions 0.80.5, 0.82.1, 0.83.0, 0.84.0, and 0.84.4. It is disabled by default and may break after Pi updates. Zentui installs an enabled startup mode before transcript restoration. A healthy installed controller lets active Streaming switch live to Rail or Tree, and lets Rail and Tree switch live between each other, without reinstalling its patch. Entering Streaming from Rail or Tree saves the choice but keeps the active structural mode until restart. Disabling live restores native thinking and releases Streaming resources. First enable and re-enable after a live disable are also restart-gated. Mode changes while disabled only preconfigure the next enable. Startup failures, missing constructors, incompatible private child layouts, parser limits, theme/render/width errors, and displaced patch ownership fail open to complete native thinking. If cleanup throws while leaving Streaming, disabling still restores native thinking and a structural selection still becomes active; the successful change warns that Streaming is unavailable for the rest of the session.
|
|
148
|
+
|
|
149
|
+
Rail shows every parsed label in each native contiguous thinking run (`│ Label`, with only the open final phase shown as `│ • Label`). Tree independently shows the latest five labels in each run (`├─ · Label`, settled `└─ · Label`, open `└─ • Label`); it never aggregates across intervening text or tool blocks. Rail and Tree follow Pi's thinking visibility. Complete strict SGR styling is stripped before parsing; every other terminal control and unsafe or unstructured content keeps the complete run native. Labels are rendered by fresh host-shaped Pi Markdown instances before cropping, so emphasis, code, links, HTML, LaTeX, custom transforms, and native `thinkingText` styling remain host-controlled. Every label occupies one terminal row: ANSI/OSC/grapheme-aware cropping adds `…` only when needed. Native horizontal padding stays external. Connectors are styled directly with the current theme's `accent` callback on every render, so custom themes control them independently. Hidden native thinking remains hidden and keeps Pi's native hidden label.
|
|
150
|
+
|
|
151
|
+
Streaming retains Pi's host-rendered final five rows under `Thinking 7.1s`, folds completed reasoning under `Thought` or current-session `Thought for 12.3s`, and owns the configured thinking-toggle binding (Ctrl+T by default) only when started in Streaming. Its input listener and timer are acquired only for an enabled Streaming session start; startup acquisition failure uses native thinking and marks Streaming unavailable. Restored completions cannot recover a duration because Pi does not persist the thinking-end timestamp. Expand/refold and lifecycle tracking are bounded to 256 retained assistant components; evicted entries are first restored natively. All modes restore/dispose on shutdown. Thinking (Experimental) never writes the Working line and does not change its existing **Thinking time** option, working text, Footer, Editor, statuses, or model behavior.
|
|
152
|
+
|
|
153
|
+
Pi 0.84 also provides a native fullscreen TUI with a sticky editor and Footer. Pi 0.84.4 is covered by a fullscreen live-transition PTY smoke in addition to the standard matrix. Zentui does not enable fullscreen automatically; select it from Pi's `/settings`, set `"tuiMode": "fullscreen"` in Pi settings, or launch Pi with `--tui-mode fullscreen`.
|
|
142
154
|
|
|
143
155
|
## Requirements
|
|
144
156
|
|
|
@@ -180,8 +192,10 @@ See [CONTRIBUTING.md](https://github.com/lmilojevicc/pi-zentui/blob/main/CONTRIB
|
|
|
180
192
|
- [Opencode](https://github.com/anomalyco/opencode) — inspiration for the Opencode editor treatment
|
|
181
193
|
- [Oh My Pi (`omp`)](https://github.com/can1357/oh-my-pi) by [Can Bölük](https://github.com/can1357) — visual inspiration for the filled, single-left-rail Accent Rail editor
|
|
182
194
|
- [Pi Custom Input](https://github.com/VinhLe1410/pi-custom-input) by [Vinh Le](https://github.com/VinhLe1410) — visual inspiration for Minimalist's framed, border-embedded session, model, context, Git, and path metadata
|
|
195
|
+
- [Pi Thinking Steps](https://github.com/crustyhacker/pi-thinking-steps) by Marc Mironescu / FluxGear — structural-step parsing and the Rail/Tree visual language; adapted in Thinking (Experimental) under the MIT License
|
|
196
|
+
- [Pi Thinking Fold](https://github.com/99percentpeople/pi-extensions/tree/master/extensions/thinking-fold) by [Zach Yuen](https://github.com/99percentpeople) — native rendered-row folding, timing, expand/refold behavior, and fail-open compatibility patterns; adapted in Thinking (Experimental) under the MIT License
|
|
183
197
|
|
|
184
|
-
Zentui
|
|
198
|
+
Most Zentui implementations are independent; these credits acknowledge product and visual inspiration. Thinking (Experimental) also adapts MIT-licensed implementation work from Pi Thinking Steps and Pi Thinking Fold. Their complete copyright and permission notices are retained in the packaged [`thinking-experimental.ts`](./extensions/zentui/thinking-experimental.ts) source.
|
|
185
199
|
|
|
186
200
|
## License
|
|
187
201
|
|