taskplane 0.24.15 → 0.24.16

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,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
2
2
  import { Type } from "@mariozechner/pi-ai";
3
3
 
4
4
  import { execSync, execFileSync } from "child_process";
5
- import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, createWriteStream } from "fs";
5
+ import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync, readFileSync, statSync, createWriteStream, renameSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { fork, type ChildProcess } from "child_process";
@@ -365,6 +365,49 @@ export interface IntegrationExecDeps {
365
365
  deleteBatchState: () => void;
366
366
  }
367
367
 
368
+ interface BatchHistorySnapshot {
369
+ filePath: string;
370
+ raw: string;
371
+ }
372
+
373
+ /**
374
+ * Preserve `.pi/batch-history.json` across integration merges.
375
+ *
376
+ * Runtime history is sidecar state, not source-controlled content. In environments
377
+ * where `.pi/batch-history.json` was previously tracked, merge/checkouts can
378
+ * replace newer runtime history with stale branch snapshots. This helper snapshots
379
+ * the file before integration and restores it afterward (best effort).
380
+ */
381
+ export function withPreservedBatchHistory<T>(stateRoot: string, operation: () => T): T {
382
+ const historyPath = join(stateRoot, ".pi", "batch-history.json");
383
+ let snapshot: BatchHistorySnapshot | null = null;
384
+ try {
385
+ if (existsSync(historyPath)) {
386
+ snapshot = {
387
+ filePath: historyPath,
388
+ raw: readFileSync(historyPath, "utf-8"),
389
+ };
390
+ }
391
+ } catch {
392
+ // Best effort only — integration should never fail due to snapshot capture.
393
+ }
394
+
395
+ try {
396
+ return operation();
397
+ } finally {
398
+ if (!snapshot) return;
399
+ try {
400
+ const dir = dirname(snapshot.filePath);
401
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
402
+ const tmpPath = snapshot.filePath + ".tmp";
403
+ writeFileSync(tmpPath, snapshot.raw);
404
+ renameSync(tmpPath, snapshot.filePath);
405
+ } catch {
406
+ // Best effort only — never block integration completion.
407
+ }
408
+ }
409
+ }
410
+
368
411
  /**
369
412
  * Execute the integration operation for the resolved context.
370
413
  *
@@ -1324,10 +1367,13 @@ export function buildIntegrationExecutor(repoRoot: string, opId?: string, stateR
1324
1367
  },
1325
1368
  };
1326
1369
 
1327
- const result = executeIntegration(mode as IntegrateMode, {
1328
- ...context,
1329
- currentBranch: context.baseBranch,
1330
- }, deps);
1370
+ const effectiveStateRoot = stateRoot ?? repoRoot;
1371
+ const result = withPreservedBatchHistory(effectiveStateRoot, () =>
1372
+ executeIntegration(mode as IntegrateMode, {
1373
+ ...context,
1374
+ currentBranch: context.baseBranch,
1375
+ }, deps),
1376
+ );
1331
1377
 
1332
1378
  // TP-051: Clean up stale task/* and saved/* branches after successful integration.
1333
1379
  // This ensures auto-mode integration (supervisor path) gets the same cleanup
@@ -3116,44 +3162,51 @@ export default function (pi: ExtensionAPI) {
3116
3162
  reposToIntegrate.push({ id: "(default)", root: repoRoot });
3117
3163
  }
3118
3164
 
3119
- let totalCommits = 0;
3120
- let allSucceeded = true;
3121
- const repoMessages: string[] = [];
3122
-
3123
- for (const repo of reposToIntegrate) {
3124
- const preCountResult = runGit(["rev-list", "--count", `HEAD..${resolvedOrchBranch}`], repo.root);
3125
- const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
3126
-
3127
- const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
3128
- runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
3129
- runCommand: (cmd: string, cmdArgs: string[]) => {
3130
- try {
3131
- const stdout = execFileSync(cmd, cmdArgs, {
3132
- encoding: "utf-8",
3133
- timeout: 60_000,
3134
- cwd: repo.root,
3135
- stdio: ["pipe", "pipe", "pipe"],
3136
- }).trim();
3137
- return { ok: true, stdout, stderr: "" };
3138
- } catch (err: unknown) {
3139
- const e = err as { stdout?: string; stderr?: string; message?: string };
3140
- return {
3141
- ok: false,
3142
- stdout: (e.stdout ?? "").toString().trim(),
3143
- stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
3144
- };
3145
- }
3146
- },
3147
- deleteBatchState: () => { /* handled once after all repos */ },
3148
- });
3165
+ const integrationRun = withPreservedBatchHistory(stateRoot, () => {
3166
+ let totalCommits = 0;
3167
+ const repoMessages: string[] = [];
3168
+
3169
+ for (const repo of reposToIntegrate) {
3170
+ const preCountResult = runGit(["rev-list", "--count", `HEAD..${resolvedOrchBranch}`], repo.root);
3171
+ const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
3149
3172
 
3150
- if (!integrationResult.success) {
3151
- return { message: `❌ Integration failed in ${repo.id}:\n${integrationResult.error}`, error: true };
3173
+ const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
3174
+ runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
3175
+ runCommand: (cmd: string, cmdArgs: string[]) => {
3176
+ try {
3177
+ const stdout = execFileSync(cmd, cmdArgs, {
3178
+ encoding: "utf-8",
3179
+ timeout: 60_000,
3180
+ cwd: repo.root,
3181
+ stdio: ["pipe", "pipe", "pipe"],
3182
+ }).trim();
3183
+ return { ok: true, stdout, stderr: "" };
3184
+ } catch (err: unknown) {
3185
+ const e = err as { stdout?: string; stderr?: string; message?: string };
3186
+ return {
3187
+ ok: false,
3188
+ stdout: (e.stdout ?? "").toString().trim(),
3189
+ stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
3190
+ };
3191
+ }
3192
+ },
3193
+ deleteBatchState: () => { /* handled once after all repos */ },
3194
+ });
3195
+
3196
+ if (!integrationResult.success) {
3197
+ return { ok: false as const, error: `❌ Integration failed in ${repo.id}:\n${integrationResult.error}` };
3198
+ }
3199
+
3200
+ totalCommits += repoCommitsBefore;
3201
+ repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
3152
3202
  }
3153
3203
 
3154
- totalCommits += repoCommitsBefore;
3155
- repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
3204
+ return { ok: true as const, totalCommits, repoMessages };
3205
+ });
3206
+ if (!integrationRun.ok) {
3207
+ return { message: integrationRun.error, error: true };
3156
3208
  }
