pi-crew 0.9.68 → 0.10.1
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 +124 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65838 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -4,6 +4,7 @@ import { listRecentRuns } from "../extension/run-index.ts";
|
|
|
4
4
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
5
5
|
import { listLiveAgents, listLiveAgentsByWorkspace } from "../runtime/live-session/live-agent-manager.ts";
|
|
6
6
|
import type { ManifestCache } from "../runtime/manifest-cache.ts";
|
|
7
|
+
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
7
8
|
import { isDisplayActiveRun } from "../runtime/process-status.ts";
|
|
8
9
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
9
10
|
import { aggregateUsage } from "../state/usage.ts";
|
|
@@ -87,6 +88,10 @@ export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUi
|
|
|
87
88
|
id: "pi-crew-steps",
|
|
88
89
|
label: "pi-crew workflow steps",
|
|
89
90
|
});
|
|
91
|
+
safeEmit(events, "powerbar:register-segment", {
|
|
92
|
+
id: "pi-crew-plan",
|
|
93
|
+
label: "pi-crew plan approval",
|
|
94
|
+
});
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
export function updatePiCrewPowerbar(
|
|
@@ -136,11 +141,40 @@ interface ActiveItem {
|
|
|
136
141
|
* Build the workflow steps segment showing: ✓explore › →plan › ○execute › ○verify
|
|
137
142
|
* with the current/active step highlighted using → arrow.
|
|
138
143
|
*/
|
|
139
|
-
function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
|
|
144
|
+
export function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
|
|
140
145
|
if (!active.length) {
|
|
141
146
|
return { id: "pi-crew-steps" };
|
|
142
147
|
}
|
|
143
148
|
const run = active[0]!.run;
|
|
149
|
+
// WP-7 (R7): plan-carrying runs show PLAN PHASES instead of workflow steps —
|
|
150
|
+
// the plan is the actual execution shape (a re-plan revision switches the
|
|
151
|
+
// segment without waiting for workflow steps that no longer apply).
|
|
152
|
+
const plans = active[0]!.snapshot?.plans;
|
|
153
|
+
if (plans && plans.length > 0) {
|
|
154
|
+
const current = plans[plans.length - 1]!;
|
|
155
|
+
const itemById = new Map(current.items.map((i) => [i.id, i]));
|
|
156
|
+
const statusOf = (ids: string[]): "completed" | "running" | "pending" => {
|
|
157
|
+
const items = ids.map((id) => itemById.get(id)?.status).filter(Boolean) as string[];
|
|
158
|
+
if (items.length && items.every((s) => s === "done")) return "completed";
|
|
159
|
+
if (items.some((s) => s === "active")) return "running";
|
|
160
|
+
return "pending";
|
|
161
|
+
};
|
|
162
|
+
const parts = current.phases.map((phase) => {
|
|
163
|
+
const status = phase.status === "done" ? "completed" : statusOf(phase.itemIds);
|
|
164
|
+
const icon = status === "completed" ? "✓" : status === "running" ? "→" : "○";
|
|
165
|
+
const name = phase.title.length > 10 ? `${phase.title.slice(0, 9)}…` : phase.title;
|
|
166
|
+
return `${icon}${name}`;
|
|
167
|
+
});
|
|
168
|
+
if (parts.length) {
|
|
169
|
+
const hasRunning = parts.some((p) => p.startsWith("→"));
|
|
170
|
+
const allComplete = parts.every((p) => p.startsWith("✓"));
|
|
171
|
+
return {
|
|
172
|
+
id: "pi-crew-steps",
|
|
173
|
+
text: `P${current.version} ${parts.join(" › ")}`,
|
|
174
|
+
color: allComplete ? "success" : hasRunning ? "accent" : "dim",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
144
178
|
const workflowName = run.workflow ?? "default";
|
|
145
179
|
// Load workflow steps
|
|
146
180
|
const workflows = allWorkflows(discoverWorkflows(run.cwd));
|
|
@@ -206,6 +240,7 @@ class PowerbarPublisher {
|
|
|
206
240
|
#lastActiveKey: string | undefined;
|
|
207
241
|
#lastProgressKey: string | undefined;
|
|
208
242
|
#lastStepsKey: string | undefined;
|
|
243
|
+
#lastPlanKey: string | undefined;
|
|
209
244
|
#latestArgs: PowerbarUpdateArgs | null = null;
|
|
210
245
|
readonly #coalescer: RenderCoalescer;
|
|
211
246
|
|
|
@@ -279,9 +314,11 @@ class PowerbarPublisher {
|
|
|
279
314
|
this.#lastActiveKey = undefined;
|
|
280
315
|
this.#lastProgressKey = undefined;
|
|
281
316
|
this.#lastStepsKey = undefined;
|
|
317
|
+
this.#lastPlanKey = undefined;
|
|
282
318
|
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
283
319
|
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
284
320
|
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
321
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
|
|
285
322
|
return;
|
|
286
323
|
}
|
|
287
324
|
const agents = active.flatMap((item) => item.agents);
|
|
@@ -373,6 +410,12 @@ class PowerbarPublisher {
|
|
|
373
410
|
} as const;
|
|
374
411
|
// Build step progress: "explorer > planner > executor > verifier" with current step highlighted
|
|
375
412
|
const stepsPayload = buildStepsPayload(active, tasks);
|
|
413
|
+
// WP-3 (H4): plan-approval segment — `plan:pending` while ANY active run is
|
|
414
|
+
// parked awaiting plan approval, clear payload otherwise. `item.run` is
|
|
415
|
+
// always the freshest manifest available (snapshot.manifest when cached).
|
|
416
|
+
const planPayload: PowerbarPayloadShape = active.some((item) => isPlanApprovalPending(item.run))
|
|
417
|
+
? { id: "pi-crew-plan", text: "plan:pending", color: "warning" }
|
|
418
|
+
: { id: "pi-crew-plan" };
|
|
376
419
|
// 1.8: dedup per segment using a key over every visible field. Previously
|
|
377
420
|
// the dedup string only carried text/suffix/running, so changes to `bar`
|
|
378
421
|
// (progress %) or `color` could be swallowed and stale UI emitted again
|
|
@@ -380,6 +423,7 @@ class PowerbarPublisher {
|
|
|
380
423
|
const activeKey = powerbarKey(activePayload);
|
|
381
424
|
const progressKey = powerbarKey(progressPayload);
|
|
382
425
|
const stepsKey = powerbarKey(stepsPayload);
|
|
426
|
+
const planKey = powerbarKey(planPayload);
|
|
383
427
|
if (activeKey !== this.#lastActiveKey) {
|
|
384
428
|
this.#lastActiveKey = activeKey;
|
|
385
429
|
safeEmit(events, "powerbar:update", activePayload);
|
|
@@ -392,6 +436,10 @@ class PowerbarPublisher {
|
|
|
392
436
|
this.#lastStepsKey = stepsKey;
|
|
393
437
|
safeEmit(events, "powerbar:update", stepsPayload);
|
|
394
438
|
}
|
|
439
|
+
if (planKey !== this.#lastPlanKey) {
|
|
440
|
+
this.#lastPlanKey = planKey;
|
|
441
|
+
safeEmit(events, "powerbar:update", planPayload);
|
|
442
|
+
}
|
|
395
443
|
// Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
|
|
396
444
|
// Powerbar only emits events; it does not set status directly
|
|
397
445
|
}
|
|
@@ -431,9 +479,11 @@ class PowerbarPublisher {
|
|
|
431
479
|
this.#lastActiveKey = undefined;
|
|
432
480
|
this.#lastProgressKey = undefined;
|
|
433
481
|
this.#lastStepsKey = undefined;
|
|
482
|
+
this.#lastPlanKey = undefined;
|
|
434
483
|
safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
|
|
435
484
|
safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
|
|
436
485
|
safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
|
|
486
|
+
safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
|
|
437
487
|
}
|
|
438
488
|
|
|
439
489
|
/** Reset dedup state on session lifecycle events. */
|
|
@@ -441,6 +491,7 @@ class PowerbarPublisher {
|
|
|
441
491
|
this.#lastActiveKey = undefined;
|
|
442
492
|
this.#lastProgressKey = undefined;
|
|
443
493
|
this.#lastStepsKey = undefined;
|
|
494
|
+
this.#lastPlanKey = undefined;
|
|
444
495
|
}
|
|
445
496
|
|
|
446
497
|
dispose(): void {
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
|
3
3
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
4
4
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
5
5
|
import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
|
|
6
|
+
import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
|
|
6
7
|
import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
|
|
7
8
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
8
9
|
import { aggregateUsage } from "../state/usage.ts";
|
|
@@ -16,6 +17,7 @@ import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts"
|
|
|
16
17
|
import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
|
|
17
18
|
import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
|
|
18
19
|
import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
|
|
20
|
+
import { renderPlanPane } from "./dashboard-panes/plan-pane.ts";
|
|
19
21
|
import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
|
|
20
22
|
import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
|
|
21
23
|
import { DynamicCrewBorder } from "./dynamic-border.ts";
|
|
@@ -75,7 +77,7 @@ export interface RunDashboardOptions {
|
|
|
75
77
|
* looking at. Resetting to "agents" on every `new RunDashboard(...)` was a
|
|
76
78
|
* UX regression.
|
|
77
79
|
*/
|
|
78
|
-
let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
|
|
80
|
+
let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = "agents";
|
|
79
81
|
|
|
80
82
|
export type RunDashboardAction =
|
|
81
83
|
| "status"
|
|
@@ -94,6 +96,8 @@ export type RunDashboardAction =
|
|
|
94
96
|
| "health-recovery"
|
|
95
97
|
| "health-kill-stale"
|
|
96
98
|
| "health-diagnostic-export"
|
|
99
|
+
| "plan-approve"
|
|
100
|
+
| "plan-deny"
|
|
97
101
|
| "notifications-dismiss";
|
|
98
102
|
export interface RunDashboardSelection {
|
|
99
103
|
runId: string;
|
|
@@ -404,7 +408,9 @@ export class RunDashboard implements DashboardComponent {
|
|
|
404
408
|
private runScrollOffset = 0;
|
|
405
409
|
private showFullProgress = false;
|
|
406
410
|
private showHelp = false;
|
|
407
|
-
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = lastActivePane;
|
|
411
|
+
private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = lastActivePane;
|
|
412
|
+
/** WP-7 (R7): pane-scoped revision-diff toggle (X). */
|
|
413
|
+
private planDiff = false;
|
|
408
414
|
private runs: TeamRunManifest[];
|
|
409
415
|
private readonly done: (selection: RunDashboardSelection | undefined) => void;
|
|
410
416
|
private readonly theme: CrewTheme;
|
|
@@ -638,7 +644,7 @@ export class RunDashboard implements DashboardComponent {
|
|
|
638
644
|
lines.push(
|
|
639
645
|
border("╭", "╮"),
|
|
640
646
|
row(
|
|
641
|
-
`${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-
|
|
647
|
+
`${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-7 pane · ↑↓ · Enter · ? help · Esc")}`,
|
|
642
648
|
),
|
|
643
649
|
sep(),
|
|
644
650
|
);
|
|
@@ -732,7 +738,9 @@ export class RunDashboard implements DashboardComponent {
|
|
|
732
738
|
registry: this.options.registry,
|
|
733
739
|
}),
|
|
734
740
|
)
|
|
735
|
-
:
|
|
741
|
+
: this.activePane === "plan"
|
|
742
|
+
? safeRenderPane("plan", () => renderPlanPane(snap, { diff: this.planDiff }))
|
|
743
|
+
: safeRenderPane("transcript", () => renderTranscriptPane(snap))
|
|
736
744
|
: [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2, this.options.snapshotCache)];
|
|
737
745
|
const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
|
|
738
746
|
if (filteredPane.length > 0) {
|
|
@@ -832,6 +840,19 @@ export class RunDashboard implements DashboardComponent {
|
|
|
832
840
|
this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
|
|
833
841
|
return;
|
|
834
842
|
}
|
|
843
|
+
// WP-3 (H4-subset): plan approval keys. Deliberately a DEDICATED branch
|
|
844
|
+
// (not the generic union block below) because plan actions must gate on
|
|
845
|
+
// the selected run actually being parked on a pending approval — a
|
|
846
|
+
// stray keystroke on a non-pending run is a silent no-op (dashboard
|
|
847
|
+
// stays open) instead of closing it via done(undefined).
|
|
848
|
+
if (action === "plan-approve" || action === "plan-deny") {
|
|
849
|
+
const run = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
|
|
850
|
+
const manifest = run ? (snapshotFor(run, this.options.snapshotCache)?.manifest ?? run) : undefined;
|
|
851
|
+
if (run && manifest && isPlanApprovalPending(manifest)) {
|
|
852
|
+
this.done({ runId: run.runId, action });
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
835
856
|
if (
|
|
836
857
|
action === "summary" ||
|
|
837
858
|
action === "artifacts" ||
|
|
@@ -875,7 +896,12 @@ export class RunDashboard implements DashboardComponent {
|
|
|
875
896
|
else if (action === "pane-output") this.activePane = "output";
|
|
876
897
|
else if (action === "pane-health") this.activePane = "health";
|
|
877
898
|
else if (action === "pane-metrics") this.activePane = "metrics";
|
|
878
|
-
else if (action === "
|
|
899
|
+
else if (action === "pane-plan") this.activePane = "plan";
|
|
900
|
+
else if (action === "plan-diff") {
|
|
901
|
+
this.planDiff = !this.planDiff;
|
|
902
|
+
this.invalidate();
|
|
903
|
+
return;
|
|
904
|
+
} else if (action === "up") this.selected = Math.max(0, this.selected - 1);
|
|
879
905
|
else if (action === "down") {
|
|
880
906
|
const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
|
|
881
907
|
this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
4
5
|
import { agentOutputPath, agentsPath, readCrewAgents, readCrewAgentsAsync } from "../runtime/crew-agent-records.ts";
|
|
5
6
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
6
7
|
import { isActiveRunStatus } from "../runtime/process-status.ts";
|
|
7
8
|
import type { MailboxMessageStatus } from "../state/coordination/mailbox.ts";
|
|
8
9
|
import type { TeamEvent } from "../state/event-log/event-log.ts";
|
|
9
10
|
import { sequencePath } from "../state/event-log/event-log.ts";
|
|
11
|
+
import { loadPlanRecords, planFilePath } from "../state/stores/plan-store.ts";
|
|
10
12
|
import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/stores/state-store.ts";
|
|
11
13
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
12
14
|
import { extractDwfPhaseState } from "./dwf-phase-display.ts";
|
|
@@ -25,6 +27,12 @@ export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
|
25
27
|
preloadAllStale(runIds: string[]): Promise<void>;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** WP-7 (R7): the plans slice + Plan pane load only when this flag is set —
|
|
31
|
+
* flag-off keeps the snapshot build byte-identical (zero extra I/O). */
|
|
32
|
+
export function isPlanUiEnabled(): boolean {
|
|
33
|
+
return getCrewEnv("PI_CREW_PLAN_UI") === "1";
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
const DEFAULT_TTL_MS = 1500;
|
|
29
37
|
const DEFAULT_MAX_ENTRIES = 24;
|
|
30
38
|
const DEFAULT_RECENT_EVENTS = 20;
|
|
@@ -44,6 +52,8 @@ interface SnapshotStamps {
|
|
|
44
52
|
agents: FileStamp;
|
|
45
53
|
events: FileStamp;
|
|
46
54
|
mailbox: FileStamp;
|
|
55
|
+
/** WP-7 (R7): present only when PI_CREW_PLAN_UI=1. */
|
|
56
|
+
plans?: FileStamp;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
interface CacheEntry {
|
|
@@ -163,15 +173,10 @@ function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord):
|
|
|
163
173
|
}
|
|
164
174
|
}
|
|
165
175
|
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
|
|
171
|
-
return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function sameStamp(a: FileStamp, b: FileStamp): boolean {
|
|
176
|
+
function sameStamp(a: FileStamp | undefined, b: FileStamp | undefined): boolean {
|
|
177
|
+
// Optional stamps (WP-7 plans): absent on both sides = unchanged; absent
|
|
178
|
+
// on one side = the flag flipped — force a rebuild.
|
|
179
|
+
if (a === undefined || b === undefined) return a === b;
|
|
175
180
|
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
176
181
|
}
|
|
177
182
|
|
|
@@ -181,19 +186,11 @@ function sameStamps(a: SnapshotStamps, b: SnapshotStamps): boolean {
|
|
|
181
186
|
sameStamp(a.tasks, b.tasks) &&
|
|
182
187
|
sameStamp(a.agents, b.agents) &&
|
|
183
188
|
sameStamp(a.events, b.events) &&
|
|
184
|
-
sameStamp(a.mailbox, b.mailbox)
|
|
189
|
+
sameStamp(a.mailbox, b.mailbox) &&
|
|
190
|
+
sameStamp(a.plans, b.plans)
|
|
185
191
|
);
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
function readTasks(tasksPath: string): TeamTaskState[] {
|
|
189
|
-
try {
|
|
190
|
-
const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")) as unknown;
|
|
191
|
-
return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
|
|
192
|
-
} catch {
|
|
193
|
-
throw new Error(`Failed to parse tasks at ${tasksPath}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
194
|
/** Tail-read JSONL lines from a file, returning parsed objects (limited). */
|
|
198
195
|
function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
|
|
199
196
|
if (limit <= 0) return [];
|
|
@@ -711,6 +708,21 @@ function computeSliceSignatures(input: Omit<RunUiSnapshot, "signature" | "fetche
|
|
|
711
708
|
event.data?.reason,
|
|
712
709
|
]),
|
|
713
710
|
),
|
|
711
|
+
...(input.plans
|
|
712
|
+
? {
|
|
713
|
+
// WP-7 (R7): plan writes (revision append / approval flip / item
|
|
714
|
+
// linkage) must invalidate the Plan pane. Plans live outside the
|
|
715
|
+
// stamped files' content — the slice hashes the records directly.
|
|
716
|
+
plans: hash(
|
|
717
|
+
input.plans.map((record) => [
|
|
718
|
+
record.version,
|
|
719
|
+
record.approval?.status,
|
|
720
|
+
record.items.map((item) => [item.id, item.status, item.taskIds]),
|
|
721
|
+
record.phases.map((p) => [p.id, p.status]),
|
|
722
|
+
]),
|
|
723
|
+
),
|
|
724
|
+
}
|
|
725
|
+
: {}),
|
|
714
726
|
};
|
|
715
727
|
}
|
|
716
728
|
|
|
@@ -723,7 +735,21 @@ function signatureFor(
|
|
|
723
735
|
const digest = createHash("sha256");
|
|
724
736
|
digest.update(
|
|
725
737
|
JSON.stringify({
|
|
726
|
-
|
|
738
|
+
// WP-3 (H4): planApproval.status is a RUN-level field surfaced by the
|
|
739
|
+
// widget badge / progress banner / powerbar segment — pending→approved
|
|
740
|
+
// must flip the signature so per-run render caches (run-dashboard keys
|
|
741
|
+
// on snapshot.signature) invalidate. `undefined` serializes as null,
|
|
742
|
+
// keeping the array shape stable for older manifests without the field.
|
|
743
|
+
run: [
|
|
744
|
+
input.manifest.runId,
|
|
745
|
+
input.manifest.status,
|
|
746
|
+
input.manifest.updatedAt,
|
|
747
|
+
input.manifest.artifacts.length,
|
|
748
|
+
input.manifest.planApproval?.status,
|
|
749
|
+
// T2/R4 (ADR-4 §2): a re-plan (revision switch) or record-side
|
|
750
|
+
// approval flip must invalidate per-run render caches too.
|
|
751
|
+
input.manifest.plan?.version ?? null,
|
|
752
|
+
],
|
|
727
753
|
tasks: sliceSignatures.tasks,
|
|
728
754
|
agents: sliceSignatures.agents,
|
|
729
755
|
progress: input.progress,
|
|
@@ -731,6 +757,7 @@ function signatureFor(
|
|
|
731
757
|
mailbox: input.mailbox,
|
|
732
758
|
groupJoins: input.groupJoins,
|
|
733
759
|
events: sliceSignatures.events,
|
|
760
|
+
...(sliceSignatures.plans ? { plans: sliceSignatures.plans } : {}),
|
|
734
761
|
cancellationReason: input.cancellationReason,
|
|
735
762
|
dwfPhaseState: input.dwfPhaseState,
|
|
736
763
|
output: input.recentOutputLines,
|
|
@@ -744,16 +771,8 @@ function signatureFor(
|
|
|
744
771
|
}
|
|
745
772
|
}
|
|
746
773
|
|
|
747
|
-
/**
|
|
748
|
-
* 1.6 / 1.7 — compute one short hash per logical slice of the snapshot so
|
|
749
|
-
* dashboard panes / widget can short-circuit when their slice hasn't moved.
|
|
750
|
-
* The slice contents must mirror what `signatureFor` packs into each branch.
|
|
751
|
-
*/
|
|
752
|
-
function sliceSignaturesFor(sliceSignatures: SliceSignatures): SliceSignatures {
|
|
753
|
-
return sliceSignatures;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
774
|
function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): SnapshotStamps {
|
|
775
|
+
// WP-7: optional plans stamp — one extra statSync, flag-gated.
|
|
757
776
|
// 1.4: use events sequence file instead of stat-ing the events log directly.
|
|
758
777
|
// 1.5: drop per-agent output.log stamping; rely on event-bus invalidation
|
|
759
778
|
// (`crew.subagent.*` and stream events) and on agents.json mtime which
|
|
@@ -765,6 +784,7 @@ function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): Snaps
|
|
|
765
784
|
agents: stampFile(agentsPath(manifest)),
|
|
766
785
|
events: eventsStamp(manifest.eventsPath),
|
|
767
786
|
mailbox: mailboxStamp(manifest),
|
|
787
|
+
...(isPlanUiEnabled() ? { plans: stampFile(planFilePath(manifest)) } : {}),
|
|
768
788
|
};
|
|
769
789
|
}
|
|
770
790
|
|
|
@@ -834,7 +854,12 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
834
854
|
let tasks: TeamTaskState[];
|
|
835
855
|
let agents: CrewAgentRecord[];
|
|
836
856
|
try {
|
|
837
|
-
|
|
857
|
+
// R10-4 (docs/refactor-plan.review.md §ROUND 10): sync/async parity.
|
|
858
|
+
// loadRunManifestById already returns tasks validated against the
|
|
859
|
+
// current tasks.json (mtime+size+generation check in state-store),
|
|
860
|
+
// so the old readTasks() re-read only doubled tasks.json I/O per
|
|
861
|
+
// sync rebuild. buildAsync() has always used loaded.tasks.
|
|
862
|
+
tasks = loaded.tasks;
|
|
838
863
|
agents = readCrewAgents(loaded.manifest);
|
|
839
864
|
} catch {
|
|
840
865
|
if (previous) return previous;
|
|
@@ -857,6 +882,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
857
882
|
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
858
883
|
recentEvents,
|
|
859
884
|
recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
|
|
885
|
+
...(isPlanUiEnabled() ? { plans: loadPlanRecords(loaded.manifest) } : {}),
|
|
860
886
|
};
|
|
861
887
|
const stamps = stampsFor(loaded.manifest, agents);
|
|
862
888
|
const sliceSignatures = computeSliceSignatures(base);
|
|
@@ -915,6 +941,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
915
941
|
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
916
942
|
recentEvents,
|
|
917
943
|
recentOutputLines: recentOutput,
|
|
944
|
+
...(isPlanUiEnabled() ? { plans: loadPlanRecords(loaded.manifest) } : {}),
|
|
918
945
|
};
|
|
919
946
|
const stamps = await stampsForAsync(loaded.manifest, agents);
|
|
920
947
|
const sliceSignatures = computeSliceSignatures(base);
|
package/src/ui/snapshot-types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
2
2
|
import type { TeamEvent } from "../state/event-log/event-log.ts";
|
|
3
|
-
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
3
|
+
import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
4
|
import type { DwfPhaseState } from "./dwf-phase-display.ts";
|
|
5
5
|
|
|
6
6
|
export interface RunUiProgress {
|
|
@@ -64,6 +64,8 @@ export interface RunUiSnapshot {
|
|
|
64
64
|
mailbox: string;
|
|
65
65
|
progress: string;
|
|
66
66
|
events: string;
|
|
67
|
+
/** WP-7 (R7): present only when the plans slice is loaded (flag on). */
|
|
68
|
+
plans?: string;
|
|
67
69
|
};
|
|
68
70
|
manifest: TeamRunManifest;
|
|
69
71
|
tasks: TeamTaskState[];
|
|
@@ -76,6 +78,9 @@ export interface RunUiSnapshot {
|
|
|
76
78
|
cancellationReason?: string;
|
|
77
79
|
/** DWF phase state derived from `recentEvents`. Null/absent for non-DWF runs. */
|
|
78
80
|
dwfPhaseState?: DwfPhaseState | null;
|
|
81
|
+
/** WP-7 (R7): plan records for the Plan pane — populated ONLY when
|
|
82
|
+
* PI_CREW_PLAN_UI=1 (flag-off → field absent, zero extra I/O). */
|
|
83
|
+
plans?: PlanRecord[];
|
|
79
84
|
recentEvents: TeamEvent[];
|
|
80
85
|
recentOutputLines: string[];
|
|
81
86
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
8
|
+
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
8
9
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
9
10
|
import { truncate } from "../../utils/visual.ts";
|
|
10
11
|
import { Box, Text } from "../layout-primitives.ts";
|
|
@@ -73,7 +74,14 @@ export function buildWidgetLines(
|
|
|
73
74
|
return Number.isFinite(age) && age < maxAgeMs;
|
|
74
75
|
});
|
|
75
76
|
const completed = agents.filter((a) => a.status === "completed").length;
|
|
76
|
-
|
|
77
|
+
// WP-3 (H4): while a run is parked awaiting plan approval, the spinner
|
|
78
|
+
// glyph is replaced by a `⚠ plan:<last-8 runId>` badge. Plain-unicode ⚠
|
|
79
|
+
// (the same glyph needs_attention already uses) stays legible in no-color
|
|
80
|
+
// mode and is colorized by the shared colorizeStatusGlyphs pass. In-place
|
|
81
|
+
// glyph swap only — no extra line, so the MAX_AGENTS_DISPLAY / maxLines
|
|
82
|
+
// budget and the truncate(width) rule are untouched.
|
|
83
|
+
const planPending = isPlanApprovalStatePending(run.planApproval);
|
|
84
|
+
const runGlyph = planPending ? `⚠ plan:${run.runId.slice(-8)}` : iconForStatus(run.status, { runningGlyph });
|
|
77
85
|
const isTerminal = isFinishedRunStatus(run.status);
|
|
78
86
|
// Run progress line. v1–v3 flickered on snapshot.tasks state, v4 was
|
|
79
87
|
// too minimal (`0/1 agents` only), v5 duplicated the worker activity
|
|
@@ -63,10 +63,6 @@ function evictOldestCacheEntry(): void {
|
|
|
63
63
|
if (oldestKey !== undefined) readCache.delete(oldestKey);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function clearReadCache(): void {
|
|
67
|
-
readCache.clear();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
66
|
export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
|
|
71
67
|
const now = Date.now();
|
|
72
68
|
const stat = (() => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fatal-filesystem errno classification (bug-026 sub-issue B).
|
|
3
|
+
*
|
|
4
|
+
* During the 2026-08-15 disk-full window, workers died with generic
|
|
5
|
+
* "unresponsive"/ETIMEDOUT diagnostics — the ENOSPC errno only surfaced
|
|
6
|
+
* inside stderr-tail strings embedded in the E007 ChildTimeout message
|
|
7
|
+
* (`errors.ts` childTimeout appends "Stderr tail: ..."), never as a raw
|
|
8
|
+
* errno object in the parent process. The classifier therefore matches BOTH
|
|
9
|
+
* shapes:
|
|
10
|
+
* - (a) `err.code` (NodeJS.ErrnoException) case-insensitively, and
|
|
11
|
+
* - (b) the error's message text — or any plain string, e.g. a stderr tail.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT placed in state/atomic-write.ts: that module has
|
|
14
|
+
* process.on side effects at module load and imports worker-atomic-writer,
|
|
15
|
+
* and it already throws raw errno errors — classification belongs at catch
|
|
16
|
+
* sites. This module is dependency-free so state/types.ts (type-only) and
|
|
17
|
+
* the team-tool run/doctor surfaces can import it cheaply.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type FatalFsCause = "enospc" | "edquot" | "emfile" | "enfile";
|
|
21
|
+
|
|
22
|
+
const FATAL_FS_CODES: ReadonlySet<string> = new Set(["enospc", "edquot", "emfile", "enfile"]);
|
|
23
|
+
|
|
24
|
+
/** Message-text fallback: catches errno codes embedded in stderr tails /
|
|
25
|
+
* synthesized error messages where no structured `.code` exists. */
|
|
26
|
+
const FATAL_FS_TEXT = /\b(ENOSPC|EDQUOT|EMFILE|ENFILE)\b/;
|
|
27
|
+
|
|
28
|
+
function classifyCode(code: unknown): FatalFsCause | undefined {
|
|
29
|
+
if (typeof code !== "string") return undefined;
|
|
30
|
+
const normalized = code.toLowerCase();
|
|
31
|
+
return FATAL_FS_CODES.has(normalized) ? (normalized as FatalFsCause) : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function classifyText(text: unknown): FatalFsCause | undefined {
|
|
35
|
+
if (typeof text !== "string") return undefined;
|
|
36
|
+
const match = FATAL_FS_TEXT.exec(text);
|
|
37
|
+
return match ? (match[1]?.toLowerCase() as FatalFsCause) : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Classify an unknown error (or a raw string, e.g. a child stderr tail) as a
|
|
42
|
+
* fatal filesystem cause. Returns undefined for non-fs errors. Pure and total:
|
|
43
|
+
* never throws on non-object inputs.
|
|
44
|
+
*/
|
|
45
|
+
export function classifyFatalFsError(err: unknown): FatalFsCause | undefined {
|
|
46
|
+
if (typeof err === "string") return classifyText(err);
|
|
47
|
+
if (err === null || err === undefined) return undefined;
|
|
48
|
+
const code = classifyCode((err as NodeJS.ErrnoException).code);
|
|
49
|
+
if (code) return code;
|
|
50
|
+
return classifyText((err as { message?: unknown }).message);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Combined classification for a finished attempt (bug-026 sub-issue B):
|
|
55
|
+
* the assembled attempt error first — a structured errno or a message that
|
|
56
|
+
* embeds one (E007 stderr tails) — then the child's stderr tail. Extracted
|
|
57
|
+
* as a pure seam so the child-executor hook is directly unit-testable.
|
|
58
|
+
*/
|
|
59
|
+
export function failureCauseForAttempt(error: string | undefined, stderr: string | undefined): FatalFsCause | undefined {
|
|
60
|
+
return classifyFatalFsError(error) ?? classifyFatalFsError(stderr);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Human label for a fatal fs cause. */
|
|
64
|
+
export function fsFailureLabel(cause: FatalFsCause): string {
|
|
65
|
+
return cause === "enospc" || cause === "edquot" ? "disk full" : "too many open files";
|
|
66
|
+
}
|
package/src/utils/fs-watch.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
|
|
|
10
10
|
* caused a permanent interactive-session busy-loop. The bounded
|
|
11
11
|
* {@link RunWatcherRegistry} (one non-recursive watcher per ACTIVE run) now
|
|
12
12
|
* replaces them. Only the two primitives below survive — they are still used by
|
|
13
|
-
* manifest-cache
|
|
13
|
+
* manifest-cache and run-watcher-registry.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export function closeWatcher(watcher: FSWatcher | null | undefined): void {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
2
|
+
|
|
1
3
|
export function logInternalError(scope: string, error: unknown, details?: string, severity?: "error" | "warn" | "debug"): void {
|
|
2
4
|
// "error" and "warn" always emit; "debug" (default) is gated behind PI_TEAMS_DEBUG
|
|
3
5
|
if (!severity || severity === "debug") {
|
|
4
|
-
if (!
|
|
6
|
+
if (!getCrewEnv("PI_TEAMS_DEBUG")) return;
|
|
5
7
|
}
|
|
6
8
|
const message =
|
|
7
9
|
error instanceof Error ? error.message : typeof error === "object" && error !== null ? JSON.stringify(error) : String(error);
|
package/src/utils/paths.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
5
6
|
|
|
6
7
|
// NEW-P1/NEW-P2 (perf): packageRoot() and userPiRoot() are invariant for a
|
|
7
8
|
// process lifetime but each call did statSync/readFileSync/lstatSync (10-15+
|
|
@@ -55,7 +56,7 @@ export function userPiRoot(): string {
|
|
|
55
56
|
// "undefined/.pi/agent" relative to cwd and silently create a junk "undefined/"
|
|
56
57
|
// tree. Treat the literal "undefined" (and empty) as unset and fall back to
|
|
57
58
|
// os.homedir().
|
|
58
|
-
const rawHome = (
|
|
59
|
+
const rawHome = getCrewEnv("PI_CREW_HOME")?.trim();
|
|
59
60
|
const home = rawHome && rawHome !== "undefined" ? rawHome : os.homedir();
|
|
60
61
|
// NEW-P2: memoize but KEY ON HOME. Tests (withIsolatedHome / isolateHome)
|
|
61
62
|
// change PI_TEAMS_HOME mid-process; an unkeyed cache would leak the first
|
|
@@ -203,8 +204,15 @@ export function findRepoRoot(cwd: string): string | undefined {
|
|
|
203
204
|
function computeRepoRoot(start: string): string | undefined {
|
|
204
205
|
let current = start;
|
|
205
206
|
const root = path.parse(current).root;
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
// Canonicalize the boundaries to match the walk chain: findRepoRoot
|
|
208
|
+
// resolves the start dir via realpathSync, so every step of the walk is a
|
|
209
|
+
// canonical path. Comparing against the LEXICAL home/tmpdir lets symlinked
|
|
210
|
+
// temp dirs (macOS: /var -> /private/var, TMPDIR=/var/folders/.../T) never
|
|
211
|
+
// match, so the walk escapes the sandbox and can latch onto an unrelated
|
|
212
|
+
// ancestor marker (seen live: GH macOS runners have one) — state then
|
|
213
|
+
// resolves to a repo root far outside the temp tree (bug-029).
|
|
214
|
+
const home = canonicalizePath(path.resolve(os.homedir()));
|
|
215
|
+
const tempRoot = canonicalizePath(path.resolve(os.tmpdir()));
|
|
208
216
|
while (current !== root) {
|
|
209
217
|
// Stop walking before checking markers at home or temp root
|
|
210
218
|
if (current === home || current === tempRoot) return undefined;
|
|
@@ -295,7 +295,7 @@ function capitalize(s: string): string {
|
|
|
295
295
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
|
|
298
|
+
const usedNames = new Set<string>();
|
|
299
299
|
|
|
300
300
|
/**
|
|
301
301
|
* Generate a unique two-word identifier (e.g., "SwiftFalcon").
|
|
@@ -328,10 +328,3 @@ export function generateTaskName(): string {
|
|
|
328
328
|
usedNames.clear();
|
|
329
329
|
return generateTaskName();
|
|
330
330
|
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Reset name generator state (for testing).
|
|
334
|
-
*/
|
|
335
|
-
export function resetTaskNames(): void {
|
|
336
|
-
usedNames = new Set<string>();
|
|
337
|
-
}
|