taskplane 0.7.2 → 0.8.1

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.
@@ -2,9 +2,10 @@
2
2
  * Lane execution, monitoring, wave execution loop
3
3
  * @module orch/execution
4
4
  */
5
- import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync } from "fs";
5
+ import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync, writeFileSync } from "fs";
6
6
  import { spawnSync } from "child_process";
7
7
  import { join, dirname, resolve, relative, delimiter as pathDelimiter } from "path";
8
+ import { tmpdir, userInfo } from "os";
8
9
 
9
10
  import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
10
11
  import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
@@ -58,6 +59,108 @@ function resolveTaskRunnerExtensionPath(repoRoot: string): string {
58
59
  return localPath;
59
60
  }
60
61
 
62
+ // ── RPC Wrapper Path Resolution ──────────────────────────────────────
63
+
64
+ /**
65
+ * Find the rpc-wrapper.mjs path for lane sessions.
66
+ *
67
+ * Resolution order mirrors resolveTaskRunnerExtensionPath:
68
+ * 1. Local project: {repoRoot}/bin/rpc-wrapper.mjs (for taskplane dev)
69
+ * 2. Global npm (Windows): {APPDATA}/npm/node_modules/taskplane/bin/rpc-wrapper.mjs
70
+ * 3. Global npm (Unix): /usr/local/lib/node_modules/taskplane/bin/rpc-wrapper.mjs
71
+ * 4. npm peer: resolve from pi's location
72
+ *
73
+ * @throws ExecutionError if rpc-wrapper.mjs cannot be found anywhere
74
+ */
75
+ export function resolveRpcWrapperPath(repoRoot: string): string {
76
+ const wrapperFile = join("bin", "rpc-wrapper.mjs");
77
+
78
+ // 1. Local project (taskplane development)
79
+ const localPath = join(resolve(repoRoot), wrapperFile);
80
+ if (existsSync(localPath)) return localPath;
81
+
82
+ // 2. Global npm install paths
83
+ const home = process.env.HOME || process.env.USERPROFILE || "";
84
+ const candidates: string[] = [];
85
+ if (process.env.APPDATA) {
86
+ candidates.push(join(process.env.APPDATA, "npm", "node_modules", "taskplane", wrapperFile));
87
+ }
88
+ if (home) {
89
+ candidates.push(join(home, "AppData", "Roaming", "npm", "node_modules", "taskplane", wrapperFile));
90
+ candidates.push(join(home, ".npm-global", "lib", "node_modules", "taskplane", wrapperFile));
91
+ }
92
+ candidates.push(join("/usr", "local", "lib", "node_modules", "taskplane", wrapperFile));
93
+
94
+ // 3. Peer of pi's package
95
+ try {
96
+ const piPath = process.argv[1] || "";
97
+ const piPkgDir = resolve(piPath, "..", "..");
98
+ candidates.push(join(piPkgDir, "..", "taskplane", wrapperFile));
99
+ } catch { /* ignore */ }
100
+
101
+ for (const candidate of candidates) {
102
+ if (existsSync(candidate)) return candidate;
103
+ }
104
+
105
+ // Fallback: return the local path (will fail at spawn time with a clear error)
106
+ return localPath;
107
+ }
108
+
109
+ // ── Telemetry Path Generation ────────────────────────────────────────
110
+
111
+ /**
112
+ * Generate telemetry file paths for a lane session.
113
+ *
114
+ * Naming contract from resilience roadmap:
115
+ * .pi/telemetry/{opId}-{batchId}-{repoId}[-{taskId}][-lane-{N}]-{role}.{ext}
116
+ *
117
+ * @param sessionName - TMUX session name (e.g., "orch-lane-1")
118
+ * @param sidecarRoot - Root dir for sidecar files (e.g., <workspace>/.pi or <repo>/.pi)
119
+ * @param taskId - Task identifier (e.g., "TP-049")
120
+ * @returns { sidecarPath, exitSummaryPath, telemetryDir }
121
+ */
122
+ export function generateTelemetryPaths(
123
+ sessionName: string,
124
+ sidecarRoot: string,
125
+ taskId?: string,
126
+ ): { sidecarPath: string; exitSummaryPath: string; telemetryDir: string } {
127
+ const telemetryTs = Date.now();
128
+
129
+ // Resolve opId: same priority chain as naming.ts resolveOperatorId()
130
+ let opId = "op";
131
+ const envOpId = process.env.TASKPLANE_OPERATOR_ID;
132
+ if (envOpId?.trim()) {
133
+ opId = envOpId.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
134
+ } else {
135
+ try {
136
+ const username = userInfo().username;
137
+ if (username?.trim()) {
138
+ opId = username.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
139
+ }
140
+ } catch { /* userInfo() can throw on some platforms */ }
141
+ }
142
+
143
+ const batchId = String(telemetryTs);
144
+ const repoId = "default";
145
+
146
+ // Extract role from sessionName — lane sessions are "worker" role
147
+ const role = "worker";
148
+ const laneMatch = sessionName.match(/lane-(\d+)/);
149
+ const laneSuffix = laneMatch ? `-lane-${laneMatch[1]}` : "";
150
+
151
+ // Include taskId when available
152
+ const taskIdSegment = taskId
153
+ ? `-${taskId.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30)}`
154
+ : "";
155
+ const telemetryBasename = `${opId}-${batchId}-${repoId}${taskIdSegment}${laneSuffix}-${role}`;
156
+ const telemetryDir = join(sidecarRoot, "telemetry");
157
+ if (!existsSync(telemetryDir)) mkdirSync(telemetryDir, { recursive: true });
158
+ const sidecarPath = join(telemetryDir, `${telemetryBasename}.jsonl`);
159
+ const exitSummaryPath = join(telemetryDir, `${telemetryBasename}-exit.json`);
160
+
161
+ return { sidecarPath, exitSummaryPath, telemetryDir };
162
+ }
163
+
61
164
  // ── Execution Helpers ────────────────────────────────────────────────