3209
+ const { totalCommits, repoMessages } = integrationRun;
3157
3210
 
3158
3211
  // Post-integration cleanup & acceptance
3159
3212
  const allRepos: { id: string; root: string }[] = [];
@@ -1841,18 +1841,21 @@ export function saveBatchHistory(repoRoot: string, summary: BatchHistorySummary)
1841
1841
  const filePath = batchHistoryPath(repoRoot);
1842
1842
  try {
1843
1843
  const history = loadBatchHistory(repoRoot);
1844
+ // Upsert by batchId so resumed batches replace their earlier partial entry
1845
+ // instead of creating duplicates.
1846
+ const nextHistory = history.filter(entry => entry.batchId !== summary.batchId);
1844
1847
  // Prepend newest first
1845
- history.unshift(summary);
1848
+ nextHistory.unshift(summary);
1846
1849
  // Trim to max
1847
- if (history.length > BATCH_HISTORY_MAX_ENTRIES) {
1848
- history.length = BATCH_HISTORY_MAX_ENTRIES;
1850
+ if (nextHistory.length > BATCH_HISTORY_MAX_ENTRIES) {
1851
+ nextHistory.length = BATCH_HISTORY_MAX_ENTRIES;
1849
1852
  }
1850
1853
  const dir = dirname(filePath);
1851
1854
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1852
1855
  const tmpPath = filePath + ".tmp";
1853
- writeFileSync(tmpPath, JSON.stringify(history, null, 2));
1856
+ writeFileSync(tmpPath, JSON.stringify(nextHistory, null, 2));
1854
1857
  renameSync(tmpPath, filePath);
1855
- execLog("batch", "history", `saved batch summary (${history.length} entries)`);
1858
+ execLog("batch", "history", `saved batch summary (${nextHistory.length} entries)`);
1856
1859
  } catch (err) {
1857
1860
  execLog("batch", "history", `failed to save batch history: ${err}`);
1858
1861
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.24.15",
3
+ "version": "0.24.16",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",