taskplane 0.8.1 → 0.8.2
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.
|
@@ -5,7 +5,7 @@
|
|
|
5
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 {
|
|
8
|
+
import { userInfo } from "os";
|
|
9
9
|
|
|
10
10
|
import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
|
|
11
11
|
import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
@@ -106,6 +106,35 @@ export function resolveRpcWrapperPath(repoRoot: string): string {
|
|
|
106
106
|
return localPath;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// ── Telemetry Helpers ────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve the operator ID for telemetry filenames.
|
|
113
|
+
*
|
|
114
|
+
* Priority: TASKPLANE_OPERATOR_ID env → OS username → "op" fallback.
|
|
115
|
+
* Shared by lane and merge telemetry path generators to avoid divergence.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveTelemOpId(): string {
|
|
118
|
+
const envOpId = process.env.TASKPLANE_OPERATOR_ID;
|
|
119
|
+
if (envOpId?.trim()) {
|
|
120
|
+
return envOpId.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const username = userInfo().username;
|
|
124
|
+
if (username?.trim()) {
|
|
125
|
+
return username.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12) || "op";
|
|
126
|
+
}
|
|
127
|
+
} catch { /* userInfo() can throw on some platforms */ }
|
|
128
|
+
return "op";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sanitize a string for use in telemetry filenames.
|
|
133
|
+
*/
|
|
134
|
+
function sanitizeForFilename(s: string, maxLen: number = 30): string {
|
|
135
|
+
return s.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLen);
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
// ── Telemetry Path Generation ────────────────────────────────────────
|
|
110
139
|
|
|
111
140
|
/**
|
|
@@ -117,31 +146,20 @@ export function resolveRpcWrapperPath(repoRoot: string): string {
|
|
|
117
146
|
* @param sessionName - TMUX session name (e.g., "orch-lane-1")
|
|
118
147
|
* @param sidecarRoot - Root dir for sidecar files (e.g., <workspace>/.pi or <repo>/.pi)
|
|
119
148
|
* @param taskId - Task identifier (e.g., "TP-049")
|
|
149
|
+
* @param batchId - Actual batch ID from batch state (falls back to timestamp)
|
|
150
|
+
* @param repoId - Repo ID for workspace mode (falls back to "default")
|
|
120
151
|
* @returns { sidecarPath, exitSummaryPath, telemetryDir }
|
|
121
152
|
*/
|
|
122
153
|
export function generateTelemetryPaths(
|
|
123
154
|
sessionName: string,
|
|
124
155
|
sidecarRoot: string,
|
|
125
156
|
taskId?: string,
|
|
157
|
+
batchId?: string,
|
|
158
|
+
repoId?: string,
|
|
126
159
|
): { sidecarPath: string; exitSummaryPath: string; telemetryDir: string } {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
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";
|
|
160
|
+
const opId = resolveTelemOpId();
|
|
161
|
+
const effectiveBatchId = batchId || String(Date.now());
|
|
162
|
+
const effectiveRepoId = repoId || "default";
|
|
145
163
|
|
|
146
164
|
// Extract role from sessionName — lane sessions are "worker" role
|
|
147
165
|
const role = "worker";
|
|
@@ -149,10 +167,8 @@ export function generateTelemetryPaths(
|
|
|
149
167
|
const laneSuffix = laneMatch ? `-lane-${laneMatch[1]}` : "";
|
|
150
168
|
|
|
151
169
|
// Include taskId when available
|
|
152
|
-
const taskIdSegment = taskId
|
|
153
|
-
|
|
154
|
-
: "";
|
|
155
|
-
const telemetryBasename = `${opId}-${batchId}-${repoId}${taskIdSegment}${laneSuffix}-${role}`;
|
|
170
|
+
const taskIdSegment = taskId ? `-${sanitizeForFilename(taskId)}` : "";
|
|
171
|
+
const telemetryBasename = `${opId}-${effectiveBatchId}-${effectiveRepoId}${taskIdSegment}${laneSuffix}-${role}`;
|
|
156
172
|
const telemetryDir = join(sidecarRoot, "telemetry");
|
|
157
173
|
if (!existsSync(telemetryDir)) mkdirSync(telemetryDir, { recursive: true });
|
|
158
174
|
const sidecarPath = join(telemetryDir, `${telemetryBasename}.jsonl`);
|
|
@@ -390,8 +406,12 @@ export function buildTmuxSpawnArgs(
|
|
|
390
406
|
// Create a minimal prompt file for the RPC wrapper.
|
|
391
407
|
// The task-runner extension handles execution via TASK_AUTOSTART;
|
|
392
408
|
// this prompt satisfies the wrapper's --prompt-file requirement.
|
|
409
|
+
// Written to the sidecar dir (not tmpdir) so it's co-located with
|
|
410
|
+
// telemetry artifacts and cleaned up with them after the batch.
|
|
393
411
|
const promptId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
394
|
-
const
|
|
412
|
+
const promptDir = dirname(sidecarPath);
|
|
413
|
+
if (!existsSync(promptDir)) mkdirSync(promptDir, { recursive: true });
|
|
414
|
+
const promptTmpFile = join(promptDir, `lane-prompt-${promptId}.txt`);
|
|
395
415
|
writeFileSync(promptTmpFile, "Execute the task as configured by the task-runner extension.");
|
|
396
416
|
|
|
397
417
|
piCommand = [
|
|
@@ -690,7 +710,7 @@ export function spawnLaneSession(
|
|
|
690
710
|
|
|
691
711
|
// Generate telemetry file paths for RPC wrapper sidecar
|
|
692
712
|
const sidecarRoot = join(workspaceRoot || repoRoot, ".pi");
|
|
693
|
-
const telemetry = generateTelemetryPaths(sessionName, sidecarRoot, task.taskId);
|
|
713
|
+
const telemetry = generateTelemetryPaths(sessionName, sidecarRoot, task.taskId, config.orchestrator?.batchId, lane.repoId);
|
|
694
714
|
execLog(laneId, task.taskId, "telemetry paths generated", {
|
|
695
715
|
sidecar: telemetry.sidecarPath,
|
|
696
716
|
exitSummary: telemetry.exitSummaryPath,
|
|
@@ -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, generateTelemetryPaths, resolveRpcWrapperPath, tmuxHasSession, tmuxKillSession, toTmuxPath } from "./execution.ts";
|
|
9
|
+
import { buildLaneEnvVars, buildTmuxSpawnArgs, execLog, generateTelemetryPaths, resolveRpcWrapperPath, resolveTelemOpId, 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";
|
|
@@ -20,47 +20,37 @@ import type { VerificationBaseline, FingerprintDiff, TestFingerprint } from "./v
|
|
|
20
20
|
|
|
21
21
|
// ── Merge Telemetry Helpers ───────────────────────────────────────────
|
|
22
22
|
|
|
23
|
-
import { userInfo } from "os";
|
|
24
|
-
|
|
25
23
|
/**
|
|
26
24
|
* Generate telemetry file paths for a merge agent session.
|
|
27
25
|
*
|
|
28
|
-
*
|
|
26
|
+
* Uses the shared resolveTelemOpId() from execution.ts to avoid
|
|
27
|
+
* opId resolution divergence.
|
|
28
|
+
*
|
|
29
|
+
* Naming: {opId}-{batchId}-{repoId}[-merge-{N}]-merger.{ext}
|
|
29
30
|
* Role is always "merger" to distinguish from worker/reviewer in the dashboard.
|
|
30
31
|
*
|
|
31
32
|
* @param sessionName - TMUX session name (e.g., "orch-merge-1")
|
|
32
33
|
* @param sidecarRoot - Root dir for sidecar files (e.g., <workspace>/.pi)
|
|
34
|
+
* @param batchId - Actual batch ID from batch state (falls back to timestamp)
|
|
35
|
+
* @param repoId - Repo ID for workspace mode (falls back to "default")
|
|
33
36
|
* @returns { sidecarPath, exitSummaryPath }
|
|
34
37
|
*/
|
|
35
38
|
function generateMergeTelemetryPaths(
|
|
36
39
|
sessionName: string,
|
|
37
40
|
sidecarRoot: string,
|
|
41
|
+
batchId?: string,
|
|
42
|
+
repoId?: string,
|
|
38
43
|
): { sidecarPath: string; exitSummaryPath: string } {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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";
|
|
44
|
+
const opId = resolveTelemOpId();
|
|
45
|
+
const effectiveBatchId = batchId || String(Date.now());
|
|
46
|
+
const effectiveRepoId = repoId || "default";
|
|
57
47
|
|
|
58
48
|
// Extract merge-specific info from sessionName (e.g., "orch-merge-1")
|
|
59
49
|
const mergeMatch = sessionName.match(/merge-(\d+)/);
|
|
60
50
|
const mergeSuffix = mergeMatch ? `-merge-${mergeMatch[1]}` : "";
|
|
61
51
|
|
|
62
52
|
const role = "merger";
|
|
63
|
-
const telemetryBasename = `${opId}-${
|
|
53
|
+
const telemetryBasename = `${opId}-${effectiveBatchId}-${effectiveRepoId}${mergeSuffix}-${role}`;
|
|
64
54
|
const telemetryDir = join(sidecarRoot, "telemetry");
|
|
65
55
|
if (!existsSync(telemetryDir)) mkdirSync(telemetryDir, { recursive: true });
|
|
66
56
|
|
|
@@ -438,7 +428,6 @@ export async function spawnMergeAgent(
|
|
|
438
428
|
};
|
|
439
429
|
|
|
440
430
|
// Generate telemetry paths for this merge session
|
|
441
|
-
// Naming: {opId}-{batchId}-{repoId}-merger.jsonl (or with wave/lane info from sessionName)
|
|
442
431
|
const sidecarRoot = join(stateRoot ?? repoRoot, ".pi");
|
|
443
432
|
const telemetry = generateMergeTelemetryPaths(sessionName, sidecarRoot);
|
|
444
433
|
execLog("merge", sessionName, "telemetry paths generated", {
|
|
@@ -448,7 +437,19 @@ export async function spawnMergeAgent(
|
|
|
448
437
|
|
|
449
438
|
// Resolve paths
|
|
450
439
|
const rpcWrapperPath = resolveRpcWrapperPath(repoRoot);
|
|
451
|
-
|
|
440
|
+
|
|
441
|
+
// Resolve merger agent definition — check existence and fall back gracefully.
|
|
442
|
+
// Fresh projects that haven't run `taskplane init` may not have .pi/agents/task-merger.md.
|
|
443
|
+
const systemPromptCandidates = [
|
|
444
|
+
agentRoot ? join(agentRoot, "task-merger.md") : "",
|
|
445
|
+
join(stateRoot ?? repoRoot, ".pi", "agents", "task-merger.md"),
|
|
446
|
+
].filter(Boolean);
|
|
447
|
+
let systemPromptPath = systemPromptCandidates.find(p => existsSync(p)) || "";
|
|
448
|
+
if (!systemPromptPath) {
|
|
449
|
+
execLog("merge", sessionName, "WARNING: merger agent definition not found — merge agent will use default system prompt", {
|
|
450
|
+
candidates: systemPromptCandidates,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
452
453
|
|
|
453
454
|
// Build RPC wrapper command
|
|
454
455
|
const wrapperParts = [
|
|
@@ -457,9 +458,14 @@ export async function spawnMergeAgent(
|
|
|
457
458
|
"--sidecar-path", shellQuote(telemetry.sidecarPath),
|
|
458
459
|
"--exit-summary-path", shellQuote(telemetry.exitSummaryPath),
|
|
459
460
|
"--prompt-file", shellQuote(mergeRequestPath),
|
|
460
|
-
"--system-prompt-file", shellQuote(systemPromptPath),
|
|
461
461
|
];
|
|
462
462
|
|
|
463
|
+
// Only pass --system-prompt-file when the file exists (fresh projects
|
|
464
|
+
// may not have .pi/agents/task-merger.md — rpc-wrapper would crash).
|
|
465
|
+
if (systemPromptPath) {
|
|
466
|
+
wrapperParts.push("--system-prompt-file", shellQuote(systemPromptPath));
|
|
467
|
+
}
|
|
468
|
+
|
|
463
469
|
// Add model args if specified
|
|
464
470
|
if (config.merge.model) {
|
|
465
471
|
wrapperParts.push("--model", shellQuote(config.merge.model));
|