62
165
 
63
166
  /**
@@ -231,7 +334,12 @@ export function toTmuxPath(pathValue: string): string {
231
334
  *
232
335
  * Constructs a properly escaped command that:
233
336
  * 1. Sets env vars (TASK_AUTOSTART, TASK_RUNNER_SPAWN_MODE, TASK_RUNNER_TMUX_PREFIX)
234
- * 2. Runs `pi --no-session -e extensions/task-runner.ts` in the worktree directory
337
+ * 2. Runs `node rpc-wrapper.mjs` to spawn pi with the task-runner extension,
338
+ * producing structured telemetry (sidecar JSONL + exit summary JSON).
339
+ *
340
+ * The RPC wrapper spawns pi in RPC mode with the task-runner extension loaded.
341
+ * The extension's TASK_AUTOSTART env var triggers task execution on init.
342
+ * A minimal prompt file is created to satisfy the wrapper's --prompt-file requirement.
235
343
  *
236
344
  * Shell escaping: env var values are single-quoted to prevent expansion.
237
345
  * Path args are single-quoted to handle spaces and special characters.
@@ -241,6 +349,8 @@ export function toTmuxPath(pathValue: string): string {
241
349
  * @param repoRoot - Absolute path to main repo (for extension absolute path)
242
350
  * @param envVars - Environment variables to set
243
351
  * @param laneLogPath - Optional path to write lane session stdout/stderr
352
+ * @param sidecarPath - Path for RPC telemetry sidecar JSONL file
353
+ * @param exitSummaryPath - Path for RPC telemetry exit summary JSON file
244
354
  * @returns Array of arguments for spawnSync("tmux", args)
245
355
  */
