pi-crew 0.6.0 → 0.6.3
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 +225 -0
- package/README.md +70 -31
- package/docs/issue-29-analysis.md +189 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
- package/package.json +2 -2
- package/src/agents/agent-config.ts +2 -1
- package/src/benchmark/feedback-loop.ts +4 -2
- package/src/config/config.ts +106 -15
- package/src/errors.ts +107 -0
- package/src/extension/async-notifier.ts +6 -2
- package/src/extension/crew-cleanup.ts +8 -5
- package/src/extension/cross-extension-rpc.ts +48 -0
- package/src/extension/management.ts +464 -109
- package/src/extension/register.ts +194 -34
- package/src/extension/registration/commands.ts +4 -3
- package/src/extension/registration/subagent-helpers.ts +2 -2
- package/src/extension/registration/subagent-tools.ts +3 -1
- package/src/extension/registration/team-tool.ts +3 -1
- package/src/extension/registration/viewers.ts +3 -2
- package/src/extension/run-export.ts +16 -1
- package/src/extension/run-import.ts +16 -0
- package/src/extension/team-tool/anchor.ts +5 -1
- package/src/extension/team-tool/api.ts +12 -7
- package/src/extension/team-tool/cancel.ts +3 -3
- package/src/extension/team-tool/config-patch.ts +15 -1
- package/src/extension/team-tool/explain.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +40 -0
- package/src/extension/team-tool/inspect.ts +3 -3
- package/src/extension/team-tool/lifecycle-actions.ts +4 -4
- package/src/extension/team-tool/respond.ts +2 -2
- package/src/extension/team-tool/run.ts +60 -11
- package/src/extension/team-tool/status.ts +1 -1
- package/src/extension/team-tool.ts +175 -47
- package/src/hooks/registry.ts +84 -12
- package/src/hooks/types.ts +12 -3
- package/src/i18n.ts +15 -2
- package/src/observability/exporters/otlp-exporter.ts +73 -0
- package/src/plugins/plugin-define.ts +6 -0
- package/src/plugins/plugin-registry.ts +32 -0
- package/src/plugins/plugins/index.ts +3 -0
- package/src/plugins/plugins/nextjs.ts +19 -0
- package/src/plugins/plugins/vite.ts +14 -0
- package/src/plugins/plugins/vitest.ts +14 -0
- package/src/runtime/adaptive-plan.ts +24 -0
- package/src/runtime/agent-control.ts +6 -3
- package/src/runtime/async-runner.ts +86 -3
- package/src/runtime/background-runner.ts +529 -144
- package/src/runtime/chain-parser.ts +5 -1
- package/src/runtime/chain-runner.ts +67 -3
- package/src/runtime/checkpoint.ts +262 -180
- package/src/runtime/child-pi.ts +165 -38
- package/src/runtime/crash-recovery.ts +25 -14
- package/src/runtime/crew-agent-records.ts +6 -3
- package/src/runtime/cross-extension-rpc.ts +34 -8
- package/src/runtime/diagnostic-export.ts +4 -5
- package/src/runtime/dynamic-script-runner.ts +9 -6
- package/src/runtime/foreground-watchdog.ts +3 -3
- package/src/runtime/heartbeat-watcher.ts +19 -2
- package/src/runtime/intercom-bridge.ts +7 -0
- package/src/runtime/iteration-hooks.ts +1 -1
- package/src/runtime/live-agent-manager.ts +6 -3
- package/src/runtime/live-irc.ts +4 -2
- package/src/runtime/manifest-cache.ts +4 -0
- package/src/runtime/orphan-worker-registry.ts +444 -0
- package/src/runtime/parallel-utils.ts +2 -1
- package/src/runtime/parent-guard.ts +70 -16
- package/src/runtime/pi-args.ts +437 -14
- package/src/runtime/pi-spawn.ts +1 -0
- package/src/runtime/post-checks.ts +11 -4
- package/src/runtime/retry-runner.ts +9 -3
- package/src/runtime/{drift-detectors.ts → run-drift.ts} +1 -1
- package/src/runtime/run-tracker.ts +38 -10
- package/src/runtime/sandbox.ts +42 -21
- package/src/runtime/scheduler.ts +25 -2
- package/src/runtime/semaphore.ts +2 -1
- package/src/runtime/settings-store.ts +14 -2
- package/src/runtime/skill-effectiveness.ts +109 -64
- package/src/runtime/skill-instructions.ts +114 -33
- package/src/runtime/stale-reconciler.ts +310 -69
- package/src/runtime/subagent-manager.ts +265 -53
- package/src/runtime/subprocess-tool-registry.ts +2 -2
- package/src/runtime/task-health.ts +76 -0
- package/src/runtime/task-id.ts +20 -0
- package/src/runtime/task-packet.ts +13 -1
- package/src/runtime/task-runner/live-executor.ts +1 -1
- package/src/runtime/task-runner/progress.ts +1 -1
- package/src/runtime/task-runner/state-helpers.ts +110 -6
- package/src/runtime/task-runner.ts +98 -67
- package/src/runtime/team-runner.ts +186 -20
- package/src/runtime/usage-tracker.ts +4 -2
- package/src/runtime/verification-gates.ts +36 -9
- package/src/schema/team-tool-schema.ts +27 -9
- package/src/skills/discover-skills.ts +9 -3
- package/src/state/active-run-registry.ts +170 -38
- package/src/state/artifact-store.ts +25 -13
- package/src/state/atomic-write-v2.ts +86 -0
- package/src/state/atomic-write.ts +346 -55
- package/src/state/blob-store.ts +178 -10
- package/src/state/contracts.ts +2 -1
- package/src/state/crew-init.ts +161 -28
- package/src/state/decision-ledger.ts +172 -111
- package/src/state/event-log-rotation.ts +82 -52
- package/src/state/event-log.ts +270 -75
- package/src/state/health-store.ts +71 -0
- package/src/state/hook-instinct-bridge.ts +2 -1
- package/src/state/locks.ts +109 -20
- package/src/state/mailbox.ts +45 -7
- package/src/state/observation-store.ts +4 -1
- package/src/state/run-graph.ts +141 -130
- package/src/state/run-metrics.ts +24 -8
- package/src/state/state-store.ts +333 -44
- package/src/state/task-claims.ts +9 -2
- package/src/tools/safe-bash.ts +69 -20
- package/src/types/new-api-types.ts +10 -5
- package/src/ui/keybinding-map.ts +2 -1
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +4 -0
- package/src/ui/overlays/agent-picker-overlay.ts +1 -1
- package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +4 -3
- package/src/ui/run-snapshot-cache.ts +1 -1
- package/src/ui/status-colors.ts +2 -1
- package/src/ui/syntax-highlight.ts +2 -1
- package/src/ui/tool-render.ts +13 -3
- package/src/utils/env-filter.ts +66 -0
- package/src/utils/file-coalescer.ts +9 -1
- package/src/utils/fs-watch.ts +4 -2
- package/src/utils/gh-protocol.ts +2 -1
- package/src/utils/paths.ts +80 -5
- package/src/utils/redaction.ts +6 -1
- package/src/utils/safe-paths.ts +287 -10
- package/src/worktree/cleanup.ts +117 -26
- package/src/worktree/worktree-manager.ts +128 -15
- package/test-bugs-all.mjs +10 -6
- package/test-integration-check.ts +4 -0
|
@@ -25,9 +25,14 @@ import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
|
25
25
|
import { locateRunCwd } from "../team-tool.ts";
|
|
26
26
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
27
27
|
|
|
28
|
-
function globMatch(value: string, pattern: string): boolean {
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export function globMatch(value: string, pattern: string): boolean {
|
|
29
|
+
// Prevent ReDoS: reject excessively long patterns
|
|
30
|
+
if (pattern.length > 200) return false;
|
|
31
|
+
const regex = pattern
|
|
32
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars
|
|
33
|
+
.replace(/\*/g, "[^/]*") // * matches non-slash characters only
|
|
34
|
+
.replace(/\?/g, "[^/]"); // ? matches single non-slash
|
|
35
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
|
|
@@ -87,7 +92,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
87
92
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
88
93
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
89
94
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
90
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
95
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
91
96
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "api", status: "error" }, true);
|
|
92
97
|
if (operation === "read-manifest") {
|
|
93
98
|
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -97,7 +102,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
97
102
|
if (!permission.allowed) return result(permission.reason ?? "Plan approval is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
98
103
|
try {
|
|
99
104
|
return withRunLockSync(loaded.manifest, () => {
|
|
100
|
-
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
105
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
|
|
101
106
|
const approval = current.manifest.planApproval;
|
|
102
107
|
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
103
108
|
const now = new Date().toISOString();
|
|
@@ -116,7 +121,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
116
121
|
if (!permission.allowed) return result(permission.reason ?? "Plan approval cancellation is not allowed in this context.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
117
122
|
try {
|
|
118
123
|
return withRunLockSync(loaded.manifest, () => {
|
|
119
|
-
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded;
|
|
124
|
+
const current = loadRunManifestById(ctx.cwd, loaded.manifest.runId) ?? loaded; // NOTE: inside withRunLockSync - consistent read
|
|
120
125
|
const approval = current.manifest.planApproval;
|
|
121
126
|
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
122
127
|
const now = new Date().toISOString();
|
|
@@ -364,7 +369,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
364
369
|
const updatedTask = claimTask(task, owner);
|
|
365
370
|
const tasks = loaded.tasks.map((item) => item.id === task.id ? updatedTask : item);
|
|
366
371
|
saveRunTasks(loaded.manifest, tasks);
|
|
367
|
-
appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token:
|
|
372
|
+
appendEvent(loaded.manifest.eventsPath, { type: "task.claimed", runId: loaded.manifest.runId, taskId: task.id, data: { owner, token: "[REDACTED]", leasedUntil: updatedTask.claim?.leasedUntil } });
|
|
368
373
|
return result(JSON.stringify(updatedTask.claim, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
369
374
|
});
|
|
370
375
|
} catch (error) {
|
|
@@ -41,7 +41,7 @@ export function abortOwned(
|
|
|
41
41
|
): AbortOwnedResult {
|
|
42
42
|
const runCwd = locateRunCwd(runId, ctx.cwd);
|
|
43
43
|
if (!runCwd) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
44
|
-
const loaded = loadRunManifestById(runCwd, runId);
|
|
44
|
+
const loaded = loadRunManifestById(runCwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
45
45
|
if (!loaded) return { abortedIds: [], missingIds: taskIds ?? [], foreignIds: [] };
|
|
46
46
|
|
|
47
47
|
const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
|
|
@@ -81,7 +81,7 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
|
|
|
81
81
|
if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
|
|
82
82
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
83
83
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
84
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
84
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
85
85
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "retry", status: "error" }, true);
|
|
86
86
|
|
|
87
87
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
@@ -146,7 +146,7 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
146
146
|
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
147
147
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
148
148
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
149
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
149
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
150
150
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
151
151
|
|
|
152
152
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { effectiveAutonomousConfig, parseConfig, type PiTeamsAutonomousConfig, type PiTeamsConfig } from "../../config/config.ts";
|
|
2
2
|
|
|
3
|
+
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
4
|
+
|
|
5
|
+
/** Recursively strip dangerous prototype-pollution keys from all levels of an object. */
|
|
6
|
+
export function sanitizeObject<T>(obj: T): T {
|
|
7
|
+
if (obj === null || obj === undefined || typeof obj !== "object") return obj;
|
|
8
|
+
if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item)) as T;
|
|
9
|
+
const safe: Record<string, unknown> = {};
|
|
10
|
+
for (const key of Object.keys(obj as Record<string, unknown>)) {
|
|
11
|
+
if (DANGEROUS_KEYS.has(key)) continue;
|
|
12
|
+
safe[key] = sanitizeObject((obj as Record<string, unknown>)[key]);
|
|
13
|
+
}
|
|
14
|
+
return safe as T;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
export function autonomousPatchFromConfig(config: unknown): PiTeamsAutonomousConfig {
|
|
4
18
|
const rootPatch = parseConfig(config).autonomous;
|
|
5
19
|
if (rootPatch) return rootPatch;
|
|
@@ -11,7 +25,7 @@ export function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export function effectiveRunConfig(base: PiTeamsConfig, rawOverride: unknown): PiTeamsConfig {
|
|
14
|
-
const patch = parseConfig(rawOverride);
|
|
28
|
+
const patch = sanitizeObject(parseConfig(rawOverride));
|
|
15
29
|
return {
|
|
16
30
|
...base,
|
|
17
31
|
...patch,
|
|
@@ -209,7 +209,7 @@ export function handleExplain(params: {
|
|
|
209
209
|
return result("explain requires runId", { action: "explain", status: "error" }, true);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
const loaded = loadRunManifestById(cwd, params.runId);
|
|
212
|
+
const loaded = loadRunManifestById(cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
213
213
|
if (!loaded) {
|
|
214
214
|
return result(`Run '${params.runId}' not found.`, { action: "explain", status: "error" }, true);
|
|
215
215
|
}
|
|
@@ -163,6 +163,43 @@ function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.t
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/** Update an existing scheduled job in persistent settings. */
|
|
167
|
+
export function persistScheduledJobUpdate(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
|
|
168
|
+
try {
|
|
169
|
+
const settings = loadCrewSettings(cwd);
|
|
170
|
+
const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
|
|
171
|
+
(settings as Record<string, unknown>).scheduledJobs,
|
|
172
|
+
)
|
|
173
|
+
? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
|
|
174
|
+
: [];
|
|
175
|
+
const updated = existingJobs.map((j) => j.id === job.id ? job : j);
|
|
176
|
+
saveCrewSettings(
|
|
177
|
+
{ ...settings, scheduledJobs: updated } as Parameters<typeof saveCrewSettings>[0],
|
|
178
|
+
cwd,
|
|
179
|
+
);
|
|
180
|
+
} catch {
|
|
181
|
+
/* best-effort persistence */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Remove a scheduled job from persistent settings. */
|
|
186
|
+
function persistScheduledJobRemove(cwd: string, jobId: string): void {
|
|
187
|
+
try {
|
|
188
|
+
const settings = loadCrewSettings(cwd);
|
|
189
|
+
const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
|
|
190
|
+
(settings as Record<string, unknown>).scheduledJobs,
|
|
191
|
+
)
|
|
192
|
+
? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
|
|
193
|
+
: [];
|
|
194
|
+
saveCrewSettings(
|
|
195
|
+
{ ...settings, scheduledJobs: existingJobs.filter((j) => j.id !== jobId) } as Parameters<typeof saveCrewSettings>[0],
|
|
196
|
+
cwd,
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
/* best-effort persistence */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
166
203
|
export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
167
204
|
const scheduler = getCrewScheduler();
|
|
168
205
|
if (!scheduler) return result("Scheduler not running.", { action: "scheduled", status: "error" }, true);
|
|
@@ -177,6 +214,9 @@ export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamConte
|
|
|
177
214
|
` Next run: ${job.nextRun ?? "(unscheduled)"}`,
|
|
178
215
|
` Runs: ${job.runCount}, Last: ${job.lastRun ?? "(never)"} [${job.lastStatus ?? "?"}]`,
|
|
179
216
|
);
|
|
217
|
+
if (job.spawnedRunIds && job.spawnedRunIds.length > 0) {
|
|
218
|
+
lines.push(` Spawned runs: ${job.spawnedRunIds.join(", ")}`);
|
|
219
|
+
}
|
|
180
220
|
}
|
|
181
221
|
return result(lines.join("\n"), { action: "scheduled", status: "ok" });
|
|
182
222
|
}
|
|
@@ -10,7 +10,7 @@ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
10
10
|
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
11
11
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
12
12
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
13
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
13
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
14
14
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "events", status: "error" }, true);
|
|
15
15
|
const events = readEvents(loaded.manifest.eventsPath);
|
|
16
16
|
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
@@ -21,7 +21,7 @@ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
21
21
|
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
22
22
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
23
23
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
24
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
24
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
25
25
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "artifacts", status: "error" }, true);
|
|
26
26
|
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
27
27
|
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -31,7 +31,7 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
31
31
|
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
32
32
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
33
33
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
34
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
34
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
35
35
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "summary", status: "error" }, true);
|
|
36
36
|
const usage = aggregateUsage(loaded.tasks);
|
|
37
37
|
const lines = [
|
|
@@ -17,7 +17,7 @@ import * as path from "node:path";
|
|
|
17
17
|
|
|
18
18
|
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
19
19
|
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
20
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
20
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
21
21
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "worktrees", status: "error" }, true);
|
|
22
22
|
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
23
23
|
const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
|
|
@@ -46,7 +46,7 @@ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
46
46
|
|
|
47
47
|
export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
48
48
|
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
49
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
49
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
50
50
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "export", status: "error" }, true);
|
|
51
51
|
|
|
52
52
|
// SECURITY: Ownership check — only the owner session may export a run.
|
|
@@ -95,7 +95,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
95
95
|
if (intentError) return intentError;
|
|
96
96
|
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
97
97
|
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
98
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
98
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
99
99
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "forget", status: "error" }, true);
|
|
100
100
|
|
|
101
101
|
// Ownership check — prevent cross-session deletion unless force is set
|
|
@@ -125,7 +125,7 @@ export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContex
|
|
|
125
125
|
const intentError = enforceDestructiveIntent("cleanup", params, ctx.config);
|
|
126
126
|
if (intentError) return intentError;
|
|
127
127
|
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
128
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
128
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
129
129
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cleanup", status: "error" }, true);
|
|
130
130
|
|
|
131
131
|
// Ownership check — prevent cross-session worktree cleanup unless force is set
|
|
@@ -20,11 +20,11 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
20
20
|
|
|
21
21
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
22
22
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
23
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
23
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
24
24
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
25
25
|
|
|
26
26
|
return withRunLockSync(loaded.manifest, () => {
|
|
27
|
-
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!);
|
|
27
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!); // NOTE: inside withRunLockSync - consistent read
|
|
28
28
|
if (!fresh) return result(`Run '${params.runId}' not found.`, { action: "respond", status: "error" }, true);
|
|
29
29
|
const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
|
|
30
30
|
if (foreignRun && !params.force) return result(`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
|
|
@@ -2,6 +2,7 @@ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
|
2
2
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
3
3
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
4
4
|
import { loadConfig } from "../../config/config.ts";
|
|
5
|
+
import { findGitRoot, assertCleanLeader } from "../../worktree/worktree-manager.ts";
|
|
5
6
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
6
7
|
import { writeArtifact } from "../../state/artifact-store.ts";
|
|
7
8
|
import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-registry.ts";
|
|
@@ -85,9 +86,57 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
85
86
|
const { ensureCrewDirectory } = await import("../../state/crew-init.ts");
|
|
86
87
|
await ensureCrewDirectory(workingDir);
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// WORKTREE FIX: If worktree mode is needed but cwd is not a git repo,
|
|
90
|
+
// auto-correct to the nearest git repo root. This prevents "not a git repository"
|
|
91
|
+
// errors when ctx.cwd points to a parent directory that isn't a git repo.
|
|
92
|
+
let resolvedCtx = ctx;
|
|
93
|
+
if (workingDir) {
|
|
94
|
+
try {
|
|
95
|
+
const gitRoot = findGitRoot(workingDir);
|
|
96
|
+
if (gitRoot && gitRoot !== workingDir) {
|
|
97
|
+
resolvedCtx = { ...ctx, cwd: gitRoot };
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// cwd is not in a git repo — validate below if worktree mode is needed
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// WORKTREE PRECONDITION CHECK: validate git repo exists and is clean
|
|
105
|
+
// BEFORE creating the run manifest, so we return a friendly error
|
|
106
|
+
// instead of crashing mid-execution in prepareTaskWorkspace.
|
|
107
|
+
if (params.workspaceMode === "worktree") {
|
|
108
|
+
let gitRoot: string | undefined;
|
|
109
|
+
try {
|
|
110
|
+
gitRoot = findGitRoot(resolvedCtx.cwd);
|
|
111
|
+
} catch {
|
|
112
|
+
// not a git repo
|
|
113
|
+
}
|
|
114
|
+
if (!gitRoot) {
|
|
115
|
+
return result(
|
|
116
|
+
`Worktree mode requires a git repository. '${resolvedCtx.cwd}' is not inside a git repo.\nUse workspaceMode: 'single' or run from a git repository.`,
|
|
117
|
+
{ action: "run", status: "error" },
|
|
118
|
+
true,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Check if clean leader is required (can be disabled via config)
|
|
122
|
+
const preCheckConfig = loadConfig(resolvedCtx.cwd);
|
|
123
|
+
if (preCheckConfig.config.requireCleanWorktreeLeader !== false) {
|
|
124
|
+
try {
|
|
125
|
+
assertCleanLeader(gitRoot);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
128
|
+
return result(
|
|
129
|
+
`${msg}\nCommit or stash changes before using worktree mode, or use workspaceMode: 'single'.`,
|
|
130
|
+
{ action: "run", status: "error" },
|
|
131
|
+
true,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const teams = allTeams(discoverTeams(resolvedCtx.cwd));
|
|
138
|
+
const workflows = allWorkflows(discoverWorkflows(resolvedCtx.cwd));
|
|
139
|
+
const agents = allAgents(discoverAgents(resolvedCtx.cwd));
|
|
91
140
|
const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
|
|
92
141
|
if (params.agent && !directAgent) return result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true);
|
|
93
142
|
const teamName = params.team ?? "default";
|
|
@@ -110,7 +159,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
110
159
|
steps: [{ id: "01_agent", role: params.role ?? "agent", task: "{goal}", model: params.model }],
|
|
111
160
|
} : workflows.find((item) => item.name === workflowName);
|
|
112
161
|
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
113
|
-
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow,
|
|
162
|
+
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, resolvedCtx.cwd);
|
|
114
163
|
|
|
115
164
|
// Check if this is a pipeline workflow - special handling for multi-stage execution
|
|
116
165
|
const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
|
|
@@ -152,7 +201,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
152
201
|
|
|
153
202
|
const skillOverride = normalizeSkillOverride(params.skill);
|
|
154
203
|
const { manifest, tasks, paths } = createRunManifest({
|
|
155
|
-
cwd:
|
|
204
|
+
cwd: resolvedCtx.cwd,
|
|
156
205
|
team,
|
|
157
206
|
workflow,
|
|
158
207
|
goal,
|
|
@@ -169,7 +218,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
169
218
|
atomicWriteJson(paths.manifestPath, updatedManifest);
|
|
170
219
|
registerActiveRun(updatedManifest);
|
|
171
220
|
|
|
172
|
-
const loadedConfig = loadConfig(
|
|
221
|
+
const loadedConfig = loadConfig(resolvedCtx.cwd);
|
|
173
222
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
174
223
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
175
224
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
@@ -205,11 +254,11 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
205
254
|
const asyncManifest = { ...effectiveManifest, async: { pid: spawned.pid, logPath: spawned.logPath, spawnedAt: new Date().toISOString() } };
|
|
206
255
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
207
256
|
void appendEventAsync(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
208
|
-
scheduleBackgroundEarlyExitGuard(
|
|
257
|
+
scheduleBackgroundEarlyExitGuard(resolvedCtx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
|
|
209
258
|
// Wait for the async run to complete and return actual results.
|
|
210
259
|
try {
|
|
211
|
-
const completed = await waitForRun(updatedManifest.runId,
|
|
212
|
-
const metrics = collectRunMetrics(
|
|
260
|
+
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
261
|
+
const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
|
|
213
262
|
const lines: string[] = [
|
|
214
263
|
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
215
264
|
`Goal: ${goal.slice(0, 100)}`,
|
|
@@ -341,8 +390,8 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
341
390
|
|
|
342
391
|
// Wait for the foreground run to complete and return actual results.
|
|
343
392
|
try {
|
|
344
|
-
const completed = await waitForRun(updatedManifest.runId,
|
|
345
|
-
const metrics = collectRunMetrics(
|
|
393
|
+
const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: 3600000 });
|
|
394
|
+
const metrics = collectRunMetrics(resolvedCtx.cwd, completed.manifest.runId);
|
|
346
395
|
const lines: string[] = [
|
|
347
396
|
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
348
397
|
`Goal: ${goal.slice(0, 100)}`,
|
|
@@ -18,7 +18,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
18
18
|
if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
|
|
19
19
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
20
20
|
if (!runCwd) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
21
|
-
const loaded = loadRunManifestById(runCwd, params.runId);
|
|
21
|
+
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
22
22
|
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "status", status: "error" }, true);
|
|
23
23
|
let { manifest, tasks } = loaded;
|
|
24
24
|
let asyncLivenessLine: string | undefined;
|