taskplane 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -295,8 +295,18 @@ function tailJsonlFile(filePath) {
295
295
  return []; // No new data
296
296
  }
297
297
 
298
- // Read new bytes from offset to end of file
299
- const bytesToRead = fileSize - tailState.offset;
298
+ // Cap read size per tick to avoid ERR_STRING_TOO_LONG on large files.
299
+ // If there's more data remaining, the next SSE tick will pick up the rest.
300
+ const MAX_TAIL_BYTES = 10 * 1024 * 1024; // 10 MB per tick
301
+
302
+ // Skip-to-tail on fresh dashboard start with large files.
303
+ // The partial-line handling below already discards the first partial line.
304
+ if (tailState.offset === 0 && fileSize > MAX_TAIL_BYTES) {
305
+ tailState.offset = fileSize - MAX_TAIL_BYTES;
306
+ }
307
+
308
+ // Read new bytes from offset, capped to MAX_TAIL_BYTES
309
+ const bytesToRead = Math.min(fileSize - tailState.offset, MAX_TAIL_BYTES);
300
310
  const buf = Buffer.alloc(bytesToRead);
301
311
  let fd;
302
312
  try {
@@ -311,7 +321,7 @@ function tailJsonlFile(filePath) {
311
321
  return []; // Read error — try again next tick
312
322
  }
313
323
  fs.closeSync(fd);
314
- tailState.offset = fileSize;
324
+ tailState.offset += bytesToRead;
315
325
 
316
326
  // Split into lines, preserving partial trailing line
317
327
  const chunk = tailState.partial + buf.toString("utf-8");
@@ -0,0 +1,416 @@
1
+ /**
2
+ * Artifact cleanup and log rotation for orchestrator runtime files.
3
+ *
4
+ * Three cleanup layers prevent unbounded disk growth:
5
+ *
6
+ * 1. **Post-Integrate Cleanup** — Deletes batch-specific telemetry and merge
7
+ * result files after successful /orch-integrate. Scoped by batchId.
8
+ *
9
+ * 2. **Age-Based Preflight Sweep** — On /orch start, removes telemetry and
10
+ * merge artifacts older than 7 days. Catches files missed by Layer 1
11
+ * (e.g., aborted batches, manual branch deletions).
12
+ *
13
+ * 3. **Size-Capped Log Rotation** — Rotates append-only supervisor logs
14
+ * (events.jsonl, actions.jsonl) at a 5MB threshold during preflight.
15
+ * Keeps one .old generation.
16
+ *
17
+ * All cleanup is **non-fatal** — failures warn but never block execution.
18
+ *
19
+ * @module orch/cleanup
20
+ * @since TP-065
21
+ */
22
+ import { existsSync, readdirSync, statSync, unlinkSync, renameSync, mkdirSync } from "fs";
23
+ import { join } from "path";
24
+
25
+ // ── Layer 1: Post-Integrate Cleanup ─────────────────────────────────
26
+
27
+ /**
28
+ * Result of post-integrate artifact cleanup.
29
+ */
30
+ export interface PostIntegrateCleanupResult {
31
+ /** Number of telemetry files deleted */
32
+ telemetryFilesDeleted: number;
33
+ /** Number of merge result/request files deleted */
34
+ mergeFilesDeleted: number;
35
+ /** Number of lane prompt files deleted */
36
+ promptFilesDeleted: number;
37
+ /** Warnings from non-fatal cleanup failures */
38
+ warnings: string[];
39
+ }
40
+
41
+ /**
42
+ * Clean up batch-specific telemetry and merge result files after integrate.
43
+ *
44
+ * Targets files whose names contain the batchId:
45
+ * - `.pi/telemetry/*-{batchId}-*.jsonl` — worker/merger sidecar files
46
+ * - `.pi/telemetry/*-{batchId}-*-exit.json` — exit summaries
47
+ * - `.pi/telemetry/lane-prompt-*.txt` — temporary prompt files (all, not scoped)
48
+ * - `.pi/merge-result-*-{batchId}.json` — merge result files
49
+ * - `.pi/merge-request-*-{batchId}.txt` — merge request files
50
+ *
51
+ * @param stateRoot - Root directory containing .pi/ (workspace root or repo root)
52
+ * @param batchId - Batch ID to scope deletion
53
+ * @returns Cleanup result with counts and warnings
54
+ */
55
+ export function cleanupPostIntegrate(stateRoot: string, batchId: string): PostIntegrateCleanupResult {
56
+ const result: PostIntegrateCleanupResult = {
57
+ telemetryFilesDeleted: 0,
58
+ mergeFilesDeleted: 0,
59
+ promptFilesDeleted: 0,
60
+ warnings: [],
61
+ };
62
+
63
+ if (!batchId) {
64
+ result.warnings.push("No batchId provided — skipping post-integrate cleanup");
65
+ return result;
66
+ }
67
+
68
+ // ── Telemetry files (.pi/telemetry/) ─────────────────────────
69
+ const telemetryDir = join(stateRoot, ".pi", "telemetry");
70
+ if (existsSync(telemetryDir)) {
71
+ try {
72
+ const entries = readdirSync(telemetryDir);
73
+ for (const entry of entries) {
74
+ // Delete batch-scoped sidecar/exit files containing the batchId
75
+ if (entry.includes(batchId) && (entry.endsWith(".jsonl") || entry.endsWith("-exit.json"))) {
76
+ try {
77
+ unlinkSync(join(telemetryDir, entry));
78
+ result.telemetryFilesDeleted++;
79
+ } catch (err: unknown) {
80
+ result.warnings.push(`Failed to delete telemetry file ${entry}: ${(err as Error).message}`);
81
+ }
82
+ }
83
+ // Delete all lane-prompt-*.txt files (not batch-scoped — they're
84
+ // temporary and should be cleaned up with any batch)
85
+ if (entry.startsWith("lane-prompt-") && entry.endsWith(".txt")) {
86
+ try {
87
+ unlinkSync(join(telemetryDir, entry));
88
+ result.promptFilesDeleted++;
89
+ } catch (err: unknown) {
90
+ result.warnings.push(`Failed to delete prompt file ${entry}: ${(err as Error).message}`);
91
+ }
92
+ }
93
+ }
94
+ } catch (err: unknown) {
95
+ result.warnings.push(`Failed to read telemetry directory: ${(err as Error).message}`);
96
+ }
97
+ }
98
+
99
+ // ── Merge result/request files (.pi/) ────────────────────────
100
+ const piDir = join(stateRoot, ".pi");
101
+ if (existsSync(piDir)) {
102
+ try {
103
+ const entries = readdirSync(piDir);
104
+ for (const entry of entries) {
105
+ if (entry.includes(batchId) && (
106
+ (entry.startsWith("merge-result-") && entry.endsWith(".json")) ||
107
+ (entry.startsWith("merge-request-") && entry.endsWith(".txt"))
108
+ )) {
109
+ try {
110
+ unlinkSync(join(piDir, entry));
111
+ result.mergeFilesDeleted++;
112
+ } catch (err: unknown) {
113
+ result.warnings.push(`Failed to delete merge file ${entry}: ${(err as Error).message}`);
114
+ }
115
+ }
116
+ }
117
+ } catch (err: unknown) {
118
+ result.warnings.push(`Failed to read .pi directory: ${(err as Error).message}`);
119
+ }
120
+ }
121
+
122
+ return result;
123
+ }
124
+
125
+ /**
126
+ * Format post-integrate cleanup result for user-facing notification.
127
+ */
128
+ export function formatPostIntegrateCleanup(result: PostIntegrateCleanupResult): string {
129
+ const parts: string[] = [];
130
+ const totalDeleted = result.telemetryFilesDeleted + result.mergeFilesDeleted + result.promptFilesDeleted;
131
+
132
+ if (totalDeleted > 0) {
133
+ const segments: string[] = [];
134
+ if (result.telemetryFilesDeleted > 0) segments.push(`${result.telemetryFilesDeleted} telemetry`);
135
+ if (result.mergeFilesDeleted > 0) segments.push(`${result.mergeFilesDeleted} merge`);
136
+ if (result.promptFilesDeleted > 0) segments.push(`${result.promptFilesDeleted} prompt`);
137
+ parts.push(`🧹 Cleaned up ${totalDeleted} artifact file(s): ${segments.join(", ")}`);
138
+ }
139
+
140
+ for (const warning of result.warnings) {
141
+ parts.push(` ⚠️ ${warning}`);
142
+ }
143
+
144
+ return parts.join("\n");
145
+ }
146
+
147
+ // ── Layer 2: Age-Based Preflight Sweep ──────────────────────────────
148
+
149
+ /** Default max age for stale artifacts (7 days in milliseconds). */
150
+ export const STALE_ARTIFACT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
151
+
152
+ /**
153
+ * Result of a preflight age-based sweep.
154
+ */
155
+ export interface PreflightSweepResult {
156
+ /** Number of stale files deleted */
157
+ staleFilesDeleted: number;
158
+ /** Whether the sweep was skipped (e.g., active batch) */
159
+ skipped: boolean;
160
+ /** Reason for skipping (if skipped) */
161
+ skipReason?: string;
162
+ /** Warnings from non-fatal cleanup failures */
163
+ warnings: string[];
164
+ }
165
+
166
+ /**
167
+ * Dependencies injected into sweepStaleArtifacts for testability.
168
+ */
169
+ export interface SweepDeps {
170
+ /** Check if a batch is currently active (phase is not terminal). */
171
+ isBatchActive: () => boolean;
172
+ /** Get the current timestamp (for deterministic testing). */
173
+ now: () => number;
174
+ }
175
+
176
+ /**
177
+ * Sweep stale artifacts older than maxAgeMs during preflight.
178
+ *
179
+ * Targets:
180
+ * - `.pi/telemetry/*.jsonl` — sidecar files
181
+ * - `.pi/telemetry/*-exit.json` — exit summaries
182
+ * - `.pi/telemetry/lane-prompt-*.txt` — temporary prompt files
183
+ * - `.pi/merge-result-*.json` — merge result files
184
+ * - `.pi/merge-request-*.txt` — merge request files
185
+ *
186
+ * Uses file mtime for age detection. Skips files modified within maxAgeMs.
187
+ * If a batch is currently active (executing/merging), skips ALL cleanup.
188
+ *
189
+ * @param stateRoot - Root directory containing .pi/
190
+ * @param deps - Injectable dependencies for testability
191
+ * @param maxAgeMs - Maximum file age in milliseconds (default: 7 days)
192
+ * @returns Sweep result with count and warnings
193
+ */
194
+ export function sweepStaleArtifacts(
195
+ stateRoot: string,
196
+ deps: SweepDeps,
197
+ maxAgeMs: number = STALE_ARTIFACT_MAX_AGE_MS,
198
+ ): PreflightSweepResult {
199
+ const result: PreflightSweepResult = {
200
+ staleFilesDeleted: 0,
201
+ skipped: false,
202
+ warnings: [],
203
+ };
204
+
205
+ // Guard: skip if batch is actively executing
206
+ try {
207
+ if (deps.isBatchActive()) {
208
+ result.skipped = true;
209
+ result.skipReason = "Active batch detected — skipping stale artifact sweep";
210
+ return result;
211
+ }
212
+ } catch {
213
+ // If we can't determine batch state, proceed cautiously
214
+ }
215
+
216
+ const now = deps.now();
217
+ const cutoff = now - maxAgeMs;
218
+
219
+ /**
220
+ * Delete files older than cutoff from a directory, matching a filter.
221
+ */
222
+ const sweepDir = (dir: string, filter: (name: string) => boolean): void => {
223
+ if (!existsSync(dir)) return;
224
+ try {
225
+ const entries = readdirSync(dir);
226
+ for (const entry of entries) {
227
+ if (!filter(entry)) continue;
228
+ const filePath = join(dir, entry);
229
+ try {
230
+ const stat = statSync(filePath);
231
+ if (!stat.isFile()) continue;
232
+ if (stat.mtimeMs < cutoff) {
233
+ unlinkSync(filePath);
234
+ result.staleFilesDeleted++;
235
+ }
236
+ } catch (err: unknown) {
237
+ result.warnings.push(`Failed to process ${entry}: ${(err as Error).message}`);
238
+ }
239
+ }
240
+ } catch (err: unknown) {
241
+ result.warnings.push(`Failed to read directory ${dir}: ${(err as Error).message}`);
242
+ }
243
+ };
244
+
245
+ // Sweep telemetry files
246
+ sweepDir(join(stateRoot, ".pi", "telemetry"), (name) =>
247
+ name.endsWith(".jsonl") ||
248
+ name.endsWith("-exit.json") ||
249
+ (name.startsWith("lane-prompt-") && name.endsWith(".txt")),
250
+ );
251
+
252
+ // Sweep merge result/request files
253
+ sweepDir(join(stateRoot, ".pi"), (name) =>
254
+ (name.startsWith("merge-result-") && name.endsWith(".json")) ||
255
+ (name.startsWith("merge-request-") && name.endsWith(".txt")),
256
+ );
257
+
258
+ return result;
259
+ }
260
+
261
+ /**
262
+ * Format preflight sweep result for logging.
263
+ */
264
+ export function formatPreflightSweep(result: PreflightSweepResult): string {
265
+ if (result.skipped) {
266
+ return `ℹ️ Preflight sweep skipped: ${result.skipReason}`;
267
+ }
268
+ if (result.staleFilesDeleted === 0 && result.warnings.length === 0) {
269
+ return ""; // Nothing to report
270
+ }
271
+ const parts: string[] = [];
272
+ if (result.staleFilesDeleted > 0) {
273
+ parts.push(`🧹 Preflight cleanup: removed ${result.staleFilesDeleted} stale artifact(s) (>7 days old)`);
274
+ }
275
+ for (const warning of result.warnings) {
276
+ parts.push(` ⚠️ ${warning}`);
277
+ }
278
+ return parts.join("\n");
279
+ }
280
+
281
+ // ── Layer 3: Size-Capped Log Rotation ───────────────────────────────
282
+
283
+ /** Default rotation threshold: 5MB. */
284
+ export const LOG_ROTATION_THRESHOLD_BYTES = 5 * 1024 * 1024;
285
+
286
+ /**
287
+ * Result of log rotation.
288
+ */
289
+ export interface LogRotationResult {
290
+ /** Files that were rotated */
291
+ rotated: string[];
292
+ /** Warnings from non-fatal rotation failures */
293
+ warnings: string[];
294
+ }
295
+
296
+ /**
297
+ * Rotate supervisor append-only logs at a size threshold.
298
+ *
299
+ * Checks `events.jsonl` and `actions.jsonl` in `.pi/supervisor/`.
300
+ * If a file exceeds the threshold, renames it to `.old` (overwriting
301
+ * any existing `.old`), allowing a fresh file to be created on next write.
302
+ *
303
+ * Only call during preflight (not mid-batch).
304
+ *
305
+ * @param stateRoot - Root directory containing .pi/
306
+ * @param thresholdBytes - Maximum file size before rotation (default: 5MB)
307
+ * @returns Rotation result
308
+ */
309
+ export function rotateSupervisorLogs(
310
+ stateRoot: string,
311
+ thresholdBytes: number = LOG_ROTATION_THRESHOLD_BYTES,
312
+ ): LogRotationResult {
313
+ const result: LogRotationResult = {
314
+ rotated: [],
315
+ warnings: [],
316
+ };
317
+
318
+ const supervisorDir = join(stateRoot, ".pi", "supervisor");
319
+ if (!existsSync(supervisorDir)) {
320
+ return result; // Nothing to rotate
321
+ }
322
+
323
+ const filesToRotate = ["events.jsonl", "actions.jsonl"];
324
+
325
+ for (const fileName of filesToRotate) {
326
+ const filePath = join(supervisorDir, fileName);
327
+ if (!existsSync(filePath)) continue;
328
+
329
+ try {
330
+ const stat = statSync(filePath);
331
+ if (!stat.isFile() || stat.size <= thresholdBytes) continue;
332
+
333
+ const oldPath = `${filePath}.old`;
334
+ renameSync(filePath, oldPath);
335
+ result.rotated.push(fileName);
336
+ } catch (err: unknown) {
337
+ result.warnings.push(`Failed to rotate ${fileName}: ${(err as Error).message}`);
338
+ }
339
+ }
340
+
341
+ return result;
342
+ }
343
+
344
+ /**
345
+ * Format log rotation result for logging.
346
+ */
347
+ export function formatLogRotation(result: LogRotationResult): string {
348
+ if (result.rotated.length === 0 && result.warnings.length === 0) {
349
+ return ""; // Nothing to report
350
+ }
351
+ const parts: string[] = [];
352
+ if (result.rotated.length > 0) {
353
+ parts.push(`🔄 Rotated ${result.rotated.length} supervisor log(s): ${result.rotated.join(", ")}`);
354
+ }
355
+ for (const warning of result.warnings) {
356
+ parts.push(` ⚠️ ${warning}`);
357
+ }
358
+ return parts.join("\n");
359
+ }
360
+
361
+ // ── Combined Preflight Cleanup ──────────────────────────────────────
362
+
363
+ /**
364
+ * Combined result of preflight cleanup (Layer 2 + Layer 3).
365
+ */
366
+ export interface PreflightCleanupResult {
367
+ sweep: PreflightSweepResult;
368
+ rotation: LogRotationResult;
369
+ }
370
+
371
+ /**
372
+ * Run all preflight cleanup operations (Layer 2 + Layer 3).
373
+ *
374
+ * Called from the engine's preflight phase before batch starts.
375
+ * Always non-fatal.
376
+ *
377
+ * @param stateRoot - Root directory containing .pi/
378
+ * @param deps - Sweep dependencies (active batch check)
379
+ * @returns Combined cleanup result
380
+ */
381
+ export function runPreflightCleanup(
382
+ stateRoot: string,
383
+ deps: SweepDeps,
384
+ ): PreflightCleanupResult {
385
+ const sweep = sweepStaleArtifacts(stateRoot, deps);
386
+ const rotation = rotateSupervisorLogs(stateRoot);
387
+ return { sweep, rotation };
388
+ }
389
+
390
+ /**
391
+ * Format combined preflight cleanup result for user notification.
392
+ *
393
+ * Returns an empty string if nothing happened (no files cleaned/rotated).
394
+ */
395
+ export function formatPreflightCleanup(result: PreflightCleanupResult): string {
396
+ const parts: string[] = [];
397
+
398
+ // Layer 2: age-based sweep
399
+ if (!result.sweep.skipped && result.sweep.staleFilesDeleted > 0) {
400
+ parts.push(`removed ${result.sweep.staleFilesDeleted} stale artifact(s) (>7 days old)`);
401
+ }
402
+
403
+ // Layer 3: log rotation
404
+ if (result.rotation.rotated.length > 0) {
405
+ parts.push(`rotated ${result.rotation.rotated.join(", ")} (>5 MB)`);
406
+ }
407
+
408
+ // Collect warnings from both layers
409
+ const warnings = [...result.sweep.warnings, ...result.rotation.warnings];
410
+ if (warnings.length > 0) {
411
+ parts.push(`⚠️ ${warnings.length} cleanup warning(s)`);
412
+ }
413
+
414
+ if (parts.length === 0) return "";
415
+ return `🧹 Preflight cleanup: ${parts.join("; ")}`;
416
+ }
@@ -16,12 +16,13 @@ import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolic
16
16
  import type { CleanupGateRepoFailure } from "./messages.ts";
17
17
  import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
18
18
  import { resolveOperatorId } from "./naming.ts";
19
- import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
19
+ import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, loadBatchState, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
20
20
  import { listOrchSessions } from "./sessions.ts";
21
21
  import { buildEngineEventBase, defaultResilienceState, FATAL_DISCOVERY_CODES, generateBatchId, TIER0_RETRYABLE_CLASSIFICATIONS, TIER0_RETRY_BUDGETS, tier0ScopeKey, tier0WaveScopeKey } from "./types.ts";
22
22
  import type { AllocatedLane, AllocatedTask, BatchHistorySummary, BatchTaskSummary, BatchWaveSummary, DiscoveryResult, EngineEventCallback, EscalationContext, LaneExecutionResult, LaneTaskOutcome, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, TaskRunnerConfig, Tier0EscalationPattern, Tier0RecoveryPattern, TokenCounts, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
23
23
  import { buildDependencyGraph, computeWaves, resolveBaseBranch, resolveRepoRoot, validateGraph } from "./waves.ts";
24
24
  import { deleteBranchBestEffort, forceCleanupWorktree, formatPreflightResults, listWorktrees, preserveFailedLaneProgress, removeAllWorktrees, removeWorktree, runPreflight, safeResetWorktree, sleepSync } from "./worktree.ts";
25
+ import { runPreflightCleanup, formatPreflightCleanup } from "./cleanup.ts";
25
26
 
26
27
  // ── Tier 0: Automatic Recovery Helpers (TP-039) ─────────────────────
27
28
 
@@ -853,6 +854,39 @@ export async function executeOrchBatch(
853
854
  return;
854
855
  }
855
856
 
857
+ // ── TP-065: Preflight artifact cleanup (Layer 2 + Layer 3) ───
858
+ // Sweep stale artifacts and rotate oversized logs before batch starts.
859
+ // Always non-fatal — failures warn but never block batch execution.
860
+ try {
861
+ // Layer 2: Age-based sweep of stale telemetry/merge artifacts (>7 days)
862
+ const sweepResult = sweepStaleArtifacts(stateRoot, {
863
+ isBatchActive: () => {
864
+ // Check persisted state — a prior batch may still be active
865
+ try {
866
+ const state = loadBatchState(stateRoot);
867
+ if (state && state.phase !== "completed" && state.phase !== "failed" && state.phase !== "stopped") {
868
+ return true;
869
+ }
870
+ } catch { /* state unreadable — safe to sweep */ }
871
+ return false;
872
+ },
873
+ now: () => Date.now(),
874
+ });
875
+ const sweepMsg = formatPreflightSweep(sweepResult);
876
+ if (sweepMsg) {
877
+ onNotify(sweepMsg, "info");
878
+ }
879
+
880
+ // Layer 3: Size-capped rotation of supervisor append-only logs
881
+ const rotationResult = rotateSupervisorLogs(stateRoot);
882
+ const rotationMsg = formatLogRotation(rotationResult);
883
+ if (rotationMsg) {
884
+ onNotify(rotationMsg, "info");
885
+ }
886
+ } catch {
887
+ // Non-fatal — never block batch start for cleanup errors
888
+ }
889
+
856
890
  // Discovery — task area paths in task-runner.yaml are workspace-relative.
857
891
  // In repo mode workspaceRoot === repoRoot, so this is always correct.
858
892
  const discoveryRoot = workspaceRoot ?? cwd;
@@ -47,6 +47,7 @@ import { buildExecutionContext } from "./workspace.ts";
47
47
  import { openSettingsTui } from "./settings-tui.ts";
48
48
  import { loadProjectConfig } from "./config-loader.ts";
49
49
  import { runMigrations } from "./migrations.ts";
50
+ import { cleanupPostIntegrate, formatPostIntegrateCleanup, sweepStaleArtifacts, formatPreflightSweep, rotateSupervisorLogs, formatLogRotation } from "./cleanup.ts";
50
51
  import {
51
52
  activateSupervisor,
52
53
  deactivateSupervisor,
@@ -977,6 +978,12 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string): Integ
977
978
  deleteStaleBranches(repoRoot, opId, context.batchId);
978
979
  dropBatchAutostash(repoRoot, context.batchId);
979
980
  } catch { /* best effort — don't fail integration for cleanup errors */ }
981
+
982
+ // TP-065: Post-integrate artifact cleanup (Layer 1).
983
+ // Also runs on the supervisor auto-integration path.
984
+ try {
985
+ cleanupPostIntegrate(repoRoot, context.batchId);
986
+ } catch { /* best effort — don't fail integration for cleanup errors */ }
980
987
  }
