taskplane 0.28.4 → 0.28.6

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 (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -215
  3. package/bin/gitignore-patterns.mjs +79 -79
  4. package/bin/rpc-wrapper.mjs +1086 -1086
  5. package/bin/taskplane.mjs +3254 -3254
  6. package/dashboard/public/app.js +2573 -2573
  7. package/dashboard/public/index.html +139 -139
  8. package/dashboard/public/style.css +1882 -1882
  9. package/dashboard/public/taskplane-word-color.svg +18 -18
  10. package/dashboard/public/taskplane-word-white.svg +18 -18
  11. package/dashboard/server.cjs +1666 -1666
  12. package/extensions/reviewer-extension.ts +119 -119
  13. package/extensions/task-orchestrator.ts +28 -28
  14. package/extensions/taskplane/abort.ts +502 -502
  15. package/extensions/taskplane/agent-bridge-extension.ts +838 -765
  16. package/extensions/taskplane/agent-host.ts +833 -745
  17. package/extensions/taskplane/cleanup.ts +747 -747
  18. package/extensions/taskplane/config-loader.ts +1328 -1322
  19. package/extensions/taskplane/config-schema.ts +692 -682
  20. package/extensions/taskplane/config.ts +73 -73
  21. package/extensions/taskplane/context-window.ts +66 -66
  22. package/extensions/taskplane/diagnostic-reports.ts +463 -463
  23. package/extensions/taskplane/diagnostics.ts +385 -385
  24. package/extensions/taskplane/engine-worker-entry.mjs +34 -34
  25. package/extensions/taskplane/engine-worker.ts +381 -381
  26. package/extensions/taskplane/engine.ts +4539 -4527
  27. package/extensions/taskplane/execution.ts +2733 -2708
  28. package/extensions/taskplane/extension.ts +30 -9
  29. package/extensions/taskplane/formatting.ts +773 -773
  30. package/extensions/taskplane/git.ts +90 -90
  31. package/extensions/taskplane/index.ts +28 -28
  32. package/extensions/taskplane/lane-runner.ts +1383 -1360
  33. package/extensions/taskplane/mailbox.ts +689 -689
  34. package/extensions/taskplane/merge.ts +3135 -3135
  35. package/extensions/taskplane/messages.ts +985 -985
  36. package/extensions/taskplane/migrations.ts +278 -278
  37. package/extensions/taskplane/naming.ts +117 -117
  38. package/extensions/taskplane/path-resolver.ts +237 -237
  39. package/extensions/taskplane/persistence.ts +2087 -2087
  40. package/extensions/taskplane/process-registry.ts +416 -416
  41. package/extensions/taskplane/quality-gate.ts +1033 -1033
  42. package/extensions/taskplane/resume.ts +2879 -2878
  43. package/extensions/taskplane/sessions.ts +57 -57
  44. package/extensions/taskplane/settings-loader.ts +136 -136
  45. package/extensions/taskplane/settings-tui.ts +1867 -1867
  46. package/extensions/taskplane/sidecar-telemetry.ts +252 -252
  47. package/extensions/taskplane/supervisor-primer.md +1694 -1694
  48. package/extensions/taskplane/supervisor.ts +4341 -4341
  49. package/extensions/taskplane/task-executor-core.ts +550 -550
  50. package/extensions/taskplane/tmux-compat.ts +37 -37
  51. package/extensions/taskplane/types.ts +4297 -4278
  52. package/extensions/taskplane/verification.ts +542 -542
  53. package/extensions/taskplane/waves.ts +1548 -1548
  54. package/extensions/taskplane/workspace.ts +705 -705
  55. package/extensions/taskplane/worktree.ts +2604 -2505
  56. package/package.json +57 -57
  57. package/skills/create-taskplane-task/SKILL.md +465 -465
  58. package/skills/create-taskplane-task/references/prompt-template.md +285 -285
  59. package/templates/agents/local/supervisor.md +33 -33
  60. package/templates/agents/local/task-merger.md +27 -27
  61. package/templates/agents/local/task-reviewer.md +30 -30
  62. package/templates/agents/local/task-worker.md +34 -34
  63. package/templates/agents/supervisor-routing.md +92 -92
  64. package/templates/agents/supervisor.md +168 -168
  65. package/templates/agents/task-merger.md +214 -214
  66. package/templates/agents/task-reviewer.md +192 -192
  67. package/templates/agents/task-worker.md +505 -429
  68. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -98
  69. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -73
  70. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -97
  71. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -73
@@ -1,381 +1,381 @@
1
- /**
2
- * Engine Child Process Entry Point (TP-071)
3
- *
4
- * This module serves two purposes:
5
- * 1. Exports types and helpers used by extension.ts (main thread)
6
- * 2. When forked as a child process, runs the engine in a separate Node.js process
7
- *
8
- * Uses child_process.fork() instead of worker_threads because Node v25's
9
- * default --experimental-strip-types rejects .ts files inside node_modules.
10
- * Fork creates a new process where --experimental-transform-types takes effect.
11
- *
12
- * Communication:
13
- * - Child → Parent: process.send() for notify, monitor-update, engine-event, state-sync, complete, error
14
- * - Parent → Child: child.send() for init, pause, resume, abort
15
- *
16
- * @module orch/engine-worker
17
- */
18
- import type {
19
- AllocatedLane,
20
- EngineEvent,
21
- MonitorState,
22
- OrchBatchPhase,
23
- OrchBatchRuntimeState,
24
- OrchestratorConfig,
25
- SupervisorAlert,
26
- TaskRunnerConfig,
27
- WorkspaceConfig,
28
- WorkspaceRepoConfig,
29
- } from "./types.ts";
30
-
31
- // ── Types for worker <-> main thread messages ────────────────────────
32
-
33
- /**
34
- * Messages sent FROM the worker TO the main thread.
35
- */
36
- export type WorkerErrorSource = "enginePromise" | "uncaughtException" | "unhandledRejection";
37
-
38
- export type WorkerToMainMessage =
39
- | { type: "notify"; msg: string; level: "info" | "warning" | "error" }
40
- | { type: "monitor-update"; state: MonitorState }
41
- | { type: "engine-event"; event: EngineEvent }
42
- | { type: "supervisor-alert"; alert: SupervisorAlert }
43
- | { type: "state-sync"; state: SerializedBatchState }
44
- | { type: "complete"; state: SerializedBatchState }
45
- | { type: "error"; message: string; stack?: string; source?: WorkerErrorSource };
46
-
47
- /**
48
- * Messages sent FROM the main thread TO the worker.
49
- */
50
- export type WorkerInMessage =
51
- | { type: "pause" }
52
- | { type: "resume" }
53
- | { type: "abort" };
54
-
55
- /**
56
- * Serializable form of OrchBatchRuntimeState fields synced to main thread.
57
- * Only includes fields the main thread needs for display/state tracking.
58
- */
59
- export interface SerializedBatchState {
60
- phase: OrchBatchPhase;
61
- batchId: string;
62
- baseBranch: string;
63
- orchBranch: string;
64
- mode: string;
65
- currentWaveIndex: number;
66
- totalWaves: number;
67
- totalTasks: number;
68
- succeededTasks: number;
69
- failedTasks: number;
70
- skippedTasks: number;
71
- blockedTasks: number;
72
- startedAt: number;
73
- endedAt: number | null;
74
- errors: string[];
75
- /** Active lanes for the current wave (synced so /orch-sessions works). */
76
- currentLanes: AllocatedLane[];
77
- }
78
-
79
- /**
80
- * Serializable form of WorkspaceConfig (Map → array of entries).
81
- */
82
- export interface SerializedWorkspaceConfig {
83
- mode: string;
84
- repos: Array<[string, WorkspaceRepoConfig]>;
85
- routing: WorkspaceConfig["routing"];
86
- configPath: string;
87
- }
88
-
89
- /**
90
- * workerData shape passed from the main thread.
91
- */
92
- export interface EngineWorkerData {
93
- /** Sentinel flag — distinguishes engine worker from test-runner worker threads */
94
- engineWorker: true;
95
- /** "execute" for new batch, "resume" for resume */
96
- mode: "execute" | "resume";
97
- /** User arguments (target string) — only for "execute" mode */
98
- args?: string;
99
- /** Orchestrator configuration */
100
- orchConfig: OrchestratorConfig;
101
- /** Task runner configuration */
102
- runnerConfig: TaskRunnerConfig;
103
- /** Repository root (cwd) */
104
- cwd: string;
105
- /** Workspace configuration (serialized) — null for repo mode */
106
- workspaceConfig?: SerializedWorkspaceConfig | null;
107
- /** Workspace root directory */
108
- workspaceRoot?: string;
109
- /** Agent root directory */
110
- agentRoot?: string;
111
- /** Force flag for resume */
112
- force?: boolean;
113
- /** Supervisor autonomy mode propagated to worker bridge tools. */
114
- supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
115
- }
116
-
117
- // ── Serialization helpers (used by both main thread and worker) ──────
118
-
119
- /**
120
- * Serialize WorkspaceConfig for cross-thread transfer.
121
- * Converts the Map to an array of entries.
122
- */
123
- export function serializeWorkspaceConfig(
124
- config: WorkspaceConfig | null | undefined,
125
- ): SerializedWorkspaceConfig | null {
126
- if (!config) return null;
127
- return {
128
- mode: config.mode,
129
- repos: [...config.repos.entries()],
130
- routing: config.routing,
131
- configPath: config.configPath,
132
- };
133
- }
134
-
135
- /**
136
- * Reconstruct WorkspaceConfig from serialized form.
137
- */
138
- export function deserializeWorkspaceConfig(
139
- serialized: SerializedWorkspaceConfig | null | undefined,
140
- ): WorkspaceConfig | null {
141
- if (!serialized) return null;
142
- return {
143
- mode: serialized.mode as WorkspaceConfig["mode"],
144
- repos: new Map(serialized.repos),
145
- routing: serialized.routing,
146
- configPath: serialized.configPath,
147
- };
148
- }
149
-
150
- /**
151
- * Extract serializable batch state for sync back to main thread.
152
- */
153
- function serializeBatchState(state: OrchBatchRuntimeState): SerializedBatchState {
154
- return {
155
- phase: state.phase,
156
- batchId: state.batchId,
157
- baseBranch: state.baseBranch,
158
- orchBranch: state.orchBranch,
159
- mode: state.mode,
160
- currentWaveIndex: state.currentWaveIndex,
161
- totalWaves: state.totalWaves,
162
- totalTasks: state.totalTasks,
163
- succeededTasks: state.succeededTasks,
164
- failedTasks: state.failedTasks,
165
- skippedTasks: state.skippedTasks,
166
- blockedTasks: state.blockedTasks,
167
- startedAt: state.startedAt,
168
- endedAt: state.endedAt,
169
- errors: [...state.errors],
170
- currentLanes: state.currentLanes,
171
- };
172
- }
173
-
174
- /**
175
- * Apply serialized batch state from worker to main-thread batch state.
176
- *
177
- * Updates only the fields that the worker thread tracks — preserves
178
- * main-thread-only fields like pauseSignal, dependencyGraph, etc.
179
- */
180
- export function applySerializedState(
181
- batchState: OrchBatchRuntimeState,
182
- serialized: SerializedBatchState,
183
- ): void {
184
- batchState.phase = serialized.phase;
185
- batchState.batchId = serialized.batchId;
186
- batchState.baseBranch = serialized.baseBranch;
187
- batchState.orchBranch = serialized.orchBranch;
188
- batchState.mode = serialized.mode as OrchBatchRuntimeState["mode"];
189
- batchState.currentWaveIndex = serialized.currentWaveIndex;
190
- batchState.totalWaves = serialized.totalWaves;
191
- batchState.totalTasks = serialized.totalTasks;
192
- batchState.succeededTasks = serialized.succeededTasks;
193
- batchState.failedTasks = serialized.failedTasks;
194
- batchState.skippedTasks = serialized.skippedTasks;
195
- batchState.blockedTasks = serialized.blockedTasks;
196
- batchState.startedAt = serialized.startedAt;
197
- batchState.endedAt = serialized.endedAt;
198
- batchState.errors = [...serialized.errors];
199
- batchState.currentLanes = serialized.currentLanes ?? [];
200
- }
201
-
202
- // ── Engine main (runs when launched as a forked child process) ───────
203
-
204
- // Guard: only run engine main when launched via fork() with the sentinel env var.
205
- if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "function") {
206
- const send = (msg: WorkerToMainMessage) => {
207
- try {
208
- process.send?.(msg);
209
- } catch {
210
- // best effort only
211
- }
212
- };
213
-
214
- const sendWithAck = (msg: WorkerToMainMessage, onFlushed: () => void) => {
215
- if (typeof process.send !== "function" || !process.connected) {
216
- onFlushed();
217
- return;
218
- }
219
-
220
- let flushed = false;
221
- const done = () => {
222
- if (flushed) return;
223
- flushed = true;
224
- onFlushed();
225
- };
226
-
227
- try {
228
- (process.send as (
229
- message: WorkerToMainMessage,
230
- sendHandle?: unknown,
231
- options?: unknown,
232
- callback?: (error: Error | null) => void,
233
- ) => boolean)(msg, undefined, undefined, () => done());
234
- setTimeout(done, 75).unref();
235
- } catch {
236
- done();
237
- }
238
- };
239
-
240
- const normalizeError = (err: unknown): { message: string; stack?: string } => {
241
- if (err instanceof Error) return { message: err.message, stack: err.stack };
242
- return { message: String(err) };
243
- };
244
-
245
- // Wait for the init message carrying workerData, then start the engine.
246
- process.once("message", async (initMsg: { type: string; data: EngineWorkerData }) => {
247
- if (initMsg?.type !== "init") return;
248
-
249
- let batchState: OrchBatchRuntimeState | null = null;
250
- let fatalHandled = false;
251
- const reportFatalAndExit = (source: WorkerErrorSource, err: unknown) => {
252
- if (fatalHandled) return;
253
- fatalHandled = true;
254
-
255
- const normalized = normalizeError(err);
256
- if (batchState && batchState.phase !== "completed" && batchState.phase !== "failed") {
257
- batchState.phase = "failed";
258
- batchState.endedAt = Date.now();
259
- batchState.errors.push(`[${source}] ${normalized.message}`);
260
- }
261
-
262
- if (batchState) send({ type: "state-sync", state: serializeBatchState(batchState) });
263
- sendWithAck(
264
- { type: "error", source, message: normalized.message, stack: normalized.stack },
265
- () => process.exit(1),
266
- );
267
- setTimeout(() => process.exit(1), 200).unref();
268
- };
269
-
270
- process.once("uncaughtException", (err: unknown) => reportFatalAndExit("uncaughtException", err));
271
- process.once("unhandledRejection", (reason: unknown) => reportFatalAndExit("unhandledRejection", reason));
272
-
273
- // Dynamic imports — only loaded in engine context to avoid circular
274
- // dependencies when this module is imported from extension.ts
275
- const { executeOrchBatch } = await import("./engine.ts");
276
- const { resumeOrchBatch } = await import("./resume.ts");
277
- const { freshOrchBatchState } = await import("./types.ts");
278
-
279
- const data = initMsg.data;
280
-
281
- // Create a fresh batch state for this process
282
- batchState = freshOrchBatchState();
283
- batchState.phase = "launching";
284
- batchState.startedAt = Date.now();
285
-
286
- // Deserialize workspace config
287
- const wsConfig = deserializeWorkspaceConfig(data.workspaceConfig);
288
-
289
- // ── Control signal listener ──────────────────────────────────
290
- // Main process sends pause/resume/abort signals via IPC.
291
- // We apply them to the in-process batchState.pauseSignal.
292
- process.on("message", (msg: WorkerInMessage) => {
293
- if (!batchState) return;
294
- switch (msg.type) {
295
- case "pause":
296
- batchState.pauseSignal.paused = true;
297
- break;
298
- case "resume":
299
- batchState.pauseSignal.paused = false;
300
- break;
301
- case "abort":
302
- batchState.pauseSignal.paused = true;
303
- break;
304
- }
305
- });
306
-
307
- // ── Callback factories (replace ctx-dependent callbacks) ─────
308
- const onNotify = (message: string, level: "info" | "warning" | "error") => {
309
- send({ type: "notify", msg: message, level });
310
- if (!batchState) return;
311
- // Sync batch state on every notify (lightweight — just the summary fields)
312
- send({ type: "state-sync", state: serializeBatchState(batchState) });
313
- };
314
-
315
- const onMonitorUpdate = (state: MonitorState) => {
316
- send({ type: "monitor-update", state });
317
- };
318
-
319
- const onEngineEvent = (event: EngineEvent) => {
320
- send({ type: "engine-event", event });
321
- };
322
-
323
- // TP-076: Supervisor alert callback — sends structured alerts to main thread
324
- const onSupervisorAlert = (alert: import("./types.ts").SupervisorAlert) => {
325
- send({ type: "supervisor-alert", alert });
326
- };
327
-
328
- // ── Execute engine ───────────────────────────────────────────
329
- const enginePromise = data.mode === "resume"
330
- ? resumeOrchBatch(
331
- data.orchConfig,
332
- data.runnerConfig,
333
- data.cwd,
334
- batchState,
335
- onNotify,
336
- onMonitorUpdate,
337
- wsConfig,
338
- data.workspaceRoot,
339
- data.agentRoot,
340
- data.force ?? false,
341
- onSupervisorAlert,
342
- data.supervisorAutonomy ?? "autonomous",
343
- )
344
- : executeOrchBatch(
345
- data.args ?? "",
346
- data.orchConfig,
347
- data.runnerConfig,
348
- data.cwd,
349
- batchState,
350
- onNotify,
351
- onMonitorUpdate,
352
- wsConfig,
353
- data.workspaceRoot,
354
- data.agentRoot,
355
- onEngineEvent,
356
- onSupervisorAlert,
357
- data.supervisorAutonomy ?? "autonomous",
358
- );
359
-
360
- enginePromise
361
- .then(() => {
362
- // Final state sync + completion signal
363
- const finalState = serializeBatchState(batchState);
364
- send({ type: "complete", state: finalState });
365
- // Disconnect IPC so the child process can exit cleanly
366
- process.disconnect?.();
367
- })
368
- .catch((err: unknown) => {
369
- const normalized = normalizeError(err);
370
- // Ensure batch state reflects the failure
371
- if (batchState.phase !== "completed" && batchState.phase !== "failed") {
372
- batchState.phase = "failed";
373
- batchState.endedAt = Date.now();
374
- batchState.errors.push(`Unhandled engine error: ${normalized.message}`);
375
- }
376
- send({ type: "state-sync", state: serializeBatchState(batchState) });
377
- send({ type: "error", source: "enginePromise", message: normalized.message, stack: normalized.stack });
378
- process.disconnect?.();
379
- });
380
- });
381
- }
1
+ /**
2
+ * Engine Child Process Entry Point (TP-071)
3
+ *
4
+ * This module serves two purposes:
5
+ * 1. Exports types and helpers used by extension.ts (main thread)
6
+ * 2. When forked as a child process, runs the engine in a separate Node.js process
7
+ *
8
+ * Uses child_process.fork() instead of worker_threads because Node v25's
9
+ * default --experimental-strip-types rejects .ts files inside node_modules.
10
+ * Fork creates a new process where --experimental-transform-types takes effect.
11
+ *
12
+ * Communication:
13
+ * - Child → Parent: process.send() for notify, monitor-update, engine-event, state-sync, complete, error
14
+ * - Parent → Child: child.send() for init, pause, resume, abort
15
+ *
16
+ * @module orch/engine-worker
17
+ */
18
+ import type {
19
+ AllocatedLane,
20
+ EngineEvent,
21
+ MonitorState,
22
+ OrchBatchPhase,
23
+ OrchBatchRuntimeState,
24
+ OrchestratorConfig,
25
+ SupervisorAlert,
26
+ TaskRunnerConfig,
27
+ WorkspaceConfig,
28
+ WorkspaceRepoConfig,
29
+ } from "./types.ts";
30
+
31
+ // ── Types for worker <-> main thread messages ────────────────────────
32
+
33
+ /**
34
+ * Messages sent FROM the worker TO the main thread.
35
+ */
36
+ export type WorkerErrorSource = "enginePromise" | "uncaughtException" | "unhandledRejection";
37
+
38
+ export type WorkerToMainMessage =
39
+ | { type: "notify"; msg: string; level: "info" | "warning" | "error" }
40
+ | { type: "monitor-update"; state: MonitorState }
41
+ | { type: "engine-event"; event: EngineEvent }
42
+ | { type: "supervisor-alert"; alert: SupervisorAlert }
43
+ | { type: "state-sync"; state: SerializedBatchState }
44
+ | { type: "complete"; state: SerializedBatchState }
45
+ | { type: "error"; message: string; stack?: string; source?: WorkerErrorSource };
46
+
47
+ /**
48
+ * Messages sent FROM the main thread TO the worker.
49
+ */
50
+ export type WorkerInMessage =
51
+ | { type: "pause" }
52
+ | { type: "resume" }
53
+ | { type: "abort" };
54
+
55
+ /**
56
+ * Serializable form of OrchBatchRuntimeState fields synced to main thread.
57
+ * Only includes fields the main thread needs for display/state tracking.
58
+ */
59
+ export interface SerializedBatchState {
60
+ phase: OrchBatchPhase;
61
+ batchId: string;
62
+ baseBranch: string;
63
+ orchBranch: string;
64
+ mode: string;
65
+ currentWaveIndex: number;
66
+ totalWaves: number;
67
+ totalTasks: number;
68
+ succeededTasks: number;
69
+ failedTasks: number;
70
+ skippedTasks: number;
71
+ blockedTasks: number;
72
+ startedAt: number;
73
+ endedAt: number | null;
74
+ errors: string[];
75
+ /** Active lanes for the current wave (synced so /orch-sessions works). */
76
+ currentLanes: AllocatedLane[];
77
+ }
78
+
79
+ /**
80
+ * Serializable form of WorkspaceConfig (Map → array of entries).
81
+ */
82
+ export interface SerializedWorkspaceConfig {
83
+ mode: string;
84
+ repos: Array<[string, WorkspaceRepoConfig]>;
85
+ routing: WorkspaceConfig["routing"];
86
+ configPath: string;
87
+ }
88
+
89
+ /**
90
+ * workerData shape passed from the main thread.
91
+ */
92
+ export interface EngineWorkerData {
93
+ /** Sentinel flag — distinguishes engine worker from test-runner worker threads */
94
+ engineWorker: true;
95
+ /** "execute" for new batch, "resume" for resume */
96
+ mode: "execute" | "resume";
97
+ /** User arguments (target string) — only for "execute" mode */
98
+ args?: string;
99
+ /** Orchestrator configuration */
100
+ orchConfig: OrchestratorConfig;
101
+ /** Task runner configuration */
102
+ runnerConfig: TaskRunnerConfig;
103
+ /** Repository root (cwd) */
104
+ cwd: string;
105
+ /** Workspace configuration (serialized) — null for repo mode */
106
+ workspaceConfig?: SerializedWorkspaceConfig | null;
107
+ /** Workspace root directory */
108
+ workspaceRoot?: string;
109
+ /** Agent root directory */
110
+ agentRoot?: string;
111
+ /** Force flag for resume */
112
+ force?: boolean;
113
+ /** Supervisor autonomy mode propagated to worker bridge tools. */
114
+ supervisorAutonomy?: "interactive" | "supervised" | "autonomous";
115
+ }
116
+
117
+ // ── Serialization helpers (used by both main thread and worker) ──────
118
+
119
+ /**
120
+ * Serialize WorkspaceConfig for cross-thread transfer.
121
+ * Converts the Map to an array of entries.
122
+ */
123
+ export function serializeWorkspaceConfig(
124
+ config: WorkspaceConfig | null | undefined,
125
+ ): SerializedWorkspaceConfig | null {
126
+ if (!config) return null;
127
+ return {
128
+ mode: config.mode,
129
+ repos: [...config.repos.entries()],
130
+ routing: config.routing,
131
+ configPath: config.configPath,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Reconstruct WorkspaceConfig from serialized form.
137
+ */
138
+ export function deserializeWorkspaceConfig(
139
+ serialized: SerializedWorkspaceConfig | null | undefined,
140
+ ): WorkspaceConfig | null {
141
+ if (!serialized) return null;
142
+ return {
143
+ mode: serialized.mode as WorkspaceConfig["mode"],
144
+ repos: new Map(serialized.repos),
145
+ routing: serialized.routing,
146
+ configPath: serialized.configPath,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Extract serializable batch state for sync back to main thread.
152
+ */
153
+ function serializeBatchState(state: OrchBatchRuntimeState): SerializedBatchState {
154
+ return {
155
+ phase: state.phase,
156
+ batchId: state.batchId,
157
+ baseBranch: state.baseBranch,
158
+ orchBranch: state.orchBranch,
159
+ mode: state.mode,
160
+ currentWaveIndex: state.currentWaveIndex,
161
+ totalWaves: state.totalWaves,
162
+ totalTasks: state.totalTasks,
163
+ succeededTasks: state.succeededTasks,
164
+ failedTasks: state.failedTasks,
165
+ skippedTasks: state.skippedTasks,
166
+ blockedTasks: state.blockedTasks,
167
+ startedAt: state.startedAt,
168
+ endedAt: state.endedAt,
169
+ errors: [...state.errors],
170
+ currentLanes: state.currentLanes,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Apply serialized batch state from worker to main-thread batch state.
176
+ *
177
+ * Updates only the fields that the worker thread tracks — preserves
178
+ * main-thread-only fields like pauseSignal, dependencyGraph, etc.
179
+ */
180
+ export function applySerializedState(
181
+ batchState: OrchBatchRuntimeState,
182
+ serialized: SerializedBatchState,
183
+ ): void {
184
+ batchState.phase = serialized.phase;
185
+ batchState.batchId = serialized.batchId;
186
+ batchState.baseBranch = serialized.baseBranch;
187
+ batchState.orchBranch = serialized.orchBranch;
188
+ batchState.mode = serialized.mode as OrchBatchRuntimeState["mode"];
189
+ batchState.currentWaveIndex = serialized.currentWaveIndex;
190
+ batchState.totalWaves = serialized.totalWaves;
191
+ batchState.totalTasks = serialized.totalTasks;
192
+ batchState.succeededTasks = serialized.succeededTasks;
193
+ batchState.failedTasks = serialized.failedTasks;
194
+ batchState.skippedTasks = serialized.skippedTasks;
195
+ batchState.blockedTasks = serialized.blockedTasks;
196
+ batchState.startedAt = serialized.startedAt;
197
+ batchState.endedAt = serialized.endedAt;
198
+ batchState.errors = [...serialized.errors];
199
+ batchState.currentLanes = serialized.currentLanes ?? [];
200
+ }
201
+
202
+ // ── Engine main (runs when launched as a forked child process) ───────
203
+
204
+ // Guard: only run engine main when launched via fork() with the sentinel env var.
205
+ if (process.env.TASKPLANE_ENGINE_FORK === "1" && typeof process.send === "function") {
206
+ const send = (msg: WorkerToMainMessage) => {
207
+ try {
208
+ process.send?.(msg);
209
+ } catch {
210
+ // best effort only
211
+ }
212
+ };
213
+
214
+ const sendWithAck = (msg: WorkerToMainMessage, onFlushed: () => void) => {
215
+ if (typeof process.send !== "function" || !process.connected) {
216
+ onFlushed();
217
+ return;
218
+ }
219
+
220
+ let flushed = false;
221
+ const done = () => {
222
+ if (flushed) return;
223
+ flushed = true;
224
+ onFlushed();
225
+ };
226
+
227
+ try {
228
+ (process.send as (
229
+ message: WorkerToMainMessage,
230
+ sendHandle?: unknown,
231
+ options?: unknown,
232
+ callback?: (error: Error | null) => void,
233
+ ) => boolean)(msg, undefined, undefined, () => done());
234
+ setTimeout(done, 75).unref();
235
+ } catch {
236
+ done();
237
+ }
238
+ };
239
+
240
+ const normalizeError = (err: unknown): { message: string; stack?: string } => {
241
+ if (err instanceof Error) return { message: err.message, stack: err.stack };
242
+ return { message: String(err) };
243
+ };
244
+
245
+ // Wait for the init message carrying workerData, then start the engine.
246
+ process.once("message", async (initMsg: { type: string; data: EngineWorkerData }) => {
247
+ if (initMsg?.type !== "init") return;
248
+
249
+ let batchState: OrchBatchRuntimeState | null = null;
250
+ let fatalHandled = false;
251
+ const reportFatalAndExit = (source: WorkerErrorSource, err: unknown) => {
252
+ if (fatalHandled) return;
253
+ fatalHandled = true;
254
+
255
+ const normalized = normalizeError(err);
256
+ if (batchState && batchState.phase !== "completed" && batchState.phase !== "failed") {
257
+ batchState.phase = "failed";
258
+ batchState.endedAt = Date.now();
259
+ batchState.errors.push(`[${source}] ${normalized.message}`);
260
+ }
261
+
262
+ if (batchState) send({ type: "state-sync", state: serializeBatchState(batchState) });
263
+ sendWithAck(
264
+ { type: "error", source, message: normalized.message, stack: normalized.stack },
265
+ () => process.exit(1),
266
+ );
267
+ setTimeout(() => process.exit(1), 200).unref();
268
+ };
269
+
270
+ process.once("uncaughtException", (err: unknown) => reportFatalAndExit("uncaughtException", err));
271
+ process.once("unhandledRejection", (reason: unknown) => reportFatalAndExit("unhandledRejection", reason));
272
+
273
+ // Dynamic imports — only loaded in engine context to avoid circular
274
+ // dependencies when this module is imported from extension.ts
275
+ const { executeOrchBatch } = await import("./engine.ts");
276
+ const { resumeOrchBatch } = await import("./resume.ts");
277
+ const { freshOrchBatchState } = await import("./types.ts");
278
+
279
+ const data = initMsg.data;
280
+
281
+ // Create a fresh batch state for this process
282
+ batchState = freshOrchBatchState();
283
+ batchState.phase = "launching";
284
+ batchState.startedAt = Date.now();
285
+
286
+ // Deserialize workspace config
287
+ const wsConfig = deserializeWorkspaceConfig(data.workspaceConfig);
288
+
289
+ // ── Control signal listener ──────────────────────────────────
290
+ // Main process sends pause/resume/abort signals via IPC.
291
+ // We apply them to the in-process batchState.pauseSignal.
292
+ process.on("message", (msg: WorkerInMessage) => {
293
+ if (!batchState) return;
294
+ switch (msg.type) {
295
+ case "pause":
296
+ batchState.pauseSignal.paused = true;
297
+ break;
298
+ case "resume":
299
+ batchState.pauseSignal.paused = false;
300
+ break;
301
+ case "abort":
302
+ batchState.pauseSignal.paused = true;
303
+ break;
304
+ }
305
+ });
306
+
307
+ // ── Callback factories (replace ctx-dependent callbacks) ─────
308
+ const onNotify = (message: string, level: "info" | "warning" | "error") => {
309
+ send({ type: "notify", msg: message, level });
310
+ if (!batchState) return;
311
+ // Sync batch state on every notify (lightweight — just the summary fields)
312
+ send({ type: "state-sync", state: serializeBatchState(batchState) });
313
+ };
314
+
315
+ const onMonitorUpdate = (state: MonitorState) => {
316
+ send({ type: "monitor-update", state });
317
+ };
318
+
319
+ const onEngineEvent = (event: EngineEvent) => {
320
+ send({ type: "engine-event", event });
321
+ };
322
+
323
+ // TP-076: Supervisor alert callback — sends structured alerts to main thread
324
+ const onSupervisorAlert = (alert: import("./types.ts").SupervisorAlert) => {
325
+ send({ type: "supervisor-alert", alert });
326
+ };
327
+
328
+ // ── Execute engine ───────────────────────────────────────────
329
+ const enginePromise = data.mode === "resume"
330
+ ? resumeOrchBatch(
331
+ data.orchConfig,
332
+ data.runnerConfig,
333
+ data.cwd,
334
+ batchState,
335
+ onNotify,
336
+ onMonitorUpdate,
337
+ wsConfig,
338
+ data.workspaceRoot,
339
+ data.agentRoot,
340
+ data.force ?? false,
341
+ onSupervisorAlert,
342
+ data.supervisorAutonomy ?? "autonomous",
343
+ )
344
+ : executeOrchBatch(
345
+ data.args ?? "",
346
+ data.orchConfig,
347
+ data.runnerConfig,
348
+ data.cwd,
349
+ batchState,
350
+ onNotify,
351
+ onMonitorUpdate,
352
+ wsConfig,
353
+ data.workspaceRoot,
354
+ data.agentRoot,
355
+ onEngineEvent,
356
+ onSupervisorAlert,
357
+ data.supervisorAutonomy ?? "autonomous",
358
+ );
359
+
360
+ enginePromise
361
+ .then(() => {
362
+ // Final state sync + completion signal
363
+ const finalState = serializeBatchState(batchState);
364
+ send({ type: "complete", state: finalState });
365
+ // Disconnect IPC so the child process can exit cleanly
366
+ process.disconnect?.();
367
+ })
368
+ .catch((err: unknown) => {
369
+ const normalized = normalizeError(err);
370
+ // Ensure batch state reflects the failure
371
+ if (batchState.phase !== "completed" && batchState.phase !== "failed") {
372
+ batchState.phase = "failed";
373
+ batchState.endedAt = Date.now();
374
+ batchState.errors.push(`Unhandled engine error: ${normalized.message}`);
375
+ }
376
+ send({ type: "state-sync", state: serializeBatchState(batchState) });
377
+ send({ type: "error", source: "enginePromise", message: normalized.message, stack: normalized.stack });
378
+ process.disconnect?.();
379
+ });
380
+ });
381
+ }