pi-crew 0.5.2 → 0.5.5

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 (80) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/docs/bugs/cross-session-notification-leakage.md +82 -0
  3. package/docs/coding-agent-optimization.md +268 -0
  4. package/docs/deep-review-report.md +384 -0
  5. package/docs/distillation/cybersecurity-patterns.md +294 -0
  6. package/docs/migration-v0.4-v0.5.md +191 -0
  7. package/docs/optimization-plan.md +642 -0
  8. package/docs/pi-mono-opportunities.md +969 -0
  9. package/docs/pi-mono-review.md +291 -0
  10. package/docs/skills/REFERENCE.md +144 -0
  11. package/package.json +7 -6
  12. package/skills/artifact-analysis-loop/SKILL.md +302 -0
  13. package/skills/async-worker-recovery/SKILL.md +19 -1
  14. package/skills/child-pi-spawning/SKILL.md +19 -6
  15. package/skills/context-artifact-hygiene/SKILL.md +19 -2
  16. package/skills/delegation-patterns/SKILL.md +68 -3
  17. package/skills/detection-pipeline-design/SKILL.md +285 -0
  18. package/skills/event-log-tracing/SKILL.md +20 -6
  19. package/skills/git-master/SKILL.md +20 -6
  20. package/skills/hunting-investigation-loop/SKILL.md +401 -0
  21. package/skills/incident-playbook-construction/SKILL.md +383 -0
  22. package/skills/live-agent-lifecycle/SKILL.md +20 -6
  23. package/skills/mailbox-interactive/SKILL.md +19 -6
  24. package/skills/model-routing-context/SKILL.md +19 -1
  25. package/skills/multi-perspective-review/SKILL.md +19 -4
  26. package/skills/observability-reliability/SKILL.md +19 -2
  27. package/skills/orchestration/SKILL.md +20 -2
  28. package/skills/ownership-session-security/SKILL.md +20 -2
  29. package/skills/pi-extension-lifecycle/SKILL.md +20 -2
  30. package/skills/post-mortem/SKILL.md +7 -2
  31. package/skills/read-only-explorer/SKILL.md +20 -6
  32. package/skills/requirements-to-task-packet/SKILL.md +23 -3
  33. package/skills/resource-discovery-config/SKILL.md +20 -2
  34. package/skills/runtime-state-reader/SKILL.md +20 -2
  35. package/skills/safe-bash/SKILL.md +21 -6
  36. package/skills/scrutinize/SKILL.md +20 -2
  37. package/skills/secure-agent-orchestration-review/SKILL.md +29 -2
  38. package/skills/security-review/SKILL.md +560 -0
  39. package/skills/state-mutation-locking/SKILL.md +22 -2
  40. package/skills/systematic-debugging/SKILL.md +8 -6
  41. package/skills/threat-hypothesis-framework/SKILL.md +175 -0
  42. package/skills/ui-render-performance/SKILL.md +20 -2
  43. package/skills/verification-before-done/SKILL.md +17 -2
  44. package/skills/widget-rendering/SKILL.md +21 -6
  45. package/skills/workspace-isolation/SKILL.md +20 -6
  46. package/skills/worktree-isolation/SKILL.md +20 -6
  47. package/src/agents/agent-config.ts +40 -1
  48. package/src/config/config.ts +22 -5
  49. package/src/config/role-tools.ts +82 -0
  50. package/src/config/types.ts +4 -0
  51. package/src/extension/crew-cleanup.ts +114 -0
  52. package/src/extension/register.ts +15 -3
  53. package/src/extension/team-tool/run.ts +7 -7
  54. package/src/observability/event-bus.ts +60 -0
  55. package/src/runtime/background-runner.ts +8 -2
  56. package/src/runtime/child-pi.ts +122 -34
  57. package/src/runtime/crew-agent-runtime.ts +1 -0
  58. package/src/runtime/foreground-control.ts +87 -17
  59. package/src/runtime/pi-args.ts +11 -1
  60. package/src/runtime/pi-json-output.ts +31 -0
  61. package/src/runtime/progress-tracker.ts +124 -0
  62. package/src/runtime/skill-effectiveness.ts +473 -0
  63. package/src/runtime/skill-instructions.ts +37 -3
  64. package/src/runtime/task-runner.ts +91 -17
  65. package/src/runtime/team-runner.ts +11 -11
  66. package/src/runtime/tool-progress.ts +10 -3
  67. package/src/runtime/verification-gates.ts +367 -0
  68. package/src/schema/team-tool-schema.ts +7 -0
  69. package/src/state/decision-ledger.ts +92 -43
  70. package/src/state/event-log.ts +136 -10
  71. package/src/state/hook-instinct-bridge.ts +5 -5
  72. package/src/state/state-store.ts +3 -1
  73. package/src/state/types.ts +4 -0
  74. package/src/types/new-api-types.ts +34 -0
  75. package/src/ui/agent-management-overlay.ts +5 -1
  76. package/src/ui/crew-widget.ts +29 -15
  77. package/src/ui/powerbar-publisher.ts +100 -7
  78. package/src/ui/tool-render.ts +15 -15
  79. package/src/utils/session-utils.ts +52 -0
  80. package/src/worktree/worktree-manager.ts +32 -13
