pi-crew 0.6.1 → 0.6.4

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 (119) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +81 -33
  3. package/docs/issue-29-analysis.md +189 -0
  4. package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +1268 -0
  5. package/docs/ui-optimization-plan.md +447 -0
  6. package/package.json +2 -2
  7. package/src/config/config.ts +106 -15
  8. package/src/errors.ts +107 -0
  9. package/src/extension/async-notifier.ts +6 -2
  10. package/src/extension/crew-cleanup.ts +8 -5
  11. package/src/extension/management.ts +464 -109
  12. package/src/extension/register.ts +213 -35
  13. package/src/extension/registration/brief-tool-overrides.ts +400 -0
  14. package/src/extension/registration/commands.ts +27 -2
  15. package/src/extension/registration/subagent-helpers.ts +2 -2
  16. package/src/extension/registration/subagent-tools.ts +9 -4
  17. package/src/extension/registration/team-tool.ts +17 -10
  18. package/src/extension/registration/viewers.ts +2 -2
  19. package/src/extension/team-tool/api.ts +3 -3
  20. package/src/extension/team-tool/cancel.ts +3 -3
  21. package/src/extension/team-tool/explain.ts +1 -1
  22. package/src/extension/team-tool/handle-schedule.ts +40 -0
  23. package/src/extension/team-tool/inspect.ts +3 -3
  24. package/src/extension/team-tool/lifecycle-actions.ts +4 -4
  25. package/src/extension/team-tool/respond.ts +2 -2
  26. package/src/extension/team-tool/run.ts +64 -14
  27. package/src/extension/team-tool/status.ts +1 -1
  28. package/src/extension/team-tool-types.ts +2 -0
  29. package/src/extension/team-tool.ts +173 -46
  30. package/src/hooks/registry.ts +77 -13
  31. package/src/hooks/types.ts +9 -0
  32. package/src/plugins/plugin-define.ts +6 -0
  33. package/src/plugins/plugin-registry.ts +32 -0
  34. package/src/plugins/plugins/index.ts +3 -0
  35. package/src/plugins/plugins/nextjs.ts +19 -0
  36. package/src/plugins/plugins/vite.ts +14 -0
  37. package/src/plugins/plugins/vitest.ts +14 -0
  38. package/src/runtime/async-runner.ts +35 -7
  39. package/src/runtime/background-runner.ts +529 -144
  40. package/src/runtime/chain-parser.ts +5 -1
  41. package/src/runtime/chain-runner.ts +9 -3
  42. package/src/runtime/checkpoint.ts +262 -180
  43. package/src/runtime/child-pi.ts +164 -37
  44. package/src/runtime/crash-recovery.ts +25 -14
  45. package/src/runtime/crew-agent-records.ts +2 -0
  46. package/src/runtime/diagnostic-export.ts +1 -1
  47. package/src/runtime/dynamic-script-runner.ts +10 -7
  48. package/src/runtime/foreground-watchdog.ts +1 -1
  49. package/src/runtime/heartbeat-watcher.ts +19 -2
  50. package/src/runtime/intercom-bridge.ts +7 -0
  51. package/src/runtime/iteration-hooks.ts +1 -1
  52. package/src/runtime/manifest-cache.ts +4 -0
  53. package/src/runtime/orphan-worker-registry.ts +444 -0
  54. package/src/runtime/parent-guard.ts +70 -16
  55. package/src/runtime/pi-args.ts +437 -14
  56. package/src/runtime/pi-spawn.ts +1 -0
  57. package/src/runtime/post-checks.ts +1 -1
  58. package/src/runtime/retry-runner.ts +9 -3
  59. package/src/runtime/run-tracker.ts +38 -10
  60. package/src/runtime/sandbox.ts +17 -2
  61. package/src/runtime/scheduler.ts +25 -2
  62. package/src/runtime/skill-effectiveness.ts +105 -62
  63. package/src/runtime/skill-instructions.ts +110 -32
  64. package/src/runtime/stale-reconciler.ts +310 -69
  65. package/src/runtime/subagent-manager.ts +251 -57
  66. package/src/runtime/task-health.ts +76 -0
  67. package/src/runtime/task-id.ts +20 -0
  68. package/src/runtime/task-runner/live-executor.ts +1 -1
  69. package/src/runtime/task-runner/progress.ts +1 -1
  70. package/src/runtime/task-runner/state-helpers.ts +110 -6
  71. package/src/runtime/task-runner.ts +92 -70
  72. package/src/runtime/team-runner.ts +186 -20
  73. package/src/schema/team-tool-schema.ts +27 -9
  74. package/src/skills/discover-skills.ts +9 -3
  75. package/src/state/active-run-registry.ts +170 -38
  76. package/src/state/artifact-store.ts +25 -13
  77. package/src/state/atomic-write-v2.ts +86 -0
  78. package/src/state/atomic-write.ts +346 -55
  79. package/src/state/blob-store.ts +178 -10
  80. package/src/state/crew-init.ts +161 -28
  81. package/src/state/decision-ledger.ts +172 -111
  82. package/src/state/event-log-rotation.ts +82 -52
  83. package/src/state/event-log.ts +254 -70
  84. package/src/state/health-store.ts +71 -0
  85. package/src/state/locks.ts +102 -20
  86. package/src/state/mailbox.ts +45 -7
  87. package/src/state/observation-store.ts +4 -1
  88. package/src/state/run-graph.ts +141 -130
  89. package/src/state/run-metrics.ts +24 -8
  90. package/src/state/session-state-map.ts +51 -0
  91. package/src/state/state-store.ts +330 -43
  92. package/src/ui/live-run-sidebar.ts +1 -1
  93. package/src/ui/loaders.ts +4 -0
  94. package/src/ui/overlays/agent-picker-overlay.ts +1 -1
  95. package/src/ui/overlays/mailbox-detail-overlay.ts +1 -1
  96. package/src/ui/powerbar-publisher.ts +1 -1
  97. package/src/ui/run-action-dispatcher.ts +2 -2
  98. package/src/ui/run-snapshot-cache.ts +1 -1
  99. package/src/ui/status-colors.ts +5 -1
  100. package/src/ui/theme-adapter.ts +80 -1
  101. package/src/ui/tool-progress-formatter.ts +9 -5
  102. package/src/ui/tool-render.ts +4 -0
  103. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  104. package/src/ui/tool-renderers/index.ts +627 -0
  105. package/src/ui/widget/index.ts +224 -0
  106. package/src/ui/widget/widget-formatters.ts +148 -0
  107. package/src/ui/widget/widget-model.ts +90 -0
  108. package/src/ui/widget/widget-renderer.ts +130 -0
  109. package/src/ui/widget/widget-types.ts +37 -0
  110. package/src/utils/env-filter.ts +66 -0
  111. package/src/utils/file-coalescer.ts +9 -1
  112. package/src/utils/guards.ts +110 -0
  113. package/src/utils/paths.ts +80 -5
  114. package/src/utils/redaction.ts +6 -1
  115. package/src/utils/safe-paths.ts +281 -10
  116. package/src/worktree/cleanup.ts +112 -24
  117. package/src/worktree/worktree-manager.ts +128 -15
  118. package/test-bugs-all.mjs +10 -6
  119. package/test-integration-check.ts +4 -0
