pi-crew 0.9.68 → 0.10.2
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 +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- 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 +68113 -60774
- 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 +503 -91
- 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 +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -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 +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- 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 +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- 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-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- 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/detached-run-results.ts +90 -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 +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- 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 +32 -10
- 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 +36 -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/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- 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 +132 -1673
- 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 +66 -26
- 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 +139 -232
- package/src/state/types.ts +199 -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/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- 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/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- 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/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -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,18 +4,30 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
7
8
|
import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
9
|
+
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
8
10
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
11
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
9
12
|
import { truncate } from "../../utils/visual.ts";
|
|
10
13
|
import { Box, Text } from "../layout-primitives.ts";
|
|
11
14
|
import { spinnerFrame } from "../spinner.ts";
|
|
12
15
|
import { colorizeStatusGlyphs, iconForStatus } from "../status-colors.ts";
|
|
13
16
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
14
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
agentActivity,
|
|
19
|
+
agentStats,
|
|
20
|
+
budgetedRow,
|
|
21
|
+
dockElapsed,
|
|
22
|
+
dockStatusIcon,
|
|
23
|
+
dockStatusLabel,
|
|
24
|
+
dockUsageText,
|
|
25
|
+
notificationBadge,
|
|
26
|
+
} from "./widget-formatters.ts";
|
|
15
27
|
import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
|
|
16
28
|
import type { WidgetRun } from "./widget-types.ts";
|
|
17
29
|
|
|
18
|
-
const MAX_AGENTS_DISPLAY = 3;
|
|
30
|
+
export const MAX_AGENTS_DISPLAY = 3;
|
|
19
31
|
const FINISHED_LINGER_MAX_AGE = 1;
|
|
20
32
|
/** Default terminal width when caller doesn't pass one explicitly. Keep <= 116
|
|
21
33
|
* (the same default used elsewhere in pi-crew tool renderers) so we never paint
|
|
@@ -42,8 +54,192 @@ export function widgetHeader(runs: WidgetRun[], runningGlyph: string, maxLines =
|
|
|
42
54
|
return `${runningGlyph} Crew agents${notificationBadge(notificationCount)} · ${parts.join(" · ")} · /team-dashboard`;
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
// ── Agent ordering (shared with the inline panel) ──────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* L-4: prioritize RUNNING > QUEUED > WAITING so the most relevant live workers
|
|
61
|
+
* are always shown first. Finished rows fill only the leftover budget and never
|
|
62
|
+
* steal a slot from an active agent.
|
|
63
|
+
*/
|
|
64
|
+
const ACTIVE_PRIORITY: Record<string, number> = { running: 0, queued: 1, waiting: 2 };
|
|
65
|
+
|
|
66
|
+
function isActiveStatus(status: string): boolean {
|
|
67
|
+
return status === "running" || status === "queued" || status === "waiting";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The agent order the widget paints, split into its two sections.
|
|
72
|
+
*
|
|
73
|
+
* Exported because the inline panel navigates the same list: if the panel
|
|
74
|
+
* derived its own order, the cursor index would drift from the rendered rows.
|
|
75
|
+
* One function, one order.
|
|
76
|
+
*/
|
|
77
|
+
export function orderWidgetAgents(entry: WidgetRun, now = Date.now()): { active: CrewAgentRecord[]; finished: CrewAgentRecord[] } {
|
|
78
|
+
const runDone = isFinishedRunStatus(entry.run.status);
|
|
79
|
+
const active = entry.agents.filter((agent) => isActiveStatus(agent.status));
|
|
80
|
+
const finished = entry.agents.filter((agent) => {
|
|
81
|
+
if (isActiveStatus(agent.status)) return false;
|
|
82
|
+
if (!agent.completedAt) return false;
|
|
83
|
+
// Mid-run, finished agents are the run's HISTORY: they must stay in
|
|
84
|
+
// the dock until the RUN itself is done, not age out after a minute
|
|
85
|
+
// while later phases are still working. The linger windows below only
|
|
86
|
+
// apply once the run reached a terminal status (and the run-level
|
|
87
|
+
// visibility grace then decides how much longer the dock shows at all).
|
|
88
|
+
if (!runDone) return true;
|
|
89
|
+
const maxAgeMs = (ERROR_STATUSES.has(agent.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
|
|
90
|
+
const age = now - new Date(agent.completedAt).getTime();
|
|
91
|
+
return Number.isFinite(age) && age < maxAgeMs;
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
active: [...active].sort((a, b) => (ACTIVE_PRIORITY[a.status] ?? 9) - (ACTIVE_PRIORITY[b.status] ?? 9)),
|
|
95
|
+
finished,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
45
99
|
// ── Line builder ──────────────────────────────────────────────────────
|
|
46
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Row layout for the per-agent lines.
|
|
103
|
+
*
|
|
104
|
+
* - `detailed` — the historical two-line tree (name row + `⊶ activity` row).
|
|
105
|
+
* - `compact` — one width-budgeted line per agent, so a wide terminal shows the
|
|
106
|
+
* full description instead of the same clip a narrow one gets.
|
|
107
|
+
*/
|
|
108
|
+
export type WidgetRowStyle = "compact" | "detailed";
|
|
109
|
+
|
|
110
|
+
export interface WidgetRenderOptions {
|
|
111
|
+
rowStyle?: WidgetRowStyle;
|
|
112
|
+
/** Task id under the inline panel cursor, if any. */
|
|
113
|
+
selectedTaskId?: string;
|
|
114
|
+
/** Task id whose transcript pane is open, if any. */
|
|
115
|
+
viewedTaskId?: string;
|
|
116
|
+
/**
|
|
117
|
+
* True while the inline panel holds the cursor. Every agent is then listed
|
|
118
|
+
* (no MAX_AGENTS_DISPLAY cap) so keyboard navigation can reach all of them;
|
|
119
|
+
* the idle widget stays capped to keep the prompt area small.
|
|
120
|
+
*/
|
|
121
|
+
focused?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Short display form of a model id: `zai/glm-5.3` → `glm-5.3`. */
|
|
125
|
+
function shortModelLabel(agent: CrewAgentRecord, run: TeamRunManifest): string | undefined {
|
|
126
|
+
const model = agent.model ?? run.modelContext?.parentModel ?? run.modelContext?.override;
|
|
127
|
+
if (typeof model !== "string" || !model) return undefined;
|
|
128
|
+
return model.split("/").at(-1) ?? model;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One flat dock row (pi-subtask style) for an agent — active or finished. */
|
|
132
|
+
function compactAgentRow(
|
|
133
|
+
run: TeamRunManifest,
|
|
134
|
+
agent: CrewAgentRecord,
|
|
135
|
+
finished: boolean,
|
|
136
|
+
runs: readonly WidgetRun[],
|
|
137
|
+
options: WidgetRenderOptions,
|
|
138
|
+
width: number,
|
|
139
|
+
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined,
|
|
140
|
+
): string {
|
|
141
|
+
const marker = options.selectedTaskId === agent.taskId ? "❯" : " ";
|
|
142
|
+
const dockGlyph = options.viewedTaskId === agent.taskId ? "⏺" : dockStatusIcon(agent.status);
|
|
143
|
+
const name = liveHandle?.agent ?? agent.agent;
|
|
144
|
+
const label = liveHandle?.description ?? agent.role ?? "";
|
|
145
|
+
// Task-first: the agent exists to run its task, so the row names the task
|
|
146
|
+
// right after the agent. With multiple runs, prefix each row with its run
|
|
147
|
+
// label so the flat dock still says which run an agent belongs to.
|
|
148
|
+
const runTag = runs.length > 1 ? `${shortRunLabel(run)} · ` : "";
|
|
149
|
+
const taskTag = agent.taskId ? ` · ${agent.taskId}` : "";
|
|
150
|
+
const roleTag = label && label !== agent.taskId && label !== name ? ` · ${label}` : "";
|
|
151
|
+
const nameText = runTag + name + taskTag + roleTag;
|
|
152
|
+
// pi-subtask activity: the worker's latest line while running, otherwise
|
|
153
|
+
// the status word.
|
|
154
|
+
const liveLine = liveHandle?.activity?.responseText
|
|
155
|
+
?.split("\n")
|
|
156
|
+
.find((line) => line.trim())
|
|
157
|
+
?.trim();
|
|
158
|
+
const activity =
|
|
159
|
+
!finished && liveHandle?.status === "running" && liveLine
|
|
160
|
+
? liveLine.length > 60
|
|
161
|
+
? `${liveLine.slice(0, 60)}…`
|
|
162
|
+
: liveLine
|
|
163
|
+
: finished
|
|
164
|
+
? dockStatusLabel(agent.status)
|
|
165
|
+
: agentActivity(agent, liveHandle);
|
|
166
|
+
const usage = dockUsageText(agent, liveHandle, { viewed: options.viewedTaskId === agent.taskId });
|
|
167
|
+
const ageText = dockElapsed(agent.completedAt ?? agent.startedAt);
|
|
168
|
+
const model = shortModelLabel(agent, run);
|
|
169
|
+
// Stats tail: `· glm-5.3 · ↑1.2k ↓350 · 41s` — the model the worker is
|
|
170
|
+
// actually on first, then usage, then elapsed.
|
|
171
|
+
const suffix = `${model ? ` · ${model}` : ""}${usage ? ` · ${usage}` : ""}${ageText ? ` · ${ageText}` : ""}`;
|
|
172
|
+
return budgetedRow({ lead: `${marker} ${dockGlyph} `, name: nameText, activity, suffix }, width);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The compact dock: hint line, the `main` conversation row, then a
|
|
177
|
+
* MAX_AGENTS_DISPLAY-row SCROLL WINDOW over the flat agent list (exactly the
|
|
178
|
+
* order the inline panel navigates — `panelRowsFromRuns` parity). The window
|
|
179
|
+
* follows the panel selection bottom-pinned: moving the cursor past the
|
|
180
|
+
* window scrolls one row at a time and the ❯ marker is always painted; idle
|
|
181
|
+
* (no selection) shows the top of the list, which the shared ordering puts at
|
|
182
|
+
* the highest-priority live agents. Hidden rows surface as `… ↑N earlier` /
|
|
183
|
+
* `… +N more` indicators.
|
|
184
|
+
*/
|
|
185
|
+
function compactDockLines(
|
|
186
|
+
runs: WidgetRun[],
|
|
187
|
+
options: WidgetRenderOptions,
|
|
188
|
+
width: number,
|
|
189
|
+
maxLines: number,
|
|
190
|
+
notificationCount: number,
|
|
191
|
+
runningGlyph: string,
|
|
192
|
+
): string[] {
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
const flat: Array<{
|
|
195
|
+
run: TeamRunManifest;
|
|
196
|
+
agent: CrewAgentRecord;
|
|
197
|
+
finished: boolean;
|
|
198
|
+
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined;
|
|
199
|
+
}> = [];
|
|
200
|
+
for (const entry of runs) {
|
|
201
|
+
const { active, finished } = orderWidgetAgents(entry, now);
|
|
202
|
+
const liveForRun = listLiveAgents().filter((a) => a.runId === entry.run.runId);
|
|
203
|
+
for (const agent of active) {
|
|
204
|
+
flat.push({ run: entry.run, agent, finished: false, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
205
|
+
}
|
|
206
|
+
for (const agent of finished) {
|
|
207
|
+
flat.push({ run: entry.run, agent, finished: true, liveHandle: liveForRun.find((h) => h.taskId === agent.taskId) });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// No agents at all: fall back to the legacy header so the space under the
|
|
212
|
+
// editor is never just blank.
|
|
213
|
+
if (flat.length === 0) return [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
214
|
+
|
|
215
|
+
const lines: string[] = [];
|
|
216
|
+
let hint: string;
|
|
217
|
+
if (options.viewedTaskId) {
|
|
218
|
+
const viewedName = flat.find((row) => row.agent.taskId === options.viewedTaskId)?.agent.agent ?? "agent";
|
|
219
|
+
hint = `viewing @${viewedName} — typing goes to the agent · ↓ switch · esc back`;
|
|
220
|
+
} else if (options.focused) {
|
|
221
|
+
hint = "enter to view · x to stop/cancel · esc back";
|
|
222
|
+
} else {
|
|
223
|
+
hint = `agents (${flat.length}) — ↓ to select`;
|
|
224
|
+
}
|
|
225
|
+
lines.push(truncate(hint, width));
|
|
226
|
+
// Filled ● = you're on the main conversation; hollow ◯ = an agent view is
|
|
227
|
+
// open (same convention as pi-subtask's main row).
|
|
228
|
+
const mainMarker = options.focused && !options.selectedTaskId ? "❯" : " ";
|
|
229
|
+
const mainIcon = options.viewedTaskId ? "◯" : "●";
|
|
230
|
+
lines.push(truncate(`${mainMarker} ${mainIcon} main`, width));
|
|
231
|
+
|
|
232
|
+
const selectedIndex = options.selectedTaskId ? flat.findIndex((row) => row.agent.taskId === options.selectedTaskId) : -1;
|
|
233
|
+
const windowStart = selectedIndex >= 0 ? Math.max(0, selectedIndex - MAX_AGENTS_DISPLAY + 1) : 0;
|
|
234
|
+
const windowEnd = Math.min(flat.length, windowStart + MAX_AGENTS_DISPLAY);
|
|
235
|
+
if (windowStart > 0) lines.push(truncate(` … ↑${windowStart} earlier (↑ to scroll)`, width));
|
|
236
|
+
for (const row of flat.slice(windowStart, windowEnd)) {
|
|
237
|
+
lines.push(compactAgentRow(row.run, row.agent, row.finished, runs, options, width, row.liveHandle));
|
|
238
|
+
}
|
|
239
|
+
if (windowEnd < flat.length) lines.push(truncate(` … +${flat.length - windowEnd} more (↓ to scroll)`, width));
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
|
|
47
243
|
export function buildWidgetLines(
|
|
48
244
|
cwd: string,
|
|
49
245
|
frame = 0,
|
|
@@ -51,7 +247,10 @@ export function buildWidgetLines(
|
|
|
51
247
|
providedRuns?: WidgetRun[],
|
|
52
248
|
notificationCount = 0,
|
|
53
249
|
width = DEFAULT_WIDGET_WIDTH,
|
|
250
|
+
options: WidgetRenderOptions = {},
|
|
54
251
|
): string[] {
|
|
252
|
+
const rowStyle: WidgetRowStyle = options.rowStyle ?? "detailed";
|
|
253
|
+
const focused = options.focused === true;
|
|
55
254
|
// Match the legacy `buildCrewWidgetLines` API: when no runs are supplied,
|
|
56
255
|
// auto-fetch via activeWidgetRuns(cwd). Otherwise widgets calling with
|
|
57
256
|
// only `(cwd, frame)` would render an empty line set (regression vs. the
|
|
@@ -60,20 +259,29 @@ export function buildWidgetLines(
|
|
|
60
259
|
if (!runs.length) return [];
|
|
61
260
|
|
|
62
261
|
const runningGlyph = spinnerFrame("widget-header");
|
|
262
|
+
|
|
263
|
+
// Compact = pi-subtask's dock: NO "Crew agents" header, NO tree — hint
|
|
264
|
+
// line, `main` row, then a 3-row scroll window over the flat agent list.
|
|
265
|
+
if (rowStyle === "compact") {
|
|
266
|
+
const lines = compactDockLines(runs, options, width, maxLines, notificationCount, runningGlyph);
|
|
267
|
+
return focused ? lines : lines.slice(0, maxLines);
|
|
268
|
+
}
|
|
269
|
+
|
|
63
270
|
const lines: string[] = [widgetHeader(runs, runningGlyph, maxLines, notificationCount)];
|
|
64
271
|
|
|
65
|
-
for (const
|
|
66
|
-
const
|
|
272
|
+
for (const entry of runs) {
|
|
273
|
+
const { run, agents } = entry;
|
|
67
274
|
const now = Date.now();
|
|
68
|
-
const finishedAgents =
|
|
69
|
-
if (item.status === "running" || item.status === "queued" || item.status === "waiting") return false;
|
|
70
|
-
if (!item.completedAt) return false;
|
|
71
|
-
const maxAgeMs = (ERROR_STATUSES.has(item.status) ? ERROR_LINGER_MAX_AGE : FINISHED_LINGER_MAX_AGE) * 60_000;
|
|
72
|
-
const age = now - new Date(item.completedAt).getTime();
|
|
73
|
-
return Number.isFinite(age) && age < maxAgeMs;
|
|
74
|
-
});
|
|
275
|
+
const { active: activeAgents, finished: finishedAgents } = orderWidgetAgents(entry, now);
|
|
75
276
|
const completed = agents.filter((a) => a.status === "completed").length;
|
|
76
|
-
|
|
277
|
+
// WP-3 (H4): while a run is parked awaiting plan approval, the spinner
|
|
278
|
+
// glyph is replaced by a `⚠ plan:<last-8 runId>` badge. Plain-unicode ⚠
|
|
279
|
+
// (the same glyph needs_attention already uses) stays legible in no-color
|
|
280
|
+
// mode and is colorized by the shared colorizeStatusGlyphs pass. In-place
|
|
281
|
+
// glyph swap only — no extra line, so the MAX_AGENTS_DISPLAY / maxLines
|
|
282
|
+
// budget and the truncate(width) rule are untouched.
|
|
283
|
+
const planPending = isPlanApprovalStatePending(run.planApproval);
|
|
284
|
+
const runGlyph = planPending ? `⚠ plan:${run.runId.slice(-8)}` : iconForStatus(run.status, { runningGlyph });
|
|
77
285
|
const isTerminal = isFinishedRunStatus(run.status);
|
|
78
286
|
// Run progress line. v1–v3 flickered on snapshot.tasks state, v4 was
|
|
79
287
|
// too minimal (`0/1 agents` only), v5 duplicated the worker activity
|
|
@@ -82,11 +290,6 @@ export function buildWidgetLines(
|
|
|
82
290
|
// activity line) and is GUARANTEED stable across ticks:
|
|
83
291
|
// - agents count — from `agents` array, always populated, never empty.
|
|
84
292
|
// - run elapsed — from `run.createdAt`, always set on manifest.
|
|
85
|
-
// Both come from sources with no race window — `agents` is read from
|
|
86
|
-
// snapshot.agents OR agentsFor(run) (both always return same length
|
|
87
|
-
// for a healthy run), and `run.createdAt` is immutable. The format
|
|
88
|
-
// shape `"X/Y agents · Ns"` is therefore truly invariant: same number
|
|
89
|
-
// of `·`-separated fields, same field meanings, every render tick.
|
|
90
293
|
//
|
|
91
294
|
// Bug 022 (timer-fix + label): for TERMINAL runs (failed/cancelled/
|
|
92
295
|
// completed) the elapsed counter previously kept ticking up forever
|
|
@@ -104,56 +307,58 @@ export function buildWidgetLines(
|
|
|
104
307
|
|
|
105
308
|
const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
|
|
106
309
|
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
const ACTIVE_PRIORITY: Record<string, number> = {
|
|
111
|
-
running: 0,
|
|
112
|
-
queued: 1,
|
|
113
|
-
waiting: 2,
|
|
114
|
-
};
|
|
115
|
-
const prioritizedActive = [...activeAgents].sort((a, b) => (ACTIVE_PRIORITY[a.status] ?? 9) - (ACTIVE_PRIORITY[b.status] ?? 9));
|
|
310
|
+
// Focused: list every agent so the keyboard cursor can reach it. Idle: keep
|
|
311
|
+
// the historical cap so the prompt area stays small.
|
|
312
|
+
const activeCap = focused ? activeAgents.length : MAX_AGENTS_DISPLAY;
|
|
116
313
|
// Finished rows only appear in slots not used by active agents (max 2). When
|
|
117
314
|
// there are >= MAX_AGENTS_DISPLAY live workers, finished rows are suppressed
|
|
118
315
|
// entirely so they cannot push a live agent's activity line off-screen.
|
|
119
|
-
const finishedSlots = Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
316
|
+
const finishedSlots = focused ? finishedAgents.length : Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
317
|
+
|
|
318
|
+
/** Cursor marker column, mirroring the inline panel's selection. */
|
|
319
|
+
const markerFor = (taskId: string): string => (options.selectedTaskId === taskId ? "❯" : " ");
|
|
120
320
|
|
|
121
|
-
const visibleAgents =
|
|
321
|
+
const visibleAgents = activeAgents.slice(0, activeCap);
|
|
122
322
|
for (const [index, agent] of visibleAgents.entries()) {
|
|
123
|
-
const last = index === visibleAgents.length - 1 && activeAgents.length <=
|
|
323
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= activeCap && finishedSlots === 0;
|
|
124
324
|
const branch = last ? "└─" : "├─";
|
|
125
|
-
const agentGlyph = iconForStatus(agent.status, { runningGlyph });
|
|
126
325
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
326
|
+
const legacyGlyph = options.viewedTaskId === agent.taskId ? "◉" : iconForStatus(agent.status, { runningGlyph });
|
|
127
327
|
const stats = agentStats(agent, liveHandle);
|
|
128
328
|
const name = liveHandle?.agent ?? agent.agent;
|
|
329
|
+
const activity = agentActivity(agent, liveHandle);
|
|
129
330
|
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
130
|
-
const _activeMain = truncate(`│ ${branch} ${
|
|
331
|
+
const _activeMain = truncate(`│ ${branch} ${legacyGlyph} ${name}${desc ? ` · ${desc}` : ` · ${agent.role}`}`, width);
|
|
131
332
|
lines.push(_activeMain);
|
|
132
|
-
const _activity = truncate(`│ ⊶ ${
|
|
333
|
+
const _activity = truncate(`│ ⊶ ${activity}${stats ? ` · ${stats}` : ""}`, width);
|
|
133
334
|
lines.push(_activity);
|
|
134
335
|
}
|
|
135
336
|
|
|
136
|
-
if (activeAgents.length >
|
|
137
|
-
lines.push(truncate(`│ └─ … +${activeAgents.length -
|
|
337
|
+
if (activeAgents.length > activeCap) {
|
|
338
|
+
lines.push(truncate(`│ └─ … +${activeAgents.length - activeCap} more agents`, width));
|
|
138
339
|
}
|
|
139
340
|
|
|
140
341
|
for (const [index, agent] of finishedAgents.slice(0, finishedSlots).entries()) {
|
|
141
342
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
142
343
|
const name = liveHandle?.agent ?? agent.agent;
|
|
143
|
-
const
|
|
344
|
+
const legacyIcon =
|
|
144
345
|
agent.status === "completed" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "needs_attention" ? "⚠" : "▪";
|
|
145
346
|
const stats = agentStats(agent, liveHandle);
|
|
146
347
|
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
147
348
|
const isLastFinished = index === Math.min(finishedAgents.length, finishedSlots) - 1;
|
|
148
349
|
const branch = isLastFinished ? "└─" : "├─";
|
|
149
|
-
const _finished = truncate(`│ ${branch} ${
|
|
350
|
+
const _finished = truncate(`│ ${branch} ${legacyIcon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`, width);
|
|
150
351
|
lines.push(_finished);
|
|
151
352
|
}
|
|
152
353
|
|
|
153
|
-
|
|
354
|
+
// Focused (inline panel cursor active): the keyboard can reach every
|
|
355
|
+
// agent, so the renderer must list them ALL — clipping here would put
|
|
356
|
+
// the cursor marker (❯) on rows that are never painted. The idle
|
|
357
|
+
// widget keeps its historical cap to hold the prompt area small.
|
|
358
|
+
if (lines.length >= maxLines && !focused) break;
|
|
154
359
|
}
|
|
155
360
|
|
|
156
|
-
return lines.slice(0, maxLines);
|
|
361
|
+
return focused ? lines : lines.slice(0, maxLines);
|
|
157
362
|
}
|
|
158
363
|
|
|
159
364
|
// ── Colorization ──────────────────────────────────────────────────────
|
|
@@ -24,16 +24,27 @@ export interface CrewWidgetModel {
|
|
|
24
24
|
/** P3 (#9): workspace/session ID for filtering runs by ownerSessionId
|
|
25
25
|
* on every render, not just the first frame. */
|
|
26
26
|
workspaceId?: string;
|
|
27
|
+
/** Per-agent row layout; defaults to the historical two-line tree. */
|
|
28
|
+
rowStyle?: import("./widget-renderer.ts").WidgetRowStyle;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface CrewWidgetState {
|
|
30
32
|
frame: number;
|
|
31
33
|
lastPlacement?: string;
|
|
32
34
|
lastVisibility?: "hidden" | "visible";
|
|
35
|
+
/** Whether the aboveEditor task-list widget (pi-crew-tasks) is mounted. */
|
|
36
|
+
lastTasksVisibility?: "hidden" | "visible";
|
|
33
37
|
lastKey?: string;
|
|
34
38
|
lastMaxLines?: number;
|
|
35
39
|
lastCwd?: string;
|
|
36
40
|
legacyCleared?: boolean;
|
|
37
41
|
model?: CrewWidgetModel;
|
|
38
42
|
notificationCount?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Non-serializable dock host used when `widgetPlacement` is `"bottom"`:
|
|
45
|
+
* the dock renders through the crew-vibes footer instead of a pi widget
|
|
46
|
+
* slot. Kept off `model` so serialization/persistence of the rest of the
|
|
47
|
+
* state stays unaffected.
|
|
48
|
+
*/
|
|
49
|
+
footerDock?: { render(width: number): string[]; dispose(): void };
|
|
39
50
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* child-process-shield.ts — stop abort-driven child-process errors from
|
|
3
|
+
* killing the host pi process.
|
|
4
|
+
*
|
|
5
|
+
* THE CRASH
|
|
6
|
+
* ---------
|
|
7
|
+
* Node spawns a child with `options.signal` and the signal fires while that
|
|
8
|
+
* child is still tracked (before its 'exit' event has dispatched), Node runs:
|
|
9
|
+
*
|
|
10
|
+
* abortChildProcess(child, killSignal, reason)
|
|
11
|
+
* try { if (child.kill(killSignal)) {
|
|
12
|
+
* child.emit("error", new AbortError(...)) // node:child_process
|
|
13
|
+
* } } catch (err) { child.emit("error", err); }
|
|
14
|
+
*
|
|
15
|
+
* `child.emit("error", ...)` THROWS synchronously when the ChildProcess has
|
|
16
|
+
* no 'error' listener (EventEmitter 'error'-event semantics). That throw is
|
|
17
|
+
* captured by the AbortSignal dispatch machinery and re-thrown on the next
|
|
18
|
+
* tick (`process.nextTick(() => { throw err; })`, node:internal/event_target)
|
|
19
|
+
* — OUTSIDE any try/catch, so it surfaces as `uncaughtException` and pi dies
|
|
20
|
+
* (`pi exiting due to uncaughtException: AbortError: The operation was
|
|
21
|
+
* aborted`). This is exactly the crash seen on /crew-view session switches
|
|
22
|
+
* mid-foreground-run: the run's signal propagates into the vendored pi SDK's
|
|
23
|
+
* session machinery, an internal controller abort reaches a signal-spawned
|
|
24
|
+
* child with no error listener, and the terminal dies.
|
|
25
|
+
*
|
|
26
|
+
* IMPORTANT: because the rethrow is asynchronous (next tick), wrapping the
|
|
27
|
+
* AbortController.abort() call in try/catch (see safe-abort.ts) CANNOT stop
|
|
28
|
+
* it. The only reliable fix is to make sure the spawned child ALWAYS has an
|
|
29
|
+
* 'error' listener before an abort can fire, so child.emit("error", ...)
|
|
30
|
+
* dispatches instead of throwing.
|
|
31
|
+
*
|
|
32
|
+
* THE SHIELD
|
|
33
|
+
* ----------
|
|
34
|
+
* 1. `ChildProcess.prototype.kill` — every abort path in node runs through
|
|
35
|
+
* `child.kill()` FIRST (abortChildProcess calls it before emitting), so a
|
|
36
|
+
* no-op 'error' listener attached inside kill() protects every signal-
|
|
37
|
+
* spawned child IN THE PROCESS, regardless of when the spawning module
|
|
38
|
+
* captured its `spawn` reference (the vendored SDK may bind `spawn` at a
|
|
39
|
+
* point we cannot observe).
|
|
40
|
+
* 2. `child_process.spawn` wrapper — attaches the same no-op 'error' listener
|
|
41
|
+
* at spawn time when `options.signal` is present, so the listener exists
|
|
42
|
+
* before Node's pre-aborted-signal nextTick abort and before spawn-failure
|
|
43
|
+
* errors (ENOENT) are emitted.
|
|
44
|
+
*
|
|
45
|
+
* The no-op listener never swallows anything a consumer cares about: 'error'
|
|
46
|
+
* events still reach every other listener (multiple listeners run), and code
|
|
47
|
+
* that previously died on an unhandled 'error' event keeps its semantics for
|
|
48
|
+
* genuinely broken spawns — it just no longer takes down the whole pi process
|
|
49
|
+
* for the abort-after-exit case, which is a semantic no-op by definition (the
|
|
50
|
+
* child was already gone).
|
|
51
|
+
*/
|
|
52
|
+
import type { ChildProcess } from "node:child_process";
|
|
53
|
+
import { createRequire } from "node:module";
|
|
54
|
+
|
|
55
|
+
const require = createRequire(import.meta.url);
|
|
56
|
+
const childProcessModule = require("node:child_process") as {
|
|
57
|
+
spawn: (file: string, args?: readonly string[] | undefined, options?: object) => ChildProcess;
|
|
58
|
+
ChildProcess: { prototype: { kill: (signal?: number | string) => boolean } };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const SHIELD_MARKER = Symbol.for("pi-crew.childProcessAbortShieldInstalled");
|
|
62
|
+
|
|
63
|
+
/** No-op 'error' listener: prevents the EventEmitter throw, never filters data. */
|
|
64
|
+
function shieldErrorListener(): void {
|
|
65
|
+
/* deliberately empty — see module docstring */
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Install the child-process abort shield. Idempotent: subsequent calls are
|
|
70
|
+
* no-ops. MUST be called at extension registration, before any team run can
|
|
71
|
+
* start (the vendored SDK is imported lazily later, so any spawn it binds
|
|
72
|
+
* afterwards already sees the shielded spawn).
|
|
73
|
+
*/
|
|
74
|
+
export function installChildProcessAbortShield(): void {
|
|
75
|
+
const proto = childProcessModule.ChildProcess.prototype as typeof childProcessModule.ChildProcess.prototype & {
|
|
76
|
+
[SHIELD_MARKER]?: boolean;
|
|
77
|
+
};
|
|
78
|
+
if (proto[SHIELD_MARKER]) return;
|
|
79
|
+
|
|
80
|
+
const originalKill = proto.kill;
|
|
81
|
+
proto.kill = function shieldedKill(this: ChildProcess, signal?: number | string): boolean {
|
|
82
|
+
// A signal-spawned child may be aborted synchronously by node's
|
|
83
|
+
// abortChildProcess right after this kill() returns; guarantee a
|
|
84
|
+
// listener is present so child.emit("error", AbortError) dispatches
|
|
85
|
+
// instead of throwing.
|
|
86
|
+
if (this.listenerCount("error") === 0) this.on("error", shieldErrorListener);
|
|
87
|
+
return originalKill.call(this, signal);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const moduleObject = childProcessModule as {
|
|
91
|
+
spawn: typeof childProcessModule.spawn;
|
|
92
|
+
};
|
|
93
|
+
const originalSpawn = moduleObject.spawn;
|
|
94
|
+
moduleObject.spawn = function shieldedSpawn(this: unknown, ...args: Parameters<typeof childProcessModule.spawn>) {
|
|
95
|
+
const child = originalSpawn.apply(this, args);
|
|
96
|
+
// Attach at spawn time: covers pre-aborted signals (node defers the
|
|
97
|
+
// abort to the next tick) and spawn-failure errors (ENOENT etc.)
|
|
98
|
+
// before any abort could fire.
|
|
99
|
+
if (child && (args[2] as { signal?: unknown } | undefined)?.signal !== undefined) {
|
|
100
|
+
child.on("error", shieldErrorListener);
|
|
101
|
+
}
|
|
102
|
+
return child;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
proto[SHIELD_MARKER] = true;
|
|
106
|
+
}
|
|
@@ -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;
|
package/src/utils/redaction.ts
CHANGED
|
@@ -34,7 +34,14 @@ export const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]
|
|
|
34
34
|
// keyword-scan matches '_token' in 'prompt_tokens' (underscore + "token"),
|
|
35
35
|
// falsely classifying usage counts as secrets. See performance/quality
|
|
36
36
|
// assessment fix #5.
|
|
37
|
+
//
|
|
38
|
+
// `tokens` alone must stay too: pi-crew state records use
|
|
39
|
+
// `progress.tokens` / `status.tokens` as the per-task usage total; the
|
|
40
|
+
// camelCase fallback scan in isSecretKey() matches the bare keyword at
|
|
41
|
+
// start+end of the name and would otherwise rewrite `15` to the literal
|
|
42
|
+
// "***" on disk, which the widget then renders verbatim as `*** tok`.
|
|
37
43
|
const TOKEN_COUNT_KEYS = new Set([
|
|
44
|
+
"tokens",
|
|
38
45
|
"prompt_tokens",
|
|
39
46
|
"completion_tokens",
|
|
40
47
|
"total_tokens",
|