@pellux/goodvibes-tui 1.1.0 → 1.9.0

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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,488 @@
1
+ // ---------------------------------------------------------------------------
2
+ // workstream-runtime.ts — /workstream
3
+ //
4
+ // UX over the OrchestrationEngine (@pellux/goodvibes-sdk/platform/orchestration,
5
+ // wo701/W4.1) via its command-facing facade wired onto RuntimeServices as
6
+ // `workstreamCommands` (src/runtime/workstream-services.ts) and threaded
7
+ // through CommandContext.session.workstreamEngine exactly like wrfcController
8
+ // already is (bootstrap-command-context.ts).
9
+ //
10
+ // Render precedent: TRANSCRIPT + subcommand approve (like /plan approve,
11
+ // planning-runtime.ts), NOT a panel — a multi-phase proposal is too rich for
12
+ // a one-line confirm overlay, and Pillar-3 doctrine keeps work visible in the
13
+ // transcript. create -> approve -> launch is a real three-step flow (edit and
14
+ // cancel apply to the pending proposal too): the engine's own createWorkstream
15
+ // immediately materializes a real, ticking-eligible Workstream with no
16
+ // pre-creation "draft" concept, so approval happens on a TUI-held draft
17
+ // (workstream-services.ts's WorkstreamDraft) before anything is created in
18
+ // the engine at all — see that module's header doc for the full reality-wins
19
+ // divergence from the wo703 design brief.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ import { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
23
+ import type { CreateWorkstreamInput, PhaseKind, PhaseRole, WorkItem, WorkItemState, Workstream, WorkstreamIsolation } from '@pellux/goodvibes-sdk/platform/orchestration';
24
+ import type { WorkstreamCommandService, WorkstreamDraft, WorkstreamDraftProvenance } from '../../runtime/workstream-services.ts';
25
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Formatting helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function shortId(id: string): string {
32
+ return id.length > 12 ? id.slice(0, 12) : id;
33
+ }
34
+
35
+ /**
36
+ * The engine's ONLY two terminal work-item states (mirrors the SDK's own
37
+ * internal TERMINAL_ITEM_STATES in platform/runtime/fleet/adapters/
38
+ * orchestration.ts and engine.ts's kill() guard — neither is exported, so
39
+ * this is kept in lockstep by hand). Deriving "active" as NOT-terminal
40
+ * instead of enumerating the non-terminal states means a state this module
41
+ * doesn't know about yet (as 'blocked-budget' once was here) is still
42
+ * correctly treated as in-flight rather than silently falling through the
43
+ * cancel path.
44
+ */
45
+ const TERMINAL_ITEM_STATES = new Set<WorkItemState>(['passed', 'failed']);
46
+
47
+ function isActiveItemState(state: WorkItemState): boolean {
48
+ return !TERMINAL_ITEM_STATES.has(state);
49
+ }
50
+
51
+ /** Mirrors the engine's templateForPhase (phase-runner.ts): only 'review'/'gate' phases run the general template — everything else, INCLUDING 'custom', runs the engineer template regardless of phase.role's text. */
52
+ function templateForPhaseKind(kind: PhaseKind): string {
53
+ return kind === 'review' || kind === 'gate' ? 'general' : 'engineer';
54
+ }
55
+
56
+ /**
57
+ * phase.role for a 'custom' phase is the free-text description passed to
58
+ * `/workstream insert-phase` — it is purely COSMETIC. Neither
59
+ * templateForPhase nor buildPhaseTask (phase-runner.ts, engine-side) ever
60
+ * reads it: a custom phase always runs the engineer template against the
61
+ * work item's own task text. Rendering it as `<kind> — <role>` like a real
62
+ * role would falsely imply the description drives what the phase does, so
63
+ * custom phases get an explicit "this is a description, not a role" label
64
+ * instead.
65
+ */
66
+ function formatPhaseLabel(phase: { readonly kind: PhaseKind; readonly role: PhaseRole }): string {
67
+ if (phase.kind === 'custom') {
68
+ return `custom: "${phase.role}" (runs the ${templateForPhaseKind(phase.kind)} template)`;
69
+ }
70
+ return `${phase.kind} — ${phase.role}`;
71
+ }
72
+
73
+ /**
74
+ * Extracts an optional `--isolation shared|worktree` flag from anywhere in
75
+ * `args` (create/edit both accept it ahead of, or interleaved with, the task
76
+ * text). Returns the flag stripped out so the remaining tokens are the task
77
+ * text exactly as before this flag existed. An unrecognized value is a hard
78
+ * error (never silently ignored or defaulted) — a typo'd isolation mode
79
+ * must never quietly launch in the wrong one.
80
+ */
81
+ function extractIsolationFlag(args: readonly string[]): { isolation?: WorkstreamIsolation; rest: string[]; error?: string } {
82
+ const rest: string[] = [];
83
+ let isolation: WorkstreamIsolation | undefined;
84
+ for (let i = 0; i < args.length; i++) {
85
+ if (args[i] === '--isolation') {
86
+ const value = args[i + 1];
87
+ if (value !== 'shared' && value !== 'worktree') {
88
+ return { rest, error: `--isolation must be "shared" or "worktree" (got: ${value ?? '<nothing>'})` };
89
+ }
90
+ isolation = value;
91
+ i += 1; // consume the value token too
92
+ continue;
93
+ }
94
+ rest.push(args[i]!);
95
+ }
96
+ return { isolation, rest };
97
+ }
98
+
99
+ /**
100
+ * Per-item merge-state text for `/workstream status` (worktree isolation
101
+ * only — see formatItemMergeState's caller). Distinct from `item.state` (the
102
+ * pipeline verdict): an item can be terminally 'passed' while its branch is
103
+ * still 'merge pending' in the integration lane, or stuck at
104
+ * 'merge-conflict' with its worktree deliberately kept for inspection.
105
+ */
106
+ function formatItemMergeState(item: WorkItem): string {
107
+ switch (item.mergeState) {
108
+ case 'merged':
109
+ return item.mergeHash ? `merged ${shortId(item.mergeHash)}` : 'merged (no changes)';
110
+ case 'conflict':
111
+ return 'merge-conflict (worktree kept for inspection)';
112
+ case 'pending':
113
+ return 'merge pending';
114
+ case 'n-a':
115
+ default:
116
+ return item.worktreeKept ? 'worktree kept' : 'not yet integrated';
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Per-item dependency truth for `/workstream status` (BIG-3 item 2). A
122
+ * dependency-blocked item shows its honest, engine-set reason verbatim
123
+ * ('waiting on: X' or 'dependency failed: X'); an item that HAS dependencies
124
+ * but is no longer blocked shows the static 'after: X, Y' provenance so the
125
+ * ordering constraint stays visible even once satisfied. An item with no
126
+ * dependencies contributes nothing.
127
+ */
128
+ function formatItemDependencyNote(item: WorkItem, ws: Workstream): string {
129
+ if (item.state === 'blocked-dependency' && item.blockedReason) return ` — ${item.blockedReason}`;
130
+ if (item.dependsOn && item.dependsOn.length > 0) {
131
+ const titleById = new Map(ws.items.map((i) => [i.id, i.title]));
132
+ return ` — after: ${item.dependsOn.map((d) => titleById.get(d) ?? shortId(d)).join(', ')}`;
133
+ }
134
+ return '';
135
+ }
136
+
137
+ /**
138
+ * Compact item + dependency graph for the draft-approval view (BIG-3 item 4):
139
+ * items in ordinal order (the spec preserves the proposal's order), each with
140
+ * an honest text 'after: X, Y' clause when it depends on siblings — no fake DAG
141
+ * art. Dependency ids are resolved back to titles from the spec itself.
142
+ */
143
+ function formatDraftItems(spec: CreateWorkstreamInput): string[] {
144
+ const titleById = new Map(spec.items.map((it) => [it.id ?? it.title, it.title] as const));
145
+ return spec.items.map((it, i) => {
146
+ const deps = it.dependsOn ?? [];
147
+ const after = deps.length > 0 ? ` (after: ${deps.map((d) => titleById.get(d) ?? d).join(', ')})` : '';
148
+ return ` ${i + 1}. ${it.title}${after}`;
149
+ });
150
+ }
151
+
152
+ function summarizeItemStates(ws: Workstream): string {
153
+ const counts = new Map<string, number>();
154
+ for (const item of ws.items) counts.set(item.state, (counts.get(item.state) ?? 0) + 1);
155
+ return Array.from(counts.entries()).map(([state, n]) => `${n} ${state}`).join(', ') || 'no items';
156
+ }
157
+
158
+ /**
159
+ * Honest one-line provenance for how the goal was decomposed. Three shapes,
160
+ * mirroring the SDK decomposition service's outcomes:
161
+ * - a planning agent decomposed it (with item count + cost/tokens + elapsed)
162
+ * - the heuristic path ran because the agent path failed (fallback + reason)
163
+ * - the heuristic path ran deliberately (configured, or the gate declined)
164
+ */
165
+ function formatProvenance(p: WorkstreamDraftProvenance): string {
166
+ if (p.kind === 'agent') {
167
+ const bits: string[] = [`${p.itemCount} item${p.itemCount === 1 ? '' : 's'}`];
168
+ if (p.agentCostUsd !== undefined) bits.push(`$${p.agentCostUsd.toFixed(4)}`);
169
+ else if (p.agentTokens !== undefined) bits.push(`${p.agentTokens} tok`);
170
+ if (p.elapsedMs !== undefined) bits.push(`${Math.round(p.elapsedMs / 1000)}s`);
171
+ return `Decomposition: planning agent (${bits.join(', ')})`;
172
+ }
173
+ if (p.kind === 'fallback') {
174
+ return `Decomposition: heuristic (agent fallback: ${p.fallbackReason ?? 'unknown'})`;
175
+ }
176
+ if (p.kind === 'gate-declined') {
177
+ return 'Decomposition: heuristic (single item; planner declined to decompose)';
178
+ }
179
+ return 'Decomposition: heuristic (configured)';
180
+ }
181
+
182
+ /** The engine's own PlanProposal is deliberately not rendered as the launchable spec — see workstream-services.ts's buildSpec doc. This renders the REAL launchable spec (so the proposal and the launch are always the same plan) plus an honest provenance line describing how the goal was decomposed. */
183
+ function renderDraftProposal(draft: WorkstreamDraft): string {
184
+ const lines: string[] = [];
185
+ lines.push(`Workstream proposal ${draft.id} — "${draft.task}"`);
186
+ lines.push(`Isolation: ${draft.spec.isolation ?? 'shared (default)'}`);
187
+ lines.push(
188
+ `Planner: strategy=${draft.gate.strategy} (${draft.gate.reasonCode}) — ${AdaptivePlanner.explainReasonCode(draft.gate.reasonCode)}`,
189
+ );
190
+ lines.push(formatProvenance(draft.provenance));
191
+ // Honest mapping boundary (BIG-3 item 4): a multi-item proposal launches as
192
+ // the REAL dependency-scheduled workstream (fromPlanProposal); a single-item
193
+ // one launches as the compat engineer→review chain (fromChainSpec). State
194
+ // which, so the preview never overstates what launch will do.
195
+ const multiItem = draft.spec.items.length > 1;
196
+ lines.push(
197
+ multiItem
198
+ ? `Mapping: multi-item plan — ${draft.spec.items.length} items run the engineer→review pipeline, dependency-scheduled.`
199
+ : 'Mapping: single-item compat chain (engineer→review) — no multi-item structure to schedule.',
200
+ );
201
+ lines.push('Phases:');
202
+ draft.spec.phases.forEach((phase, i) => {
203
+ lines.push(` ${i + 1}. ${formatPhaseLabel(phase)} (capacity ${phase.capacity})`);
204
+ });
205
+ lines.push('Work items (ordinal order):');
206
+ lines.push(...formatDraftItems(draft.spec));
207
+ lines.push(
208
+ draft.approved
209
+ ? `Approved. Launch with: /workstream launch ${draft.id}`
210
+ : `Approve with: /workstream approve ${draft.id}`,
211
+ );
212
+ lines.push(`Edit the task: /workstream edit ${draft.id} <new task...> Discard: /workstream cancel ${draft.id}`);
213
+ // Honest limitation (see workstream-services.ts's REALITY-WINS doc): the
214
+ // engine has no pre-creation draft concept, so this proposal is
215
+ // process-lifetime, in-memory state only — never journaled like a launched
216
+ // Workstream is. State that plainly, the same way an unsent chat draft
217
+ // would be, rather than letting a restart silently make it vanish.
218
+ lines.push('Note: not saved — lost if the TUI restarts before launch.');
219
+ return lines.join('\n');
220
+ }
221
+
222
+ function renderWorkstreamStatus(ws: Workstream): string {
223
+ const isolated = ws.isolation === 'worktree';
224
+ const lines: string[] = [];
225
+ lines.push(`Workstream ${ws.id} — "${ws.title}"`);
226
+ lines.push(`Isolation: ${ws.isolation ?? 'shared'}`);
227
+ // Origin provenance (BIG-3 item 1) — only present on workstreams assembled
228
+ // from a decomposition proposal; absent for compat/authored ones.
229
+ if (ws.provenance) {
230
+ const pv = ws.provenance;
231
+ const bits: string[] = [];
232
+ if (pv.decomposedBy) bits.push(`${pv.decomposedBy}-decomposed`);
233
+ if (pv.proposalId) bits.push(`plan ${pv.proposalId}`);
234
+ if (pv.strategy) bits.push(pv.strategy);
235
+ if (bits.length > 0) lines.push(`Origin: ${bits.join(', ')}`);
236
+ }
237
+ lines.push('Phases:');
238
+ for (const phase of ws.phases) {
239
+ lines.push(` [${phase.ordinal}] ${formatPhaseLabel(phase)} (capacity ${phase.capacity})`);
240
+ }
241
+ lines.push('Items:');
242
+ for (const item of ws.items) {
243
+ const mergeNote = isolated ? ` — ${formatItemMergeState(item)}` : '';
244
+ const depNote = formatItemDependencyNote(item, ws);
245
+ lines.push(` ${shortId(item.id)} [${item.state}] ${item.title} — phase: ${item.currentPhaseId ?? '—'}${depNote}${mergeNote}`);
246
+ }
247
+ if (isolated) {
248
+ // Honest terminal-summary truth (never inferred from item.state alone —
249
+ // an item can be terminally 'passed' with its branch still unmerged):
250
+ // an item counts as unmerged the instant it enters the integration lane
251
+ // (mergeState 'pending') and stays counted through a conflict or any
252
+ // KEPT worktree, until a clean merge clears it.
253
+ const unmerged = ws.items.filter((item) => item.mergeState === 'pending' || item.mergeState === 'conflict' || item.worktreeKept);
254
+ lines.push(
255
+ unmerged.length > 0
256
+ ? `Unmerged items: ${unmerged.length} (${unmerged.map((item) => shortId(item.id)).join(', ')}) — this run is NOT fully integrated yet.`
257
+ : 'Unmerged items: none — every terminated item is merged (or had nothing to merge).',
258
+ );
259
+ }
260
+ return lines.join('\n');
261
+ }
262
+
263
+ function renderWorkstreamList(service: WorkstreamCommandService): string {
264
+ const drafts = service.listDrafts();
265
+ const live = service.engine.listWorkstreams();
266
+ if (drafts.length === 0 && live.length === 0) {
267
+ return 'No workstreams yet. Use /workstream create <task...> to propose one.';
268
+ }
269
+ const sections: string[] = [];
270
+ if (drafts.length > 0) {
271
+ const rows = drafts.map((d) => ` ${d.id} ${d.approved ? '[approved]' : '[draft] '} "${d.task}"`);
272
+ sections.push([`Pending proposals (${drafts.length}, not yet launched):`, ...rows].join('\n'));
273
+ }
274
+ if (live.length > 0) {
275
+ const rows = live.map((ws) => ` ${shortId(ws.id)} "${ws.title}" (${ws.phases.length} phases, ${summarizeItemStates(ws)})`);
276
+ sections.push([`Workstreams (${live.length}):`, ...rows].join('\n'));
277
+ }
278
+ return sections.join('\n\n');
279
+ }
280
+
281
+ function resolveWorkstream(service: WorkstreamCommandService, ref: string): Workstream | undefined {
282
+ const exact = service.engine.getWorkstream(ref);
283
+ if (exact) return exact;
284
+ return service.engine.listWorkstreams().find((ws) => ws.id.startsWith(ref));
285
+ }
286
+
287
+ /**
288
+ * approve/edit/launch all fail this way when `id` doesn't resolve to a held
289
+ * draft. A bare "No pending proposal found" reads like a typo'd id even when
290
+ * the real cause is that drafts are process-lifetime, in-memory-only state
291
+ * (see renderDraftProposal's note and workstream-services.ts's REALITY-WINS
292
+ * doc) and a TUI restart wiped every one of them. Only hint at that when the
293
+ * service is holding zero drafts at all — with other drafts present, a typo
294
+ * or stale id is the more honest guess and the plain message stays.
295
+ */
296
+ function draftNotFoundMessage(service: WorkstreamCommandService, id: string): string {
297
+ const base = `No pending proposal found: ${id}`;
298
+ if (service.listDrafts().length > 0) return base;
299
+ return `${base} (proposals aren't saved — a TUI restart since you created it would explain this; recreate it with /workstream create)`;
300
+ }
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // Command
304
+ // ---------------------------------------------------------------------------
305
+
306
+ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): void {
307
+ registry.register({
308
+ name: 'workstream',
309
+ description: 'Author and oversee multi-phase agent workstreams (orchestration engine)',
310
+ usage: 'create [--isolation shared|worktree] <task...> | list | status [id] | insert-phase <id> <description...> | approve <id> | edit <id> [--isolation shared|worktree] <task...> | launch <id> | cancel <id>',
311
+ argsHint: 'create [--isolation worktree] <task> | list | status [id] | approve | edit | launch | cancel',
312
+ handler: async (args, ctx: CommandContext) => {
313
+ const service = ctx.session.workstreamEngine;
314
+ if (!service) {
315
+ ctx.print('Workstreams are not available in this session.');
316
+ return;
317
+ }
318
+
319
+ const sub = args[0];
320
+
321
+ if (!sub || sub === 'list') {
322
+ ctx.print(renderWorkstreamList(service));
323
+ return;
324
+ }
325
+
326
+ if (sub === 'create') {
327
+ const { isolation, rest, error } = extractIsolationFlag(args.slice(1));
328
+ if (error) {
329
+ ctx.print(`Usage: /workstream create [--isolation shared|worktree] <task...>\n${error}`);
330
+ return;
331
+ }
332
+ const task = rest.join(' ').trim();
333
+ if (!task) {
334
+ ctx.print('Usage: /workstream create [--isolation shared|worktree] <task...>');
335
+ return;
336
+ }
337
+ const draft = await service.proposeDraft(task, isolation);
338
+ ctx.print(renderDraftProposal(draft));
339
+ return;
340
+ }
341
+
342
+ if (sub === 'status') {
343
+ const ref = args[1];
344
+ const live = service.engine.listWorkstreams();
345
+ const target = ref ? resolveWorkstream(service, ref) : (live.length === 1 ? live[0] : undefined);
346
+ if (!target) {
347
+ if (!ref && live.length > 1) {
348
+ ctx.print(`Multiple workstreams running — specify one: ${live.map((ws) => shortId(ws.id)).join(', ')}`);
349
+ return;
350
+ }
351
+ ctx.print(ref ? `No workstream found: ${ref}` : 'No workstreams running. Use /workstream list to see pending proposals.');
352
+ return;
353
+ }
354
+ ctx.print(renderWorkstreamStatus(target));
355
+ return;
356
+ }
357
+
358
+ if (sub === 'insert-phase') {
359
+ const id = args[1];
360
+ const description = args.slice(2).join(' ').trim();
361
+ if (!id || !description) {
362
+ ctx.print('Usage: /workstream insert-phase <id> <description...>');
363
+ return;
364
+ }
365
+ const ws = resolveWorkstream(service, id);
366
+ if (!ws) {
367
+ ctx.print(`No workstream found: ${id}`);
368
+ return;
369
+ }
370
+ const lastOrdinal = ws.phases.reduce((max, phase) => Math.max(max, phase.ordinal), 0);
371
+ const templateGate = ws.phases.at(-1)?.gate ?? { scope: 'scoped' as const, gates: [] };
372
+ const inserted = service.engine.insertPhase(ws.id, lastOrdinal, {
373
+ role: description,
374
+ capacity: 1,
375
+ kind: 'custom',
376
+ gate: templateGate,
377
+ });
378
+ if (!inserted) {
379
+ ctx.print(`Could not insert phase — is "${id}" still an active workstream?`);
380
+ return;
381
+ }
382
+ ctx.print(`Inserted phase "${description}" into ${shortId(ws.id)} after ordinal ${lastOrdinal} (new ordinal ${inserted.ordinal}).`);
383
+ return;
384
+ }
385
+
386
+ if (sub === 'approve') {
387
+ const id = args[1];
388
+ if (!id) {
389
+ ctx.print('Usage: /workstream approve <id>');
390
+ return;
391
+ }
392
+ const draft = service.approveDraft(id);
393
+ if (!draft) {
394
+ ctx.print(draftNotFoundMessage(service, id));
395
+ return;
396
+ }
397
+ ctx.print(`Approved ${id}. Launch with: /workstream launch ${id}`);
398
+ return;
399
+ }
400
+
401
+ if (sub === 'edit') {
402
+ const id = args[1];
403
+ const { isolation, rest, error } = extractIsolationFlag(args.slice(2));
404
+ if (error) {
405
+ ctx.print(`Usage: /workstream edit <id> [--isolation shared|worktree] <new task...>\n${error}`);
406
+ return;
407
+ }
408
+ const task = rest.join(' ').trim();
409
+ if (!id || !task) {
410
+ ctx.print('Usage: /workstream edit <id> [--isolation shared|worktree] <new task...>');
411
+ return;
412
+ }
413
+ const draft = await service.editDraft(id, task, isolation);
414
+ if (!draft) {
415
+ ctx.print(draftNotFoundMessage(service, id));
416
+ return;
417
+ }
418
+ ctx.print(renderDraftProposal(draft));
419
+ return;
420
+ }
421
+
422
+ if (sub === 'launch') {
423
+ const id = args[1];
424
+ if (!id) {
425
+ ctx.print('Usage: /workstream launch <id>');
426
+ return;
427
+ }
428
+ const draft = service.getDraft(id);
429
+ if (!draft) {
430
+ ctx.print(draftNotFoundMessage(service, id));
431
+ return;
432
+ }
433
+ if (!draft.approved) {
434
+ ctx.print(`Proposal ${id} is not approved yet. Run /workstream approve ${id} first.`);
435
+ return;
436
+ }
437
+ const result = service.launchDraft(id);
438
+ if (!result) {
439
+ ctx.print(`Could not launch ${id}.`);
440
+ return;
441
+ }
442
+ ctx.print(`Launched workstream ${result.workstreamId} — track it with /workstream status ${result.workstreamId} or the Fleet panel.`);
443
+ return;
444
+ }
445
+
446
+ if (sub === 'cancel') {
447
+ const id = args[1];
448
+ if (!id) {
449
+ ctx.print('Usage: /workstream cancel <id>');
450
+ return;
451
+ }
452
+ if (service.getDraft(id)) {
453
+ service.removeDraft(id);
454
+ ctx.print(`Discarded pending proposal ${id}.`);
455
+ return;
456
+ }
457
+ const ws = resolveWorkstream(service, id);
458
+ if (!ws) {
459
+ ctx.print(`No workstream or pending proposal found: ${id}`);
460
+ return;
461
+ }
462
+ const active = ws.items.filter((item) => isActiveItemState(item.state));
463
+ let killedCount = 0;
464
+ for (const item of active) {
465
+ if (service.engine.kill(item.id)) killedCount++;
466
+ }
467
+ ctx.print(
468
+ killedCount > 0
469
+ ? `Cancelled ${killedCount} of ${active.length} in-flight work item(s) in ${shortId(ws.id)}.`
470
+ : `${shortId(ws.id)} has no in-flight work items to cancel.`,
471
+ );
472
+ return;
473
+ }
474
+
475
+ ctx.print(
476
+ 'Usage:\n'
477
+ + ' /workstream create [--isolation shared|worktree] <task...>\n'
478
+ + ' /workstream list\n'
479
+ + ' /workstream status [id]\n'
480
+ + ' /workstream insert-phase <id> <description...>\n'
481
+ + ' /workstream approve <id>\n'
482
+ + ' /workstream edit <id> [--isolation shared|worktree] <new task...>\n'
483
+ + ' /workstream launch <id>\n'
484
+ + ' /workstream cancel <id>',
485
+ );
486
+ },
487
+ });
488
+ }
@@ -15,6 +15,7 @@ import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts'
15
15
  import { registerPluginRuntimeCommands } from './commands/plugin-runtime.ts';
16
16
  import { registerDiffRuntimeCommands } from './commands/diff-runtime.ts';
17
17
  import { registerGitRuntimeCommands } from './commands/git-runtime.ts';
18
+ import { registerTestRuntimeCommands } from './commands/test-runtime.ts';
18
19
  import { registerNotifyRuntimeCommands } from './commands/notify-runtime.ts';
19
20
  import { registerReplayRuntimeCommands } from './commands/replay-runtime.ts';
20
21
  import { registerShareRuntimeCommands } from './commands/share-runtime.ts';
@@ -36,6 +37,9 @@ import { registerHooksRuntimeCommands } from './commands/hooks-runtime.ts';
36
37
  import { registerControlRoomRuntimeCommands } from './commands/control-room-runtime.ts';
37
38
  import { registerMcpRuntimeCommands } from './commands/mcp-runtime.ts';
38
39
  import { registerSessionContentCommands } from './commands/session-content.ts';
40
+ import { registerCheckpointRuntimeCommands } from './commands/checkpoint-runtime.ts';
41
+ import { registerWorkstreamRuntimeCommands } from './commands/workstream-runtime.ts';
42
+ import { registerCodebaseRuntimeCommands } from './commands/codebase-runtime.ts';
39
43
  import { registerLocalRuntimeCommands } from './commands/local-runtime.ts';
40
44
  import { registerExperienceRuntimeCommands } from './commands/experience-runtime.ts';
41
45
  import { registerIncidentRuntimeCommands } from './commands/incident-runtime.ts';
@@ -57,6 +61,8 @@ import { registerTtsRuntimeCommands } from './commands/tts-runtime.ts';
57
61
  import { registerCloudflareRuntimeCommands } from './commands/cloudflare-runtime.ts';
58
62
  import { registerWorkPlanRuntimeCommands } from './commands/work-plan-runtime.ts';
59
63
  import { registerCostRuntimeCommands } from './commands/cost-runtime.ts';
64
+ import { registerWebSearchRuntimeCommands } from './commands/websearch-runtime.ts';
65
+ import { registerImageRuntimeCommands } from './commands/image-runtime.ts';
60
66
 
61
67
  /**
62
68
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -69,6 +75,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
69
75
  registerPluginRuntimeCommands(registry);
70
76
  registerDiffRuntimeCommands(registry);
71
77
  registerGitRuntimeCommands(registry);
78
+ registerTestRuntimeCommands(registry);
72
79
  registerNotifyRuntimeCommands(registry);
73
80
  registerReplayRuntimeCommands(registry);
74
81
  registerShareRuntimeCommands(registry);
@@ -115,6 +122,11 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
115
122
  registerScheduleRuntimeCommands(registry);
116
123
  registerBranchRuntimeCommands(registry);
117
124
  registerSessionContentCommands(registry);
125
+ registerCheckpointRuntimeCommands(registry);
126
+ registerWorkstreamRuntimeCommands(registry);
127
+ registerCodebaseRuntimeCommands(registry);
128
+ registerWebSearchRuntimeCommands(registry);
129
+ registerImageRuntimeCommands(registry);
118
130
 
119
131
  // ── /policy ───────────────────────────────────────────────────────────────
120
132
  registry.register(policyCommand);