@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.
Files changed (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. 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-parallel-structure-${i}`);
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-parallel-before-create-0");
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", `pi-parallel-${runId}-*`]);
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 four Pi surfaces independent ownership and styling:
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. Changes are applied live and saved to:
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.84 also provides a native fullscreen TUI with a sticky editor and Footer. Zentui does not enable it automatically; select fullscreen from Pi's `/settings`, set `"tuiMode": "fullscreen"` in Pi settings, or launch Pi with `--tui-mode fullscreen`.
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's implementations are independent; these credits acknowledge product and visual inspiration rather than source-code reuse.
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