pi-crew 0.9.28 → 0.9.29
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/dist/build-meta.json +158 -91
- package/dist/index.mjs +893 -507
- package/dist/index.mjs.map +4 -4
- package/docs/REVIEW-FINDINGS-2026-07-CORE.md +139 -0
- package/docs/REVIEW-FINDINGS-2026-07.md +125 -0
- package/docs/bugs/bug-quota-display-truncation.md +223 -0
- package/docs/stories/README.md +3 -1
- package/docs/stories/US-DEPS-major-upgrade.md +62 -0
- package/package.json +4 -4
- package/src/config/config.ts +4 -0
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/footer.ts +292 -0
- package/src/extension/crew-vibes/index.ts +67 -52
- package/src/prompt/prompt-runtime.ts +1 -1
- package/src/runtime/child-pi.ts +52 -43
- package/src/runtime/cross-extension-rpc.ts +1 -1
- package/src/runtime/post-exit-stdio-guard.ts +22 -4
- package/src/runtime/stale-reconciler.ts +9 -4
- package/src/runtime/team-runner.ts +11 -16
- package/src/state/atomic-write.ts +46 -19
- package/src/state/event-log.ts +77 -24
- package/src/state/mailbox.ts +6 -3
- package/src/ui/pi-ui-compat.ts +11 -0
- package/src/utils/conflict-detect.ts +9 -3
- package/src/utils/paths.ts +7 -1
- package/src/worktree/cleanup.ts +19 -0
- package/src/worktree/worktree-manager.ts +145 -34
|
@@ -551,6 +551,100 @@ export function overlaySeedPaths(repoRoot: string, worktreePath: string, seedPat
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Snapshot uncommitted work in a reused worktree to a recovery artifact BEFORE
|
|
556
|
+
* discarding it, so the data is never silently lost. Captures tracked changes
|
|
557
|
+
* (git diff HEAD) and inlines untracked file contents so the work can be
|
|
558
|
+
* recovered via `git apply` / manual restore. Best-effort: a snapshot failure
|
|
559
|
+
* only logs (it must not block the clean-slate reuse flow).
|
|
560
|
+
*/
|
|
561
|
+
function snapshotDirtyWorktree(manifest: TeamRunManifest, task: TeamTaskState, worktreePath: string, dirtyStatus: string): void {
|
|
562
|
+
try {
|
|
563
|
+
const parts: string[] = [
|
|
564
|
+
`# Worktree recovery snapshot`,
|
|
565
|
+
`runId: ${manifest.runId} taskId: ${task.id} path: ${worktreePath}`,
|
|
566
|
+
`capturedAt: ${new Date().toISOString()}`,
|
|
567
|
+
"",
|
|
568
|
+
];
|
|
569
|
+
let trackedDiff = "";
|
|
570
|
+
try {
|
|
571
|
+
trackedDiff = git(worktreePath, ["diff", "HEAD"]);
|
|
572
|
+
} catch {
|
|
573
|
+
trackedDiff = "";
|
|
574
|
+
}
|
|
575
|
+
if (trackedDiff.trim()) {
|
|
576
|
+
parts.push("## Tracked changes (`git diff HEAD`)", "```diff", trackedDiff, "```", "");
|
|
577
|
+
}
|
|
578
|
+
for (const line of dirtyStatus.split("\n")) {
|
|
579
|
+
if (!line.startsWith("?? ")) continue;
|
|
580
|
+
// Strip the "?? " prefix and surrounding git quotes.
|
|
581
|
+
const rel = line.slice(3).replace(/^"|"$/g, "");
|
|
582
|
+
if (!rel) continue;
|
|
583
|
+
try {
|
|
584
|
+
const abs = path.join(worktreePath, rel);
|
|
585
|
+
if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory()) continue;
|
|
586
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
587
|
+
parts.push(`## Untracked file: ${rel}`, "```", content, "```", "");
|
|
588
|
+
} catch {
|
|
589
|
+
/* skip unreadable/unstat-able entry */
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
writeArtifact(manifest.artifactsRoot, {
|
|
593
|
+
kind: "diff",
|
|
594
|
+
relativePath: `worktree-recovery/${task.id}-${Date.now()}.md`,
|
|
595
|
+
content: parts.join("\n"),
|
|
596
|
+
producer: "worktree-manager.snapshotDirtyWorktree",
|
|
597
|
+
retention: "run",
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
logInternalError(
|
|
601
|
+
"worktree.recovery.snapshotFailed",
|
|
602
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
603
|
+
`runId=${manifest.runId}, taskId=${task.id}`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Best-effort removal of a just-created worktree + its branch. Used when a
|
|
610
|
+
* post-creation step (setup hook, node_modules link, seed overlay) fails, so
|
|
611
|
+
* the run does not leak an orphaned worktree dir + branch that would block the
|
|
612
|
+
* next run with an "already checked out" error.
|
|
613
|
+
*/
|
|
614
|
+
function cleanupCreatedWorktree(repoRoot: string, worktreePath: string, branch: string): void {
|
|
615
|
+
try {
|
|
616
|
+
git(repoRoot, ["worktree", "remove", "--force", worktreePath]);
|
|
617
|
+
} catch {
|
|
618
|
+
try {
|
|
619
|
+
if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
620
|
+
} catch {
|
|
621
|
+
/* best-effort */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
git(repoRoot, ["branch", "-D", branch]);
|
|
626
|
+
} catch {
|
|
627
|
+
/* best-effort */
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function cleanupCreatedWorktreeAsync(repoRoot: string, worktreePath: string, branch: string): Promise<void> {
|
|
632
|
+
try {
|
|
633
|
+
await gitAsync(repoRoot, ["worktree", "remove", "--force", worktreePath]);
|
|
634
|
+
} catch {
|
|
635
|
+
try {
|
|
636
|
+
if (fs.existsSync(worktreePath)) fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
637
|
+
} catch {
|
|
638
|
+
/* best-effort */
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await gitAsync(repoRoot, ["branch", "-D", branch]);
|
|
643
|
+
} catch {
|
|
644
|
+
/* best-effort */
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
554
648
|
export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskState, stepSeedPaths?: string[]): PreparedTaskWorkspace {
|
|
555
649
|
if (manifest.workspaceMode !== "worktree") return { cwd: task.cwd };
|
|
556
650
|
const repoRoot = findGitRoot(manifest.cwd);
|
|
@@ -626,10 +720,12 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
|
|
|
626
720
|
// Check for uncommitted changes from previous run before reusing
|
|
627
721
|
const dirtyStatus = git(worktreePath, ["status", "--porcelain"]);
|
|
628
722
|
if (dirtyStatus.trim()) {
|
|
629
|
-
//
|
|
723
|
+
// Snapshot uncommitted work to a recovery artifact BEFORE discarding, so the
|
|
724
|
+
// previous run's changes are never silently destroyed on reuse.
|
|
725
|
+
snapshotDirtyWorktree(manifest, task, worktreePath, dirtyStatus);
|
|
630
726
|
logInternalError(
|
|
631
727
|
"worktree.reused.dirty",
|
|
632
|
-
new Error(`Discarding uncommitted changes in reused worktree at ${worktreePath}`),
|
|
728
|
+
new Error(`Discarding uncommitted changes in reused worktree at ${worktreePath} (snapshot saved to artifacts)`),
|
|
633
729
|
`runId=${manifest.runId}, taskId=${task.id}, dirtyStatus=${dirtyStatus.trim()}`,
|
|
634
730
|
);
|
|
635
731
|
git(worktreePath, ["checkout", "--", "."]);
|
|
@@ -679,23 +775,30 @@ export function prepareTaskWorkspace(manifest: TeamRunManifest, task: TeamTaskSt
|
|
|
679
775
|
}
|
|
680
776
|
throw error;
|
|
681
777
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
778
|
+
try {
|
|
779
|
+
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
780
|
+
const nodeModulesLinked =
|
|
781
|
+
loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
782
|
+
// Overlay seed paths from config + step-level seedPaths
|
|
783
|
+
const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
|
|
784
|
+
const merged = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
|
|
785
|
+
if (merged.length > 0) {
|
|
786
|
+
overlaySeedPaths(repoRoot, worktreePath, merged);
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
cwd: worktreePath,
|
|
790
|
+
worktreePath,
|
|
791
|
+
branch,
|
|
792
|
+
reused: false,
|
|
793
|
+
nodeModulesLinked,
|
|
794
|
+
syntheticPaths,
|
|
795
|
+
};
|
|
796
|
+
} catch (setupError) {
|
|
797
|
+
// Hook/link/seed failure AFTER worktree+branch creation: clean up to avoid
|
|
798
|
+
// an orphaned worktree dir + branch that would block the next run.
|
|
799
|
+
cleanupCreatedWorktree(repoRoot, worktreePath, branch);
|
|
800
|
+
throw setupError;
|
|
690
801
|
}
|
|
691
|
-
return {
|
|
692
|
-
cwd: worktreePath,
|
|
693
|
-
worktreePath,
|
|
694
|
-
branch,
|
|
695
|
-
reused: false,
|
|
696
|
-
nodeModulesLinked,
|
|
697
|
-
syntheticPaths,
|
|
698
|
-
};
|
|
699
802
|
}
|
|
700
803
|
|
|
701
804
|
/** Async version of prepareTaskWorkspace — yields the event loop during git operations. */
|
|
@@ -766,9 +869,10 @@ export async function prepareTaskWorkspaceAsync(
|
|
|
766
869
|
}
|
|
767
870
|
const dirtyStatus = await gitAsync(worktreePath, ["status", "--porcelain"]);
|
|
768
871
|
if (dirtyStatus.trim()) {
|
|
872
|
+
snapshotDirtyWorktree(manifest, task, worktreePath, dirtyStatus);
|
|
769
873
|
logInternalError(
|
|
770
874
|
"worktree.reused.dirty",
|
|
771
|
-
new Error(`Discarding uncommitted changes in reused worktree at ${worktreePath}`),
|
|
875
|
+
new Error(`Discarding uncommitted changes in reused worktree at ${worktreePath} (snapshot saved to artifacts)`),
|
|
772
876
|
`runId=${manifest.runId}, taskId=${task.id}, dirtyStatus=${dirtyStatus.trim()}`,
|
|
773
877
|
);
|
|
774
878
|
await gitAsync(worktreePath, ["checkout", "--", "."]);
|
|
@@ -818,22 +922,29 @@ export async function prepareTaskWorkspaceAsync(
|
|
|
818
922
|
}
|
|
819
923
|
throw error;
|
|
820
924
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
925
|
+
try {
|
|
926
|
+
const syntheticPaths = runSetupHook(manifest, task, repoRoot, worktreePath, branch);
|
|
927
|
+
const nodeModulesLinked =
|
|
928
|
+
loadedConfig.config.worktree?.linkNodeModules === true ? linkNodeModulesIfPresent(repoRoot, worktreePath) : false;
|
|
929
|
+
const globalSeedPaths = loadedConfig.config.worktree?.seedPaths ?? [];
|
|
930
|
+
const merged = normalizeSeedPaths([...globalSeedPaths, ...(stepSeedPaths ?? [])], repoRoot);
|
|
931
|
+
if (merged.length > 0) {
|
|
932
|
+
overlaySeedPaths(repoRoot, worktreePath, merged);
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
cwd: worktreePath,
|
|
936
|
+
worktreePath,
|
|
937
|
+
branch,
|
|
938
|
+
reused: false,
|
|
939
|
+
nodeModulesLinked,
|
|
940
|
+
syntheticPaths,
|
|
941
|
+
};
|
|
942
|
+
} catch (setupError) {
|
|
943
|
+
// Hook/link/seed failure AFTER worktree+branch creation: clean up to avoid
|
|
944
|
+
// an orphaned worktree dir + branch that would block the next run.
|
|
945
|
+
await cleanupCreatedWorktreeAsync(repoRoot, worktreePath, branch);
|
|
946
|
+
throw setupError;
|
|
828
947
|
}
|
|
829
|
-
return {
|
|
830
|
-
cwd: worktreePath,
|
|
831
|
-
worktreePath,
|
|
832
|
-
branch,
|
|
833
|
-
reused: false,
|
|
834
|
-
nodeModulesLinked,
|
|
835
|
-
syntheticPaths,
|
|
836
|
-
};
|
|
837
948
|
}
|
|
838
949
|
|
|
839
950
|
export function captureWorktreeDiffStat(worktreePath: string): WorktreeDiffStat {
|