981
988
 
982
989
  return result;
@@ -2282,6 +2289,29 @@ export default function (pi: ExtensionAPI) {
2282
2289
 
2283
2290
  try { deleteBatchState(repoRoot); } catch { /* best effort */ }
2284
2291
 
2292
+ // ── TP-065: Post-integrate artifact cleanup (Layer 1) ────
2293
+ // Delete batch-specific telemetry and merge result files.
2294
+ // Non-fatal — failures warn but don't block integration.
2295
+ if (batchId) {
2296
+ try {
2297
+ const artifactCleanup = cleanupPostIntegrate(repoRoot, batchId);
2298
+ const totalCleaned = artifactCleanup.telemetryFilesDeleted + artifactCleanup.mergeFilesDeleted + artifactCleanup.promptFilesDeleted;
2299
+ if (totalCleaned > 0) {
2300
+ outputLines.push(
2301
+ `🧹 Cleaned up ${artifactCleanup.telemetryFilesDeleted} telemetry file(s), ` +
2302
+ `${artifactCleanup.mergeFilesDeleted} merge result(s), ` +
2303
+ `${artifactCleanup.promptFilesDeleted} prompt file(s) for batch ${batchId}`,
2304
+ );
2305
+ }
2306
+ if (artifactCleanup.warnings.length > 0) {
2307
+ hasWarning = true;
2308
+ outputLines.push(`⚠️ Artifact cleanup warnings: ${artifactCleanup.warnings.join("; ")}`);
2309
+ }
2310
+ } catch {
2311
+ // Non-fatal — never block integration for cleanup failures
2312
+ }
2313
+ }
2314
+
2285
2315
  const integrationSummary = wsConfig
2286
2316
  ? `✅ Integrated ${resolvedOrchBranch} across ${reposToIntegrate.length} repo(s).\n${repoMessages.join("\n")}\n${totalCommits} total commit(s) applied.`
2287
2317
  : `${repoMessages[0] || "✅ Integrated."}\n${commitsAhead} commit(s) applied.`;
@@ -25,3 +25,4 @@ export * from "./workspace.ts";
25
25
  export * from "./diagnostics.ts";
26
26
  export * from "./supervisor.ts";
27
27
  export * from "./migrations.ts";
28
+ export * from "./cleanup.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",