pi-crew 0.9.44 → 0.9.47

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +38 -3
  3. package/dist/build-meta.json +349 -203
  4. package/dist/index.mjs +2229 -2968
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/decisions/2026-07-21-broker-phase4-default-on.md +77 -0
  7. package/docs/decisions/2026-07-21-broker-windows-perms.md +91 -0
  8. package/docs/decisions/2026-07-22-broker-phase4-gated-on.md +99 -0
  9. package/docs/decisions/README.md +3 -0
  10. package/docs/publishing.md +26 -0
  11. package/package.json +3 -1
  12. package/scripts/build-bundle.mjs +7 -0
  13. package/scripts/postinstall.mjs +35 -1
  14. package/scripts/pty_probe.py +174 -0
  15. package/skills/real-test-pi-crew/SKILL.md +659 -0
  16. package/src/agents/discover-agents.ts +1 -1
  17. package/src/config/config.ts +42 -1
  18. package/src/config/defaults.ts +45 -1
  19. package/src/config/types.ts +19 -0
  20. package/src/extension/register.ts +6 -1
  21. package/src/extension/registration/context-builder.ts +4 -0
  22. package/src/extension/registration/lifecycle-handlers.ts +200 -6
  23. package/src/extension/registration/registration-types.ts +9 -0
  24. package/src/extension/registration/subagent-manager-setup.ts +178 -59
  25. package/src/extension/run-import.ts +21 -1
  26. package/src/extension/team-tool/api.ts +4 -2
  27. package/src/prompt/prompt-runtime.ts +108 -0
  28. package/src/runtime/async-runner.ts +9 -1
  29. package/src/runtime/broker-issuer.ts +37 -0
  30. package/src/runtime/child-pi-spawn.ts +53 -0
  31. package/src/runtime/child-pi.ts +42 -11
  32. package/src/runtime/crew-broker-child.ts +88 -0
  33. package/src/runtime/crew-broker-client.ts +673 -0
  34. package/src/runtime/crew-broker-tokens.ts +84 -0
  35. package/src/runtime/crew-broker.ts +1276 -0
  36. package/src/runtime/dynamic-workflow-context.ts +7 -3
  37. package/src/runtime/dynamic-workflow-runner.ts +1 -1
  38. package/src/runtime/manifest-cache.ts +30 -0
  39. package/src/runtime/plan-templates.ts +8 -6
  40. package/src/runtime/resilient-edit.ts +16 -15
  41. package/src/runtime/role-permission.ts +27 -2
  42. package/src/runtime/run-coalesced-task-group.ts +72 -15
  43. package/src/runtime/task-packet.ts +1 -1
  44. package/src/schema/config-schema.ts +14 -0
  45. package/src/state/event-log.ts +88 -34
  46. package/src/state/locks.ts +53 -0
  47. package/src/state/mailbox.ts +208 -4
  48. package/src/state/run-metrics.ts +40 -12
  49. package/src/ui/key-utils.ts +42 -0
  50. package/src/ui/keybinding-map.ts +29 -3
  51. package/src/ui/live-run-sidebar.ts +1 -9
  52. package/src/ui/run-dashboard.ts +29 -9
  53. package/src/ui/settings-overlay.ts +42 -22
  54. package/src/utils/incremental-reader.ts +105 -0
  55. package/src/utils/ndjson.ts +115 -0
  56. package/src/utils/session-utils.ts +30 -0
  57. package/src/utils/socket-path.ts +127 -0
  58. package/src/utils/visual.ts +27 -91
  59. package/workflows/default.workflow.md +1 -1
  60. package/workflows/fast-fix.workflow.md +1 -1
  61. package/workflows/plan-execute.workflow.md +1 -1
  62. package/workflows/review.workflow.md +1 -1
  63. package/src/runtime/auto-resume.ts +0 -100
  64. package/src/runtime/notebook-helpers.ts +0 -88
  65. package/src/runtime/orphan-sentinel.ts +0 -7
