pi-crew 0.11.0 → 0.11.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 +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -6,30 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { getCrewScheduler, getScheduledJobs, getScheduledJobsHiddenCountView } from "../../extension/team-tool/handle-schedule.ts";
|
|
8
8
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
9
|
-
import type { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
|
|
10
9
|
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
11
10
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
12
11
|
import type { ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
13
|
-
import type { TeamRunManifest } from "../../state/types.ts";
|
|
14
12
|
import { formatRelativeTime } from "../../utils/relative-time.ts";
|
|
15
|
-
import { truncate } from "../../utils/visual.ts";
|
|
13
|
+
import { truncate, visibleWidth } from "../../utils/visual.ts";
|
|
16
14
|
import { Box, Text } from "../layout-primitives.ts";
|
|
15
|
+
import { ACTIVE, RAIL, type RailSlot, railLeaders, railRaw, shortId, statusSlot } from "../rail.ts";
|
|
17
16
|
import { spinnerFrame } from "../spinner.ts";
|
|
18
17
|
import { colorizeStatusGlyphs } from "../status-colors.ts";
|
|
19
|
-
import type
|
|
20
|
-
import {
|
|
21
|
-
agentActivity,
|
|
22
|
-
budgetedRow,
|
|
23
|
-
dockElapsed,
|
|
24
|
-
dockStatusIcon,
|
|
25
|
-
dockStatusLabel,
|
|
26
|
-
dockUsageText,
|
|
27
|
-
notificationBadge,
|
|
28
|
-
} from "./widget-formatters.ts";
|
|
18
|
+
import { asCrewTheme, type CrewTheme } from "../theme-adapter.ts";
|
|
19
|
+
import { notificationBadge } from "./widget-formatters.ts";
|
|
29
20
|
import { activeWidgetRuns, shortRunLabel } from "./widget-model.ts";
|
|
30
21
|
import type { WidgetRun } from "./widget-types.ts";
|
|
31
22
|
|
|
32
|
-
export const MAX_AGENTS_DISPLAY = 3;
|
|
33
23
|
const FINISHED_LINGER_MAX_AGE = 1;
|
|
34
24
|
/** Default terminal width when caller doesn't pass one explicitly. Keep <= 116
|
|
35
25
|
* (the same default used elsewhere in pi-crew tool renderers) so we never paint
|
|
@@ -41,32 +31,158 @@ export const TASK_DESC_MAX = 60;
|
|
|
41
31
|
const ERROR_LINGER_MAX_AGE = 2;
|
|
42
32
|
const ERROR_STATUSES = new Set(["failed", "cancelled", "stopped", "needs_attention"]);
|
|
43
33
|
|
|
34
|
+
// ── RAIL dock composition (design system §2.B) ─────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// The dock is the ONE surface that must never grow a second row: it speaks the
|
|
37
|
+
// rail grammar with the BODY glyph (`┃`) only — never `┏`/`┗`, which imply a
|
|
38
|
+
// multi-line card. The builders below return PLAIN text (that is the exported
|
|
39
|
+
// contract: `buildWidgetLines` is the raw row, `colorWidgetLine` the paint
|
|
40
|
+
// pass), but they COMPOSE through the shared rail helpers with a no-op theme
|
|
41
|
+
// so the glyph/leader vocabulary lives in `rail.ts` alone.
|
|
42
|
+
const PLAIN_THEME = asCrewTheme(undefined);
|
|
43
|
+
|
|
44
|
+
/** Identity word of the dock + status bar. */
|
|
45
|
+
const DOCK_WORD = "CREW";
|
|
46
|
+
|
|
47
|
+
/** The dock's tail: four leaders + the one-key entry hint. */
|
|
48
|
+
const DOCK_HINT_RIGHT = "↓·enter";
|
|
49
|
+
const DOCK_HINT = `···· ${DOCK_HINT_RIGHT}`;
|
|
50
|
+
|
|
51
|
+
/** The dock's FOCUS marker (RAIL §2.B: "focused row keeps the `❯ ` prefix").
|
|
52
|
+
* It is deliberately not `rail.ts`'s `CURSOR` (`›`) — that glyph marks the
|
|
53
|
+
* selected row of a LIST; the dock is a single line, and `❯` is the marker
|
|
54
|
+
* the prompt area has always acknowledged the ↓ keystroke with. */
|
|
55
|
+
const FOCUS_MARKER = "❯";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `···· ↓·enter` appended to a dock row. Composed through `railLeaders` with a
|
|
59
|
+
* budget that pins the leader run at exactly four dots — the dock is a
|
|
60
|
+
* left-aligned single row, so the leaders must NOT stretch to the terminal
|
|
61
|
+
* width the way a card's metrics→elapsed leaders do.
|
|
62
|
+
*/
|
|
63
|
+
function dockTail(left: string, theme: CrewTheme, maxWidth?: number): string {
|
|
64
|
+
const pinned = visibleWidth(left) + visibleWidth(DOCK_HINT_RIGHT) + 6;
|
|
65
|
+
// The pin gives a fixed four-dot leader on a wide terminal. On a NARROW one
|
|
66
|
+
// the budget shrinks instead, so `railLeaders` trims the LEFT segment (with
|
|
67
|
+
// `…`) and the actionable `↓·enter` hint never gets clipped away — the live
|
|
68
|
+
// row used to end in `↓…` at 50 columns.
|
|
69
|
+
const budget = maxWidth === undefined ? pinned : Math.min(pinned, maxWidth - 2);
|
|
70
|
+
return railLeaders(left, DOCK_HINT_RIGHT, budget, theme);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `CREW ▸ <subject>` — the dock identity (colour is applied on the paint pass). */
|
|
74
|
+
function dockIdentity(subject: string): string {
|
|
75
|
+
return subject ? `${DOCK_WORD} ${ACTIVE} ${subject}` : DOCK_WORD;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `┃ <content>` — the dock rail, unpadded (`truncate` stays the single clip). */
|
|
79
|
+
function dockLine(content: string, theme: CrewTheme, slot: RailSlot): string {
|
|
80
|
+
return railRaw(RAIL.body, slot, content, theme);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Aggregate run status → the dock's rail colour (§2.B: rail colour = state).
|
|
85
|
+
* A failure outranks live work; live work outranks idle. Zero runs is idle.
|
|
86
|
+
*/
|
|
87
|
+
export function widgetRailSlot(runs: readonly WidgetRun[]): RailSlot {
|
|
88
|
+
if (runs.length === 0) return "border";
|
|
89
|
+
const slots = runs.map((entry) => statusSlot(entry.run.status));
|
|
90
|
+
if (slots.includes("error")) return "error";
|
|
91
|
+
if (slots.includes("borderAccent")) return "borderAccent";
|
|
92
|
+
return slots[0] ?? "border";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** `<team>` (or `team/workflow` when they differ); several live runs collapse
|
|
96
|
+
* to `<n> runs` — the counts on the row stay aggregate. */
|
|
97
|
+
function dockSubject(runs: WidgetRun[]): string {
|
|
98
|
+
if (runs.length > 1) return `${runs.length} runs`;
|
|
99
|
+
const first = runs[0];
|
|
100
|
+
return first ? shortRunLabel(first.run) : "idle";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The zero-run dock row: `┃ CREW ▸ idle · ⏰ 1 sched ···· ↓·enter`.
|
|
105
|
+
*
|
|
106
|
+
* Returns `undefined` when nothing schedules-related paints. The keep-alive
|
|
107
|
+
* path MUST then render NOTHING (`[]`) — a bare hint, and above all the
|
|
108
|
+
* literal `undefined — ↓·enter`, is the live regression this guards against.
|
|
109
|
+
*/
|
|
110
|
+
export function idleWidgetLine(schedLine: string | undefined, focused = false, maxWidth?: number): string | undefined {
|
|
111
|
+
if (!schedLine) return undefined;
|
|
112
|
+
const left = `${dockIdentity("idle")} · ${schedLine}`;
|
|
113
|
+
const line = dockLine(dockTail(left, PLAIN_THEME, maxWidth), PLAIN_THEME, "border");
|
|
114
|
+
return focused ? `${FOCUS_MARKER} ${line}` : line;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The dock's leading activity glyph: a braille spinner ONLY while something is
|
|
119
|
+
* actually running, otherwise the outcome glyph of the aggregate state.
|
|
120
|
+
*
|
|
121
|
+
* Live-run bug (2026-09-16): the dock spun forever because `buildWidgetLines`
|
|
122
|
+
* always passed `spinnerFrame("widget-header")` — a finished run painted
|
|
123
|
+
* `┃ ⠹ CREW ▸ fast-fix · 0 running · 3/3 done`, i.e. a spinner with nothing to
|
|
124
|
+
* spin for. `✓`/`✗` are used (not `●`/`✖`) because they are in
|
|
125
|
+
* `STATUS_GLYPH_CHARS`, so the shared colorizer paints them.
|
|
126
|
+
*/
|
|
127
|
+
export function widgetActivityGlyph(runs: readonly WidgetRun[]): string {
|
|
128
|
+
const anyAgentRunning = runs.some((entry) => entry.agents.some((agent) => agent.status === "running"));
|
|
129
|
+
const anyRunRunning = runs.some((entry) => entry.run.status === "running");
|
|
130
|
+
if (anyAgentRunning || anyRunRunning) return spinnerFrame("widget-header");
|
|
131
|
+
const slot = widgetRailSlot(runs);
|
|
132
|
+
if (slot === "error") return "✗";
|
|
133
|
+
if (slot === "success") return "✓";
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
|
|
44
137
|
// ── Header ────────────────────────────────────────────────────────────
|
|
45
138
|
|
|
46
|
-
|
|
139
|
+
/**
|
|
140
|
+
* The dock's ONE row (RAIL §2.B):
|
|
141
|
+
*
|
|
142
|
+
* `┃ ⠧ CREW ▸ fast-fix · 2 running · 3/5 done · ⏰ 1 sched ···· ↓·enter`
|
|
143
|
+
*
|
|
144
|
+
* Zero runs paint `┃ CREW ▸ idle · …` (see `idleWidgetLine`) instead. Returns
|
|
145
|
+
* PLAIN text — the component colorizes index 0 with `colorWidgetLine`.
|
|
146
|
+
*/
|
|
147
|
+
export function widgetHeader(
|
|
148
|
+
runs: WidgetRun[],
|
|
149
|
+
runningGlyph: string,
|
|
150
|
+
maxLines = 20,
|
|
151
|
+
notificationCount = 0,
|
|
152
|
+
schedSegment?: string,
|
|
153
|
+
maxWidth?: number,
|
|
154
|
+
): string {
|
|
47
155
|
const agents = runs.flatMap((item) => item.agents);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
156
|
+
const segments: string[] = [];
|
|
157
|
+
if (runs.length > 0) {
|
|
158
|
+
const runningAgents = agents.filter((a) => a.status === "running").length;
|
|
159
|
+
const queuedAgents = agents.filter((a) => a.status === "queued").length;
|
|
160
|
+
const waitingAgents = agents.filter((a) => a.status === "waiting").length;
|
|
161
|
+
const completedAgents = agents.filter((a) => a.status === "completed").length;
|
|
162
|
+
// Zero counts are noise on a one-line dock: a finished run reads
|
|
163
|
+
// `3/3 done`, not `0 running · 3/3 done`.
|
|
164
|
+
if (runningAgents) segments.push(`${runningAgents} running`);
|
|
165
|
+
if (queuedAgents) segments.push(`${queuedAgents} queued`);
|
|
166
|
+
if (waitingAgents) segments.push(`${waitingAgents} waiting`);
|
|
167
|
+
if (completedAgents) segments.push(`${completedAgents}/${agents.length} done`);
|
|
168
|
+
// WP-3 on the single line (2026-09-14 round 2): a run parked awaiting plan
|
|
169
|
+
// approval surfaces as a `⚠ plan:<run8>` segment — the row-level badge is
|
|
170
|
+
// gone with the run tree, so the count row carries the signal.
|
|
171
|
+
const planPending = runs.find((item) => isPlanApprovalStatePending(item.run.planApproval));
|
|
172
|
+
if (planPending) segments.push(`⚠ plan:${shortId(planPending.run.runId)}`);
|
|
173
|
+
}
|
|
174
|
+
// Tier C: `schedSegment` is the ALREADY-BUILT `⏰ …` string (jobs, hidden
|
|
175
|
+
// count and clock are all injected upstream — buildWidgetLines); undefined
|
|
176
|
+
// means nothing schedules-related paints.
|
|
177
|
+
if (schedSegment) segments.push(schedSegment);
|
|
178
|
+
// Bug 021: the alerts badge is one more segment (no 🔔, capped at 99+).
|
|
179
|
+
const badge = notificationBadge(notificationCount)
|
|
180
|
+
.replace(/^\s*·\s*/, "")
|
|
181
|
+
.trim();
|
|
182
|
+
if (badge) segments.push(badge);
|
|
183
|
+
const head = runningGlyph ? `${runningGlyph} ${dockIdentity(dockSubject(runs))}` : dockIdentity(dockSubject(runs));
|
|
184
|
+
const left = segments.length > 0 ? `${head} · ${segments.join(" · ")}` : head;
|
|
185
|
+
return dockLine(dockTail(left, PLAIN_THEME, maxWidth), PLAIN_THEME, "border");
|
|
70
186
|
}
|
|
71
187
|
|
|
72
188
|
// ── Agent ordering (shared with the inline panel) ──────────────────────
|
|
@@ -113,25 +229,15 @@ export function orderWidgetAgents(entry: WidgetRun, now = Date.now()): { active:
|
|
|
113
229
|
|
|
114
230
|
// ── Line builder ──────────────────────────────────────────────────────
|
|
115
231
|
|
|
116
|
-
/**
|
|
117
|
-
* Row layout for the per-agent lines.
|
|
118
|
-
*
|
|
119
|
-
* - `detailed` — the historical two-line tree (name row + `⊶ activity` row).
|
|
120
|
-
* - `compact` — one width-budgeted line per agent, so a wide terminal shows the
|
|
121
|
-
* full description instead of the same clip a narrow one gets.
|
|
122
|
-
*/
|
|
123
|
-
export type WidgetRowStyle = "compact" | "detailed";
|
|
124
|
-
|
|
125
232
|
export interface WidgetRenderOptions {
|
|
126
|
-
rowStyle?: WidgetRowStyle;
|
|
127
233
|
/** Task id under the inline panel cursor, if any. */
|
|
128
234
|
selectedTaskId?: string;
|
|
129
235
|
/** Task id whose transcript pane is open, if any. */
|
|
130
236
|
viewedTaskId?: string;
|
|
131
237
|
/**
|
|
132
|
-
* True while the inline panel holds the cursor.
|
|
133
|
-
*
|
|
134
|
-
*
|
|
238
|
+
* True while the inline panel holds the cursor. The inline panel then
|
|
239
|
+
* gets a visible cursor acknowledgement; the idle widget stays compact
|
|
240
|
+
* to keep the prompt area small.
|
|
135
241
|
*/
|
|
136
242
|
focused?: boolean;
|
|
137
243
|
/**
|
|
@@ -142,58 +248,6 @@ export interface WidgetRenderOptions {
|
|
|
142
248
|
now?: Date;
|
|
143
249
|
}
|
|
144
250
|
|
|
145
|
-
/** Short display form of a model id: `zai/glm-5.3` → `glm-5.3`. */
|
|
146
|
-
function shortModelLabel(agent: CrewAgentRecord, run: TeamRunManifest): string | undefined {
|
|
147
|
-
const model = agent.model ?? run.modelContext?.parentModel ?? run.modelContext?.override;
|
|
148
|
-
if (typeof model !== "string" || !model) return undefined;
|
|
149
|
-
return model.split("/").at(-1) ?? model;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** One flat dock row (pi-subtask style) for an agent — active or finished. */
|
|
153
|
-
function compactAgentRow(
|
|
154
|
-
run: TeamRunManifest,
|
|
155
|
-
agent: CrewAgentRecord,
|
|
156
|
-
finished: boolean,
|
|
157
|
-
runs: readonly WidgetRun[],
|
|
158
|
-
options: WidgetRenderOptions,
|
|
159
|
-
width: number,
|
|
160
|
-
liveHandle: ReturnType<typeof listLiveAgents>[number] | undefined,
|
|
161
|
-
nowMs: number,
|
|
162
|
-
): string {
|
|
163
|
-
const marker = options.selectedTaskId === agent.taskId ? "❯" : " ";
|
|
164
|
-
const dockGlyph = options.viewedTaskId === agent.taskId ? "⏺" : dockStatusIcon(agent.status);
|
|
165
|
-
const name = liveHandle?.agent ?? agent.agent;
|
|
166
|
-
const label = liveHandle?.description ?? agent.role ?? "";
|
|
167
|
-
// Task-first: the agent exists to run its task, so the row names the task
|
|
168
|
-
// right after the agent. With multiple runs, prefix each row with its run
|
|
169
|
-
// label so the flat dock still says which run an agent belongs to.
|
|
170
|
-
const runTag = runs.length > 1 ? `${shortRunLabel(run)} · ` : "";
|
|
171
|
-
const taskTag = agent.taskId ? ` · ${agent.taskId}` : "";
|
|
172
|
-
const roleTag = label && label !== agent.taskId && label !== name ? ` · ${label}` : "";
|
|
173
|
-
const nameText = runTag + name + taskTag + roleTag;
|
|
174
|
-
// pi-subtask activity: the worker's latest line while running, otherwise
|
|
175
|
-
// the status word.
|
|
176
|
-
const liveLine = liveHandle?.activity?.responseText
|
|
177
|
-
?.split("\n")
|
|
178
|
-
.find((line) => line.trim())
|
|
179
|
-
?.trim();
|
|
180
|
-
const activity =
|
|
181
|
-
!finished && liveHandle?.status === "running" && liveLine
|
|
182
|
-
? liveLine.length > 60
|
|
183
|
-
? `${liveLine.slice(0, 60)}…`
|
|
184
|
-
: liveLine
|
|
185
|
-
: finished
|
|
186
|
-
? dockStatusLabel(agent.status)
|
|
187
|
-
: agentActivity(agent, liveHandle);
|
|
188
|
-
const usage = dockUsageText(agent, liveHandle, { viewed: options.viewedTaskId === agent.taskId, nowMs });
|
|
189
|
-
const ageText = dockElapsed(agent.completedAt ?? agent.startedAt);
|
|
190
|
-
const model = shortModelLabel(agent, run);
|
|
191
|
-
// Stats tail: `· glm-5.3 · ↑1.2k ↓350 · 41s` — the model the worker is
|
|
192
|
-
// actually on first, then usage, then elapsed.
|
|
193
|
-
const suffix = `${model ? ` · ${model}` : ""}${usage ? ` · ${usage}` : ""}${ageText ? ` · ${ageText}` : ""}`;
|
|
194
|
-
return budgetedRow({ lead: `${marker} ${dockGlyph} `, name: nameText, activity, suffix }, width);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
251
|
// ── Schedules line (Tier C) ───────────────────────────────────────────
|
|
198
252
|
|
|
199
253
|
/**
|
|
@@ -294,48 +348,67 @@ export function buildWidgetLines(
|
|
|
294
348
|
width = DEFAULT_WIDGET_WIDTH,
|
|
295
349
|
options: WidgetRenderOptions = {},
|
|
296
350
|
): string[] {
|
|
297
|
-
// SINGLE-LINE
|
|
298
|
-
// paints EXACTLY ONE row — counts
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
351
|
+
// SINGLE-LINE DOCK (design system §2.B, maintainer design 2026-09-14): the
|
|
352
|
+
// dock paints EXACTLY ONE row — `┃ <identity> · counts · schedules ····
|
|
353
|
+
// ↓·enter`. No per-agent rows, no run tree, no scroll window; ALL browsing
|
|
354
|
+
// lives in the Agents & Jobs browser, opened by ↓·enter from THIS line
|
|
355
|
+
// (crew-editor: idle enter at the line target = browser). The rail glyph is
|
|
356
|
+
// always `┃` — `┏`/`┗` would imply a multi-line card.
|
|
303
357
|
//
|
|
304
358
|
// Focused (the ↓ cursor sits ON this line): prefix a ❯ marker so the
|
|
305
|
-
// keystroke is visibly acknowledged — the line itself is the cursor
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
// budget.
|
|
359
|
+
// keystroke is visibly acknowledged — the line itself is the cursor target;
|
|
360
|
+
// there is no second row to land on. maxLines/frame remain in the signature
|
|
361
|
+
// for call-site compatibility; a single line is always within budget.
|
|
309
362
|
const schedLine = schedulesWidgetLine(cwd, options.now ?? new Date());
|
|
310
363
|
const runs = providedRuns ?? activeWidgetRuns(cwd);
|
|
364
|
+
const focused = options.focused === true;
|
|
311
365
|
if (!runs.length) {
|
|
312
366
|
// Zero runs keep-alive (Tier C): jobs are exactly what run while no
|
|
313
|
-
// interactive run is active. The ⏰ segment stands alone as the row
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
return [truncate(
|
|
367
|
+
// interactive run is active. The ⏰ segment stands alone as the row; with
|
|
368
|
+
// no schedules either there is NOTHING to paint — `[]`, never a bare
|
|
369
|
+
// hint and never the literal `undefined — ↓·enter` (live bug 2026-09-16).
|
|
370
|
+
const idle = idleWidgetLine(schedLine, focused);
|
|
371
|
+
return idle ? [truncate(idle, width)] : [];
|
|
318
372
|
}
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
return [truncate(options.focused ? `❯ ${base}` : base, width)];
|
|
373
|
+
const base = widgetHeader(runs, widgetActivityGlyph(runs), maxLines, notificationCount, schedLine, width);
|
|
374
|
+
return [truncate(focused ? `${FOCUS_MARKER} ${base}` : base, width)];
|
|
322
375
|
}
|
|
323
376
|
|
|
324
377
|
// ── Colorization ──────────────────────────────────────────────────────
|
|
325
378
|
|
|
326
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Paint pass for a PLAIN dock/plan line (`index 0` = the identity row).
|
|
381
|
+
*
|
|
382
|
+
* The builders above return plain text, so this adds the identity/rail/chrome
|
|
383
|
+
* colours: the leading `┃` (or plan `┏`) takes the rail slot — `statusSlot` of
|
|
384
|
+
* the aggregate run status, injected by the component — the identity word goes
|
|
385
|
+
* accent+bold, and the tail hint is dimmed. Lines that were ALREADY built with
|
|
386
|
+
* a real theme (the task-list variant builds through `rail.ts` directly) carry
|
|
387
|
+
* escapes and are passed through untouched, apart from the shared glyph
|
|
388
|
+
* colorizer below.
|
|
389
|
+
*/
|
|
390
|
+
export function colorWidgetLine(line: string, index: number, theme: CrewTheme, slot: RailSlot = "border"): string {
|
|
327
391
|
let result = line;
|
|
328
|
-
if (index === 0) {
|
|
329
|
-
|
|
392
|
+
if (index === 0 && !result.includes("\u001b")) {
|
|
393
|
+
// `┏|┃ <WORD> ▸ <subject>`: rail glyph takes the state slot, the identity
|
|
394
|
+
// word accent+bold, the subject toolTitle+bold. The subject is bounded by
|
|
395
|
+
// the first ` ·` so a count segment can never be swallowed.
|
|
396
|
+
result = result.replace(
|
|
397
|
+
/^([❯] )?([┃┏]) (?:(\S+) )?((?:CREW|PLAN)(?: ▸ [^·]+?)?)(?= ·|$)/,
|
|
398
|
+
(_match, cursor: string | undefined, glyph: string, spinner: string | undefined, identity: string) => {
|
|
399
|
+
const [word = "", subject] = identity.split(" ▸ ");
|
|
400
|
+
const tail = subject ? ` ${theme.fg("dim", ACTIVE)} ${theme.fg("toolTitle", theme.bold(subject))}` : "";
|
|
401
|
+
// The spinner is re-emitted raw: the shared glyph colorizer below
|
|
402
|
+
// paints the braille range accent.
|
|
403
|
+
return `${cursor ?? ""}${theme.fg(slot, glyph)} ${spinner ? `${spinner} ` : ""}${theme.fg("accent", theme.bold(word))}${tail}`;
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
result = result.replace(DOCK_HINT, theme.fg("dim", DOCK_HINT));
|
|
330
407
|
}
|
|
331
408
|
// Shared glyph colorizer covers ALL status glyphs — including ⏳ (waiting),
|
|
332
409
|
// ⚠ (needs_attention), and the braille spinner range ⠁-⣿ (running) — which the
|
|
333
410
|
// previous local statusGlyphColor map + regex omitted (F-1, V-3).
|
|
334
|
-
|
|
335
|
-
if (index === 0) {
|
|
336
|
-
result = theme.fg("accent", result);
|
|
337
|
-
}
|
|
338
|
-
return result;
|
|
411
|
+
return colorizeStatusGlyphs(result, theme);
|
|
339
412
|
}
|
|
340
413
|
|
|
341
414
|
export function renderLines(lines: string[], width: number): string[] {
|
|
@@ -24,8 +24,6 @@ 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;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
export interface CrewWidgetState {
|
|
@@ -157,7 +157,7 @@ export function readLinesSince(filePath: string, state: IncrementalReadState): I
|
|
|
157
157
|
totalRead += bytesRead;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
const content = buf.
|
|
160
|
+
const content = buf.subarray(0, totalRead);
|
|
161
161
|
const lines: string[] = [];
|
|
162
162
|
let lineCount = state.lineCount;
|
|
163
163
|
let committedOffset = state.byteOffset;
|
|
@@ -165,8 +165,16 @@ export function readLinesSince(filePath: string, state: IncrementalReadState): I
|
|
|
165
165
|
let searchFrom = 0;
|
|
166
166
|
let newlineIdx: number;
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
// F07 / RR-017: scan the BUFFER, not a decoded string. A string index is a
|
|
169
|
+
// CHARACTER index, so using it as a byte offset silently mis-commits the
|
|
170
|
+
// watermark on any multi-byte UTF-8 line (Vietnamese diacritics, emoji,
|
|
171
|
+
// CJK): the next read then restarts mid-line and produces a corrupt line
|
|
172
|
+
// (measured: a delta read of a 4-line file returned one `raw` garbage
|
|
173
|
+
// event plus the three real ones). Byte offsets are what the API promises,
|
|
174
|
+
// so `Buffer.indexOf(0x0a)` is the correct scan and decoding happens only
|
|
175
|
+
// per complete line.
|
|
176
|
+
while ((newlineIdx = content.indexOf(0x0a, searchFrom)) !== -1) {
|
|
177
|
+
const lineText = content.toString("utf-8", searchFrom, newlineIdx);
|
|
170
178
|
committedOffset = state.byteOffset + newlineIdx + 1;
|
|
171
179
|
searchFrom = newlineIdx + 1;
|
|
172
180
|
if (lineText.length > 0) {
|
package/src/utils/paths.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { getCrewEnv } from "../config/env-vars.ts";
|
|
6
|
+
import { PROJECT_DIR_MARKERS, PROJECT_FILE_MARKERS } from "./project-markers.ts";
|
|
6
7
|
|
|
7
8
|
// NEW-P1/NEW-P2 (perf): packageRoot() and userPiRoot() are invariant for a
|
|
8
9
|
// process lifetime but each call did statSync/readFileSync/lstatSync (10-15+
|
|
@@ -111,17 +112,9 @@ export function userPiRoot(): string {
|
|
|
111
112
|
return resolved;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"pyproject.toml",
|
|
118
|
-
"Cargo.toml",
|
|
119
|
-
"go.mod",
|
|
120
|
-
"pom.xml",
|
|
121
|
-
"composer.json",
|
|
122
|
-
"build.gradle",
|
|
123
|
-
"build.gradle.kts",
|
|
124
|
-
];
|
|
115
|
+
// Marker lists live in ./project-markers.ts (RR-020 Fix 1) so that
|
|
116
|
+
// src/state/crew-init.ts:findProjectRoot() cannot drift from this resolver
|
|
117
|
+
// again — the drift produced two different project roots for one cwd.
|
|
125
118
|
|
|
126
119
|
// 2.10 — cache findRepoRoot results so repeated lookups during render ticks
|
|
127
120
|
// (loadConfig, state-store helpers, powerbar, snapshot-cache, ...) skip the
|
|
@@ -180,7 +173,14 @@ export function findRepoRoot(cwd: string): string | undefined {
|
|
|
180
173
|
// newly-created test directories and shouldn't propagate as a crash.
|
|
181
174
|
let startKey: string;
|
|
182
175
|
try {
|
|
183
|
-
|
|
176
|
+
// Canonicalize with .native (long-name form on Windows). The home/temp
|
|
177
|
+
// boundaries in computeRepoRoot are canonicalized with realpathSync.native
|
|
178
|
+
// too — a non-native start preserves 8.3 short names (RUNNER~1) on win32,
|
|
179
|
+
// so the walk chain NEVER textually matches the long-name boundary and
|
|
180
|
+
// escapes the sandbox, latching onto an ancestor marker (live CI: runs
|
|
181
|
+
// created under a sandboxed home landed in the REAL home's .pi/teams).
|
|
182
|
+
// On Linux/macOS native and non-native resolve identically.
|
|
183
|
+
startKey = fs.realpathSync.native(cwd);
|
|
184
184
|
} catch (error) {
|
|
185
185
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
186
186
|
startKey = path.resolve(cwd);
|
|
@@ -252,3 +252,85 @@ export function projectCrewRoot(cwd: string): string {
|
|
|
252
252
|
export function userCrewRoot(): string {
|
|
253
253
|
return path.join(userPiRoot(), "extensions", "pi-crew");
|
|
254
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Relative path of the run-state directory under a pi-crew root, per layout:
|
|
258
|
+
* - `.crew` → `<root>/.crew/state/runs` (classic layout)
|
|
259
|
+
* - `.pi/teams` → `<root>/.pi/teams/state/runs` (`.pi`-based layout)
|
|
260
|
+
* Mirrors `projectCrewRoot`'s layout choice, so scanners can recognise the
|
|
261
|
+
* SAME two layouts the rest of pi-crew writes.
|
|
262
|
+
*/
|
|
263
|
+
export const RUN_STATE_RUNS_SUBPATHS = [path.join(".crew", "state", "runs"), path.join(".pi", "teams", "state", "runs")];
|
|
264
|
+
|
|
265
|
+
/** Layout dirs derived from RUN_STATE_RUNS_SUBPATHS — the single list (a
|
|
266
|
+
* literal copy here is exactly the drift Fix 1 killed for project markers). */
|
|
267
|
+
const RUN_STATE_LAYOUT_DIRS = RUN_STATE_RUNS_SUBPATHS.map((rel) => path.dirname(path.dirname(rel)));
|
|
268
|
+
|
|
269
|
+
/** Layout dirs split into components for the per-component symlink walk
|
|
270
|
+
* (cold-verify F-1: lstat only sees the FINAL path component, so for the
|
|
271
|
+
* `.pi/teams` layout the intermediate `.pi` was never checked). */
|
|
272
|
+
const RUN_STATE_LAYOUT_SEGMENTS = RUN_STATE_LAYOUT_DIRS.map((layout) => layout.split(/[\\/]+/).filter(Boolean));
|
|
273
|
+
|
|
274
|
+
/** True when `p` exists and is NOT a symlink (dir-or-file). A planted symlink
|
|
275
|
+
* never passes: scanners must reject rather than trust it. */
|
|
276
|
+
function existsNoSymlink(p: string): boolean {
|
|
277
|
+
try {
|
|
278
|
+
return !fs.lstatSync(p).isSymbolicLink();
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** True when EVERY component of `path.join(dir, ...segments)` exists and is
|
|
285
|
+
* not a symlink. lstat rejects only the FINAL component, so intermediate
|
|
286
|
+
* symlinks must be walked component-by-component — cold-verify F-1 reproduced
|
|
287
|
+
* out-of-tree read AND write through `ws/.pi -> /outside` because `.pi` was
|
|
288
|
+
* intermediate in all three lstat calls of the earlier fix. */
|
|
289
|
+
function existsSymlinkFreePath(dir: string, segments: string[]): boolean {
|
|
290
|
+
let current = dir;
|
|
291
|
+
for (const seg of segments) {
|
|
292
|
+
current = path.join(current, seg);
|
|
293
|
+
if (!existsNoSymlink(current)) return false;
|
|
294
|
+
}
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* True when `dir` holds pi-crew run state under EITHER supported layout
|
|
300
|
+
* (`.crew/` or `.pi/teams/`) — used by /tmp debris scanners so a temp
|
|
301
|
+
* workspace with live `.pi/teams/state/runs` state is not deleted.
|
|
302
|
+
*
|
|
303
|
+
* A SYMLINKED layout dir never counts: scanners must be able to reject a
|
|
304
|
+
* planted symlink instead of treating it as protection.
|
|
305
|
+
*/
|
|
306
|
+
export function hasRunStateLayout(dir: string): boolean {
|
|
307
|
+
for (const segments of RUN_STATE_LAYOUT_SEGMENTS) {
|
|
308
|
+
if (existsSymlinkFreePath(dir, segments)) return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Resolve the run-state dir (`<layout>/state/runs`) that actually exists under
|
|
315
|
+
* `dir`, checking BOTH supported layouts. Returns undefined when neither
|
|
316
|
+
* layout has a runs dir (i.e. `dir` holds no pi-crew run state).
|
|
317
|
+
*
|
|
318
|
+
* Symlink policy matches `hasRunStateLayout` (cold-verify correction: the
|
|
319
|
+
* first version used existsSync, which FOLLOWS symlinks — a planted
|
|
320
|
+
* `.crew`/`.pi/teams`/`state/runs` symlink let stale-reconciler and
|
|
321
|
+
* health-monitor read — and reconcile — run manifests OUTSIDE the scanned
|
|
322
|
+
* tree). Both the layout dir and the runs dir itself must be symlink-free.
|
|
323
|
+
*/
|
|
324
|
+
export function findRunStateDir(dir: string): string | undefined {
|
|
325
|
+
for (const segments of RUN_STATE_LAYOUT_SEGMENTS) {
|
|
326
|
+
// Cold-verify F-1/F-A: EVERY component must be symlink-free — lstat only
|
|
327
|
+
// sees the final path component, so intermediate symlinks (`.pi` of the
|
|
328
|
+
// `.pi/teams` layout, the middle `state`, …) were traversed transparently
|
|
329
|
+
// and let scanners READ and WRITE run manifests OUTSIDE the scanned tree
|
|
330
|
+
// (reproduced: reconcileOrphanedTempWorkspaces flipped an outside manifest
|
|
331
|
+
// running→cancelled through both a `.crew/state` and a `.pi` symlink).
|
|
332
|
+
if (!existsSymlinkFreePath(dir, [...segments, "state", "runs"])) continue;
|
|
333
|
+
return path.join(dir, ...segments, "state", "runs");
|
|
334
|
+
}
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* project-markers.ts — SINGLE source of truth for the project-root marker lists.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists (RR-020 Fix 1): the marker lists were duplicated and
|
|
5
|
+
* had DRIFTED apart:
|
|
6
|
+
* - `src/utils/paths.ts` (computeRepoRoot) used 15 markers (.pi/.crew/
|
|
7
|
+
* .factory/.omc + 8 build-file markers)
|
|
8
|
+
* - `src/state/crew-init.ts` (findProjectRoot) used 7 markers (.git/.hg/.svn
|
|
9
|
+
* + package.json/pyproject.toml/Cargo.toml/go.mod)
|
|
10
|
+
* For cwd=`parent/subproject` where `parent/.git` and `parent/subproject/.pi`
|
|
11
|
+
* exist, paths.ts resolved the root to `subproject` (→ `subproject/.pi/teams`)
|
|
12
|
+
* while crew-init.ts walked past `.pi` up to `parent/.git` (→ `parent/.crew`),
|
|
13
|
+
* so ONE run could end up with TWO roots (`run-intent.ts` → ensureCrewDirectory).
|
|
14
|
+
* Both resolvers now read the lists from here.
|
|
15
|
+
*
|
|
16
|
+
* CONTRACT — do not break:
|
|
17
|
+
* - NO imports at all (plain string[] literals only). `crew-init.ts` is
|
|
18
|
+
* dynamically `import()`'d from concurrent child Pi subprocesses and under
|
|
19
|
+
* load jiti's ESM/CJS interop can leave namespace bindings `undefined`
|
|
20
|
+
* (issue #28). Importing anything here (even `node:path`) would re-introduce
|
|
21
|
+
* that race for the very module that was hardened against it.
|
|
22
|
+
* - Values must stay IDENTICAL to the historical `paths.ts` lists — they are
|
|
23
|
+
* the widest set, so unifying on them never resolves a root LOWER than
|
|
24
|
+
* before (the direction that would break existing projects).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Directory names that mark a project root. */
|
|
28
|
+
export const PROJECT_DIR_MARKERS = [".git", ".pi", ".crew", ".hg", ".svn", ".factory", ".omc"];
|
|
29
|
+
|
|
30
|
+
/** Build-system file names that mark a project root. */
|
|
31
|
+
export const PROJECT_FILE_MARKERS = [
|
|
32
|
+
"package.json",
|
|
33
|
+
"pyproject.toml",
|
|
34
|
+
"Cargo.toml",
|
|
35
|
+
"go.mod",
|
|
36
|
+
"pom.xml",
|
|
37
|
+
"composer.json",
|
|
38
|
+
"build.gradle",
|
|
39
|
+
"build.gradle.kts",
|
|
40
|
+
];
|
package/src/utils/visual.ts
CHANGED
|
@@ -151,10 +151,6 @@ export function pad(value: string, width: number): string {
|
|
|
151
151
|
return `${value}${" ".repeat(width - current)}`;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export function boxLine(text: string, innerWidth: number): string {
|
|
155
|
-
return `│ ${truncate(text, innerWidth - 4)} │`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
154
|
function readAnsiCode(input: string, index: number): string | undefined {
|
|
159
155
|
const ansiLength = consumeAnsi(input, index);
|
|
160
156
|
if (ansiLength > 0) return input.slice(index, index + ansiLength);
|