@@ -3,15 +3,24 @@ import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import type { AgentConfig } from "../agents/agent-config.ts";
5
5
  import type { TeamRole } from "../teams/team-config.ts";
6
+ import {
7
+ isSafePathId,
8
+ resolveContainedPath,
9
+ resolveRealContainedPath,
10
+ } from "../utils/safe-paths.ts";
6
11
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
7
- import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
8
12
  import {
13
+ CONFIDENCE_THRESHOLDS,
9
14
  getWeightedSkillsForRole,
10
15
  registerSkillEffectivenessHooks,
11
- CONFIDENCE_THRESHOLDS,
12
16
  } from "./skill-effectiveness.ts";
13
17
 
14
- const PACKAGE_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
18
+ const PACKAGE_SKILLS_DIR = path.resolve(
19
+ path.dirname(fileURLToPath(import.meta.url)),
20
+ "..",
21
+ "..",
22
+ "skills",
23
+ );
15
24
  const MAX_SKILL_CHARS = 1500;
16
25
  const MAX_TOTAL_CHARS = 6000;
17
26
  const MAX_SKILL_NAME_CHARS = 80;
@@ -23,7 +32,11 @@ const DEFAULT_ROLE_SKILLS: Record<string, string[]> = {
23
32
  analyst: ["read-only-explorer", "requirements-to-task-packet"],
24
33
  planner: ["delegation-patterns", "requirements-to-task-packet"],
25
34
  critic: ["read-only-explorer", "multi-perspective-review"],
26
- executor: ["state-mutation-locking", "safe-bash", "verification-before-done"],
35
+ executor: [
36
+ "state-mutation-locking",
37
+ "safe-bash",
38
+ "verification-before-done",
39
+ ],
27
40
  reviewer: ["read-only-explorer", "multi-perspective-review"],
28
41
  // SECURITY NOTE: The following skill names are trusted package-level skills.
29
42
  // If a project has a skills/ directory containing subdirectories with these names,
@@ -31,7 +44,10 @@ const DEFAULT_ROLE_SKILLS: Record<string, string[]> = {
31
44
  // project dir before package dir) and their content injected verbatim into prompts.
32
45
  // The "Applicable Skills" block will add an untrusted-content warning for project skills,
33
46
  // but be aware this is a potential supply-chain risk in multi-contributor projects.
34
- "security-reviewer": ["secure-agent-orchestration-review", "ownership-session-security"],
47
+ "security-reviewer": [
48
+ "secure-agent-orchestration-review",
49
+ "ownership-session-security",
50
+ ],
35
51
  "test-engineer": ["verification-before-done", "safe-bash"],
36
52
  verifier: ["verification-before-done", "runtime-state-reader"],
37
53
  writer: ["context-artifact-hygiene", "verify-evidence"],
@@ -50,11 +66,18 @@ export interface RenderSkillInstructionsInput extends ResolveTaskSkillsInput {
50
66
  }
51
67
 
52
68
  function isValidSkillName(name: string): boolean {
53
- return name.length > 0 && name.length <= MAX_SKILL_NAME_CHARS && isSafePathId(name);
69
+ return (
70
+ name.length > 0 &&
71
+ name.length <= MAX_SKILL_NAME_CHARS &&
72
+ isSafePathId(name)
73
+ );
54
74
  }
55
75
 
56
76
  function sanitizeSkillName(name: string): string {
57
- return name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_SKILL_NAME_CHARS) || "invalid";
77
+ return (
78
+ name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_SKILL_NAME_CHARS) ||
79
+ "invalid"
80
+ );
58
81
  }