@@ -90,12 +90,12 @@ export function renderTeamToolCall(
90
90
  if (!context.expanded) {
91
91
  const preview = goal.length > 60 ? goal.slice(0, 60) + "…" : goal;
92
92
  return new Text(
93
- `${theme.fg("toolTitle", theme.bold("team"))} action=${theme.fg("accent", `'${action}'`)}${team}${theme.fg("dim", preview ? ` "${preview.replace(/\n/g, " ")}"` : "")}`,
93
+ `${theme.fg("toolTitle", theme.bold("team"))} action=${theme.fg("accent", `'${action}'`)}${team}${theme.fg("dim", preview ? ` "${preview.replace(/\n/g, " ")}"` : "")}`,
94
94
  0, 0,
95
95
  );
96
96
  }
97
97
  const c = context.lastComponent instanceof Container ? (context.lastComponent.clear(), context.lastComponent) : new Container();
98
- c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("team"))} action=${theme.fg("accent", `'${action}'`)}${team}`, 0, 0));
98
+ c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("team"))} action=${theme.fg("accent", `'${action}'`)}${team}`, 0, 0));
99
99
  if (goal) { c.addChild(new Spacer(1)); c.addChild(new Text(theme.fg("text", goal), 0, 0)); }
100
100
  return c;
101
101
  }
@@ -110,13 +110,13 @@ export function renderAgentToolCall(
110
110
  if (!context.expanded) {
111
111
  const preview = prompt.length > 60 ? prompt.slice(0, 60) + "…" : prompt;
112
112
  return new Text(
113
- `${theme.fg("toolTitle", theme.bold("agent"))} ${theme.fg("accent", agentName)}${theme.fg("dim", preview ? ` "${preview.replace(/\n/g, " ")}"` : "")}`,
113
+ `${theme.fg("toolTitle", theme.bold("agent"))} ${theme.fg("accent", agentName)}${theme.fg("dim", preview ? ` "${preview.replace(/\n/g, " ")}"` : "")}`,
114
114
  0, 0,
115
115
  );
116
116
  }
117
117
  const c = context.lastComponent instanceof Container ? (context.lastComponent.clear(), context.lastComponent) : new Container();
118
- const cwdLabel = args.cwd ? theme.fg("dim", ` (cwd: ${args.cwd})`) : "";
119
- c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("agent"))} ${theme.fg("accent", agentName)}${cwdLabel}`, 0, 0));
118
+ const cwdLabel = args.cwd ? theme.fg("dim", ` (cwd: ${args.cwd})`) : "";
119
+ c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("agent"))} ${theme.fg("accent", agentName)}${cwdLabel}`, 0, 0));
120
120
  if (prompt) { c.addChild(new Spacer(1)); c.addChild(new Text(theme.fg("text", prompt), 0, 0)); }
121
121
  return c;
122
122
  }
