pi-crew 0.1.49 → 0.2.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.
- package/CHANGELOG.md +74 -1
- package/README.md +176 -781
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +70 -11
- package/agents/writer.md +11 -11
- package/docs/actions-reference.md +595 -0
- package/docs/commands-reference.md +347 -0
- package/docs/runtime-flow.md +148 -148
- package/index.ts +6 -6
- package/package.json +99 -99
- package/skills/async-worker-recovery/SKILL.md +42 -42
- package/skills/context-artifact-hygiene/SKILL.md +52 -52
- package/skills/delegation-patterns/SKILL.md +54 -54
- package/skills/mailbox-interactive/SKILL.md +40 -40
- package/skills/model-routing-context/SKILL.md +39 -39
- package/skills/multi-perspective-review/SKILL.md +58 -58
- package/skills/observability-reliability/SKILL.md +41 -41
- package/skills/orchestration/SKILL.md +157 -157
- package/skills/ownership-session-security/SKILL.md +41 -41
- package/skills/pi-extension-lifecycle/SKILL.md +39 -39
- package/skills/requirements-to-task-packet/SKILL.md +63 -63
- package/skills/resource-discovery-config/SKILL.md +41 -41
- package/skills/runtime-state-reader/SKILL.md +44 -44
- package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
- package/skills/state-mutation-locking/SKILL.md +42 -42
- package/skills/systematic-debugging/SKILL.md +67 -67
- package/skills/ui-render-performance/SKILL.md +39 -39
- package/skills/verification-before-done/SKILL.md +57 -57
- package/skills/worktree-isolation/SKILL.md +39 -39
- package/src/adapters/claude-adapter.ts +25 -0
- package/src/adapters/codex-adapter.ts +21 -0
- package/src/adapters/cursor-adapter.ts +17 -0
- package/src/adapters/export-util.ts +137 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +18 -0
- package/src/adapters/types.ts +23 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/agent-search.ts +98 -98
- package/src/agents/discover-agents.ts +2 -1
- package/src/config/config.ts +14 -1
- package/src/config/defaults.ts +5 -5
- package/src/config/drift-detector.ts +211 -0
- package/src/config/markers.ts +327 -0
- package/src/config/resilient-parser.ts +108 -0
- package/src/config/suggestions.ts +74 -0
- package/src/extension/cross-extension-rpc.ts +103 -82
- package/src/extension/project-init.ts +36 -4
- package/src/extension/register.ts +67 -22
- package/src/extension/registration/commands.ts +77 -8
- package/src/extension/registration/subagent-tools.ts +10 -1
- package/src/extension/registration/team-tool.ts +10 -1
- package/src/extension/registration/viewers.ts +48 -34
- package/src/extension/run-bundle-schema.ts +89 -89
- package/src/extension/run-export.ts +26 -12
- package/src/extension/run-import.ts +25 -1
- package/src/extension/run-index.ts +5 -1
- package/src/extension/run-maintenance.ts +142 -68
- package/src/extension/team-manager-command.ts +10 -1
- package/src/extension/team-tool/context.ts +1 -1
- package/src/extension/team-tool/doctor.ts +28 -3
- package/src/extension/team-tool/handle-settings.ts +195 -188
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/intent-policy.ts +42 -42
- package/src/extension/team-tool/lifecycle-actions.ts +27 -8
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/run.ts +12 -1
- package/src/extension/team-tool.ts +14 -3
- package/src/i18n.ts +184 -184
- package/src/observability/exporters/otlp-exporter.ts +92 -77
- package/src/prompt/prompt-runtime.ts +72 -72
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -28
- package/src/runtime/auto-resume.ts +100 -0
- package/src/runtime/background-runner.ts +11 -1
- package/src/runtime/cancellation-token.ts +89 -89
- package/src/runtime/cancellation.ts +61 -61
- package/src/runtime/capability-inventory.ts +116 -116
- package/src/runtime/child-pi.ts +7 -2
- package/src/runtime/compaction-summary.ts +271 -0
- package/src/runtime/completion-guard.ts +190 -190
- package/src/runtime/concurrency.ts +3 -1
- package/src/runtime/crash-recovery.ts +33 -0
- package/src/runtime/delta-conflict.ts +360 -0
- package/src/runtime/diagnostic-export.ts +3 -1
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/event-stream-bridge.ts +3 -1
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +124 -124
- package/src/runtime/iteration-hooks.ts +262 -0
- package/src/runtime/live-agent-control.ts +88 -88
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-extension-bridge.ts +150 -150
- package/src/runtime/live-irc.ts +92 -92
- package/src/runtime/live-session-health.ts +100 -100
- package/src/runtime/loop-gates.ts +129 -0
- package/src/runtime/metric-parser.ts +40 -0
- package/src/runtime/notebook-helpers.ts +90 -90
- package/src/runtime/orphan-sentinel.ts +7 -7
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/phase-progress.ts +217 -0
- package/src/runtime/pi-args.ts +38 -2
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/pi-spawn.ts +74 -6
- package/src/runtime/policy-engine.ts +79 -79
- package/src/runtime/post-checks.ts +122 -0
- package/src/runtime/process-status.ts +14 -1
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/prose-compressor.ts +164 -164
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/result-extractor.ts +121 -121
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/sensitive-paths.ts +3 -3
- package/src/runtime/session-resources.ts +25 -25
- package/src/runtime/session-snapshot.ts +59 -59
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stream-preview.ts +177 -177
- package/src/runtime/supervisor-contact.ts +59 -59
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-graph.ts +207 -0
- package/src/runtime/task-quality.ts +207 -0
- package/src/runtime/task-runner/capabilities.ts +78 -78
- package/src/runtime/task-runner/live-executor.ts +7 -1
- package/src/runtime/task-runner/progress.ts +119 -119
- package/src/runtime/task-runner/prompt-builder.ts +1 -1
- package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/run-projection.ts +103 -103
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +126 -7
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/runtime/workflow-state.ts +187 -0
- package/src/runtime/workspace-tree.ts +298 -298
- package/src/schema/config-schema.ts +12 -0
- package/src/schema/validation-types.ts +148 -0
- package/src/skills/skill-templates.ts +374 -0
- package/src/state/active-run-registry.ts +35 -11
- package/src/state/atomic-write.ts +33 -26
- package/src/state/contracts.ts +1 -0
- package/src/state/event-reconstructor.ts +217 -0
- package/src/state/locks.ts +2 -11
- package/src/state/mailbox.ts +4 -3
- package/src/state/state-store.ts +32 -14
- package/src/state/task-claims.ts +44 -44
- package/src/state/types.ts +9 -0
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/team-serializer.ts +38 -38
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +9 -4
- package/src/ui/dashboard-panes/cancellation-pane.ts +42 -42
- package/src/ui/dashboard-panes/capability-pane.ts +59 -59
- package/src/ui/dashboard-panes/mailbox-pane.ts +35 -35
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dashboard-panes/progress-pane.ts +11 -0
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/powerbar-publisher.ts +6 -0
- package/src/ui/render-coalescer.ts +51 -51
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +143 -143
- package/src/ui/run-action-dispatcher.ts +10 -1
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -58
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-entries.ts +258 -258
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/frontmatter.ts +68 -68
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +17 -17
- package/src/utils/incremental-reader.ts +104 -104
- package/src/utils/names.ts +27 -27
- package/src/utils/redaction.ts +44 -44
- package/src/utils/safe-paths.ts +47 -47
- package/src/utils/scan-cache.ts +136 -136
- package/src/utils/sleep.ts +40 -26
- package/src/utils/task-name-generator.ts +337 -337
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +11 -3
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +30 -29
- package/workflows/fast-fix.workflow.md +23 -22
- package/workflows/implementation.workflow.md +43 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
- package/docs/refactor-tasks-phase3.md +0 -394
- package/docs/refactor-tasks-phase4.md +0 -564
- package/docs/refactor-tasks-phase5.md +0 -402
- package/docs/refactor-tasks-phase6.md +0 -662
- package/docs/refactor-tasks.md +0 -1484
- package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +0 -261
- package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +0 -111
- package/docs/research/AUDIT_OH_MY_PI.md +0 -261
- package/docs/research/AUDIT_PI_CREW.md +0 -457
- package/docs/research/CAVEMAN-DEEP-RESEARCH.md +0 -281
- package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +0 -264
- package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +0 -343
- package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +0 -480
- package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +0 -354
- package/docs/research/IMPLEMENTATION_PLAN.md +0 -385
- package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +0 -502
- package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +0 -266
- package/docs/research/REMAINING-GAPS-PLAN.md +0 -363
- package/docs/research/SESSION-SUMMARY-2026-05-08.md +0 -146
- package/docs/research/UI-RESPONSIVENESS-AUDIT.md +0 -173
- package/docs/research-awesome-agent-skills-distillation.md +0 -100
- package/docs/research-extension-examples.md +0 -297
- package/docs/research-extension-system.md +0 -324
- package/docs/research-oh-my-pi-distillation.md +0 -369
- package/docs/research-optimization-plan.md +0 -548
- package/docs/research-phase10-distillation.md +0 -199
- package/docs/research-phase11-distillation.md +0 -201
- package/docs/research-phase8-operator-experience-plan.md +0 -819
- package/docs/research-phase9-observability-reliability-plan.md +0 -1190
- package/docs/research-pi-coding-agent.md +0 -357
- package/docs/research-source-pi-crew-reference.md +0 -174
- package/docs/research-ui-optimization-plan.md +0 -480
- package/docs/source-runtime-refactor-map.md +0 -107
- package/src/utils/atomic-write.ts +0 -33
|
@@ -1,34 +1,48 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
3
|
-
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
4
|
-
import { loadConfig } from "../../config/config.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
3
|
+
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
4
|
+
import { loadConfig } from "../../config/config.ts";
|
|
5
|
+
// Lazy-loaded: DurableTranscriptViewer is 658ms — only needed for /crew transcript command
|
|
6
|
+
import type { DurableTranscriptViewer as DurableTranscriptViewerType } from "../../ui/transcript-viewer.ts";
|
|
7
|
+
let _cachedViewer: typeof DurableTranscriptViewerType | undefined;
|
|
8
|
+
let _viewerPromise: Promise<typeof DurableTranscriptViewerType> | undefined;
|
|
9
|
+
async function getViewer(): Promise<typeof DurableTranscriptViewerType> {
|
|
10
|
+
if (_cachedViewer) return _cachedViewer;
|
|
11
|
+
if (!_viewerPromise) {
|
|
12
|
+
_viewerPromise = import("../../ui/transcript-viewer.ts").then((mod) => {
|
|
13
|
+
_cachedViewer = mod.DurableTranscriptViewer;
|
|
14
|
+
return mod.DurableTranscriptViewer;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return _viewerPromise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function selectAgentTask(ctx: ExtensionCommandContext, runId: string | undefined, taskId?: string): Promise<{ runId: string; taskId?: string } | undefined> {
|
|
21
|
+
if (!runId) return undefined;
|
|
22
|
+
if (taskId) return { runId, taskId };
|
|
23
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
24
|
+
if (!loaded) return { runId };
|
|
25
|
+
const agents = readCrewAgents(loaded.manifest);
|
|
26
|
+
if (ctx.hasUI && agents.length > 1) {
|
|
27
|
+
const choice = await ctx.ui.select("Select pi-crew agent", agents.map((agent) => `${agent.taskId} ${agent.role}→${agent.agent} [${agent.status}]`));
|
|
28
|
+
return { runId, taskId: choice?.split(" ")[0] };
|
|
29
|
+
}
|
|
30
|
+
return { runId, taskId: agents[0]?.taskId };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function openTranscriptViewer(ctx: ExtensionCommandContext, initialRunId: string | undefined, initialTaskId?: string): Promise<boolean> {
|
|
34
|
+
const selected = await selectAgentTask(ctx, initialRunId, initialTaskId);
|
|
35
|
+
if (!selected) return false;
|
|
36
|
+
const runId = selected.runId;
|
|
37
|
+
const taskId = selected.taskId;
|
|
38
|
+
if (!runId || !ctx.hasUI) return false;
|
|
39
|
+
const loaded = loadRunManifestById(ctx.cwd, runId);
|
|
40
|
+
if (!loaded) return false;
|
|
41
|
+
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
42
|
+
const DurableTranscriptViewer = await getViewer();
|
|
43
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTranscriptViewer(loaded.manifest, theme, done, taskId, { maxTailBytes: uiConfig?.transcriptTailBytes }), {
|
|
44
|
+
overlay: true,
|
|
45
|
+
overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" },
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
-
import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
|
|
3
|
-
import type { TeamEvent } from "../state/event-log.ts";
|
|
4
|
-
import type { ExportedRunBundle } from "./run-export.ts";
|
|
5
|
-
|
|
6
|
-
export interface BundleValidationResult {
|
|
7
|
-
ok: boolean;
|
|
8
|
-
errors: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
|
|
16
|
-
if (!isRecord(value)) {
|
|
17
|
-
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
const before = errors.length;
|
|
21
|
-
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
22
|
-
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
23
|
-
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
24
|
-
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
25
|
-
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
26
|
-
return errors.length === before;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
30
|
-
if (!isRecord(value)) {
|
|
31
|
-
errors.push("manifest must be an object.");
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const before = errors.length;
|
|
35
|
-
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
36
|
-
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
37
|
-
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
38
|
-
}
|
|
39
|
-
if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
|
|
40
|
-
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
41
|
-
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
42
|
-
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
43
|
-
return errors.length === before;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
47
|
-
if (!isRecord(value)) {
|
|
48
|
-
errors.push(`tasks[${index}] must be an object.`);
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
const before = errors.length;
|
|
52
|
-
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
53
|
-
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
54
|
-
}
|
|
55
|
-
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
56
|
-
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
57
|
-
return errors.length === before;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
61
|
-
if (!isRecord(value)) {
|
|
62
|
-
errors.push(`events[${index}] must be an object.`);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
const before = errors.length;
|
|
66
|
-
for (const field of ["time", "type", "runId"] as const) {
|
|
67
|
-
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
68
|
-
}
|
|
69
|
-
return errors.length === before;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
73
|
-
const errors: string[] = [];
|
|
74
|
-
if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
|
|
75
|
-
if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
|
|
76
|
-
if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
|
|
77
|
-
validateManifest(value.manifest, errors);
|
|
78
|
-
if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
|
|
79
|
-
else value.tasks.forEach((task, index) => validateTask(task, index, errors));
|
|
80
|
-
if (!Array.isArray(value.events)) errors.push("events must be an array.");
|
|
81
|
-
else value.events.forEach((event, index) => validateEvent(event, index, errors));
|
|
82
|
-
if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
|
|
83
|
-
return { ok: errors.length === 0, errors };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
|
|
87
|
-
const validation = validateRunBundle(value);
|
|
88
|
-
if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
89
|
-
}
|
|
1
|
+
import { isTeamRunStatus, isTeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
+
import type { TeamRunManifest, TeamTaskState, ArtifactDescriptor } from "../state/types.ts";
|
|
3
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
4
|
+
import type { ExportedRunBundle } from "./run-export.ts";
|
|
5
|
+
|
|
6
|
+
export interface BundleValidationResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateArtifact(value: unknown, index: number, errors: string[]): value is ArtifactDescriptor {
|
|
16
|
+
if (!isRecord(value)) {
|
|
17
|
+
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const before = errors.length;
|
|
21
|
+
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
22
|
+
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
23
|
+
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
24
|
+
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
25
|
+
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
26
|
+
return errors.length === before;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
errors.push("manifest must be an object.");
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const before = errors.length;
|
|
35
|
+
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
36
|
+
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
37
|
+
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
38
|
+
}
|
|
39
|
+
if (!isTeamRunStatus(value.status)) errors.push("manifest.status is invalid.");
|
|
40
|
+
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
41
|
+
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
42
|
+
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
43
|
+
return errors.length === before;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
errors.push(`tasks[${index}] must be an object.`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const before = errors.length;
|
|
52
|
+
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
53
|
+
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
54
|
+
}
|
|
55
|
+
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
56
|
+
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
57
|
+
return errors.length === before;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
61
|
+
if (!isRecord(value)) {
|
|
62
|
+
errors.push(`events[${index}] must be an object.`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const before = errors.length;
|
|
66
|
+
for (const field of ["time", "type", "runId"] as const) {
|
|
67
|
+
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
68
|
+
}
|
|
69
|
+
return errors.length === before;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
if (!isRecord(value)) return { ok: false, errors: ["bundle must be an object."] };
|
|
75
|
+
if (value.schemaVersion !== 1) errors.push("schemaVersion must be 1.");
|
|
76
|
+
if (typeof value.exportedAt !== "string") errors.push("exportedAt must be a string.");
|
|
77
|
+
validateManifest(value.manifest, errors);
|
|
78
|
+
if (!Array.isArray(value.tasks)) errors.push("tasks must be an array.");
|
|
79
|
+
else value.tasks.forEach((task, index) => validateTask(task, index, errors));
|
|
80
|
+
if (!Array.isArray(value.events)) errors.push("events must be an array.");
|
|
81
|
+
else value.events.forEach((event, index) => validateEvent(event, index, errors));
|
|
82
|
+
if (!Array.isArray(value.artifactPaths) || !value.artifactPaths.every((item) => typeof item === "string")) errors.push("artifactPaths must be an array of strings.");
|
|
83
|
+
return { ok: errors.length === 0, errors };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function assertRunBundle(value: unknown): asserts value is ExportedRunBundle {
|
|
87
|
+
const validation = validateRunBundle(value);
|
|
88
|
+
if (!validation.ok) throw new Error(`File is not a valid pi-crew exported run bundle:\n${validation.errors.map((error) => `- ${error}`).join("\n")}`);
|
|
89
|
+
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
3
4
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
5
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
6
|
import { readEvents, type TeamEvent } from "../state/event-log.ts";
|
|
7
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
8
|
+
|
|
9
|
+
/** Replace absolute paths containing home directory with ~/ */
|
|
10
|
+
function redactHomePaths<T>(obj: T): T {
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
if (!home) return redactSecrets(obj) as T;
|
|
13
|
+
const json = JSON.stringify(obj);
|
|
14
|
+
const safe = json.split(home).join("~");
|
|
15
|
+
return redactSecrets(JSON.parse(safe)) as T;
|
|
16
|
+
}
|
|
6
17
|
|
|
7
18
|
export interface ExportedRunBundle {
|
|
8
19
|
schemaVersion: 1;
|
|
@@ -15,13 +26,16 @@ export interface ExportedRunBundle {
|
|
|
15
26
|
|
|
16
27
|
export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[]): { jsonPath: string; markdownPath: string } {
|
|
17
28
|
const events = readEvents(manifest.eventsPath);
|
|
29
|
+
const safeManifest = redactHomePaths(manifest);
|
|
30
|
+
const safeTasks = redactHomePaths(tasks);
|
|
31
|
+
const safeEvents = redactHomePaths(events);
|
|
18
32
|
const bundle: ExportedRunBundle = {
|
|
19
33
|
schemaVersion: 1,
|
|
20
34
|
exportedAt: new Date().toISOString(),
|
|
21
|
-
manifest,
|
|
22
|
-
tasks,
|
|
23
|
-
events,
|
|
24
|
-
artifactPaths:
|
|
35
|
+
manifest: safeManifest as TeamRunManifest,
|
|
36
|
+
tasks: safeTasks as TeamTaskState[],
|
|
37
|
+
events: safeEvents as TeamEvent[],
|
|
38
|
+
artifactPaths: safeManifest.artifacts.map((artifact) => artifact.path),
|
|
25
39
|
};
|
|
26
40
|
const json = writeArtifact(manifest.artifactsRoot, {
|
|
27
41
|
kind: "metadata",
|
|
@@ -34,22 +48,22 @@ export function exportRunBundle(manifest: TeamRunManifest, tasks: TeamTaskState[
|
|
|
34
48
|
relativePath: "export/run-export.md",
|
|
35
49
|
producer: "run-export",
|
|
36
50
|
content: [
|
|
37
|
-
`# pi-crew export ${
|
|
51
|
+
`# pi-crew export ${safeManifest.runId}`,
|
|
38
52
|
"",
|
|
39
53
|
`Exported: ${bundle.exportedAt}`,
|
|
40
|
-
`Status: ${
|
|
41
|
-
`Team: ${
|
|
42
|
-
`Workflow: ${
|
|
43
|
-
`Goal: ${
|
|
54
|
+
`Status: ${safeManifest.status}`,
|
|
55
|
+
`Team: ${safeManifest.team}`,
|
|
56
|
+
`Workflow: ${safeManifest.workflow ?? "(none)"}`,
|
|
57
|
+
`Goal: ${safeManifest.goal}`,
|
|
44
58
|
"",
|
|
45
59
|
"## Tasks",
|
|
46
|
-
...
|
|
60
|
+
...safeTasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
47
61
|
"",
|
|
48
62
|
"## Artifacts",
|
|
49
|
-
...(
|
|
63
|
+
...(safeManifest.artifacts.length ? safeManifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}`) : ["- (none)"]),
|
|
50
64
|
"",
|
|
51
65
|
"## Recent Events",
|
|
52
|
-
...(
|
|
66
|
+
...(safeEvents.slice(-20).map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`)),
|
|
53
67
|
"",
|
|
54
68
|
].join("\n"),
|
|
55
69
|
});
|
|
@@ -4,12 +4,14 @@ import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
|
4
4
|
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
5
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
6
6
|
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
7
|
+
import { detectImportConflicts, type ConflictReport } from "../runtime/delta-conflict.ts";
|
|
7
8
|
|
|
8
9
|
export interface ImportedRunBundleInfo {
|
|
9
10
|
runId: string;
|
|
10
11
|
importedAt: string;
|
|
11
12
|
bundlePath: string;
|
|
12
13
|
summaryPath: string;
|
|
14
|
+
conflictReport?: ConflictReport;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
@@ -19,10 +21,32 @@ function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
|
19
21
|
|
|
20
22
|
export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
|
|
21
23
|
const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
|
|
24
|
+
// Path containment: only allow reading bundles from cwd or user home
|
|
25
|
+
const allowedBases = [cwd];
|
|
26
|
+
try { allowedBases.push(userCrewRoot()); } catch { /* ignore */ }
|
|
27
|
+
try { allowedBases.push(projectCrewRoot(cwd)); } catch { /* ignore */ }
|
|
28
|
+
const isContained = allowedBases.some((base) => resolvedPath.startsWith(base + path.sep) || resolvedPath === base);
|
|
29
|
+
if (!isContained) throw new Error(`Import path must be within project directory or crew root: ${resolvedPath}`);
|
|
22
30
|
const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
|
23
31
|
assertRunBundle(raw);
|
|
24
32
|
const runId = assertSafePathId("runId", raw.manifest.runId);
|
|
25
33
|
const importedAt = new Date().toISOString();
|
|
34
|
+
|
|
35
|
+
// Non-blocking conflict detection: compare incoming bundle against any existing state.
|
|
36
|
+
let conflictReport: ConflictReport | undefined;
|
|
37
|
+
try {
|
|
38
|
+
const existingManifestPath = path.join(importRoot(cwd, scope), runId, "run-export.json");
|
|
39
|
+
if (fs.existsSync(existingManifestPath)) {
|
|
40
|
+
const existingRaw = JSON.parse(fs.readFileSync(existingManifestPath, "utf-8")) as { manifest?: Record<string, unknown>; tasks?: unknown[] };
|
|
41
|
+
conflictReport = detectImportConflicts(
|
|
42
|
+
{ manifest: raw.manifest as unknown as Record<string, unknown>, tasks: raw.tasks as unknown[] },
|
|
43
|
+
{ manifest: existingRaw.manifest, tasks: existingRaw.tasks },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Conflict detection is best-effort; do not block import on failure.
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
const importsRoot = importRoot(cwd, scope);
|
|
27
51
|
fs.mkdirSync(importsRoot, { recursive: true });
|
|
28
52
|
if (fs.lstatSync(importsRoot).isSymbolicLink()) throw new Error(`Invalid import root: ${importsRoot}`);
|
|
@@ -56,5 +80,5 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
|
|
|
56
80
|
...raw.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
57
81
|
"",
|
|
58
82
|
].join("\n"), "utf-8");
|
|
59
|
-
return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary };
|
|
83
|
+
return { runId, importedAt, bundlePath: targetJson, summaryPath: targetSummary, ...(conflictReport?.hasConflicts ? { conflictReport } : {}) };
|
|
60
84
|
}
|
|
@@ -33,7 +33,11 @@ function collectRuns(root: string, maxEntries?: number, signal?: AbortSignal): T
|
|
|
33
33
|
if (i % 10 === 0) token.heartbeat(`collectRuns:${i}/${selected.length}`);
|
|
34
34
|
try {
|
|
35
35
|
const manifest = readManifest(path.join(resolveRealContainedPath(runsRoot, selected[i]), DEFAULT_PATHS.state.manifestFile));
|
|
36
|
-
if (manifest)
|
|
36
|
+
if (!manifest) continue;
|
|
37
|
+
// Filter out ghost runs: active status but CWD no longer exists.
|
|
38
|
+
// These are deadletter/replay/temp runs whose temp dirs were cleaned up.
|
|
39
|
+
if ((manifest.status === "queued" || manifest.status === "running" || manifest.status === "planning") && manifest.cwd && !fs.existsSync(manifest.cwd)) continue;
|
|
40
|
+
results.push(manifest);
|
|
37
41
|
} catch { /* skip unreadable manifests */ }
|
|
38
42
|
}
|
|
39
43
|
return results;
|
|
@@ -1,68 +1,142 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
|
-
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
5
|
-
import { projectCrewRoot } from "../utils/paths.ts";
|
|
6
|
-
import { listRuns } from "./run-index.ts";
|
|
7
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
-
import { redactSecrets } from "../utils/redaction.ts";
|
|
9
|
-
import { createCancellationToken } from "../runtime/cancellation-token.ts";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
5
|
+
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
6
|
+
import { listRuns } from "./run-index.ts";
|
|
7
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
8
|
+
import { redactSecrets } from "../utils/redaction.ts";
|
|
9
|
+
import { createCancellationToken } from "../runtime/cancellation-token.ts";
|
|
10
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
11
|
+
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
12
|
+
|
|
13
|
+
export interface PruneRunsResult {
|
|
14
|
+
kept: string[];
|
|
15
|
+
removed: string[];
|
|
16
|
+
auditPath?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PruneRunsOptions {
|
|
20
|
+
intent?: string;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isFinished(run: TeamRunManifest): boolean {
|
|
25
|
+
return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
|
|
29
|
+
try {
|
|
30
|
+
const crewRoot = run.stateRoot.startsWith(userCrewRoot() + path.sep) ? userCrewRoot() : projectCrewRoot(cwd);
|
|
31
|
+
resolveRealContainedPath(crewRoot, run.stateRoot);
|
|
32
|
+
resolveRealContainedPath(crewRoot, run.artifactsRoot);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendPruneAudit(cwd: string, payload: Record<string, unknown>): string | undefined {
|
|
40
|
+
try {
|
|
41
|
+
const filePath = path.join(projectCrewRoot(cwd), "audit", "prune.jsonl");
|
|
42
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
43
|
+
fs.appendFileSync(filePath, `${JSON.stringify(redactSecrets({ ...payload, auditedAt: new Date().toISOString() }))}\n`, "utf-8");
|
|
44
|
+
return filePath;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logInternalError("prune.audit-write", error, `cwd=${cwd}`);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function pruneFinishedRuns(cwd: string, keep: number, options: PruneRunsOptions = {}): PruneRunsResult {
|
|
52
|
+
const token = createCancellationToken({ signal: options.signal });
|
|
53
|
+
const finished = listRuns(cwd, options.signal).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
54
|
+
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
55
|
+
const removed: string[] = [];
|
|
56
|
+
const toRemove = finished.slice(keep);
|
|
57
|
+
for (let i = 0; i < toRemove.length; i++) {
|
|
58
|
+
if (i % 5 === 0) token.heartbeat(`prune:${i}/${toRemove.length}`);
|
|
59
|
+
const run = toRemove[i];
|
|
60
|
+
if (!isSafeToPrune(cwd, run)) {
|
|
61
|
+
logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
65
|
+
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
66
|
+
removed.push(run.runId);
|
|
67
|
+
}
|
|
68
|
+
const auditPath = appendPruneAudit(cwd, { action: "prune", keep, intent: options.intent, kept, removed });
|
|
69
|
+
return { kept, removed, auditPath };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Prune finished run directories at the user level (~/.pi/agent/extensions/pi-crew/state/runs/).
|
|
74
|
+
*
|
|
75
|
+
* This handles runs created without a project root (e.g. `team action='run'` from home directory)
|
|
76
|
+
* that would otherwise accumulate forever.
|
|
77
|
+
*
|
|
78
|
+
* @param keep Number of most recent finished runs to retain
|
|
79
|
+
* @returns kept and removed run IDs
|
|
80
|
+
*/
|
|
81
|
+
export function pruneUserLevelRuns(keep: number): PruneRunsResult {
|
|
82
|
+
const crewRoot = userCrewRoot();
|
|
83
|
+
const runsRoot = path.join(crewRoot, DEFAULT_PATHS.state.runsSubdir);
|
|
84
|
+
if (!fs.existsSync(runsRoot)) return { kept: [], removed: [] };
|
|
85
|
+
|
|
86
|
+
// Read all run directories, parse manifests, filter to finished
|
|
87
|
+
const MAX_DIRS = 500;
|
|
88
|
+
const finished: Array<{ runId: string; updatedAt: string; stateRoot: string; artifactsRoot: string }> = [];
|
|
89
|
+
const ghostRemoved: string[] = [];
|
|
90
|
+
const dirs = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
91
|
+
.filter((entry) => entry.isDirectory() && isSafePathId(entry.name))
|
|
92
|
+
.slice(0, MAX_DIRS)
|
|
93
|
+
.map((entry) => entry.name);
|
|
94
|
+
|
|
95
|
+
for (const dir of dirs) {
|
|
96
|
+
const manifestPath = path.join(runsRoot, dir, DEFAULT_PATHS.state.manifestFile);
|
|
97
|
+
let manifest: TeamRunManifest | undefined;
|
|
98
|
+
try {
|
|
99
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest;
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Ghost run cleanup: active status but CWD no longer exists.
|
|
105
|
+
// These are deadletter/replay/temp runs from dead Pi sessions.
|
|
106
|
+
const isActive = manifest.status === "queued" || manifest.status === "running" || manifest.status === "planning";
|
|
107
|
+
if (isActive && manifest.cwd && !fs.existsSync(manifest.cwd)) {
|
|
108
|
+
fs.rmSync(path.join(runsRoot, dir), { recursive: true, force: true });
|
|
109
|
+
ghostRemoved.push(manifest.runId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isFinished(manifest)) continue;
|
|
114
|
+
|
|
115
|
+
// Safety check: ensure stateRoot and artifactsRoot are contained within user crew root
|
|
116
|
+
try {
|
|
117
|
+
resolveRealContainedPath(crewRoot, manifest.stateRoot);
|
|
118
|
+
resolveRealContainedPath(crewRoot, manifest.artifactsRoot);
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
finished.push({
|
|
124
|
+
runId: manifest.runId,
|
|
125
|
+
updatedAt: manifest.updatedAt,
|
|
126
|
+
stateRoot: manifest.stateRoot,
|
|
127
|
+
artifactsRoot: manifest.artifactsRoot,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sort newest first, keep top N, remove the rest
|
|
132
|
+
finished.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
133
|
+
const kept = finished.slice(0, keep).map((r) => r.runId);
|
|
134
|
+
const removed: string[] = [];
|
|
135
|
+
for (const run of finished.slice(keep)) {
|
|
136
|
+
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
137
|
+
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
138
|
+
removed.push(run.runId);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { kept, removed: [...removed, ...ghostRemoved] };
|
|
142
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { listRuns } from "./run-index.ts";
|
|
3
|
-
|
|
3
|
+
// Lazy-loaded: team-tool.ts pulls in entire runtime chain.
|
|
4
|
+
import type { handleTeamTool as HandleTeamToolFn } from "./team-tool.ts";
|
|
5
|
+
let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
|
|
6
|
+
async function handleTeamTool(params: Parameters<typeof HandleTeamToolFn>[0], ctx: Parameters<typeof HandleTeamToolFn>[1]): Promise<Awaited<ReturnType<typeof HandleTeamToolFn>>> {
|
|
7
|
+
if (!_cachedHandleTeamTool) {
|
|
8
|
+
const mod = await import("./team-tool.ts");
|
|
9
|
+
_cachedHandleTeamTool = mod.handleTeamTool;
|
|
10
|
+
}
|
|
11
|
+
return _cachedHandleTeamTool(params, ctx);
|
|
12
|
+
}
|
|
4
13
|
import { isToolError, textFromToolResult } from "./tool-result.ts";
|
|
5
14
|
|
|
6
15
|
async function notifyResult(ctx: ExtensionCommandContext, result: Awaited<ReturnType<typeof handleTeamTool>>): Promise<void> {
|