pi-crew 0.11.7 → 0.11.8
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/.githooks/pre-commit +22 -0
- package/.githooks/pre-push +56 -0
- package/CHANGELOG.md +121 -1
- package/dist/index.mjs +528 -510
- package/docs/publishing.md +29 -0
- package/index.ts +3 -3
- package/package.json +8 -5
- package/scripts/install-hooks.mjs +32 -0
- package/skills/real-test-pi-crew/SKILL.md +31 -18
- package/src/config/config-validation.ts +8 -1
- package/src/config/env-vars.ts +16 -1
- package/src/config/types.ts +28 -4
- package/src/extension/registration/foreground-run-controller.ts +19 -10
- package/src/extension/registration/lifecycle-handlers.ts +6 -491
- package/src/extension/registration/lifecycle.ts +7 -90
- package/src/extension/registration/render-loop.ts +502 -0
- package/src/extension/team-tool/doctor.ts +6 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +7 -0
- package/src/runtime/child-pi/child-pi.ts +37 -2
- package/src/runtime/child-pi/pi-version.ts +187 -0
- package/src/runtime/classifier/classifier-service.ts +298 -0
- package/src/runtime/dispatch-batch.ts +4 -0
- package/src/runtime/live-session/live-session-runtime.ts +31 -0
- package/src/runtime/model/pi-args.ts +62 -3
- package/src/runtime/process/zombie-scanner.ts +168 -7
- package/src/runtime/rpc/README.md +64 -25
- package/src/runtime/rpc/rpc-worker.ts +146 -26
- package/src/runtime/run-worker.ts +89 -15
- package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
- package/src/runtime/skill-instructions.ts +66 -1
- package/src/runtime/surface/resolve-surface.ts +34 -19
- package/src/runtime/task-runner/child-executor.ts +132 -0
- package/src/runtime/task-runner/pre-execution.ts +8 -0
- package/src/runtime/task-runner.ts +6 -0
- package/src/schema/config-schema.ts +20 -8
- package/src/schema/team-tool-schema.ts +4 -2
- package/src/state/contracts.ts +5 -0
- package/src/ui/format-helpers.ts +11 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
- package/src/ui/rail.ts +14 -1
- package/src/ui/settings-overlay.ts +11 -2
- package/src/ui/widget/task-list.ts +10 -3
- package/src/ui/widget/widget-renderer.ts +53 -4
- package/src/utils/relative-time.ts +11 -3
- package/src/utils/visual.ts +32 -2
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* canopy/hint rows are NOT part of the split any more, so the hint keeps its
|
|
15
15
|
* full width instead of being cropped to 60% of the overlay.
|
|
16
16
|
*
|
|
17
|
+
* R3-6 (IME): the ACTIVE field emits pi-tui's zero-width `CURSOR_MARKER` at
|
|
18
|
+
* its value end — the host TUI strips it and parks the hardware cursor there
|
|
19
|
+
* so the IME candidate window follows the logical text cursor (CJK input).
|
|
20
|
+
*
|
|
17
21
|
* Keys/behaviour are unchanged (same `overlay:*` dispatch, same free-text
|
|
18
22
|
* passthrough, same validation errors, same discard confirmation). Hints come
|
|
19
23
|
* from `formatHint` (discard LAST, keys through `keyToken`).
|
|
20
24
|
*/
|
|
21
25
|
|
|
26
|
+
import { CURSOR_MARKER } from "@earendil-works/pi-tui";
|
|
22
27
|
import type { MailboxDirection } from "../../state/coordination/mailbox.ts";
|
|
23
28
|
import { pad, sanitizeLine, truncate } from "../../utils/visual.ts";
|
|
24
29
|
import { overlayActionForKey } from "../keybinding-map.ts";
|
|
@@ -117,7 +122,16 @@ export class MailboxComposeOverlay {
|
|
|
117
122
|
// (the field is a single-line cell; multi-line content belongs to the
|
|
118
123
|
// preview column).
|
|
119
124
|
const value = sanitizeLine(this.fields[field] ?? "");
|
|
120
|
-
|
|
125
|
+
// R3-6 (IME): CURSOR_MARKER is pi-tui's zero-width APC sequence — the host
|
|
126
|
+
// TUI finds it, positions the HARDWARE cursor there (IME candidate window
|
|
127
|
+
// anchor for CJK input), and strips it before painting. This overlay's
|
|
128
|
+
// fields append/backspace at end-of-value only, so the logical cursor IS
|
|
129
|
+
// the value end — same pattern as pi-tui's own Input component
|
|
130
|
+
// (marker + fake cursor). The `›` prefix stays: it is the RAIL list-selection
|
|
131
|
+
// glyph, orthogonal to the text cursor. Direction checkbox row gets NO
|
|
132
|
+
// marker — it toggles on space and has no text insertion point.
|
|
133
|
+
const marker = active && !this.confirm ? CURSOR_MARKER : "";
|
|
134
|
+
return `${active ? CURSOR : " "} ${label}: ${truncate(value, Math.max(8, width - label.length - 5))}${marker}`;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
private activeName(): FieldName {
|
package/src/ui/rail.ts
CHANGED
|
@@ -58,7 +58,20 @@ export const RAIL = { open: "┏", section: "┣", body: "┃", close: "┗" } a
|
|
|
58
58
|
*/
|
|
59
59
|
export const PI_EXPAND_CHORD = "ctrl+o";
|
|
60
60
|
|
|
61
|
-
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
61
|
+
/** Selection cursor (lists) and active/step marker (sections, goals).
|
|
62
|
+
*
|
|
63
|
+
* R3-6 audit (2026-10-04): this glyph marks LIST SELECTION in dashboards,
|
|
64
|
+
* pickers, settings menus, and compose field rows — a navigation affordance,
|
|
65
|
+
* not a text insertion point. It therefore deliberately does NOT adopt
|
|
66
|
+
* pi-tui's `CURSOR_MARKER` (the zero-width APC sequence that anchors the
|
|
67
|
+
* IME candidate window at a TEXT cursor). Sites that DO own a text cursor —
|
|
68
|
+
* mailbox-compose field values, settings text-input/edit buffers — emit
|
|
69
|
+
* `CURSOR_MARKER` at their buffer end in addition to any visual glyph.
|
|
70
|
+
* Not converted, by design: run-dashboard run rows, settings menu rows,
|
|
71
|
+
* agents-jobs-browser rows, mailbox-detail rows, agent-picker rows,
|
|
72
|
+
* schedules-pane job rows (all list selection); widget dock `❯` (a
|
|
73
|
+
* single-line focus acknowledgement, not an input); mascot `█` frames
|
|
74
|
+
* (animation glyphs). */
|
|
62
75
|
export const CURSOR = "›";
|
|
63
76
|
export const ACTIVE = "▸";
|
|
64
77
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* label/value alignment, inline toggle, select submenu, and text input.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { CURSOR_MARKER } from "@earendil-works/pi-tui";
|
|
7
8
|
import { truncateToWidth, visibleWidth } from "../utils/visual.ts";
|
|
8
9
|
import { keyOf, matchesKey } from "./key-utils.ts";
|
|
9
10
|
import { CURSOR, canopyLine, formatHint, overflowHint, RAIL, railLine } from "./rail.ts";
|
|
@@ -601,7 +602,12 @@ class TextinputSubmenu {
|
|
|
601
602
|
lines.push(this.theme.fg("muted", this.description));
|
|
602
603
|
}
|
|
603
604
|
lines.push("");
|
|
604
|
-
|
|
605
|
+
// R3-6 (IME): the marker before the fake `█` cursor is pi-tui's
|
|
606
|
+
// zero-width CURSOR_MARKER — the host TUI strips it and positions the
|
|
607
|
+
// hardware cursor there so the IME candidate window tracks the buffer
|
|
608
|
+
// end (this submenu's buffer appends/erases at end only). Emitted
|
|
609
|
+
// unconditionally: the row renders only while this input owns keys.
|
|
610
|
+
lines.push(` ${this.buffer}${CURSOR_MARKER}█`);
|
|
605
611
|
lines.push("");
|
|
606
612
|
lines.push(
|
|
607
613
|
this.theme.fg(
|
|
@@ -730,7 +736,10 @@ class AgentOverridesSubmenu {
|
|
|
730
736
|
const lines: string[] = [];
|
|
731
737
|
lines.push(this.theme.bold(this.theme.fg("accent", `Edit ${agent} ${field}`)));
|
|
732
738
|
lines.push("");
|
|
733
|
-
|
|
739
|
+
// R3-6 (IME): same CURSOR_MARKER contract as TextinputSubmenu — the
|
|
740
|
+
// edit buffer is end-append only, so the marker rides immediately before
|
|
741
|
+
// the fake `█` cursor (pi-tui Input's marker+cursorChar pattern).
|
|
742
|
+
lines.push(` ${this.editBuffer}${CURSOR_MARKER}█`);
|
|
734
743
|
lines.push("");
|
|
735
744
|
lines.push(
|
|
736
745
|
this.theme.fg(
|
|
@@ -84,8 +84,15 @@ function taskStatusIcon(task: TeamTaskState): string {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Plan-card elapsed dialect (QW#5): `45s` / `2m 49s` / `1h 03m`, empty
|
|
89
|
+
* string for non-finite/negative input. Deliberately NOT ui/format-helpers'
|
|
90
|
+
* canonical `formatDuration` (that one renders `12.3s`/`3m5s` with no hours
|
|
91
|
+
* bucket and no invalid-input guard) — the spaced, zero-padded, hour-capable
|
|
92
|
+
* format here is pinned by the pi-tasks plan-card display. Renamed from a
|
|
93
|
+
* local `formatDuration` to end the three-way name collision.
|
|
94
|
+
*/
|
|
95
|
+
function formatTaskElapsed(ms: number): string {
|
|
89
96
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
90
97
|
const totalSeconds = Math.floor(ms / 1000);
|
|
91
98
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
@@ -110,7 +117,7 @@ function runningSuffix(task: TeamTaskState): string {
|
|
|
110
117
|
const start = Date.parse(task.startedAt ?? "");
|
|
111
118
|
if (Number.isFinite(start) && start > 0) {
|
|
112
119
|
const end = task.finishedAt ? Date.parse(task.finishedAt) : Date.now();
|
|
113
|
-
const elapsed =
|
|
120
|
+
const elapsed = formatTaskElapsed(end - start);
|
|
114
121
|
if (elapsed) parts.push(elapsed);
|
|
115
122
|
}
|
|
116
123
|
const input = task.usage?.input ?? 0;
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* Extracted from crew-widget.ts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import * as piTui from "@earendil-works/pi-tui";
|
|
7
9
|
import { getCrewScheduler, getScheduledJobs, getScheduledJobsHiddenCountView } from "../../extension/team-tool/handle-schedule.ts";
|
|
8
10
|
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
9
11
|
import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
|
|
10
12
|
import { isFinishedRunStatus } from "../../runtime/process-status.ts";
|
|
11
13
|
import type { ScheduledJob } from "../../runtime/scheduling/scheduler.ts";
|
|
14
|
+
import type { TeamRunManifest } from "../../state/types.ts";
|
|
12
15
|
import { formatRelativeTime } from "../../utils/relative-time.ts";
|
|
13
16
|
import { truncate, visibleWidth } from "../../utils/visual.ts";
|
|
14
17
|
import { Box, Text } from "../layout-primitives.ts";
|
|
@@ -95,9 +98,52 @@ export function widgetRailSlot(runs: readonly WidgetRun[]): RailSlot {
|
|
|
95
98
|
/** `<team>` (or `team/workflow` when they differ); several live runs collapse
|
|
96
99
|
* to `<n> runs` — the counts on the row stay aggregate. */
|
|
97
100
|
function dockSubject(runs: WidgetRun[]): string {
|
|
98
|
-
if (runs.length
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
+
if (runs.length === 0) return "idle";
|
|
102
|
+
const label = runs.length > 1 ? `${runs.length} runs` : shortRunLabel(runs[0]!.run);
|
|
103
|
+
// R3-9: the dock's ONE clickable action — the subject opens the LATEST
|
|
104
|
+
// run's artifacts directory (`file://` OSC-8 link). Terminals without
|
|
105
|
+
// OSC-8 support ignore the escapes and render the plain label.
|
|
106
|
+
return linkify(label, latestArtifactsUrl(runs));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── R3-9: OSC-8 dock link ─────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
type TuiHyperlink = (text: string, url: string) => string;
|
|
112
|
+
|
|
113
|
+
/** pi-tui's OSC-8 helper, resolved defensively: the peer range is `*`, so a
|
|
114
|
+
* host running a pi-tui build without the export must still render the
|
|
115
|
+
* plain dock instead of failing the namespace import. */
|
|
116
|
+
const tuiHyperlink: TuiHyperlink | undefined = (() => {
|
|
117
|
+
const candidate = (piTui as { hyperlink?: unknown }).hyperlink;
|
|
118
|
+
return typeof candidate === "function" ? (candidate as TuiHyperlink) : undefined;
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
/** `file://` URL for an artifacts dir; `undefined` keeps the label plain. */
|
|
122
|
+
function artifactsDirUrl(artifactsRoot: string | undefined): string | undefined {
|
|
123
|
+
if (!artifactsRoot) return undefined;
|
|
124
|
+
try {
|
|
125
|
+
return pathToFileURL(artifactsRoot).href;
|
|
126
|
+
} catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Latest (by createdAt) run's artifacts dir — the dock link target. */
|
|
132
|
+
function latestArtifactsUrl(runs: readonly WidgetRun[]): string | undefined {
|
|
133
|
+
let latest: TeamRunManifest | undefined;
|
|
134
|
+
for (const entry of runs) {
|
|
135
|
+
const candidate = entry.run;
|
|
136
|
+
if (!candidate.artifactsRoot) continue;
|
|
137
|
+
if (!latest || (candidate.createdAt ?? "") > (latest.createdAt ?? "")) latest = candidate;
|
|
138
|
+
}
|
|
139
|
+
return artifactsDirUrl(latest?.artifactsRoot);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Wrap in OSC-8 when both the pi-tui helper and a target exist; plain text
|
|
143
|
+
* otherwise (clean degrade on non-hyperlink terminals and older hosts). */
|
|
144
|
+
function linkify(text: string, url: string | undefined): string {
|
|
145
|
+
if (!url || !tuiHyperlink) return text;
|
|
146
|
+
return tuiHyperlink(text, url);
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
/**
|
|
@@ -389,7 +435,10 @@ export function buildWidgetLines(
|
|
|
389
435
|
*/
|
|
390
436
|
export function colorWidgetLine(line: string, index: number, theme: CrewTheme, slot: RailSlot = "border"): string {
|
|
391
437
|
let result = line;
|
|
392
|
-
|
|
438
|
+
// R3-9: the guard detects THEME styling (CSI `ESC[`), not any escape — the
|
|
439
|
+
// dock's OSC-8 artifacts link (introduced by `dockSubject`) is not styling
|
|
440
|
+
// and must NOT opt the line out of the identity paint pass.
|
|
441
|
+
if (index === 0 && !result.includes("\u001b[")) {
|
|
393
442
|
// `┏|┃ <WORD> ▸ <subject>`: rail glyph takes the state slot, the identity
|
|
394
443
|
// word accent+bold, the subject toolTitle+bold. The subject is bounded by
|
|
395
444
|
// the first ` ·` so a count segment can never be swallowed.
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
export function formatRelativeTime(now: Date, target: Date): string {
|
|
16
16
|
const deltaMs = target.getTime() - now.getTime();
|
|
17
17
|
if (deltaMs === 0) return "now";
|
|
18
|
-
if (deltaMs > 0) return `in ${
|
|
19
|
-
return `${
|
|
18
|
+
if (deltaMs > 0) return `in ${formatRelativeCompact(deltaMs)}`;
|
|
19
|
+
return `${formatRelativeCompact(-deltaMs)} ago`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Relative-time bucket ladder (dialect D6-T4): `45s` / `84m` / `14h` /
|
|
24
|
+
* `2d14h`, zero-hour remainder collapses to `3d`. A distinct dialect from
|
|
25
|
+
* ui/format-helpers' canonical `formatDuration` (QW#5 collision resolution):
|
|
26
|
+
* the bucket boundaries (60s / 120m / 48h) are design-approved for relative
|
|
27
|
+
* timestamps and must not adopt the generic `3m5s` rendering. Formerly named
|
|
28
|
+
* `formatDurationCompact`; private to this module.
|
|
29
|
+
*/
|
|
30
|
+
function formatRelativeCompact(ms: number): string {
|
|
23
31
|
const totalSeconds = Math.floor(ms / 1000);
|
|
24
32
|
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
25
33
|
const totalMinutes = Math.floor(totalSeconds / 60);
|
package/src/utils/visual.ts
CHANGED
|
@@ -60,11 +60,41 @@ export function __test__visibleWidthCacheSize(): number {
|
|
|
60
60
|
function consumeAnsi(input: string, index: number): number {
|
|
61
61
|
const char = input[index];
|
|
62
62
|
if (!char || char !== "\u001b") return 0;
|
|
63
|
-
|
|
63
|
+
const next = input[index + 1];
|
|
64
|
+
if (next === "[") {
|
|
65
|
+
let i = index + 2;
|
|
66
|
+
while (i < input.length) {
|
|
67
|
+
const code = input.charCodeAt(i);
|
|
68
|
+
if (code >= 0x40 && code <= 0x7e) return i - index + 1;
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
// R3-9: OSC sequences (`ESC ] … BEL` or `ESC ] … ESC \`) — OSC-8 hyperlinks
|
|
74
|
+
// (pi-tui `hyperlink()`, used by the crew widget) ride this branch. The
|
|
75
|
+
// truncation/wrap/sanitize helpers must treat the whole sequence as ONE
|
|
76
|
+
// zero-width unit, matching pi-tui's `visibleWidth` (which already strips
|
|
77
|
+
// OSC), or a clipped line would slice mid-escape and leak raw bytes into
|
|
78
|
+
// the terminal. Malformed payloads (stray control char) fall back to the
|
|
79
|
+
// pre-existing behaviour of treating the ESC as ordinary data.
|
|
80
|
+
//
|
|
81
|
+
// U6/DR4 (pi 1.0.4 adoption, 2026-10-06): APC sequences (`ESC _ … BEL` or
|
|
82
|
+
// `ESC _ … ESC \`) share the exact same grammar, so they ride the same
|
|
83
|
+
// scanner. pi-tui's `CURSOR_MARKER` — now a documented public contract —
|
|
84
|
+
// is an APC string (`\u001b_pi:c\u0007`) that focused components emit at
|
|
85
|
+
// the cursor position. Without this branch, truncateToWidth/wrapHard
|
|
86
|
+
// walked the marker's `_pi:c` payload as ordinary text (~6 visible
|
|
87
|
+
// columns) and could slice the sequence in half; with it, the whole
|
|
88
|
+
// marker is consumed as ONE zero-width unit, matching pi-tui's
|
|
89
|
+
// `visibleWidth` (which already zero-widths APC). Same malformed fallback:
|
|
90
|
+
// a stray control char cannot belong to an OSC/APC payload either.
|
|
91
|
+
if (next !== "]" && next !== "_") return 0;
|
|
64
92
|
let i = index + 2;
|
|
65
93
|
while (i < input.length) {
|
|
66
94
|
const code = input.charCodeAt(i);
|
|
67
|
-
if (code
|
|
95
|
+
if (code === 0x07) return i - index + 1; // BEL terminator
|
|
96
|
+
if (code === 0x1b && input[i + 1] === "\\") return i - index + 2; // ST terminator
|
|
97
|
+
if (code < 0x20) return 0; // control char cannot belong to an OSC/APC payload
|
|
68
98
|
i++;
|
|
69
99
|
}
|
|
70
100
|
return 0;
|