59
82
 
60
83
  function unique(items: string[]): string[] {
@@ -69,11 +92,18 @@ function unique(items: string[]): string[] {
69
92
  return result;
70
93
  }
71
94
 
72
- export function normalizeSkillOverride(value: string | string[] | boolean | undefined): string[] | false | undefined {
95
+ export function normalizeSkillOverride(
96
+ value: string | string[] | boolean | undefined,
97
+ ): string[] | false | undefined {
73
98
  if (value === false) return false;
74
- if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
99
+ if (typeof value === "string")
100
+ return value
101
+ .split(",")
102
+ .map((entry) => entry.trim())
103
+ .filter(Boolean);
75
104
  if (value === true) return undefined;
76
- if (Array.isArray(value)) return value.map((entry) => entry.trim()).filter(Boolean);
105
+ if (Array.isArray(value))
106
+ return value.map((entry) => entry.trim()).filter(Boolean);
77
107
  return undefined;
78
108
  }
79
109
 
@@ -81,13 +111,17 @@ export function defaultSkillsForRole(role: string): string[] {
81
111
  return DEFAULT_ROLE_SKILLS[role] ?? [];
82
112
  }
83
113
 
84
- function collectTaskSkillNames(input: ResolveTaskSkillsInput | undefined): string[] {
114
+ function collectTaskSkillNames(
115
+ input: ResolveTaskSkillsInput | undefined,
116
+ ): string[] {
85
117
  if (!input) return [];
86
118
  if (input.override === false) return [];
87
- const roleDefaultsDisabled = input.teamRole?.skills === false || input.step?.skills === false;
119
+ const roleDefaultsDisabled =
120
+ input.teamRole?.skills === false || input.step?.skills === false;
88
121
  const names = roleDefaultsDisabled ? [] : defaultSkillsForRole(input.role);
89
122
  if (input.agent?.skills?.length) names.push(...input.agent.skills);
90
- if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
123
+ if (Array.isArray(input.teamRole?.skills))
124
+ names.push(...input.teamRole.skills);
91
125
  if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
92
126
  if (Array.isArray(input.override)) names.push(...input.override);
93
127
  return unique(names);
@@ -103,10 +137,12 @@ export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
103
137
  // See: SECURITY-ISSUES.md SEC-003
104
138
  // ═══════════════════════════════════════════════════════════════════════════
105
139
 
106
- function candidateSkillDirs(cwd: string): Array<{ root: string; source: "project" | "package" }> {
140
+ function candidateSkillDirs(
141
+ cwd: string,
142
+ ): Array<{ root: string; source: "project" | "package" }> {
107
143
  return [
108
- { root: PACKAGE_SKILLS_DIR, source: "package" }, // ✓ Trusted first
109
- { root: path.resolve(cwd, "skills"), source: "project" }, // ⚠️ Override second
144
+ { root: PACKAGE_SKILLS_DIR, source: "package" }, // ✓ Trusted first
145
+ { root: path.resolve(cwd, "skills"), source: "project" }, // ⚠️ Override second
110
146
  ];
111
147
  }
112
148
 
@@ -120,7 +156,10 @@ interface CachedSkillMarkdown {
120
156
 
121
157
  const skillReadCache = new Map<string, CachedSkillMarkdown>();
122
158
 
123
- function rememberSkill(key: string, value: CachedSkillMarkdown): CachedSkillMarkdown {
159
+ function rememberSkill(
160
+ key: string,
161
+ value: CachedSkillMarkdown,
162
+ ): CachedSkillMarkdown {
124
163
  if (skillReadCache.has(key)) skillReadCache.delete(key);
125
164
  skillReadCache.set(key, value);
126
165
  while (skillReadCache.size > SKILL_CACHE_MAX_ENTRIES) {
@@ -144,7 +183,12 @@ function cachedSkillFresh(value: CachedSkillMarkdown): boolean {
144
183
  }
145
184
  }
146
185
 
147
- function readSkillMarkdown(cwd: string, name: string): { path: string; source: "project" | "package"; content: string } | undefined {
186
+ function readSkillMarkdown(
187
+ cwd: string,
188
+ name: string,
189
+ ):
190
+ | { path: string; source: "project" | "package"; content: string }
191
+ | undefined {
148
192
  if (!isValidSkillName(name)) return undefined;
149
193
  const cacheKey = `${path.resolve(cwd)}:${name}`;
150
194
  const cached = skillReadCache.get(cacheKey);
@@ -158,9 +202,14 @@ function readSkillMarkdown(cwd: string, name: string): { path: string; source: "
158
202
  if (fs.lstatSync(contained).isSymbolicLink()) continue;
159
203
  const filePath = resolveRealContainedPath(entry.root, relative);
160
204
  const stat = fs.statSync(filePath);
161
- return rememberSkill(cacheKey, { path: filePath, source: entry.source, content: fs.readFileSync(filePath, "utf-8"), mtimeMs: stat.mtimeMs, size: stat.size });
162
- } catch {
163
- }
205
+ return rememberSkill(cacheKey, {
206
+ path: filePath,
207
+ source: entry.source,
208
+ content: fs.readFileSync(filePath, "utf-8"),
209
+ mtimeMs: stat.mtimeMs,
210
+ size: stat.size,
211
+ });
212
+ } catch {}
164
213
  }
165
214
  return undefined;
166
215
  }
@@ -168,7 +217,9 @@ function readSkillMarkdown(cwd: string, name: string): { path: string; source: "
168
217
  function frontmatterDescription(content: string): string | undefined {
169
218
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
170
219
  if (!match) return undefined;
171
- const line = match[1].split(/\r?\n/).find((entry) => entry.startsWith("description:"));
220
+ const line = match[1]
221
+ .split(/\r?\n/)
222
+ .find((entry) => entry.startsWith("description:"));
172
223
  return line?.slice("description:".length).trim();
173
224
  }
174
225
 
@@ -179,8 +230,12 @@ function stripFrontmatter(content: string): string {
179
230
  function compactSkillContent(content: string): string {
180
231
  const body = stripFrontmatter(content);
181
232
  if (body.length <= MAX_SKILL_CHARS) return body;
182
- const preferred = body.split(/\r?\n## Verification\r?\n/)[0]?.trim() ?? body;
183
- const truncated = preferred.length > MAX_SKILL_CHARS ? preferred.slice(0, MAX_SKILL_CHARS - 40).trimEnd() : preferred;
233
+ const preferred =
234
+ body.split(/\r?\n## Verification\r?\n/)[0]?.trim() ?? body;
235
+ const truncated =
236
+ preferred.length > MAX_SKILL_CHARS
237
+ ? preferred.slice(0, MAX_SKILL_CHARS - 40).trimEnd()
238
+ : preferred;
184
239
  return `${truncated}\n\n[skill instructions truncated]`;
185
240
  }
186
241
 
@@ -197,7 +252,11 @@ export interface RenderedSkillInstructions {
197
252
  }>;
198
253
  }
199
254
 
200
- export function renderSkillInstructions(input: RenderSkillInstructionsInput & { runId?: string } = {} as RenderSkillInstructionsInput & { runId?: string }): RenderedSkillInstructions {
255
+ export function renderSkillInstructions(
256
+ input: RenderSkillInstructionsInput & {
257
+ runId?: string;
258
+ } = {} as RenderSkillInstructionsInput & { runId?: string },
259
+ ): RenderedSkillInstructions {
201
260
  const allNames = collectTaskSkillNames(input);
202
261
  const names = allNames.slice(0, MAX_SELECTED_SKILLS);
203
262
  const overflowCount = Math.max(0, allNames.length - names.length);
@@ -208,12 +267,18 @@ export function renderSkillInstructions(input: RenderSkillInstructionsInput & {
208
267
  let omittedCount = overflowCount;
209
268
 
210
269
  // ECC INSTINCT: Get confidence-weighted skills if runId is provided
211
- let weightedSkills: RenderedSkillInstructions["weightedSkills"] = undefined;
270
+ let weightedSkills: RenderedSkillInstructions["weightedSkills"];
212
271
  if (input.runId) {
213
272
  // Register effectiveness hooks once per process
214
273
  registerSkillEffectivenessHooks();
215
- const weighted = getWeightedSkillsForRole(input.role, names, input.runId, CONFIDENCE_THRESHOLDS.TENTATIVE);
216
- weightedSkills = weighted.map(w => ({
274
+ const weighted = getWeightedSkillsForRole(
275
+ input.cwd,
276
+ input.role,
277
+ names,
278
+ input.runId,
279
+ CONFIDENCE_THRESHOLDS.TENTATIVE,
280
+ );
281
+ weightedSkills = weighted.map((w) => ({
217
282
  skillId: w.skillId,
218
283
  confidence: w.confidence,
219
284
  behavior: w.behavior,
@@ -237,13 +302,26 @@ export function renderSkillInstructions(input: RenderSkillInstructionsInput & {
237
302
  }
238
303
  skillPaths.push(path.dirname(loaded.path));
239
304
  const description = frontmatterDescription(loaded.content);
240
- const source = loaded.source === "project" ? `project:skills/${safeName}` : `package:skills/${safeName}`;
305
+ const source =
306
+ loaded.source === "project"
307
+ ? `project:skills/${safeName}`
308
+ : `package:skills/${safeName}`;
241
309
 
242
310
  // ECC INSTINCT: Add confidence annotation from weighted skills
243
- const weighted = weightedSkills?.find(w => w.skillId === name);
244
- const confidenceNote = weighted ? ` [Confidence: ${(weighted.confidence * 100).toFixed(0)}% — ${weighted.threshold}]` : "";
311
+ const weighted = weightedSkills?.find((w) => w.skillId === name);
312
+ const confidenceNote = weighted
313
+ ? ` [Confidence: ${(weighted.confidence * 100).toFixed(0)}% — ${weighted.threshold}]`
314
+ : "";
245
315
 
246
- const header = [`## ${safeName}`, description ? `Description: ${description}${confidenceNote}` : undefined, `Source: ${source}`].filter(Boolean).join("\n");
316
+ const header = [
317
+ `## ${safeName}`,
318
+ description
319
+ ? `Description: ${description}${confidenceNote}`
320
+ : undefined,
321
+ `Source: ${source}`,
322
+ ]
323
+ .filter(Boolean)
324
+ .join("\n");
247
325
  const rawContent = compactSkillContent(loaded.content);
248
326
  // Wrap skill content with provenance markers to help LLMs distinguish skill instructions
249
327
  const wrappedContent = `<!-- skill: ${safeName} -->\n${rawContent}\n<!-- end-skill: ${safeName} -->`;