246
356
  export function buildTmuxSpawnArgs(
@@ -249,6 +359,8 @@ export function buildTmuxSpawnArgs(
249
359
  repoRoot: string,
250
360
  envVars: Record<string, string>,
251
361
  laneLogPath?: string,
362
+ sidecarPath?: string,
363
+ exitSummaryPath?: string,
252
364
  ): string[] {
253
365
  // Shell-quote a value for safe embedding in a command string.
254
366
  // Wraps in single quotes, escaping any internal single quotes.
@@ -260,18 +372,44 @@ export function buildTmuxSpawnArgs(
260
372
  };
261
373
 
262
374
  // Build the command string that runs inside the TMUX session.
263
- // Format: ENV_VAR1=value1 ENV_VAR2=value2 pi --no-session -e extensions/task-runner.ts
264
375
  const envParts = Object.entries(envVars)
265
376
  .map(([key, val]) => `${key}=${shellQuote(val)}`)
266
377
  .join(" ");
267
378
 
268
379
  const taskRunnerExtPath = resolveTaskRunnerExtensionPath(repoRoot);
269
- const basePiCommand = `${envParts} pi --no-session -e ${shellQuote(taskRunnerExtPath)}`;
380
+
381
+ let piCommand: string;
382
+
383
+ if (sidecarPath && exitSummaryPath) {
384
+ // ── RPC Wrapper mode: structured telemetry ──────────────
385
+ // Spawn `node rpc-wrapper.mjs` instead of `pi` directly.
386
+ // The wrapper runs pi in RPC mode, captures telemetry to
387
+ // sidecar JSONL, and writes exit summary on process exit.
388
+ const rpcWrapperPath = resolveRpcWrapperPath(repoRoot);
389
+
390
+ // Create a minimal prompt file for the RPC wrapper.
391
+ // The task-runner extension handles execution via TASK_AUTOSTART;
392
+ // this prompt satisfies the wrapper's --prompt-file requirement.
393
+ const promptId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
394
+ const promptTmpFile = join(tmpdir(), `pi-lane-prompt-${promptId}.txt`);
395
+ writeFileSync(promptTmpFile, "Execute the task as configured by the task-runner extension.");
396
+
397
+ piCommand = [
398
+ envParts,
399
+ "node", shellQuote(rpcWrapperPath),
400
+ "--sidecar-path", shellQuote(sidecarPath),
401
+ "--exit-summary-path", shellQuote(exitSummaryPath),
402
+ "--prompt-file", shellQuote(promptTmpFile),
403
+ "--extensions", shellQuote(taskRunnerExtPath),
404
+ ].filter(Boolean).join(" ");
405
+ } else {
406
+ // ── Legacy mode: direct pi spawn (no telemetry) ─────────
407
+ piCommand = `${envParts} pi --no-session -e ${shellQuote(taskRunnerExtPath)}`;
408
+ }
270
409
 
271
410
  // NOTE: Do not redirect lane output here. Shell redirection has proven
272
411
  // fragile across Windows + tmux environments and can prevent session spawn.
273
412
  // Diagnostics use tmux pane capture + STATUS tail in pollUntilTaskComplete().
274
- const piCommand = basePiCommand;
275
413
 
276
414
  const tmuxWorktreePath = toTmuxPath(worktreePath);
277
415
  const wrappedCommand = `cd ${shellQuote(tmuxWorktreePath)} && ${piCommand}`;
@@ -550,8 +688,16 @@ export function spawnLaneSession(
550
688
  // Best effort — session can still run without log file setup
551
689
  }
552
690
 
553
- // Build tmux args
554
- const tmuxArgs = buildTmuxSpawnArgs(sessionName, lane.worktreePath, repoRoot, envVars, laneLogRelativePath);
691
+ // Generate telemetry file paths for RPC wrapper sidecar
692
+ const sidecarRoot = join(workspaceRoot || repoRoot, ".pi");
693
+ const telemetry = generateTelemetryPaths(sessionName, sidecarRoot, task.taskId);
694
+ execLog(laneId, task.taskId, "telemetry paths generated", {
695
+ sidecar: telemetry.sidecarPath,
696
+ exitSummary: telemetry.exitSummaryPath,
697
+ });
698
+
699
+ // Build tmux args (with RPC wrapper telemetry)
700
+ const tmuxArgs = buildTmuxSpawnArgs(sessionName, lane.worktreePath, repoRoot, envVars, laneLogRelativePath, telemetry.sidecarPath, telemetry.exitSummaryPath);
555
701
 
556
702
  // Clean up stale session if exists
557
703
  if (tmuxHasSession(sessionName)) {
@@ -43,6 +43,7 @@ import {
43
43
  } from "./index.ts";
44
44
  import { buildExecutionContext } from "./workspace.ts";
45
45
  import { openSettingsTui } from "./settings-tui.ts";
46
+ import { loadProjectConfig } from "./config-loader.ts";
46
47
  import {
47
48
  activateSupervisor,
48
49
  deactivateSupervisor,
@@ -767,10 +768,11 @@ export function validateModelAvailability(
767
768
  runnerConfig: TaskRunnerConfig,
768
769
  supervisorConfig: SupervisorConfig,
769
770
  ctx: ExtensionContext,
771
+ agentModels?: { workerModel?: string; reviewerModel?: string },
770
772
  ): ModelCheckResult[] {
771
773
  const entries: ModelCheckEntry[] = [
772
- { role: "Worker", modelStr: runnerConfig.worker?.model ?? "" },
773
- { role: "Reviewer", modelStr: runnerConfig.reviewer?.model ?? "" },
774
+ { role: "Worker", modelStr: agentModels?.workerModel ?? (runnerConfig as any).worker?.model ?? "" },
775
+ { role: "Reviewer", modelStr: agentModels?.reviewerModel ?? (runnerConfig as any).reviewer?.model ?? "" },
774
776
  { role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
775
777
  { role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
776
778
  ];
@@ -1401,7 +1403,18 @@ export default function (pi: ExtensionAPI) {
1401
1403
  // Validate that all configured agent models are resolvable in
1402
1404
  // the model registry before starting. Catches misconfigured
1403
1405
  // model names early instead of failing hours into a batch.
1404
- const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx);
1406
+ // Note: runnerConfig (TaskRunnerConfig) is a stripped type without
1407
+ // worker/reviewer model fields. Load the full unified config to
1408
+ // get the actual agent model strings (including user preferences).
1409
+ let agentModels: { workerModel?: string; reviewerModel?: string } | undefined;
1410
+ try {
1411
+ const fullConfig = loadProjectConfig(execCtx!.repoRoot);
1412
+ agentModels = {
1413
+ workerModel: fullConfig.taskRunner.worker.model || "",
1414
+ reviewerModel: fullConfig.taskRunner.reviewer.model || "",
1415
+ };
1416
+ } catch { /* fall through — validateModelAvailability handles empty strings */ }
1417
+ const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx, agentModels);
1405
1418
  const modelFailures = modelResults.filter(r => r.status === "not-found");
1406
1419
  ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
1407
1420
  if (modelFailures.length > 0) {
@@ -6,7 +6,7 @@ import { readFileSync, writeFileSync, existsSync, unlinkSync, copyFileSync, mkdi
6
6
  import { execSync, spawnSync } from "child_process";
7
7
  import { join, dirname, resolve, relative } from "path";
8
8
 
9
- import { buildLaneEnvVars, buildTmuxSpawnArgs, execLog, tmuxHasSession, tmuxKillSession, toTmuxPath } from "./execution.ts";
9
+ import { buildLaneEnvVars, buildTmuxSpawnArgs, execLog, generateTelemetryPaths, resolveRpcWrapperPath, tmuxHasSession, tmuxKillSession, toTmuxPath } from "./execution.ts";
10
10
  import { resolveOperatorId } from "./naming.ts";
11
11
  import { MERGE_POLL_INTERVAL_MS, MERGE_RESULT_GRACE_MS, MERGE_RESULT_READ_RETRIES, MERGE_RESULT_READ_RETRY_DELAY_MS, MERGE_SPAWN_RETRY_MAX, MERGE_TIMEOUT_MAX_RETRIES, MERGE_TIMEOUT_MS, MergeError, VALID_MERGE_STATUSES } from "./types.ts";
12
12
  import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
@@ -18,6 +18,58 @@ import { loadOrchestratorConfig } from "./config.ts";
18
18
  import { captureBaseline, diffFingerprints, runVerificationCommands, parseTestOutput, deduplicateFingerprints } from "./verification.ts";
19
19
  import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./verification.ts";
20
20
 
21
+ // ── Merge Telemetry Helpers ───────────────────────────────────────────
22
+
23
+ import { userInfo } from "os";
24
+
25
+ /**
26
+ * Generate telemetry file paths for a merge agent session.
27
+ *
28
+ * Naming: {opId}-{batchId}-{repoId}[-merge-w{N}-lane-{N}]-merger.{ext}
29
+ * Role is always "merger" to distinguish from worker/reviewer in the dashboard.
30
+ *
31
+ * @param sessionName - TMUX session name (e.g., "orch-merge-1")
32
+ * @param sidecarRoot - Root dir for sidecar files (e.g., <workspace>/.pi)
33
+ * @returns { sidecarPath, exitSummaryPath }
34
+ */
35
+ function generateMergeTelemetryPaths(
36
+ sessionName: string,
37
+ sidecarRoot: string,
38
+ ): { sidecarPath: string; exitSummaryPath: string } {
39
+ const telemetryTs = Date.now();
40
+
41
+ // Resolve opId: same priority chain as execution.ts
42
+ let opId = "op";
43
+ const envOpId = process.env.TASKPLANE_OPERATOR_ID;
44
+ if (envOpId?.trim()) {
45
+ opId = envOpId.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
46
+ } else {
47
+ try {
48
+ const username = userInfo().username;
49
+ if (username?.trim()) {
50
+ opId = username.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
51
+ }
52
+ } catch { /* userInfo() can throw on some platforms */ }
53
+ }
54
+
55
+ const batchId = String(telemetryTs);
56
+ const repoId = "default";
57
+
58
+ // Extract merge-specific info from sessionName (e.g., "orch-merge-1")
59
+ const mergeMatch = sessionName.match(/merge-(\d+)/);
60
+ const mergeSuffix = mergeMatch ? `-merge-${mergeMatch[1]}` : "";
61
+
62
+ const role = "merger";
63
+ const telemetryBasename = `${opId}-${batchId}-${repoId}${mergeSuffix}-${role}`;
64
+ const telemetryDir = join(sidecarRoot, "telemetry");
65
+ if (!existsSync(telemetryDir)) mkdirSync(telemetryDir, { recursive: true });
66
+
67
+ return {
68
+ sidecarPath: join(telemetryDir, `${telemetryBasename}.jsonl`),
69
+ exitSummaryPath: join(telemetryDir, `${telemetryBasename}-exit.json`),
70
+ };
71
+ }
72
+
21
73
  // ── Merge Implementation ─────────────────────────────────────────────
22
74
 
23
75
  /**
@@ -374,10 +426,10 @@ export async function spawnMergeAgent(
374
426
  await sleepAsync(500);
375
427
  }
376
428
 
377
- // Build the pi command for the merge agent.
378
- // Uses --no-session to prevent interactive session management.
379
- // --append-system-prompt loads the merger agent definition.
380
- // The merge request file is passed as a prompt via @file syntax.
429
+ // Build the merge agent command.
430
+ // Uses rpc-wrapper.mjs to produce structured telemetry (sidecar JSONL + exit summary).
431
+ // The merger agent definition is loaded via --system-prompt-file.
432
+ // The merge request is passed as the --prompt-file.
381
433
  const shellQuote = (s: string): string => {
382
434
  if (/[\s"'`$\\!&|;()<>{}#*?~]/.test(s)) {
383
435
  return `'${s.replace(/'/g, "'\\''")}'`;
@@ -385,25 +437,43 @@ export async function spawnMergeAgent(
385
437
  return s;
386
438
  };
387
439
 
388
- // Build model args if specified
389
- const modelArgs = config.merge.model ? `--model ${shellQuote(config.merge.model)}` : "";
440
+ // Generate telemetry paths for this merge session
441
+ // Naming: {opId}-{batchId}-{repoId}-merger.jsonl (or with wave/lane info from sessionName)
442
+ const sidecarRoot = join(stateRoot ?? repoRoot, ".pi");
443
+ const telemetry = generateMergeTelemetryPaths(sessionName, sidecarRoot);
444
+ execLog("merge", sessionName, "telemetry paths generated", {
445
+ sidecar: telemetry.sidecarPath,
446
+ exitSummary: telemetry.exitSummaryPath,
447
+ });
448
+
449
+ // Resolve paths
450
+ const rpcWrapperPath = resolveRpcWrapperPath(repoRoot);
451
+ const systemPromptPath = agentRoot ? join(agentRoot, "task-merger.md") : join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md");
452
+
453
+ // Build RPC wrapper command
454
+ const wrapperParts = [
455
+ "TERM=xterm-256color",
456
+ "node", shellQuote(rpcWrapperPath),
457
+ "--sidecar-path", shellQuote(telemetry.sidecarPath),
458
+ "--exit-summary-path", shellQuote(telemetry.exitSummaryPath),
459
+ "--prompt-file", shellQuote(mergeRequestPath),
460
+ "--system-prompt-file", shellQuote(systemPromptPath),
461
+ ];
390
462
 
391
- // Build tools override if specified
392
- const toolsArgs = config.merge.tools ? `--tools ${shellQuote(config.merge.tools)}` : "";
463
+ // Add model args if specified
464
+ if (config.merge.model) {
465
+ wrapperParts.push("--model", shellQuote(config.merge.model));
466
+ }
467
+
468
+ // Add tools override if specified
469
+ if (config.merge.tools) {
470
+ wrapperParts.push("--tools", shellQuote(config.merge.tools));
471
+ }
393
472
 
394
- const piCommand = [
395
- "pi --no-session",
396
- modelArgs,
397
- toolsArgs,
398
- `--append-system-prompt ${shellQuote(agentRoot ? join(agentRoot, "task-merger.md") : join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md"))}`,
399
- `@${shellQuote(mergeRequestPath)}`,
400
- ].filter(Boolean).join(" ");
473
+ const piCommand = wrapperParts.filter(Boolean).join(" ");
401
474
 
402
475
  const tmuxMergeDir = toTmuxPath(mergeWorkDir);
403
- // Pi's TUI (ink/react) hangs silently with TERM=tmux-256color (tmux default).
404
- // Force xterm-256color so pi can render and start execution.
405
- // Same fix as buildTmuxSpawnArgs / buildLaneEnvVars.
406
- const wrappedCommand = `cd ${shellQuote(tmuxMergeDir)} && TERM=xterm-256color ${piCommand}`;
476
+ const wrappedCommand = `cd ${shellQuote(tmuxMergeDir)} && ${piCommand}`;
407
477
  const tmuxArgs = [
408
478
  "new-session", "-d",
409
479
  "-s", sessionName,
@@ -449,7 +519,7 @@ export async function spawnMergeAgent(
449
519
  export function reloadMergeTimeoutMs(configRoot: string, pointerConfigRoot?: string): number {
450
520
  try {
451
521
  const freshConfig = loadOrchestratorConfig(configRoot, pointerConfigRoot);
452
- const minutes = freshConfig.merge.timeout_minutes ?? 10;
522
+ const minutes = freshConfig.merge.timeout_minutes ?? 90;
453
523
  return minutes * 60 * 1000;
454
524
  } catch (err: unknown) {
455
525
  // Config re-read is best-effort — fall back to default on failure
@@ -2028,7 +2028,38 @@ When the conversation reaches the config generation phase, create ALL of these
2028
2028
  - \`.gitignore\` entries — add Taskplane working file patterns if not already present
2029
2029
 
2030
2030
  Use conservative creation: check if each file exists before writing. If files
2031
- already exist (partial setup), read and merge rather than overwrite.`;
2031
+ already exist (partial setup), read and merge rather than overwrite.
2032
+
2033
+ ### CRITICAL: Task Area Registration
2034
+
2035
+ **Every task folder MUST be registered in \`.pi/taskplane-config.json\` under
2036
+ \`taskRunner.taskAreas\`.** Without registration, \`/orch all\` will fail with
2037
+ "no task areas configured" — even if the folders and tasks physically exist.
2038
+
2039
+ When creating a task folder (e.g., \`taskplane-tasks/\`):
2040
+ 1. Create the folder and its \`CONTEXT.md\`
2041
+ 2. Register it in \`.pi/taskplane-config.json\`:
2042
+ \`\`\`json
2043
+ {
2044
+ "taskRunner": {
2045
+ "taskAreas": {
2046
+ "general": {
2047
+ "path": "taskplane-tasks",
2048
+ "prefix": "TP",
2049
+ "context": "taskplane-tasks/CONTEXT.md"
2050
+ }
2051
+ }
2052
+ }
2053
+ }
2054
+ \`\`\`
2055
+ 3. **Verify** by reading the config back to confirm the area is registered
2056
+
2057
+ When creating tasks inside an area, check that the area is registered first.
2058
+ If it's not (e.g., operator created the folder manually), register it before
2059
+ proceeding.
2060
+
2061
+ This also applies when creating tasks later in the conversation — always verify
2062
+ the task area is registered in the config before offering to run \`/orch all\`.`;
2032
2063
  break;
2033
2064
 
2034
2065
  case "pending-tasks":
@@ -2068,7 +2099,14 @@ Follow the primer's **"Script 6: Batch Planning"** section
2068
2099
  5. **Offer a health check** (Script 7) if the operator prefers to assess
2069
2100
  project state rather than create tasks
2070
2101
  6. **Graceful fallback**: If \`gh\` CLI is unavailable, skip GitHub checks and
2071
- mention it to the operator — continue with CONTEXT.md and TODO scanning`;
2102
+ mention it to the operator — continue with CONTEXT.md and TODO scanning
2103
+
2104
+ ### Important: Task Area Verification
2105
+
2106
+ Before creating any tasks, verify that the target task area folder is registered
2107
+ in \`.pi/taskplane-config.json\` under \`taskRunner.taskAreas\`. If it's missing
2108
+ (e.g., the folder exists but was never registered), register it first. Without
2109
+ registration, \`/orch all\` will fail with "no task areas configured."`;
2072
2110
  break;
2073
2111
 
2074
2112
  case "completed-batch":
@@ -3045,11 +3083,16 @@ export function startHeartbeat(
3045
3083
  return;
3046
3084
  }
3047
3085
 
3048
- // Update heartbeat
3086
+ // Update heartbeat (and refresh batchId if it was initially unknown)
3049
3087
  try {
3050
3088
  const lock = readLockfile(stateRoot);
3051
3089
  if (lock && lock.sessionId === sessionId) {
3052
3090
  lock.heartbeat = new Date().toISOString();
3091
+ // TP-130: batchId may have been "(initializing)" at lock creation
3092
+ // because the batch hadn't started yet. Refresh from live state ref.
3093
+ if (state.batchStateRef?.batchId && lock.batchId !== state.batchStateRef.batchId) {
3094
+ lock.batchId = state.batchStateRef.batchId;
3095
+ }
3053
3096
  writeLockfile(stateRoot, lock);
3054
3097
  }
3055
3098
  } catch {
@@ -182,7 +182,7 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
182
182
  tools: "read,write,edit,bash,grep,find,ls",
183
183
  verify: [],
184
184
  order: "fewest-files-first",
185
- timeout_minutes: 10,
185
+ timeout_minutes: 90,
186
186
  },
187
187
  failure: {
188
188
  on_task_failure: "skip-dependents",
@@ -1225,7 +1225,7 @@ export class MergeError extends Error {
1225
1225
  * is generous and covers verification (go build) on large codebases.
1226
1226
  */
1227
1227
  /** Default merge agent timeout. Use config.merge.timeout_minutes to override. */
1228
- export const MERGE_TIMEOUT_MS = 10 * 60 * 1000;
1228
+ export const MERGE_TIMEOUT_MS = 90 * 60 * 1000;
1229
1229
 
1230
1230
  /**
1231
1231
  * Polling interval for merge result file (ms).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -13,8 +13,9 @@ name: task-worker
13
13
 
14
14
  The base prompt (maintained by taskplane) handles:
15
15
  - STATUS.md-first workflow and checkpoint discipline
16
- - Fresh-context loop behavior and iteration rules
17
- - Git commit conventions and .DONE file creation
16
+ - Multi-step execution (worker handles all remaining steps per invocation)
17
+ - Iteration recovery (context limit next invocation resumes from STATUS.md)
18
+ - Git commit conventions (per-step commits) and .DONE file creation
18
19
  - Review response handling
19
20
 
20
21
  Add project-specific rules below. Common examples:
@@ -1,21 +1,25 @@
1
1
  ---
2
2
  name: task-worker
3
- description: Autonomous task execution agent — works on individual steps with checkpoint discipline
3
+ description: Autonomous task execution agent — works through remaining steps with checkpoint discipline
4
4
  tools: read,write,edit,bash,grep,find,ls
5
5
  # model:
6
6
  ---
7
- You are a task execution agent running in a **fresh-context loop**. Each time you
8
- are invoked, you have ZERO memory of prior invocations. STATUS.md on disk is your
9
- ONLY memory.
7
+ You are a task execution agent. You may be invoked multiple times across
8
+ iterations each invocation starts with ZERO memory of prior ones.
9
+ STATUS.md on disk is your ONLY memory.
10
+
11
+ Your prompt tells you which steps remain. Work through them **in order**,
12
+ completing each step before moving to the next.
10
13
 
11
14
  ## Resume Algorithm (MANDATORY — Do This First)
12
15
 
13
16
  1. Read STATUS.md completely
14
- 2. Find the step you have been assigned (specified in your prompt)
17
+ 2. Find the **first incomplete step** listed in your prompt
15
18
  3. **Hydrate if needed** (see STATUS.md Hydration below)
16
19
  4. Within that step, find the **first unchecked checkbox** (`- [ ]`)
17
20
  5. Resume from there — do NOT redo checked items (`- [x]`)
18
- 6. If all items in your assigned step are checked, report completion
21
+ 6. When a step's items are all checked, proceed to the next incomplete step
22
+ 7. If all steps are complete, report completion
19
23
 
20
24
  ## Checkpoint Discipline (CRITICAL)
21
25
 
@@ -68,9 +72,9 @@ dozens of micro-commits that nobody reads.
68
72
 
69
73
  STATUS.md is the worker's memory, not git. Checking off items in STATUS.md
70
74
  ensures the next worker iteration knows where to resume. Git commits preserve
71
- file changes at meaningful milestones. Per-checkbox commits waste tool calls
72
- on git housekeeping without adding recovery value — the files are already on
73
- disk in the worktree.
75
+ file changes at meaningful milestones — one per completed step. Per-checkbox
76
+ commits waste tool calls on git housekeeping without adding recovery value —
77
+ the files are already on disk in the worktree.
74
78
 
75
79
  ## STATUS.md Hydration (MANDATORY)
76
80
 
@@ -94,7 +98,7 @@ instead of solving the problem.
94
98
 
95
99
  Before implementing anything, assess whether the step needs expansion:
96
100
 
97
- 1. **Read the PROMPT.md step details** for your assigned step
101
+ 1. **Read the PROMPT.md step details** for the step you're entering
98
102
  2. **Look for `⚠️ Hydrate` markers** — these signal the task creator expected
99
103
  you to expand based on runtime discoveries
100
104
  3. **If expansion is needed**, add checkboxes for **distinct outcomes** you've
@@ -149,9 +153,9 @@ When a reviewer returns REVISE with specific feedback items:
149
153
 
150
154
  ## Scope Rules
151
155
 
152
- - Work ONLY on the step assigned in your prompt
153
- - Do NOT proceed to other steps
154
- - Do NOT expand task scope
156
+ - Work through all remaining steps listed in your prompt, **in order**
157
+ - Do NOT skip ahead complete each step before starting the next
158
+ - Do NOT expand task scope beyond what the steps require
155
159
  - If you discover something out of scope, note it in STATUS.md Discoveries table
156
160
 
157
161
  ## Self-Documentation
@@ -77,7 +77,7 @@ merge:
77
77
  order: "fewest-files-first"
78
78
 
79
79
  # Merge agent timeout in minutes. Increase for large batches with many files.
80
- timeout_minutes: 10
80
+ timeout_minutes: 90
81
81
 
82
82
  # ── Failure Handling ──────────────────────────────────────────────────
83
83
 
@@ -57,9 +57,9 @@ reviewer:
57
57
  thinking: "off"
58
58
 
59
59
  context:
60
- worker_context_window: 200000
61
- warn_percent: 70
62
- kill_percent: 85
60
+ # worker_context_window: 200000 # 0 or omit = auto-detect from model registry; set explicitly to override
61
+ warn_percent: 85
62
+ kill_percent: 95
63
63
  max_worker_iterations: 20
64
64
  max_review_cycles: 2
65
65
  no_progress_limit: 3