pi-crew 0.2.20 → 0.2.21
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/CHANGELOG.md +23 -10
- package/README.md +4 -2
- package/docs/PROJECT_REVIEW.md +271 -0
- package/docs/PROJECT_REVIEW_FIXES.md +343 -0
- package/docs/PROJECT_REVIEW_ROUND4.md +156 -0
- package/docs/PROJECT_REVIEW_ROUND5.md +86 -0
- package/docs/fixes/BATCH_A_H1_H2.md +86 -0
- package/docs/fixes/bug-006-foreground-cancel-concurrent.md +78 -0
- package/docs/fixes/bug-007-async-notifier-stale-ctx.md +112 -0
- package/docs/fixes/bug-008-child-process-silent-timeout.md +100 -0
- package/docs/fixes/bug-009-executor-yield-limit-needs-attention.md +75 -0
- package/docs/fixes/bug-010-child-process-api-key-filtered.md +109 -0
- package/docs/fixes/bug-011-spawn-pi-enoent.md +92 -0
- package/docs/fixes/bug-012-essential-env-stripped.md +89 -0
- package/docs/fixes/bug-013-background-runner-death.md +84 -0
- package/docs/fixes/bug-014-infinite-retry-loop-needs-attention.md +82 -0
- package/docs/fixes/bug-015-background-runner-sigterm.md +65 -0
- package/docs/fixes/bug-017-background-runner-session-shutdown.md +66 -0
- package/docs/fixes/bug-017-background-runner-sigkill-double-fork.md +28 -0
- package/docs/fixes/bug-018-child-pi-worker-stdin-hang.md +61 -0
- package/docs/fixes/bug-019-phantom-runs-temp-workspace.md +52 -0
- package/docs/pi-crew-bugs.md +954 -0
- package/docs/pi-crew-investigation-report.md +411 -0
- package/docs/pi-crew-test-final.md +120 -0
- package/docs/pi-crew-test-results.md +260 -0
- package/docs/pi-crew-test-round2.md +136 -0
- package/docs/pi-crew-test-round4.md +100 -0
- package/docs/pi-crew-test-round5.md +70 -0
- package/docs/pi-crew-test-round6.md +110 -0
- package/docs/usage.md +14 -0
- package/package.json +4 -2
- package/src/adapters/export-util.ts +12 -6
- package/src/agents/agent-config.ts +2 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/markers.ts +22 -17
- package/src/config/resilient-parser.ts +1 -1
- package/src/extension/async-notifier.ts +4 -2
- package/src/extension/management.ts +52 -0
- package/src/extension/register.ts +47 -10
- package/src/extension/run-index.ts +20 -2
- package/src/extension/run-maintenance.ts +2 -2
- package/src/extension/team-tool/parallel-dispatch.ts +1 -1
- package/src/extension/team-tool/run.ts +3 -6
- package/src/extension/team-tool.ts +67 -11
- package/src/observability/event-to-metric.ts +2 -1
- package/src/runtime/async-runner.ts +42 -34
- package/src/runtime/background-runner.ts +165 -7
- package/src/runtime/child-pi.ts +111 -18
- package/src/runtime/code-summary.ts +1 -1
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-agent-runtime.ts +2 -1
- package/src/runtime/heartbeat-watcher.ts +4 -0
- package/src/runtime/live-agent-manager.ts +1 -1
- package/src/runtime/live-session-runtime.ts +2 -1
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +2 -1
- package/src/runtime/phase-progress.ts +1 -1
- package/src/runtime/pi-args.ts +3 -1
- package/src/runtime/pi-spawn.ts +6 -0
- package/src/runtime/prose-compressor.ts +1 -1
- package/src/runtime/result-extractor.ts +0 -1
- package/src/runtime/retry-executor.ts +1 -1
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/skill-instructions.ts +0 -1
- package/src/runtime/stale-reconciler.ts +30 -3
- package/src/runtime/subagent-manager.ts +2 -0
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-graph-scheduler.ts +1 -1
- package/src/runtime/task-runner/tail-read.ts +26 -0
- package/src/runtime/task-runner.ts +1007 -383
- package/src/runtime/team-runner.ts +9 -5
- package/src/runtime/worker-startup.ts +3 -1
- package/src/schema/team-tool-schema.ts +2 -1
- package/src/state/active-run-registry.ts +8 -2
- package/src/state/atomic-write.ts +17 -0
- package/src/state/contracts.ts +5 -2
- package/src/state/event-log-rotation.ts +118 -31
- package/src/state/event-log.ts +33 -5
- package/src/state/event-reconstructor.ts +4 -2
- package/src/state/mailbox.ts +5 -1
- package/src/state/schedule.ts +146 -0
- package/src/state/types.ts +40 -0
- package/src/state/usage.ts +20 -0
- package/src/ui/crew-widget.ts +2 -2
- package/src/ui/run-event-bus.ts +1 -1
- package/src/ui/run-snapshot-cache.ts +2 -1
- package/src/ui/snapshot-types.ts +1 -0
- package/src/utils/gh-protocol.ts +2 -2
- package/src/utils/names.ts +1 -1
- package/src/utils/sse-parser.ts +0 -2
- package/src/worktree/branch-freshness.ts +1 -1
- package/src/worktree/cleanup.ts +54 -14
- package/src/worktree/worktree-manager.ts +19 -9
package/src/state/types.ts
CHANGED
|
@@ -276,6 +276,14 @@ export interface TeamTaskState {
|
|
|
276
276
|
|
|
277
277
|
/** Parsed metric key-values from worker output (CREW_METRIC lines). */
|
|
278
278
|
metrics?: Record<string, number>;
|
|
279
|
+
|
|
280
|
+
/** Lifetime token usage accumulated via message_end events. Survives compaction
|
|
281
|
+
* (session.stats reset on compaction, but this is an independent accumulator). */
|
|
282
|
+
lifetimeUsage?: { input: number; output: number; cacheWrite: number };
|
|
283
|
+
|
|
284
|
+
/** Steering messages queued before the task's session was ready.
|
|
285
|
+
* Delivered when the session initializes (mirrors pi-subagents3 pendingSteers pattern). */
|
|
286
|
+
pendingSteers?: string[];
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
export interface ControlReservation {
|
|
@@ -283,3 +291,35 @@ export interface ControlReservation {
|
|
|
283
291
|
controllerId: string;
|
|
284
292
|
acceptsControlEvents: boolean;
|
|
285
293
|
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A task scheduled to fire on a cron expression, interval, or one-shot.
|
|
297
|
+
* Persisted at `<cwd>/.crew/state/schedules/<sessionId>.json`.
|
|
298
|
+
* Session-scoped: survives /resume, resets on /new.
|
|
299
|
+
*/
|
|
300
|
+
export interface ScheduledTask {
|
|
301
|
+
id: string;
|
|
302
|
+
name: string;
|
|
303
|
+
description: string;
|
|
304
|
+
/** Raw schedule: cron expr | "+10m" | "5m" | ISO timestamp */
|
|
305
|
+
schedule: string;
|
|
306
|
+
scheduleType: "cron" | "interval" | "once";
|
|
307
|
+
intervalMs?: number;
|
|
308
|
+
/** Workflow/step to execute when the schedule fires */
|
|
309
|
+
workflowName: string;
|
|
310
|
+
stepId?: string;
|
|
311
|
+
/** Resolved at create time from workflow/step config */
|
|
312
|
+
agentName: string;
|
|
313
|
+
model?: string;
|
|
314
|
+
enabled: boolean;
|
|
315
|
+
createdAt: string;
|
|
316
|
+
lastRun?: string;
|
|
317
|
+
lastStatus?: "success" | "error" | "running";
|
|
318
|
+
nextRun?: string;
|
|
319
|
+
runCount: number;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface ScheduleStoreData {
|
|
323
|
+
version: 1;
|
|
324
|
+
jobs: ScheduledTask[];
|
|
325
|
+
}
|
package/src/state/usage.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Lifetime usage — accumulated via message_end events, survives compaction.
|
|
5
|
+
* cacheRead is excluded because each turn's cacheRead is the cumulative cached
|
|
6
|
+
* prefix re-read on that one call — summing across turns would count it N times.
|
|
7
|
+
* See: https://github.com/nichekate/pi-subagents3/issues/38
|
|
8
|
+
*/
|
|
9
|
+
export type LifetimeUsage = { input: number; output: number; cacheWrite: number };
|
|
10
|
+
|
|
11
|
+
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
12
|
+
export function getLifetimeTotal(u?: LifetimeUsage): number {
|
|
13
|
+
return u ? u.input + u.output + u.cacheWrite : 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Add a usage delta into a target accumulator (mutates target). */
|
|
17
|
+
export function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void {
|
|
18
|
+
into.input += delta.input;
|
|
19
|
+
into.output += delta.output;
|
|
20
|
+
into.cacheWrite += delta.cacheWrite;
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
24
|
const total: UsageState = {};
|
|
5
25
|
let found = false;
|
package/src/ui/crew-widget.ts
CHANGED
|
@@ -40,7 +40,7 @@ const MAX_AGENTS_DISPLAY = 3;
|
|
|
40
40
|
/** R1: How many turns finished agents linger before disappearing. */
|
|
41
41
|
const FINISHED_LINGER_MAX_AGE = 1;
|
|
42
42
|
const ERROR_LINGER_MAX_AGE = 2;
|
|
43
|
-
const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped"]);
|
|
43
|
+
const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped", "needs_attention"]);
|
|
44
44
|
/** R3: Faster refresh when live agents are running. Aligned with spinner frame. */
|
|
45
45
|
const LIVE_REFRESH_MS = 160;
|
|
46
46
|
|
|
@@ -303,7 +303,7 @@ export function buildCrewWidgetLines(cwd: string, frame = 0, maxLines = 8, provi
|
|
|
303
303
|
for (const agent of finishedAgents.slice(0, 2)) {
|
|
304
304
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
305
305
|
const name = liveHandle?.agent ?? agent.agent;
|
|
306
|
-
const icon = agent.status === "completed" ? "\u2713" : agent.status === "failed" ? "\u2717" : "\u25AA";
|
|
306
|
+
const icon = agent.status === "completed" ? "\u2713" : agent.status === "failed" ? "\u2717" : agent.status === "needs_attention" ? "\u26A0" : "\u25AA";
|
|
307
307
|
const stats = agentStats(agent, liveHandle);
|
|
308
308
|
const desc = liveHandle?.description ?? agent.role;
|
|
309
309
|
lines.push(`\u2502 \u251C\u2500 ${icon} ${name} \u00B7 ${desc}${stats ? ` \u00B7 ${stats}` : ""}`);
|
package/src/ui/run-event-bus.ts
CHANGED
|
@@ -26,7 +26,7 @@ const WORKER_PROGRESS_TYPES = new Set([
|
|
|
26
26
|
"tool_execution_start", "tool_result", "agent_progress", "worker_status",
|
|
27
27
|
]);
|
|
28
28
|
const WORKER_LIFECYCLE_TYPES = new Set([
|
|
29
|
-
"task.started", "task.completed", "task.failed",
|
|
29
|
+
"task.started", "task.completed", "task.failed", "task.needs_attention",
|
|
30
30
|
"task_started", "task_completed", "task_failed", "task_cancelled",
|
|
31
31
|
"run.started", "run.completed", "run.cancelled", "run.failed",
|
|
32
32
|
"run_started", "run_completed", "run_cancelled", "run_blocked",
|
|
@@ -307,7 +307,7 @@ async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAge
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
310
|
-
const progress: RunUiProgress = { total: tasks.length, completed: 0, running: 0, failed: 0, queued: 0, waiting: 0, cancelled: 0, skipped: 0 };
|
|
310
|
+
const progress: RunUiProgress = { total: tasks.length, completed: 0, running: 0, failed: 0, queued: 0, waiting: 0, cancelled: 0, skipped: 0, needsAttention: 0 };
|
|
311
311
|
for (const task of tasks) {
|
|
312
312
|
if (task.status === "completed") progress.completed += 1;
|
|
313
313
|
else if (task.status === "running") progress.running += 1;
|
|
@@ -316,6 +316,7 @@ function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
|
|
|
316
316
|
else if (task.status === "waiting") progress.waiting = (progress.waiting ?? 0) + 1;
|
|
317
317
|
else if (task.status === "cancelled") progress.cancelled = (progress.cancelled ?? 0) + 1;
|
|
318
318
|
else if (task.status === "skipped") progress.skipped = (progress.skipped ?? 0) + 1;
|
|
319
|
+
else if (task.status === "needs_attention") progress.needsAttention = (progress.needsAttention ?? 0) + 1;
|
|
319
320
|
}
|
|
320
321
|
return progress;
|
|
321
322
|
}
|
package/src/ui/snapshot-types.ts
CHANGED
package/src/utils/gh-protocol.ts
CHANGED
|
@@ -386,7 +386,7 @@ export function resolveGitHubUrl(parsed: Parsed, scheme: "issue" | "pr", cwd: st
|
|
|
386
386
|
if (parsed.mode === "list") {
|
|
387
387
|
try {
|
|
388
388
|
const raw = execFileSync(
|
|
389
|
-
"gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"
|
|
389
|
+
"gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"(.filename) +(.additions) -(.deletions) [(.status)]\""],
|
|
390
390
|
{ cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
|
|
391
391
|
);
|
|
392
392
|
const fileLines = raw.split("\n").filter(Boolean);
|
|
@@ -408,7 +408,7 @@ export function resolveGitHubUrl(parsed: Parsed, scheme: "issue" | "pr", cwd: st
|
|
|
408
408
|
if (parsed.mode === "slice" && parsed.index !== undefined) {
|
|
409
409
|
try {
|
|
410
410
|
const raw = execFileSync(
|
|
411
|
-
"gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"
|
|
411
|
+
"gh", ["pr", "view", String(parsed.number), "--repo", repo, "--json", "files", "--jq", ".files[] | \"(.filename) +(.additions) -(.deletions) [(.status)]\""],
|
|
412
412
|
{ cwd, encoding: "utf-8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] },
|
|
413
413
|
);
|
|
414
414
|
const fileLines = raw.split("\n").filter(Boolean);
|
package/src/utils/names.ts
CHANGED
|
@@ -23,5 +23,5 @@ export function parseConfigObject(config: unknown): { value?: Record<string, unk
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function hasOwn(obj: Record<string, unknown>, key: string): boolean {
|
|
26
|
-
return
|
|
26
|
+
return Object.hasOwn(obj, key);
|
|
27
27
|
}
|
package/src/utils/sse-parser.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface BranchFreshness {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function git(cwd: string, args: string[]): string {
|
|
18
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
18
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function count(cwd: string, range: string): number {
|
package/src/worktree/cleanup.ts
CHANGED
|
@@ -10,10 +10,12 @@ export interface WorktreeCleanupResult {
|
|
|
10
10
|
removed: string[];
|
|
11
11
|
preserved: Array<{ path: string; reason: string }>;
|
|
12
12
|
artifactPaths: string[];
|
|
13
|
+
/** Branch names created from dirty worktrees that were committed. */
|
|
14
|
+
committedBranches: string[];
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function git(cwd: string, args: string[]): string {
|
|
16
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
18
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true }).trim();
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function isDirty(worktreePath: string): boolean {
|
|
@@ -35,23 +37,61 @@ function captureDiff(worktreePath: string): string {
|
|
|
35
37
|
|
|
36
38
|
export function cleanupRunWorktrees(manifest: TeamRunManifest, options: { force?: boolean; signal?: AbortSignal } = {}): WorktreeCleanupResult {
|
|
37
39
|
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, manifest.runId);
|
|
38
|
-
const result: WorktreeCleanupResult = { removed: [], preserved: [], artifactPaths: [] };
|
|
40
|
+
const result: WorktreeCleanupResult = { removed: [], preserved: [], artifactPaths: [], committedBranches: [] };
|
|
39
41
|
if (!fs.existsSync(worktreeRoot)) return result;
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
// M3 fix: use withFileTypes to avoid race between readdirSync and statSync.
|
|
44
|
+
const withFileTypes = fs.readdirSync(worktreeRoot, { withFileTypes: true });
|
|
45
|
+
for (const entry of withFileTypes) {
|
|
42
46
|
if (options.signal?.aborted) break;
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
if (!entry.isDirectory()) continue;
|
|
48
|
+
const worktreePath = path.join(worktreeRoot, entry.name);
|
|
49
|
+
try {
|
|
50
|
+
const stat = fs.statSync(worktreePath);
|
|
51
|
+
if (!stat.isDirectory()) continue;
|
|
52
|
+
} catch {
|
|
53
|
+
// Entry deleted between readdir and stat — skip safely.
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
45
56
|
const dirty = isDirty(worktreePath);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const branchName = `pi-crew/${manifest.runId}/${entry.name}`;
|
|
58
|
+
if (dirty) {
|
|
59
|
+
// Commit changes to a branch instead of just preserving the worktree
|
|
60
|
+
try {
|
|
61
|
+
execFileSync("git", ["add", "-A"], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
|
|
62
|
+
const safeDesc = entry.name.slice(0, 200);
|
|
63
|
+
execFileSync("git", ["commit", "-m", `pi-crew: ${safeDesc}`], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
|
|
64
|
+
// Create branch in the main repo pointing to this worktree's HEAD
|
|
65
|
+
try {
|
|
66
|
+
execFileSync("git", ["branch", branchName], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
|
|
67
|
+
} catch {
|
|
68
|
+
// Branch already exists — use timestamp suffix
|
|
69
|
+
const tsBranch = `${branchName}-${Date.now()}`;
|
|
70
|
+
execFileSync("git", ["branch", tsBranch], { cwd: worktreePath, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true });
|
|
71
|
+
}
|
|
72
|
+
result.committedBranches.push(branchName);
|
|
73
|
+
// Remove the worktree (branch persists)
|
|
74
|
+
const removeArgs = ["worktree", "remove", "--force", worktreePath];
|
|
75
|
+
git(manifest.cwd, removeArgs);
|
|
76
|
+
result.removed.push(worktreePath);
|
|
77
|
+
const artifact = writeArtifact(manifest.artifactsRoot, {
|
|
78
|
+
kind: "metadata",
|
|
79
|
+
relativePath: `metadata/worktree-branch-${entry.name}.json`,
|
|
80
|
+
content: JSON.stringify({ worktreePath, branch: branchName, committedAt: new Date().toISOString(), mergeCommand: `git merge ${branchName}` }, null, 2),
|
|
81
|
+
producer: "worktree-cleanup",
|
|
82
|
+
});
|
|
83
|
+
result.artifactPaths.push(artifact.path);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Fallback to preserving dirty worktree
|
|
86
|
+
const artifact = writeArtifact(manifest.artifactsRoot, {
|
|
87
|
+
kind: "diff",
|
|
88
|
+
relativePath: `cleanup/${entry}.diff`,
|
|
89
|
+
content: captureDiff(worktreePath),
|
|
90
|
+
producer: "worktree-cleanup",
|
|
91
|
+
});
|
|
92
|
+
result.artifactPaths.push(artifact.path);
|
|
93
|
+
result.preserved.push({ path: worktreePath, reason: `dirty worktree preserved (commit failed: ${error instanceof Error ? error.message : String(error)})` });
|
|
94
|
+
}
|
|
55
95
|
continue;
|
|
56
96
|
}
|
|
57
97
|
const args = ["worktree", "remove"];
|
|
@@ -25,7 +25,7 @@ export interface WorktreeDiffStat {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function git(cwd: string, args: string[]): string {
|
|
28
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" } }).trim();
|
|
28
|
+
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LANG: "C", LC_ALL: "C" }, windowsHide: true }).trim();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function sanitizeBranchPart(value: string): string {
|
|
@@ -50,10 +50,13 @@ function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boole
|
|
|
50
50
|
try { sourceStat = fs.statSync(source); } catch { return false; }
|
|
51
51
|
if (!sourceStat.isDirectory()) return false;
|
|
52
52
|
if (fs.existsSync(target)) return false;
|
|
53
|
+
// M5 fix: log symlink failure reason, especially on Windows non-admin.
|
|
53
54
|
try {
|
|
54
55
|
fs.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
55
56
|
return true;
|
|
56
|
-
} catch {
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const isWindows = process.platform === "win32";
|
|
59
|
+
logInternalError("worktree.symlink-fail", error, isWindows ? "Windows non-admin: SeCreateSymbolicLinkPrivilege needed for node_modules symlink" : String(error));
|
|
57
60
|
return false;
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -86,12 +89,19 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
|
|
|
86
89
|
const trimmed = result.stdout.trim();
|
|
87
90
|
if (!trimmed) return [];
|
|
88
91
|
try {
|
|
89
|
-
// Extract JSON
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
// Extract JSON — hooks may output debug logging before JSON.
|
|
93
|
+
// M4 fix: try full trimmed (multi-line JSON object) before falling back to last line.
|
|
94
|
+
const lines = trimmed.split(/\r?\n/);
|
|
95
|
+
let parsed: { syntheticPaths?: unknown } | null = null;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(trimmed) as { syntheticPaths?: unknown };
|
|
98
|
+
} catch { /* fall through — try last line */ }
|
|
99
|
+
if (!parsed && lines.length > 0) {
|
|
100
|
+
const lastLine = lines[lines.length - 1];
|
|
101
|
+
try { parsed = JSON.parse(lastLine) as { syntheticPaths?: unknown }; } catch { /* give up */ }
|
|
102
|
+
}
|
|
103
|
+
if (!parsed || !Array.isArray(parsed.syntheticPaths)) return [];
|
|
104
|
+
return [...new Set(parsed.syntheticPaths.filter((entry): entry is string => typeof entry === "string").map((entry) => normalizeSyntheticPath(worktreePath, entry)))];
|
|
95
105
|
} catch (error) {
|
|
96
106
|
logInternalError("worktree.setupHook.parse", error, `lastLine=${(trimmed.split(/\r?\n/).pop() ?? "").slice(0, 200)}`);
|
|
97
107
|
return [];
|
|
@@ -105,7 +115,7 @@ function branchExists(repoRoot: string, branch: string): { local: boolean; remot
|
|
|
105
115
|
// Check remote-tracking branch
|
|
106
116
|
try {
|
|
107
117
|
const out = execFileSync("git", ["for-each-ref", "--format=%(refname)", `refs/remotes/*/${branch}`],
|
|
108
|
-
{ cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
118
|
+
{ cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
109
119
|
return { local: false, remoteOnly: out.length > 0 };
|
|
110
120
|
} catch { return { local: false, remoteOnly: false }; }
|
|
111
121
|
}
|