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.
- package/dashboard/public/app.js +66 -15
- package/dashboard/server.cjs +12 -4
- package/extensions/task-runner.ts +346 -127
- package/extensions/taskplane/config-loader.ts +1 -1
- package/extensions/taskplane/config-schema.ts +7 -5
- package/extensions/taskplane/execution.ts +153 -7
- package/extensions/taskplane/extension.ts +16 -3
- package/extensions/taskplane/merge.ts +91 -21
- package/extensions/taskplane/supervisor.ts +46 -3
- package/extensions/taskplane/types.ts +2 -2
- package/package.json +1 -1
- package/templates/agents/local/task-worker.md +3 -2
- package/templates/agents/task-worker.md +17 -13
- package/templates/config/task-orchestrator.yaml +1 -1
- package/templates/config/task-runner.yaml +3 -3
|
@@ -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 `
|
|
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
|
-
|
|
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
|
-
//
|
|
554
|
-
const
|
|
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
|
-
|
|
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
|
|
378
|
-
// Uses
|
|
379
|
-
//
|
|
380
|
-
// The merge request
|
|
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
|
-
//
|
|
389
|
-
|
|
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
|
-
//
|
|
392
|
-
|
|
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
|
-
|
|
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 ??
|
|
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:
|
|
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 =
|
|
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
|
@@ -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
|
-
-
|
|
17
|
-
-
|
|
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
|
|
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
|
|
8
|
-
|
|
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
|
|
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.
|
|
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
|
|
72
|
-
on git housekeeping without adding recovery value —
|
|
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
|
|
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
|
|
153
|
-
- Do NOT
|
|
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
|
|
@@ -57,9 +57,9 @@ reviewer:
|
|
|
57
57
|
thinking: "off"
|
|
58
58
|
|
|
59
59
|
context:
|
|
60
|
-
worker_context_window: 200000
|
|
61
|
-
warn_percent:
|
|
62
|
-
kill_percent:
|
|
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
|