@@ -153,21 +153,21 @@ export function renderAgentProgress(
153
153
  const stats = `${prog?.toolCount ?? record.toolUses ?? 0} tools · ${formatDuration(durationMs)}`;
154
154
  const modelStr = record.model ? ` (${record.model})` : "";
155
155
  const roleLabel = record.role || record.agent || "agent";
156
- addLine(`${icon} ${theme.fg("toolTitle", theme.bold(roleLabel))}${theme.fg("dim", modelStr)} ${theme.fg("dim", stats)}`);
156
+ addLine(`${icon} ${theme.fg("toolTitle", theme.bold(roleLabel))}${theme.fg("dim", modelStr)} ${theme.fg("dim", stats)}`);
157
157
 
158
158
  // Current tool (running)
159
159
  if (isRunning && prog?.currentTool) {
160
160
  const toolLabel = formatToolPreview(prog.currentTool, parseArgs(prog.currentToolArgs));
161
- addLine(theme.fg("warning", `▸ ${prog.currentTool}: ${toolLabel}`));
161
+ addLine(theme.fg("warning", `▸ ${prog.currentTool}: ${toolLabel}`));
162
162
  }
163
163
 
164
164
  // Recent tools log
165
165
  if (prog?.recentTools?.length) {
166
166
  for (const tool of prog.recentTools) {
167
- const detail = tool.args ? `: ${tool.args}` : "";
167
+ const detail = tool.args ? `: ${tool.args}` : "";
168
168
  const line = tool.endedAt
169
169
  ? theme.fg("muted", ` ${tool.tool}${detail}`)
170
- : theme.fg("warning", `▸ ${tool.tool}${detail}`);
170
+ : theme.fg("warning", `▸ ${tool.tool}${detail}`);
171
171
  addLine(line);
172
172
  }
173
173
  }
@@ -231,7 +231,7 @@ export function renderTeamToolResult(
231
231
  if ((result as any).runId) parts.push(`runId=${(result as any).runId}`);
232
232
  if ((result as any).error) parts.push(theme.fg("error", `error`));
233
233
  if ((result as any).goal && parts.length === 0) parts.push(theme.fg("dim", truncLine((result as any).goal, 116)));
234
- return new Text(parts.join(" "), 0, 0);
234
+ return new Text(parts.join(" · "), 0, 0);
235
235
  }
236
236
  // No details found, fall back to content
237
237
  const text = extractText(result?.content).slice(0, 200);
@@ -253,7 +253,7 @@ export function renderTeamToolResult(
253
253
  if (d.error) parts.push(theme.fg("error", `error=${d.error}`));
254
254
  if (d.goal) parts.push(theme.fg("dim", truncLine(d.goal, 116)));
255
255
  if (parts.length === 0) return new Text(theme.fg("muted", "(no output)"), 0, 0);
256
- return new Text(parts.join(" · "), 0, 0);
256
+ return new Text(parts.join(" · "), 0, 0);
257
257
  }
258
258
 
259
259
  /** agent tool result: shows agent output rows with status icons */
@@ -275,9 +275,9 @@ export function renderAgentToolResult(
275
275
  : item.status === "running" ? theme.fg("warning", "⟳")
276
276
  : theme.fg("dim", "○");
277
277
  const label = item.agentId || "agent";
278
- c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))}`, 0, 0));
278
+ c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))}`, 0, 0));
279
279
  if (item.error) {
280
- c.addChild(new Text(theme.fg("error", ` Error: ${item.error}`), 0, 0));
280
+ c.addChild(new Text(theme.fg("error", ` Error: ${item.error}`), 0, 0));
281
281
  } else if (item.output) {
282
282
  for (const line of item.output.split("\n").slice(0, 5))
283
283
  c.addChild(new Text(theme.fg("dim", ` ${truncLine(line, w - 2)}`), 0, 0));
@@ -293,9 +293,9 @@ export function renderAgentToolResult(
293
293
  : d.status === "running" ? theme.fg("warning", "⟳")
294
294
  : theme.fg("dim", "○");
295
295
  const label = d.agentId;
296
- c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))}`, 0, 0));
296
+ c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(label))}`, 0, 0));
297
297
  if (d.error) {
298
- c.addChild(new Text(theme.fg("error", ` Error: ${d.error}`), 0, 0));
298
+ c.addChild(new Text(theme.fg("error", ` Error: ${d.error}`), 0, 0));
299
299
  } else if (d.output) {
300
300
  for (const line of d.output.split("\n").slice(0, 5))
301
301
  c.addChild(new Text(theme.fg("dim", ` ${truncLine(line, w - 2)}`), 0, 0));
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Session ID utilities for pi-crew / pi session alignment.
3
+ *
4
+ * pi's session IDs use the format:
5
+ * ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
6
+ *
7
+ * This module provides utilities to generate valid pi session IDs
8
+ * that align with pi-crew run IDs for easy cross-referencing.
9
+ */
10
+
11
+ /**
12
+ * Validate session ID format per pi's requirements.
13
+ * Format: ^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$
14
+ */
15
+ export function assertValidSessionId(id: string): void {
16
+ if (!id || !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
17
+ throw new Error(
18
+ `Invalid session id: must be non-empty, alphanumeric with '-', '_', '.' and start/end with alphanumeric`,
19
+ );
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Convert a pi-crew run ID to a valid pi session ID.
25
+ *
26
+ * - Strips non-alphanumeric characters
27
+ * - Lowercases
28
+ * - Prefixes with "crew-"
29
+ * - Truncates to 16 chars for safety
30
+ *
31
+ * @param runId - The pi-crew run ID (e.g., "team_20260528133725_02e05cc5480d0175")
32
+ * @returns Valid pi session ID (e.g., "crew-team20260528133")
33
+ */
34
+ export function toPiSessionId(runId: string): string {
35
+ // Strip non-alphanumeric, lowercase, prefix with "crew-"
36
+ const sanitized = runId.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
37
+ return `crew-${sanitized.slice(0, 16)}`;
38
+ }
39
+
40
+ /**
41
+ * Validate and convert a run ID to a pi session ID.
42
+ * Returns the session ID if valid, or undefined if conversion would produce invalid ID.
43
+ */
44
+ export function safeToPiSessionId(runId: string): string | undefined {
45
+ try {
46
+ const sessionId = toPiSessionId(runId);
47
+ assertValidSessionId(sessionId);
48
+ return sessionId;
49
+ } catch {
50
+ return undefined;
51
+ }
52
+ }
@@ -128,19 +128,38 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
128
128
  return [];
129
129
  }
130
130
  const nodeHook = hookPath.endsWith(".js") || hookPath.endsWith(".cjs") || hookPath.endsWith(".mjs");
131
- // On Windows, set shell:true to ensure PATH resolution and .cjs/.bat file associations work.
132
- const useShell = process.platform === "win32";
133
- const result = spawnSync(nodeHook ? process.execPath : hookPath, nodeHook ? [hookPath] : [], {
134
- cwd: worktreePath,
135
- encoding: "utf-8",
136
- input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
137
- timeout: cfg.setupHookTimeoutMs ?? 30_000,
138
- shell: useShell,
139
- env: sanitizeEnvSecrets(process.env, {
140
- allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "PI_*"],
141
- }),
142
- windowsHide: true,
143
- });
131
+ // For .bat/.cmd files on Windows, execute via cmd.exe /c directly
132
+ const isBatchFile = hookPath.endsWith(".bat") || hookPath.endsWith(".cmd");
133
+ // SECURITY: Never use shell:true prevents command injection from untrusted hooks.
134
+ // Non-node, non-batch hooks on Windows will fail to execute rather than
135
+ // running through a shell that could interpret malicious filenames.
136
+ const useShell = false;
137
+ if (process.platform === "win32" && !nodeHook && !isBatchFile) {
138
+ logInternalError("worktree.setupHook.windowsNoShell", new Error("Non-node, non-batch hook skipped on Windows (shell:true disabled for security)"), `hook=${hookPath}`);
139
+ }
140
+ const result = isBatchFile
141
+ ? spawnSync("cmd.exe", ["/c", hookPath], {
142
+ cwd: worktreePath,
143
+ encoding: "utf-8",
144
+ input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
145
+ timeout: cfg.setupHookTimeoutMs ?? 30_000,
146
+ shell: false, // cmd.exe /c handles batch files safely
147
+ env: sanitizeEnvSecrets(process.env, {
148
+ allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "PI_*"],
149
+ }),
150
+ windowsHide: true,
151
+ })
152
+ : spawnSync(nodeHook ? process.execPath : hookPath, nodeHook ? [hookPath] : [], {
153
+ cwd: worktreePath,
154
+ encoding: "utf-8",
155
+ input: JSON.stringify({ version: 1, repoRoot, worktreePath, agentCwd: worktreePath, branch, runId: manifest.runId, taskId: task.id, agent: task.agent }),
156
+ timeout: cfg.setupHookTimeoutMs ?? 30_000,
157
+ shell: useShell,
158
+ env: sanitizeEnvSecrets(process.env, {
159
+ allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL", "PI_*"],
160
+ }),
161
+ windowsHide: true,
162
+ });
144
163
  if (result.error) throw new Error(`worktree setup hook failed: ${result.error.message}`);
145
164
  if (result.status !== 0) throw new Error(`worktree setup hook failed with exit code ${result.status}: ${result.stderr || result.stdout || "no output"}`);
146
165
  const trimmed = result.stdout.trim();