@@ -1,98 +1,28 @@
1
+ import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
2
+
1
3
  export const ANSI_PATTERN = /\u001b\[[0-?]*[ -/]*[@-~]/g;
2
4
 
3
5
  const WIDTH_CACHE_LIMIT = 256;
4
6
  const widthCache = new Map<string, number>();
5
7
 
6
- /** Code-point ranges that render as width 2 in most terminals (CJK + emoji). */
7
- const WIDE_RANGES: Array<[number, number]> = [
8
- // CJK Unified Ideographs
9
- [0x4e00, 0x9fff],
10
- // CJK Extension A
11
- [0x3400, 0x4dbf],
12
- // CJK Compatibility Ideographs
13
- [0xf900, 0xfaff],
14
- // Hangul Syllables
15
- [0xac00, 0xd7af],
16
- // CJK Symbols and Punctuation, Hiragana, Katakana
17
- [0x3000, 0x33ff],
18
- // Fullwidth forms
19
- [0xff01, 0xff60],
20
- // Emoji blocks
21
- // Emoji-presentation codepoints in 0x2600-0x27BF (narrow chars like ✓✗★ excluded)
22
- [0x2615, 0x2615],
23
- [0x2648, 0x2653],
24
- [0x267f, 0x267f],
25
- [0x2693, 0x2693],
26
- [0x26a1, 0x26a1],
27
- [0x26aa, 0x26ab],
28
- [0x26bd, 0x26be],
29
- [0x26c4, 0x26c5],
30
- [0x26ce, 0x26ce],
31
- [0x26d4, 0x26d4],
32
- [0x26ea, 0x26ea],
33
- [0x26f2, 0x26f3],
34
- [0x26f5, 0x26f5],
35
- [0x26fa, 0x26fa],
36
- [0x26fd, 0x26fd],
37
- [0x2702, 0x2702],
38
- [0x2705, 0x2705],
39
- [0x2708, 0x270d],
40
- [0x270f, 0x270f],
41
- [0x2712, 0x2712],
42
- [0x2714, 0x2714],
43
- [0x2716, 0x2716],
44
- [0x271d, 0x271d],
45
- [0x2721, 0x2721],
46
- [0x2728, 0x2728],
47
- [0x2733, 0x2734],
48
- [0x2744, 0x2744],
49
- [0x2747, 0x2747],
50
- [0x274c, 0x274c],
51
- [0x274e, 0x274e],
52
- [0x2753, 0x2755],
53
- [0x2757, 0x2757],
54
- [0x2763, 0x2764],
55
- [0x2795, 0x2797],
56
- [0x27a1, 0x27a1],
57
- [0x27b0, 0x27b0],
58
- [0x27bf, 0x27bf],
59
- // Geometric Shapes / Misc Symbols-Arrows emoji that pi-tui upstream counts as
60
- // width=2 (RGI emoji). Mismatch here caused the "Rendered line N exceeds
61
- // terminal width (160 > 159)" TUI crash: pi-crew truncated to width 159 by
62
- // its own (mismatched) measure, then Box padded to 159 chars, but pi-tui
63
- // re-measured the padded line at 160 because ⬜ counts as 2 upstream.
64
- [0x2b1b, 0x2b1c], // ⬛ BLACK LARGE SQUARE, ⬜ WHITE LARGE SQUARE
65
- [0x1f300, 0x1f9ff], // Misc Symbols, Emoticons, Transport, Map, Supplement
66
- [0x1fa00, 0x1faff], // Symbols Extended-A
67
- [0x1f000, 0x1f02f], // Mahjong, Dominos
68
- [0xfe00, 0xfe0f], // Variation Selectors (emoji presentation)
69
- ];
70
- // NOTE: U+200D (Zero Width Joiner, ZWJ) is intentionally NOT listed as wide.
71
- // ZWJ has zero advance width by Unicode definition; miscounting it as 2 caused
72
- // slight over-truncation/over-padding of compound-emoji goals (T-1).
73
-
74
- function isWideCodePoint(code: number): boolean {
75
- for (const [lo, hi] of WIDE_RANGES) {
76
- if (code >= lo && code <= hi) return true;
77
- }
78
- return false;
79
- }
8
+ // NOTE: width measurement is delegated to pi-tui's `visibleWidth` (see below).
9
+ // pi-tui is the renderer that HARD-ABORTS the session if any line exceeds the
10
+ // terminal width by ITS measure, so our truncate/pad/wrap MUST agree with it.
11
+ // A previous hand-maintained WIDE_RANGES table diverged from pi-tui's
12
+ // grapheme+RGI-emoji model on codepoints like ⏳ U+23F3 (hourglass: we counted
13
+ // 1, pi-tui counts 2) → lines we believed fit were rejected →
14
+ // "Rendered line N exceeds terminal width (160 > 159)" crash. Delegating makes
15
+ // divergence structurally impossible.
80
16
 
81
17
  export function visibleWidth(value: string): number {
82
- // Skip caching for very long strings to avoid memory pressure.
83
- if (value.length > 4096) {
84
- let length = 0;
85
- for (const char of value.replace(ANSI_PATTERN, "")) {
86
- if (char !== "\n") length += isWideCodePoint(char.codePointAt(0) ?? 0) ? 2 : 1;
87
- }
88
- return length;
89
- }
18
+ // Delegate to pi-tui's authoritative width model so our truncate/pad/wrap
19
+ // can never disagree with the renderer's doRender measurement. Keep our own
20
+ // LRU cache on top (pi-tui caches too, but this avoids repeated calls for the
21
+ // hot render path).
22
+ if (value.length > 4096) return tuiVisibleWidth(value);
90
23
  const cached = widthCache.get(value);
91
24
  if (cached !== undefined) return cached;
92
- let length = 0;
93
- for (const char of value.replace(ANSI_PATTERN, "")) {
94
- if (char !== "\n") length += isWideCodePoint(char.codePointAt(0) ?? 0) ? 2 : 1;
95
- }
25
+ const length = tuiVisibleWidth(value);
96
26
  if (widthCache.size >= WIDTH_CACHE_LIMIT) {
97
27
  const firstKey = widthCache.keys().next().value;
98
28
  if (firstKey !== undefined) widthCache.delete(firstKey);
@@ -132,15 +62,21 @@ export function truncateToWidth(value: string, width: number, ellipsis = "…"):
132
62
  if (width <= ellipsis.length) return ellipsis.slice(0, width);
133
63
  let output = "";
134
64
  let renderedWidth = 0;
135
- for (let i = 0; i < value.length; i++) {
65
+ for (let i = 0; i < value.length; ) {
136
66
  const ansiLen = consumeAnsi(value, i);
137
67
  if (ansiLen) {
138
68
  output += value.slice(i, i + ansiLen);
139
- i += ansiLen - 1;
69
+ i += ansiLen;
140
70
  continue;
141
71
  }
142
- const char = value[i] as string;
143
- const nextIndex = (char.codePointAt(0) ?? 0) > 0xffff ? i + 2 : i + 1;
72
+ // Read the codepoint at position i on the FULL string — NOT
73
+ // `value[i].codePointAt(0)`, which for a surrogate pair returns the
74
+ // lone half (≤ 0xFFFF) and slices each half separately. pi-tui's
75
+ // visibleWidth zero-widths lone `\p{Surrogate}` halves, so 🤖 would
76
+ // undercount as 0 and truncation would never cut (latent bug unmasked
77
+ // once visibleWidth delegated to pi-tui).
78
+ const codepoint = value.codePointAt(i) ?? 0;
79
+ const nextIndex = codepoint > 0xffff ? i + 2 : i + 1;
144
80
  const segment = value.slice(i, nextIndex);
145
81
  const charWidth = visibleWidth(segment);
146
82
  if (renderedWidth + charWidth > width - ellipsis.length) {
@@ -148,7 +84,7 @@ export function truncateToWidth(value: string, width: number, ellipsis = "…"):
148
84
  }
149
85
  output += segment;
150
86
  renderedWidth += charWidth;
151
- i = nextIndex - 1;
87
+ i = nextIndex;
152
88
  }
153
89
  return output;
154
90
  }
@@ -28,4 +28,4 @@ dependsOn: execute
28
28
  verify: true
29
29
 
30
30
  Verify completion for: {goal}
31
- Run tests ONCE (cache to .crew/cache/), read changed files from executor context. Cross-reference test output with the changes. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
31
+ Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with the changes. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
@@ -21,4 +21,4 @@ dependsOn: execute
21
21
  verify: true
22
22
 
23
23
  Verify the fix with available evidence.
24
- Run tests ONCE (cache to .crew/cache/), read changed files from executor context. Cross-reference test output with the fix. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
24
+ Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with the fix. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
@@ -27,4 +27,4 @@ dependsOn: execute
27
27
  verify: true
28
28
 
29
29
  Verify completion for: {goal}
30
- Run tests ONCE (cache to .crew/cache/), read changed files from executor context. Cross-reference test output with the changes. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
30
+ Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with the changes. Do NOT re-run tests. Give PASS or FAIL with specific test evidence.
@@ -28,4 +28,4 @@ role: verifier
28
28
  dependsOn: code-review, security-review
29
29
  verify: true
30
30
 
31
- Run the project test suite ONCE (cache to .crew/cache/), then cross-reference test results with reviewer and security-reviewer findings. Confirm each finding against real test output. Give PASS if findings match evidence, FAIL if critical findings are false positives or tests reveal new issues.
31
+ Run FAST checks ONCE (cache output to .crew/cache/): `npm run test:critical && npx tsc --noEmit` (completes in <60s). Do NOT run `npm run test:unit` or `npm test` — too slow (642 files, >4 min). Cross-reference cached output with reviewer and security-reviewer findings. Confirm each finding against real test output. Give PASS if findings match evidence, FAIL if critical findings are false positives or tests reveal new issues.
@@ -1,100 +0,0 @@
1
- /**
2
- * Auto-resume controller with settle window — prevents premature context injection
3
- * by waiting for Pi's setTimeout(0) + setTimeout(100) to drain before resuming.
4
- *
5
- * Distilled from pi-autoresearch's auto-resume / settle-window pattern.
6
- */
7
-
8
- /** Time to wait before auto-resuming (ms). Outlasts Pi's setTimeout(0) + setTimeout(100). */
9
- export const SETTLE_WINDOW_MS = 800;
10
-
11
- /** Maximum number of auto-resume turns before forcing a stop. */
12
- export const MAX_AUTORESUME_TURNS = 20;
13
-
14
- /**
15
- * Controller for scheduling auto-resume actions with a settle window.
16
- *
17
- * The settle window ensures that pending async operations (e.g., Pi's
18
- * internal setTimeout(0) and setTimeout(100) callbacks) have completed
19
- * before the resume callback fires.
20
- */
21
- export class AutoResumeController {
22
- private timerId: ReturnType<typeof setTimeout> | null = null;
23
- private turnCount = 0;
24
- private cancelled = false;
25
-
26
- /**
27
- * Get the current auto-resume turn count.
28
- */
29
- get currentTurnCount(): number {
30
- return this.turnCount;
31
- }
32
-
33
- /**
34
- * Get the maximum allowed auto-resume turns.
35
- */
36
- get maxTurns(): number {
37
- return MAX_AUTORESUME_TURNS;
38
- }
39
-
40
- /**
41
- * Schedule an auto-resume callback after the settle window elapses.
42
- *
43
- * If a resume is already pending, it is cancelled first (debounce behavior).
44
- * If the turn limit has been reached, the callback is not scheduled.
45
- *
46
- * @param message - Description of why the resume is being scheduled
47
- * @param callback - Function to call after the settle window
48
- */
49
- scheduleResume(message: string, callback: () => void): void {
50
- // Cancel any existing pending resume
51
- this.cancelResume();
52
-
53
- // Enforce turn limit
54
- if (this.turnCount >= MAX_AUTORESUME_TURNS) {
55
- return;
56
- }
57
-
58
- this.cancelled = false;
59
- this.turnCount++;
60
-
61
- this.timerId = setTimeout(() => {
62
- if (!this.cancelled) {
63
- this.timerId = null;
64
- callback();
65
- }
66
- }, SETTLE_WINDOW_MS);
67
-
68
- // Prevent the timer from keeping the process alive
69
- if (this.timerId && typeof this.timerId === "object" && "unref" in this.timerId) {
70
- this.timerId.unref();
71
- }
72
- }
73
-
74
- /**
75
- * Cancel any pending auto-resume.
76
- */
77
- cancelResume(): void {
78
- if (this.timerId !== null) {
79
- clearTimeout(this.timerId);
80
- this.timerId = null;
81
- }
82
- this.cancelled = true;
83
- }
84
-
85
- /**
86
- * Check whether an auto-resume is currently pending.
87
- */
88
- hasPendingResume(): boolean {
89
- return this.timerId !== null;
90
- }
91
-
92
- /**
93
- * Reset the turn counter. Called when a new agent starts processing
94
- * to allow a fresh set of auto-resume turns.
95
- */
96
- resetTurnCount(): void {
97
- this.turnCount = 0;
98
- this.cancelled = false;
99
- }
100
- }
@@ -1,88 +0,0 @@
1
- export interface NotebookCell {
2
- index: number;
3
- cellType: "code" | "markdown" | "raw";
4
- source: string;
5
- outputs?: unknown[];
6
- metadata?: Record<string, unknown>;
7
- }
8
-
9
- export interface Notebook {
10
- cells: NotebookCell[];
11
- metadata?: Record<string, unknown>;
12
- nbformat: number;
13
- }
14
-
15
- /** Check if a file path is a notebook */
16
- export function isNotebookPath(filePath: string): boolean {
17
- return filePath.toLowerCase().endsWith(".ipynb");
18
- }
19
-
20
- function normalizeSource(source: unknown): string {
21
- if (typeof source === "string") return source;
22
- if (Array.isArray(source)) return source.join("");
23
- return "";
24
- }
25
-
26
- /** Parse a .ipynb JSON file into a Notebook structure */
27
- export function parseNotebook(content: string): Notebook {
28
- let raw: unknown;
29
- try {
30
- raw = JSON.parse(content);
31
- } catch {
32
- return { cells: [], nbformat: 4 };
33
- }
34
- if (typeof raw !== "object" || raw === null) return { cells: [], nbformat: 4 };
35
- const obj = raw as Record<string, unknown>;
36
- const nbformat = typeof obj.nbformat === "number" ? obj.nbformat : 4;
37
- const rawCells = Array.isArray(obj.cells) ? obj.cells : [];
38
- const cells: NotebookCell[] = rawCells
39
- .map((c: unknown, i: number): NotebookCell | null => {
40
- if (typeof c !== "object" || c === null) return null;
41
- const cell = c as Record<string, unknown>;
42
- const cellType = cell.cell_type;
43
- if (cellType !== "code" && cellType !== "markdown" && cellType !== "raw") return null;
44
- return {
45
- index: i,
46
- cellType,
47
- source: normalizeSource(cell.source),
48
- outputs: Array.isArray(cell.outputs) ? cell.outputs : undefined,
49
- metadata:
50
- cell.metadata && typeof cell.metadata === "object" && !Array.isArray(cell.metadata)
51
- ? (cell.metadata as Record<string, unknown>)
52
- : undefined,
53
- };
54
- })
55
- .filter((c: NotebookCell | null): c is NotebookCell => c !== null);
56
- const metadata =
57
- obj.metadata && typeof obj.metadata === "object" && !Array.isArray(obj.metadata)
58
- ? (obj.metadata as Record<string, unknown>)
59
- : undefined;
60
- return { cells, metadata, nbformat };
61
- }
62
-
63
- /** Get a specific cell by index */
64
- export function getCell(notebook: Notebook, index: number): NotebookCell | undefined {
65
- return notebook.cells.find((c) => c.index === index);
66
- }
67
-
68
- /** Update a cell's source content, returning a new Notebook */
69
- export function updateCell(notebook: Notebook, index: number, source: string): Notebook {
70
- const cells = notebook.cells.map((c) => (c.index === index ? { ...c, source } : c));
71
- return { ...notebook, cells };
72
- }
73
-
74
- /** Serialize a Notebook back to .ipynb JSON string */
75
- export function serializeNotebook(notebook: Notebook): string {
76
- const raw = {
77
- nbformat: notebook.nbformat,
78
- nbformat_minor: 5,
79
- metadata: notebook.metadata ?? {},
80
- cells: notebook.cells.map((c) => ({
81
- cell_type: c.cellType,
82
- source: c.source,
83
- metadata: c.metadata ?? {},
84
- ...(c.cellType === "code" ? { outputs: c.outputs ?? [], execution_count: null } : {}),
85
- })),
86
- };
87
- return JSON.stringify(raw, null, 2) + "\n";
88
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Orphan sentinel — DEPRECATED, replaced by parent-guard.ts.
3
- *
4
- * Keeping this file as a no-op placeholder until all references are removed.
5
- * Workers now self-monitor via startParentGuard() instead of relying on
6
- * an external sentinel process.
7
- */