pi-crew 0.1.41 → 0.1.44
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 +47 -0
- package/README.md +51 -0
- 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 +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-phase10-distillation.md +199 -0
- package/docs/research-phase11-distillation.md +201 -0
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +5 -4
- package/src/config/config.ts +28 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +130 -8
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/registration/team-tool.ts +2 -1
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +23 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +2 -2
- package/src/extension/team-tool/cancel.ts +76 -4
- package/src/extension/team-tool/context.ts +1 -0
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/respond.ts +67 -0
- package/src/extension/team-tool/run.ts +6 -4
- package/src/extension/team-tool/status.ts +99 -93
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +5 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +10 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- package/src/runtime/agent-control.ts +63 -63
- 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 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +95 -4
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +32 -1
- package/src/runtime/crew-agent-runtime.ts +59 -58
- package/src/runtime/deadletter.ts +14 -4
- package/src/runtime/delivery-coordinator.ts +143 -0
- package/src/runtime/direct-run.ts +35 -35
- 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 +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +272 -261
- package/src/runtime/overflow-recovery.ts +157 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +179 -0
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/supervisor-contact.ts +59 -0
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +14 -0
- package/src/runtime/team-runner.ts +9 -10
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +2 -1
- package/src/schema/team-tool-schema.ts +115 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +109 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -42
- 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/discover-teams.ts +2 -2
- 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 +5 -4
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +7 -6
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +336 -36
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/atomic-write.ts +33 -0
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -1
- 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 +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { listRecentRuns } from "../run-index.ts";
|
|
3
|
-
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
4
|
-
|
|
5
|
-
export interface RegisterCompactionGuardOptions {
|
|
6
|
-
foregroundControllers: Set<AbortController>;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const TRIGGER_RATIO = 0.75;
|
|
10
|
-
const HARD_RATIO = 0.95;
|
|
11
|
-
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
-
const MAX_ARTIFACT_INDEX_RUNS = 10;
|
|
13
|
-
const MAX_ARTIFACT_INDEX_ITEMS = 80;
|
|
14
|
-
|
|
15
|
-
function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
|
|
16
|
-
const value = ctx.model?.contextWindow;
|
|
17
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
|
|
21
|
-
const tokens = ctx.getContextUsage()?.tokens;
|
|
22
|
-
if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
|
|
23
|
-
return tokens / contextWindow(ctx);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface CrewArtifactIndexEntry {
|
|
27
|
-
runId: string;
|
|
28
|
-
status: TeamRunManifest["status"];
|
|
29
|
-
team: string;
|
|
30
|
-
workflow?: string;
|
|
31
|
-
goal: string;
|
|
32
|
-
artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
|
|
36
|
-
const entries: CrewArtifactIndexEntry[] = [];
|
|
37
|
-
for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
|
|
38
|
-
for (const artifact of run.artifacts) {
|
|
39
|
-
entries.push({
|
|
40
|
-
runId: run.runId,
|
|
41
|
-
status: run.status,
|
|
42
|
-
team: run.team,
|
|
43
|
-
workflow: run.workflow,
|
|
44
|
-
goal: run.goal,
|
|
45
|
-
artifact: {
|
|
46
|
-
kind: artifact.kind,
|
|
47
|
-
path: artifact.path,
|
|
48
|
-
producer: artifact.producer,
|
|
49
|
-
sizeBytes: artifact.sizeBytes,
|
|
50
|
-
createdAt: artifact.createdAt,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return entries;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
|
|
60
|
-
if (!entries.length) return "";
|
|
61
|
-
const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
|
|
64
|
-
}
|
|
65
|
-
return lines.join("\n");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
|
|
69
|
-
let pendingCompactReason: string | null = null;
|
|
70
|
-
let compactionInProgress = false;
|
|
71
|
-
|
|
72
|
-
const startCompact = (ctx: ExtensionContext, reason: string): void => {
|
|
73
|
-
if (compactionInProgress) return;
|
|
74
|
-
compactionInProgress = true;
|
|
75
|
-
const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
|
|
76
|
-
if (artifactIndex.length > 0) {
|
|
77
|
-
pi.appendEntry("crew:artifact-index", {
|
|
78
|
-
reason,
|
|
79
|
-
createdAt: new Date().toISOString(),
|
|
80
|
-
artifacts: artifactIndex,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
ctx.compact({
|
|
84
|
-
customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
|
|
85
|
-
onComplete: () => {
|
|
86
|
-
compactionInProgress = false;
|
|
87
|
-
ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
|
|
88
|
-
},
|
|
89
|
-
onError: (error) => {
|
|
90
|
-
compactionInProgress = false;
|
|
91
|
-
ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// Phase 1.2: Defer compaction during foreground runs unless context is critically full.
|
|
97
|
-
pi.on("session_before_compact", async (_event, ctx) => {
|
|
98
|
-
if (options.foregroundControllers.size === 0) return;
|
|
99
|
-
const ratio = usageRatio(ctx);
|
|
100
|
-
if (ratio !== undefined && ratio >= HARD_RATIO) {
|
|
101
|
-
ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
pendingCompactReason = "deferred-during-foreground-run";
|
|
105
|
-
ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
|
|
106
|
-
return { cancel: true };
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
|
|
110
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
111
|
-
if (compactionInProgress) return;
|
|
112
|
-
if (options.foregroundControllers.size === 0 && pendingCompactReason) {
|
|
113
|
-
pendingCompactReason = null;
|
|
114
|
-
startCompact(ctx, "deferred");
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const ratio = usageRatio(ctx);
|
|
118
|
-
if (ratio === undefined || ratio < TRIGGER_RATIO) return;
|
|
119
|
-
if (options.foregroundControllers.size > 0) {
|
|
120
|
-
pendingCompactReason = "threshold-during-foreground-run";
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
startCompact(ctx, "threshold");
|
|
124
|
-
});
|
|
125
|
-
}
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { listRecentRuns } from "../run-index.ts";
|
|
3
|
+
import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
|
|
4
|
+
|
|
5
|
+
export interface RegisterCompactionGuardOptions {
|
|
6
|
+
foregroundControllers: Set<AbortController>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TRIGGER_RATIO = 0.75;
|
|
10
|
+
const HARD_RATIO = 0.95;
|
|
11
|
+
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
|
+
const MAX_ARTIFACT_INDEX_RUNS = 10;
|
|
13
|
+
const MAX_ARTIFACT_INDEX_ITEMS = 80;
|
|
14
|
+
|
|
15
|
+
function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
|
|
16
|
+
const value = ctx.model?.contextWindow;
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
|
|
21
|
+
const tokens = ctx.getContextUsage()?.tokens;
|
|
22
|
+
if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
|
|
23
|
+
return tokens / contextWindow(ctx);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CrewArtifactIndexEntry {
|
|
27
|
+
runId: string;
|
|
28
|
+
status: TeamRunManifest["status"];
|
|
29
|
+
team: string;
|
|
30
|
+
workflow?: string;
|
|
31
|
+
goal: string;
|
|
32
|
+
artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
|
|
36
|
+
const entries: CrewArtifactIndexEntry[] = [];
|
|
37
|
+
for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
|
|
38
|
+
for (const artifact of run.artifacts) {
|
|
39
|
+
entries.push({
|
|
40
|
+
runId: run.runId,
|
|
41
|
+
status: run.status,
|
|
42
|
+
team: run.team,
|
|
43
|
+
workflow: run.workflow,
|
|
44
|
+
goal: run.goal,
|
|
45
|
+
artifact: {
|
|
46
|
+
kind: artifact.kind,
|
|
47
|
+
path: artifact.path,
|
|
48
|
+
producer: artifact.producer,
|
|
49
|
+
sizeBytes: artifact.sizeBytes,
|
|
50
|
+
createdAt: artifact.createdAt,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
|
|
60
|
+
if (!entries.length) return "";
|
|
61
|
+
const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
|
|
69
|
+
let pendingCompactReason: string | null = null;
|
|
70
|
+
let compactionInProgress = false;
|
|
71
|
+
|
|
72
|
+
const startCompact = (ctx: ExtensionContext, reason: string): void => {
|
|
73
|
+
if (compactionInProgress) return;
|
|
74
|
+
compactionInProgress = true;
|
|
75
|
+
const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
|
|
76
|
+
if (artifactIndex.length > 0) {
|
|
77
|
+
pi.appendEntry("crew:artifact-index", {
|
|
78
|
+
reason,
|
|
79
|
+
createdAt: new Date().toISOString(),
|
|
80
|
+
artifacts: artifactIndex,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
ctx.compact({
|
|
84
|
+
customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
|
|
85
|
+
onComplete: () => {
|
|
86
|
+
compactionInProgress = false;
|
|
87
|
+
ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
|
|
88
|
+
},
|
|
89
|
+
onError: (error) => {
|
|
90
|
+
compactionInProgress = false;
|
|
91
|
+
ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Phase 1.2: Defer compaction during foreground runs unless context is critically full.
|
|
97
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
98
|
+
if (options.foregroundControllers.size === 0) return;
|
|
99
|
+
const ratio = usageRatio(ctx);
|
|
100
|
+
if (ratio !== undefined && ratio >= HARD_RATIO) {
|
|
101
|
+
ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
pendingCompactReason = "deferred-during-foreground-run";
|
|
105
|
+
ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
|
|
106
|
+
return { cancel: true };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
|
|
110
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
111
|
+
if (compactionInProgress) return;
|
|
112
|
+
if (options.foregroundControllers.size === 0 && pendingCompactReason) {
|
|
113
|
+
pendingCompactReason = null;
|
|
114
|
+
startCompact(ctx, "deferred");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const ratio = usageRatio(ctx);
|
|
118
|
+
if (ratio === undefined || ratio < TRIGGER_RATIO) return;
|
|
119
|
+
if (options.foregroundControllers.size > 0) {
|
|
120
|
+
pendingCompactReason = "threshold-during-foreground-run";
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
startCompact(ctx, "threshold");
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -7,6 +7,7 @@ import { readPersistedSubagentRecord, savePersistedSubagentRecord, type Subagent
|
|
|
7
7
|
import { loadConfig } from "../../config/config.ts";
|
|
8
8
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
9
9
|
import { __test__subagentSpawnParams, formatSubagentRecord, readSubagentRunResult, refreshPersistedSubagentRecord, subagentToolResult } from "./subagent-helpers.ts";
|
|
10
|
+
import { t } from "../../i18n.ts";
|
|
10
11
|
|
|
11
12
|
export interface SubagentToolRegistrationOptions {
|
|
12
13
|
ownerSessionGeneration?: () => number;
|
|
@@ -37,7 +38,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
37
38
|
if (!permission.allowed) return subagentToolResult(permission.reason ?? "Current role cannot spawn subagents.", { role: currentRole, mode: permission.mode }, true);
|
|
38
39
|
const spawnOptions = __test__subagentSpawnParams(params as Record<string, unknown>, ctx);
|
|
39
40
|
spawnOptions.ownerSessionGeneration = options.ownerSessionGeneration?.();
|
|
40
|
-
if (!spawnOptions.prompt.trim()) return subagentToolResult("
|
|
41
|
+
if (!spawnOptions.prompt.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
|
|
41
42
|
const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => handleTeamTool({ action: "run", agent: currentOptions.type, goal: currentOptions.prompt, model: currentOptions.model, async: currentOptions.background, config: currentOptions.maxTurns ? { runtime: { maxTurns: currentOptions.maxTurns } } : undefined } as TeamToolParamsValue, currentOptions.background ? { ...ctx, signal: childSignal } : { ...ctx, signal: childSignal });
|
|
42
43
|
const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
|
|
43
44
|
if (spawnOptions.background || record.status === "queued") {
|
|
@@ -45,11 +46,11 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
45
46
|
// Phase 1.6: Record was terminated for telemetry.
|
|
46
47
|
record.terminated = true;
|
|
47
48
|
savePersistedSubagentRecord(ctx.cwd, record);
|
|
48
|
-
return { ...subagentToolResult([
|
|
49
|
+
return { ...subagentToolResult([t("agent.started", { state: record.status === "queued" ? "queued" : "started" }), t("agent.id", { id: record.id }), t("agent.type", { type: record.type }), t("agent.description", { description: record.description }), t("agent.retrieveHint")].join("\n"), { agentId: record.id, status: record.status }), terminate: true };
|
|
49
50
|
}
|
|
50
51
|
await record.promise;
|
|
51
|
-
const output = readSubagentRunResult(ctx, record) ?? record.result ?? "
|
|
52
|
-
const foregroundResult = subagentToolResult([
|
|
52
|
+
const output = readSubagentRunResult(ctx, record) ?? record.result ?? t("agent.noOutput");
|
|
53
|
+
const foregroundResult = subagentToolResult([t("agent.foregroundStatus", { id: record.id, status: record.status }), "", output].join("\n"), { agentId: record.id, runId: record.runId, status: record.status }, record.status === "failed" || record.status === "error");
|
|
53
54
|
if (loadConfig(ctx.cwd).config.tools?.terminateOnForeground === true) {
|
|
54
55
|
record.terminated = true;
|
|
55
56
|
savePersistedSubagentRecord(ctx.cwd, record);
|
|
@@ -66,14 +67,14 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
66
67
|
parameters: Type.Object({ agent_id: Type.String({ description: "Agent ID returned by Agent." }), wait: Type.Optional(Type.Boolean({ description: "Wait for completion before returning." })), verbose: Type.Optional(Type.Boolean({ description: "Include status metadata before output." })) }) as never,
|
|
67
68
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
68
69
|
const p = params as { agent_id?: string; wait?: boolean; verbose?: boolean };
|
|
69
|
-
if (!p.agent_id) return subagentToolResult("
|
|
70
|
+
if (!p.agent_id) return subagentToolResult(t("result.requiresAgentId"), {}, true);
|
|
70
71
|
const inMemory = subagentManager.getRecord(p.agent_id);
|
|
71
72
|
const record = inMemory ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id);
|
|
72
|
-
if (!record) return subagentToolResult(
|
|
73
|
+
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id }), {}, true);
|
|
73
74
|
let current = refreshPersistedSubagentRecord(ctx, record);
|
|
74
75
|
if (inMemory && current !== inMemory) Object.assign(inMemory, current);
|
|
75
76
|
if (!inMemory && !current.runId && (current.status === "running" || current.status === "queued")) {
|
|
76
|
-
current = { ...current, status: "error", error: "
|
|
77
|
+
current = { ...current, status: "error", error: t("result.unrecoverable"), completedAt: current.completedAt ?? Date.now() };
|
|
77
78
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
78
79
|
}
|
|
79
80
|
if (p.wait && (current.status === "running" || current.status === "queued")) {
|
|
@@ -83,16 +84,24 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
83
84
|
current.resultConsumed = false;
|
|
84
85
|
if (inMemory) inMemory.resultConsumed = false;
|
|
85
86
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
} else {
|
|
88
|
+
const waitStartMs = Date.now();
|
|
89
|
+
const maxWaitMs = 300_000; // 5 minutes
|
|
90
|
+
while (current.status === "running" || current.status === "queued") {
|
|
91
|
+
if (signal?.aborted) {
|
|
92
|
+
current = { ...current, status: "error", error: t("result.waitAborted"), completedAt: Date.now() };
|
|
93
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (Date.now() - waitStartMs > maxWaitMs) {
|
|
97
|
+
current = { ...current, status: "error", error: t("result.waitTimeout"), completedAt: Date.now() };
|
|
98
|
+
savePersistedSubagentRecord(ctx.cwd, current);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
102
|
+
current = refreshPersistedSubagentRecord(ctx, current);
|
|
103
|
+
if (!current.runId) break;
|
|
92
104
|
}
|
|
93
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
94
|
-
current = refreshPersistedSubagentRecord(ctx, current);
|
|
95
|
-
if (!current.runId) break;
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
const output = readSubagentRunResult(ctx, current);
|
|
@@ -101,7 +110,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
101
110
|
if (inMemory) inMemory.resultConsumed = true;
|
|
102
111
|
savePersistedSubagentRecord(ctx.cwd, current);
|
|
103
112
|
}
|
|
104
|
-
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? "
|
|
113
|
+
const text = [p.verbose ? formatSubagentRecord(current) : undefined, output ? `${p.verbose ? "\n" : ""}${output}` : current.status === "running" || current.status === "queued" ? t("result.stillRunning") : current.error ?? t("agent.noOutput")].filter((line): line is string => Boolean(line)).join("\n");
|
|
105
114
|
return subagentToolResult(text, { agentId: current.id, runId: current.runId, status: current.status }, current.status === "failed" || current.status === "error");
|
|
106
115
|
},
|
|
107
116
|
};
|
|
@@ -114,8 +123,8 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
114
123
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
115
124
|
const p = params as { agent_id?: string; message?: string };
|
|
116
125
|
const record = p.agent_id ? subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id) : undefined;
|
|
117
|
-
if (!record) return subagentToolResult(
|
|
118
|
-
return subagentToolResult([
|
|
126
|
+
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
|
|
127
|
+
return subagentToolResult([t("steer.noted", { id: record.id }), t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined].filter((line): line is string => Boolean(line)).join("\n"), { agentId: record.id, runId: record.runId, status: record.status });
|
|
119
128
|
},
|
|
120
129
|
};
|
|
121
130
|
|
|
@@ -17,6 +17,7 @@ export interface RegisterTeamToolDeps {
|
|
|
17
17
|
getRunSnapshotCache?: (cwd: string) => ReturnType<typeof createRunSnapshotCache>;
|
|
18
18
|
getMetricRegistry?: () => MetricRegistry | undefined;
|
|
19
19
|
widgetState: CrewWidgetState;
|
|
20
|
+
onJsonEvent?: (taskId: string, runId: string, event: unknown) => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
|
|
@@ -38,7 +39,7 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
38
39
|
const runLabel = resolved.team ?? resolved.agent ?? "direct";
|
|
39
40
|
pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
|
|
40
41
|
}
|
|
41
|
-
const output = await handleTeamTool(resolved, { ...ctx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId) });
|
|
42
|
+
const output = await handleTeamTool(resolved, { ...ctx, signal: controller.signal, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId), onJsonEvent: deps.onJsonEvent });
|
|
42
43
|
if (resolved.action === "run") {
|
|
43
44
|
pi.appendEntry("crew:run-started", {
|
|
44
45
|
runId: output.details?.runId,
|
|
@@ -61,7 +61,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
61
61
|
coalescer.schedule(file, RESULT_WATCHER_POLL_MS);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
-
const key = buildCompletionKey(payload as Record<string, unknown
|
|
64
|
+
const key = buildCompletionKey(payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record<string, unknown> : {}, `file:${file}`);
|
|
65
65
|
if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
|
|
66
66
|
events.emit(eventName, payload);
|
|
67
67
|
}
|
|
@@ -78,7 +78,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
78
78
|
const startPolling = () => {
|
|
79
79
|
if (pollTimer) return;
|
|
80
80
|
pollTimer = setInterval(poll, RESULT_WATCHER_POLL_MS);
|
|
81
|
-
pollTimer.unref
|
|
81
|
+
pollTimer.unref();
|
|
82
82
|
poll();
|
|
83
83
|
};
|
|
84
84
|
const stopPolling = () => {
|
|
@@ -98,7 +98,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
98
98
|
logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
|
|
99
99
|
}
|
|
100
100
|
}, RESULT_WATCHER_RESTART_MS);
|
|
101
|
-
restartTimer.unref
|
|
101
|
+
restartTimer.unref();
|
|
102
102
|
};
|
|
103
103
|
const handle: ResultWatcherHandle = {
|
|
104
104
|
start() {
|
|
@@ -110,7 +110,7 @@ export function createResultWatcher(events: ResultWatcherEvents, resultsDir: str
|
|
|
110
110
|
coalescer.schedule(fileName.toString());
|
|
111
111
|
}, scheduleRestart);
|
|
112
112
|
if (watcher) stopPolling();
|
|
113
|
-
watcher?.unref
|
|
113
|
+
watcher?.unref();
|
|
114
114
|
},
|
|
115
115
|
prime() {
|
|
116
116
|
poll();
|
|
@@ -17,12 +17,13 @@ function validateArtifact(value: unknown, index: number, errors: string[]): valu
|
|
|
17
17
|
errors.push(`manifest.artifacts[${index}] must be an object.`);
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
|
+
const before = errors.length;
|
|
20
21
|
if (typeof value.kind !== "string") errors.push(`manifest.artifacts[${index}].kind must be a string.`);
|
|
21
22
|
if (typeof value.path !== "string") errors.push(`manifest.artifacts[${index}].path must be a string.`);
|
|
22
23
|
if (typeof value.createdAt !== "string") errors.push(`manifest.artifacts[${index}].createdAt must be a string.`);
|
|
23
24
|
if (typeof value.producer !== "string") errors.push(`manifest.artifacts[${index}].producer must be a string.`);
|
|
24
25
|
if (value.retention !== "run" && value.retention !== "project" && value.retention !== "temporary") errors.push(`manifest.artifacts[${index}].retention is invalid.`);
|
|
25
|
-
return
|
|
26
|
+
return errors.length === before;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function validateManifest(value: unknown, errors: string[]): value is TeamRunManifest {
|
|
@@ -30,6 +31,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
|
|
|
30
31
|
errors.push("manifest must be an object.");
|
|
31
32
|
return false;
|
|
32
33
|
}
|
|
34
|
+
const before = errors.length;
|
|
33
35
|
if (value.schemaVersion !== 1) errors.push("manifest.schemaVersion must be 1.");
|
|
34
36
|
for (const field of ["runId", "team", "goal", "createdAt", "updatedAt", "cwd", "stateRoot", "artifactsRoot", "tasksPath", "eventsPath"] as const) {
|
|
35
37
|
if (typeof value[field] !== "string") errors.push(`manifest.${field} must be a string.`);
|
|
@@ -38,7 +40,7 @@ function validateManifest(value: unknown, errors: string[]): value is TeamRunMan
|
|
|
38
40
|
if (value.workspaceMode !== "single" && value.workspaceMode !== "worktree") errors.push("manifest.workspaceMode must be single or worktree.");
|
|
39
41
|
if (!Array.isArray(value.artifacts)) errors.push("manifest.artifacts must be an array.");
|
|
40
42
|
else value.artifacts.forEach((artifact, index) => validateArtifact(artifact, index, errors));
|
|
41
|
-
return
|
|
43
|
+
return errors.length === before;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function validateTask(value: unknown, index: number, errors: string[]): value is TeamTaskState {
|
|
@@ -46,12 +48,13 @@ function validateTask(value: unknown, index: number, errors: string[]): value is
|
|
|
46
48
|
errors.push(`tasks[${index}] must be an object.`);
|
|
47
49
|
return false;
|
|
48
50
|
}
|
|
51
|
+
const before = errors.length;
|
|
49
52
|
for (const field of ["id", "runId", "role", "agent", "title", "cwd"] as const) {
|
|
50
53
|
if (typeof value[field] !== "string") errors.push(`tasks[${index}].${field} must be a string.`);
|
|
51
54
|
}
|
|
52
55
|
if (!isTeamTaskStatus(value.status)) errors.push(`tasks[${index}].status is invalid.`);
|
|
53
56
|
if (!Array.isArray(value.dependsOn)) errors.push(`tasks[${index}].dependsOn must be an array.`);
|
|
54
|
-
return
|
|
57
|
+
return errors.length === before;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function validateEvent(value: unknown, index: number, errors: string[]): value is TeamEvent {
|
|
@@ -59,10 +62,11 @@ function validateEvent(value: unknown, index: number, errors: string[]): value i
|
|
|
59
62
|
errors.push(`events[${index}] must be an object.`);
|
|
60
63
|
return false;
|
|
61
64
|
}
|
|
65
|
+
const before = errors.length;
|
|
62
66
|
for (const field of ["time", "type", "runId"] as const) {
|
|
63
67
|
if (typeof value[field] !== "string") errors.push(`events[${index}].${field} must be a string.`);
|
|
64
68
|
}
|
|
65
|
-
return
|
|
69
|
+
return errors.length === before;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export function validateRunBundle(value: unknown): BundleValidationResult {
|
|
@@ -29,6 +29,10 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
|
|
|
29
29
|
resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
|
|
30
30
|
const root = resolveContainedRelativePath(importsRoot, runId, "runId");
|
|
31
31
|
fs.mkdirSync(root, { recursive: true });
|
|
32
|
+
// TOCTOU note: mkdirSync would throw EEXIST if a symlink already existed.
|
|
33
|
+
// The lstatSync check catches a symlink swapped in between mkdirSync and the check
|
|
34
|
+
// (theoretically possible but requires local attacker with exact timing).
|
|
35
|
+
// resolveRealContainedPath provides an additional real-path containment barrier.
|
|
32
36
|
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
|
|
33
37
|
resolveRealContainedPath(importsRoot, runId);
|
|
34
38
|
const targetJson = path.join(root, "run-export.json");
|
|
@@ -40,7 +40,10 @@ function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function scopedRunRoots(cwd: string): string[] {
|
|
43
|
-
|
|
43
|
+
const roots: string[] = [userCrewRoot()];
|
|
44
|
+
const projectRoot = findRepoRoot(cwd);
|
|
45
|
+
if (projectRoot) roots.unshift(projectCrewRoot(cwd));
|
|
46
|
+
return roots;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function listRuns(cwd: string): TeamRunManifest[] {
|
|
@@ -52,3 +55,22 @@ export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
|
|
|
52
55
|
const roots = scopedRunRoots(cwd);
|
|
53
56
|
return mergeRuns(roots.map((root) => collectRuns(root, max)), max);
|
|
54
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* List runs filtered to a specific scope.
|
|
61
|
+
* - "project": only runs in the project crew root
|
|
62
|
+
* - "user": only runs in the user crew root
|
|
63
|
+
* - "all" (default): merge both scopes (current behavior)
|
|
64
|
+
*/
|
|
65
|
+
export function listRunsByScope(cwd: string, scope: "project" | "user" | "all" = "all", max?: number): TeamRunManifest[] {
|
|
66
|
+
const projectRoot = findRepoRoot(cwd);
|
|
67
|
+
switch (scope) {
|
|
68
|
+
case "project":
|
|
69
|
+
return projectRoot ? collectRuns(projectCrewRoot(cwd), max) : [];
|
|
70
|
+
case "user":
|
|
71
|
+
return collectRuns(userCrewRoot(), max);
|
|
72
|
+
case "all":
|
|
73
|
+
default:
|
|
74
|
+
return max !== undefined ? listRecentRuns(cwd, max) : listRuns(cwd);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,24 +1,43 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
4
|
+
import { projectCrewRoot } from "../utils/paths.ts";
|
|
5
|
+
import { listRuns } from "./run-index.ts";
|
|
6
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
+
|
|
8
|
+
export interface PruneRunsResult {
|
|
9
|
+
kept: string[];
|
|
10
|
+
removed: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isFinished(run: TeamRunManifest): boolean {
|
|
14
|
+
return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSafeToPrune(cwd: string, run: TeamRunManifest): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const crewRoot = projectCrewRoot(cwd);
|
|
20
|
+
resolveRealContainedPath(crewRoot, run.stateRoot);
|
|
21
|
+
resolveRealContainedPath(crewRoot, run.artifactsRoot);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
29
|
+
const crewRoot = projectCrewRoot(cwd);
|
|
30
|
+
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
31
|
+
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
32
|
+
const removed: string[] = [];
|
|
33
|
+
for (const run of finished.slice(keep)) {
|
|
34
|
+
if (!isSafeToPrune(cwd, run)) {
|
|
35
|
+
logInternalError("prune.path-unsafe", new Error(`Skipping unsafe prune: stateRoot=${run.stateRoot}, artifactsRoot=${run.artifactsRoot}`), `runId=${run.runId}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
39
|
+
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
40
|
+
removed.push(run.runId);
|
|
41
|
+
}
|
|
42
|
+
return { kept, removed };
|
|
43
|
+
}
|
|
@@ -22,7 +22,7 @@ import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
|
22
22
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
23
23
|
|
|
24
24
|
function globMatch(value: string, pattern: string): boolean {
|
|
25
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
25
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\?/g, "\\?").replace(/\*/g, ".*");
|
|
26
26
|
return new RegExp(`^${escaped}$`).test(value);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -110,7 +110,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
110
110
|
const approval = current.manifest.planApproval;
|
|
111
111
|
if (!approval?.required || approval.status !== "pending") return result("Run has no pending plan approval request.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
112
112
|
const now = new Date().toISOString();
|
|
113
|
-
const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
|
|
113
|
+
const tasks = current.tasks.map((task) => task.status === "queued" || task.status === "running" || task.status === "waiting" ? { ...task, status: "cancelled" as const, finishedAt: now, error: "Plan approval was cancelled." } : task);
|
|
114
114
|
let manifest: typeof current.manifest = { ...current.manifest, updatedAt: now, planApproval: { ...approval, status: "cancelled" as const, cancelledAt: now, updatedAt: now } };
|
|
115
115
|
saveRunManifest(manifest);
|
|
116
116
|
saveRunTasks(manifest, tasks);
|