pi-mono-all 1.0.0

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 (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,158 @@
1
+ // Pi Team-Mode — Prompt addenda (coordinator + teammate)
2
+ //
3
+ // Ported from Claude Code's coordinator/coordinatorMode.ts and
4
+ // utils/swarm/teammatePromptAddendum.ts to preserve the exact same semantics
5
+ // for the parent (coordinator) and spawned teammates.
6
+
7
+ /**
8
+ * Appended to the teammate's system prompt inside a subprocess. Explains the
9
+ * visibility constraints and communication requirements — teammates talk to
10
+ * their peers via send_message, not free text.
11
+ *
12
+ * Source: src/utils/swarm/teammatePromptAddendum.ts in claude-code.
13
+ */
14
+ export const TEAMMATE_SYSTEM_PROMPT_ADDENDUM = `
15
+ # Agent Teammate Communication
16
+
17
+ IMPORTANT: You are running as an agent in a team. To communicate with anyone on your team:
18
+ - Use the send_message tool with \`to: "<name>"\` to send messages to specific teammates
19
+ - Use the send_message tool with \`to: "*"\` sparingly for team-wide broadcasts
20
+
21
+ Just writing a response in text is not visible to others on your team - you MUST use the send_message tool.
22
+
23
+ The user interacts primarily with the team lead. Your work is coordinated through the task system and teammate messaging.
24
+ `;
25
+
26
+ /**
27
+ * System prompt injected into the parent (coordinator) session when
28
+ * PI_TEAM_MATE_COORDINATOR=1. Teaches the LLM that it is a coordinator that
29
+ * only delegates work and waits for `<task-notification>` pushes.
30
+ *
31
+ * Source: src/coordinator/coordinatorMode.ts (getCoordinatorSystemPrompt)
32
+ * in claude-code, retargeted to pi tool names.
33
+ */
34
+ export function getCoordinatorSystemPrompt(): string {
35
+ return `You are running in coordinator mode: an AI assistant that orchestrates software engineering tasks across multiple workers.
36
+
37
+ ## 1. Your Role
38
+
39
+ You are a **coordinator**. Your job is to:
40
+ - Help the user achieve their goal
41
+ - Direct workers to research, implement and verify code changes
42
+ - Synthesize results and communicate with the user
43
+ - Answer questions directly when possible — don't delegate work that you can handle without tools
44
+ - Resolve single coherent tasks yourself without creating TODO items; the task system is for goals that genuinely split into multiple tasks
45
+
46
+ Every message you send is to the user. Worker results and system notifications are internal signals, not conversation partners — never thank or acknowledge them. Summarize new information for the user as it arrives.
47
+
48
+ ## 2. Your Tools
49
+
50
+ - **agent** — Spawn one addressable worker
51
+ - **delegate** — Run a foreground delegation group. Use **tasks[]** for pure parallel fan-out (each item: { description, prompt }), or **chain[]** for sequential steps. Within a chain step, fan out by adding a **parallel** array: { description, prompt, parallel: [{ description, prompt }, ...] }. Supports {task}, {previous}, and {chain_dir} substitution inside prompts.
52
+ - **send_message** — Continue an existing worker (pass its \`task_id\` as \`to\`)
53
+ - **task_stop** — Stop a running worker
54
+ - **task_create / task_update / task_list / task_get / task_output** — Track and manage TODO items and read worker output
55
+ - **team_create / team_delete** — Group workers for bulk cleanup and shared isolation defaults
56
+
57
+ When calling agent:
58
+ - Do not use one worker to check on another. Workers will notify you when they are done.
59
+ - Do not use workers to trivially report file contents or run commands. Give them higher-level tasks.
60
+ - Continue workers whose work is complete via send_message to take advantage of their loaded context.
61
+ - After launching agents, briefly tell the user what you launched and end your response. Never fabricate or predict agent results — results arrive as separate messages.
62
+
63
+ For a single coherent task: do it yourself using your normal tools and do not call task_create/task_update just to track it. Creating exactly one TODO item is overhead, not coordination.
64
+
65
+ For multi-task goals: when the user's goal needs to be decomposed into two or more meaningful pieces (or has dependencies that need tracking), create tasks with task_create, fan out independent work with delegate({ tasks }), synthesize findings yourself, then create new tasks when discoveries appear. Use send_message when continuing a named worker's loaded context is clearly useful.
66
+
67
+ ### agent Results
68
+
69
+ Worker results arrive as **user-role messages** containing \`<task-notification>\` XML. They look like user messages but are not. Distinguish them by the \`<task-notification>\` opening tag.
70
+
71
+ Format:
72
+
73
+ \`\`\`xml
74
+ <task-notification>
75
+ <task-id>{task_id}</task-id>
76
+ <status>completed|failed|killed</status>
77
+ <summary>{human-readable status summary}</summary>
78
+ <result>{worker's final text response}</result>
79
+ <usage>
80
+ <tool_uses>N</tool_uses>
81
+ <duration_ms>N</duration_ms>
82
+ </usage>
83
+ </task-notification>
84
+ \`\`\`
85
+
86
+ - \`<result>\` and \`<usage>\` are optional sections
87
+ - Use send_message with the \`task_id\` as \`to\` to continue that worker
88
+
89
+ ## 3. Task Workflow
90
+
91
+ Only use the shared TODO workflow when there are multiple tasks inside the user's goal. Most multi-task goals break down into: Research (workers, parallel) → Synthesis (YOU) → Implementation (workers) → Verification (workers).
92
+
93
+ ### Concurrency
94
+
95
+ **Parallelism is your superpower.** Launch independent workers concurrently whenever possible — make multiple agent tool calls in a single message. Read-only tasks (research) run in parallel freely. Write-heavy tasks (implementation) should be one at a time per set of files.
96
+
97
+ ### Handling Worker Failures
98
+
99
+ When a worker reports failure, continue the same worker with send_message — it has the full error context.
100
+
101
+ ## 4. Writing Worker Prompts
102
+
103
+ **Workers can't see your conversation.** Every prompt must be self-contained with everything the worker needs.
104
+
105
+ ### Always synthesize — your most important job
106
+
107
+ When workers report research findings, **you must understand them before directing follow-up work**. Read the findings. Identify the approach. Then write a prompt that proves you understood by including specific file paths, line numbers, and exactly what to change. Never write "based on your findings" — that delegates understanding to the worker instead of doing it yourself.
108
+
109
+ ### Add a purpose statement
110
+
111
+ Include a brief purpose so workers can calibrate depth and emphasis: "This is to plan an implementation — report file paths, line numbers, and type signatures."
112
+ `;
113
+ }
114
+
115
+ /**
116
+ * Format a task-notification XML payload matching the Claude Code shape.
117
+ */
118
+ export function formatTaskNotification(params: {
119
+ taskId: string;
120
+ status: "completed" | "failed" | "killed";
121
+ summary: string;
122
+ result?: string;
123
+ toolUses?: number;
124
+ durationMs?: number;
125
+ }): string {
126
+ const parts = [
127
+ "<task-notification>",
128
+ `<task-id>${escapeXml(params.taskId)}</task-id>`,
129
+ `<status>${params.status}</status>`,
130
+ `<summary>${escapeXml(params.summary)}</summary>`,
131
+ ];
132
+ if (params.result && params.result.trim()) {
133
+ parts.push(`<result>${escapeXml(params.result)}</result>`);
134
+ }
135
+ if (params.toolUses !== undefined || params.durationMs !== undefined) {
136
+ parts.push("<usage>");
137
+ if (params.toolUses !== undefined) parts.push(` <tool_uses>${params.toolUses}</tool_uses>`);
138
+ if (params.durationMs !== undefined) parts.push(` <duration_ms>${params.durationMs}</duration_ms>`);
139
+ parts.push("</usage>");
140
+ }
141
+ parts.push("</task-notification>");
142
+ return parts.join("\n");
143
+ }
144
+
145
+ function escapeXml(s: string): string {
146
+ return s
147
+ .replace(/&/g, "&amp;")
148
+ .replace(/</g, "&lt;")
149
+ .replace(/>/g, "&gt;")
150
+ .replace(/"/g, "&quot;")
151
+ .replace(/'/g, "&apos;");
152
+ }
153
+
154
+ /** True when the parent session should act as a coordinator (delegating only). */
155
+ export function isCoordinatorMode(): boolean {
156
+ const v = process.env.PI_TEAM_MATE_COORDINATOR;
157
+ return v === "1" || v === "true";
158
+ }
@@ -0,0 +1,156 @@
1
+ // Pi Team-Mode — Persistence Layer
2
+
3
+ import { mkdir, rm } from "node:fs/promises";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { atomicWriteJson, listSubdirs, readJson, slugify } from "./fs-utils.js";
9
+ import type { TeamRecord, TeammateRecord } from "./types.js";
10
+
11
+ const DIR_TEAMMATES = "teammates";
12
+ const DIR_TEAMS = "teams";
13
+ const DIR_RUNTIME = "runtime";
14
+ const FILE_RECORD = "record.json";
15
+ const FILE_INDEX = "index.json";
16
+ const DIR_SESSIONS = "sessions";
17
+
18
+ /** Defaults to `~/.pi/agent/extensions/team-mode`. Override with `PI_TEAM_MATE_STORAGE_ROOT`. */
19
+ export function getStorageRoot(): string {
20
+ const override = process.env.PI_TEAM_MATE_STORAGE_ROOT;
21
+ if (override) return override;
22
+ return path.join(os.homedir(), ".pi", "agent", "extensions", "team-mode");
23
+ }
24
+
25
+ export function generateTeammateId(name?: string): string {
26
+ // Prefix with "agent-" so the id matches Claude Code's task_id namespace
27
+ // (task_stop / send_message accept this id).
28
+ return `agent-${slugify(name ?? "teammate", "teammate")}-${randomUUID().slice(0, 8)}`;
29
+ }
30
+
31
+ export function generateTeamId(name: string): string {
32
+ return `${slugify(name, "team")}-${randomUUID().slice(0, 8)}`;
33
+ }
34
+
35
+ /**
36
+ * Persistent store for teammates, teams, and the per-session name index.
37
+ *
38
+ * Writes are atomic (write-temp + rename). A write-through in-memory cache
39
+ * serves `listTeammates` and `listTeams` so the widget's high-frequency
40
+ * refreshes don't hit the disk for every event.
41
+ */
42
+ export class TeamMateStore {
43
+ private teammateCache: Map<string, TeammateRecord> | null = null;
44
+ private teamCache: Map<string, TeamRecord> | null = null;
45
+
46
+ constructor(private readonly root: string = getStorageRoot()) {}
47
+
48
+ teammateDir(teammateId: string): string {
49
+ return path.join(this.root, DIR_TEAMMATES, teammateId);
50
+ }
51
+
52
+ teammateSessionDir(teammateId: string): string {
53
+ return path.join(this.teammateDir(teammateId), DIR_SESSIONS);
54
+ }
55
+
56
+ /** Absolute path of the pi session file used by `--session`. */
57
+ teammateSessionFile(teammateId: string): string {
58
+ return path.join(this.teammateSessionDir(teammateId), `${teammateId}.jsonl`);
59
+ }
60
+
61
+ teamDir(teamId: string): string {
62
+ return path.join(this.root, DIR_TEAMS, teamId);
63
+ }
64
+
65
+ runtimeDir(parentSessionId: string): string {
66
+ return path.join(this.root, DIR_RUNTIME, parentSessionId);
67
+ }
68
+
69
+ // --- teammates ---
70
+
71
+ async saveTeammate(record: TeammateRecord): Promise<void> {
72
+ const dir = this.teammateDir(record.id);
73
+ await mkdir(path.join(dir, DIR_SESSIONS), { recursive: true });
74
+ await atomicWriteJson(path.join(dir, FILE_RECORD), record);
75
+ if (this.teammateCache) this.teammateCache.set(record.id, record);
76
+ }
77
+
78
+ async loadTeammate(teammateId: string): Promise<TeammateRecord | null> {
79
+ if (this.teammateCache?.has(teammateId)) {
80
+ return this.teammateCache.get(teammateId) ?? null;
81
+ }
82
+ const record = await readJson<TeammateRecord>(
83
+ path.join(this.teammateDir(teammateId), FILE_RECORD),
84
+ );
85
+ if (record && this.teammateCache) this.teammateCache.set(record.id, record);
86
+ return record;
87
+ }
88
+
89
+ async listTeammates(): Promise<TeammateRecord[]> {
90
+ if (this.teammateCache) return [...this.teammateCache.values()];
91
+ const ids = await listSubdirs(path.join(this.root, DIR_TEAMMATES));
92
+ const records = (await Promise.all(ids.map((id) => this.loadTeammate(id)))).filter(
93
+ (r): r is TeammateRecord => r !== null,
94
+ );
95
+ const cache = new Map<string, TeammateRecord>();
96
+ for (const r of records) cache.set(r.id, r);
97
+ this.teammateCache = cache;
98
+ return records;
99
+ }
100
+
101
+ async deleteTeammate(teammateId: string): Promise<void> {
102
+ await rm(this.teammateDir(teammateId), { recursive: true, force: true });
103
+ this.teammateCache?.delete(teammateId);
104
+ }
105
+
106
+ // --- teams ---
107
+
108
+ async saveTeam(record: TeamRecord): Promise<void> {
109
+ const dir = this.teamDir(record.id);
110
+ await mkdir(dir, { recursive: true });
111
+ await atomicWriteJson(path.join(dir, FILE_RECORD), record);
112
+ if (this.teamCache) this.teamCache.set(record.id, record);
113
+ }
114
+
115
+ async loadTeam(teamId: string): Promise<TeamRecord | null> {
116
+ if (this.teamCache?.has(teamId)) return this.teamCache.get(teamId) ?? null;
117
+ const record = await readJson<TeamRecord>(path.join(this.teamDir(teamId), FILE_RECORD));
118
+ if (record && this.teamCache) this.teamCache.set(record.id, record);
119
+ return record;
120
+ }
121
+
122
+ async listTeams(): Promise<TeamRecord[]> {
123
+ if (this.teamCache) return [...this.teamCache.values()];
124
+ const ids = await listSubdirs(path.join(this.root, DIR_TEAMS));
125
+ const records = (await Promise.all(ids.map((id) => this.loadTeam(id)))).filter(
126
+ (r): r is TeamRecord => r !== null,
127
+ );
128
+ const cache = new Map<string, TeamRecord>();
129
+ for (const r of records) cache.set(r.id, r);
130
+ this.teamCache = cache;
131
+ return records;
132
+ }
133
+
134
+ async deleteTeam(teamId: string): Promise<void> {
135
+ await rm(this.teamDir(teamId), { recursive: true, force: true });
136
+ this.teamCache?.delete(teamId);
137
+ }
138
+
139
+ // --- runtime name index (name -> teammateId, per parent session) ---
140
+
141
+ async getNameIndex(parentSessionId: string): Promise<Record<string, string>> {
142
+ return (
143
+ (await readJson<Record<string, string>>(path.join(this.runtimeDir(parentSessionId), FILE_INDEX))) ?? {}
144
+ );
145
+ }
146
+
147
+ async setNameIndex(parentSessionId: string, index: Record<string, string>): Promise<void> {
148
+ const dir = this.runtimeDir(parentSessionId);
149
+ await mkdir(dir, { recursive: true });
150
+ await atomicWriteJson(path.join(dir, FILE_INDEX), index);
151
+ }
152
+
153
+ async clearNameIndex(parentSessionId: string): Promise<void> {
154
+ await rm(this.runtimeDir(parentSessionId), { recursive: true, force: true });
155
+ }
156
+ }
@@ -0,0 +1,99 @@
1
+ // Pi Team-Mode — Task Board
2
+ //
3
+ // A shared TODO list scoped per parent session, matching Claude Code's
4
+ // TaskCreate/TaskUpdate/TaskList/TaskGet shape. The coordinator (parent LLM
5
+ // or human via /tasks) assigns tasks to teammates via TaskUpdate; there is
6
+ // no auto-claim — that is intentional.
7
+
8
+ import { mkdir, readdir, rm } from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+
12
+ import { atomicWriteJson, readJson, slugify } from "./fs-utils.js";
13
+ import { getStorageRoot } from "./store.js";
14
+
15
+ export type TaskStatus = "pending" | "in_progress" | "completed" | "failed" | "deleted";
16
+
17
+ export type TaskRecord = {
18
+ id: string;
19
+ subject: string;
20
+ description?: string;
21
+ activeForm?: string;
22
+ status: TaskStatus;
23
+ owner?: string | null;
24
+ blockedBy: string[];
25
+ blocks: string[];
26
+ metadata?: Record<string, unknown>;
27
+ parentSessionId: string;
28
+ teamId?: string;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ /** Optimistic CAS counter. Bumped on every successful save. */
32
+ version: number;
33
+ /** Final summary (filled by whoever completes the task). */
34
+ result?: string;
35
+ /** Output captured from a quality-gate hook (if any ran). */
36
+ hookOutput?: string;
37
+ };
38
+
39
+ const DIR_TASKS = "tasks";
40
+ const FILE_TASK_SUFFIX = ".json";
41
+
42
+ export class TaskStore {
43
+ constructor(private readonly root: string = getStorageRoot()) {}
44
+
45
+ dir(parentSessionId: string): string {
46
+ return path.join(this.root, DIR_TASKS, parentSessionId);
47
+ }
48
+
49
+ private file(parentSessionId: string, taskId: string): string {
50
+ return path.join(this.dir(parentSessionId), `${taskId}${FILE_TASK_SUFFIX}`);
51
+ }
52
+
53
+ async save(record: TaskRecord): Promise<void> {
54
+ const dir = this.dir(record.parentSessionId);
55
+ await mkdir(dir, { recursive: true });
56
+ await atomicWriteJson(this.file(record.parentSessionId, record.id), record);
57
+ }
58
+
59
+ async load(parentSessionId: string, taskId: string): Promise<TaskRecord | null> {
60
+ return readJson<TaskRecord>(this.file(parentSessionId, taskId));
61
+ }
62
+
63
+ async list(parentSessionId: string): Promise<TaskRecord[]> {
64
+ const dir = this.dir(parentSessionId);
65
+ let entries: string[];
66
+ try {
67
+ entries = await readdir(dir);
68
+ } catch (err) {
69
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return [];
70
+ throw err;
71
+ }
72
+ const files = entries.filter((e) => e.endsWith(FILE_TASK_SUFFIX));
73
+ return (
74
+ await Promise.all(files.map((f) => readJson<TaskRecord>(path.join(dir, f))))
75
+ ).filter((r): r is TaskRecord => r !== null);
76
+ }
77
+
78
+ async delete(parentSessionId: string, taskId: string): Promise<void> {
79
+ await rm(this.file(parentSessionId, taskId), { force: true });
80
+ }
81
+
82
+ async clear(parentSessionId: string): Promise<void> {
83
+ await rm(this.dir(parentSessionId), { recursive: true, force: true });
84
+ }
85
+ }
86
+
87
+ export function generateTaskId(subject: string): string {
88
+ return `task-${slugify(subject, "task")}-${randomUUID().slice(0, 8)}`;
89
+ }
90
+
91
+ /** True when every dependency in `blockedBy` is completed or deleted. */
92
+ export function isUnblocked(task: TaskRecord, byId: Map<string, TaskRecord>): boolean {
93
+ for (const depId of task.blockedBy) {
94
+ const dep = byId.get(depId);
95
+ if (!dep) continue;
96
+ if (dep.status !== "completed" && dep.status !== "deleted") return false;
97
+ }
98
+ return true;
99
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pi Team-Mode — Teammate Spec Loader
3
+ *
4
+ * Reads role specs from `.pi/teammates/<role>.md` or `.claude/teammates/<role>.md`
5
+ * (checked in that order). Frontmatter fields drive runtime behavior; the
6
+ * markdown body becomes the teammate's system prompt.
7
+ */
8
+
9
+ import { readFile, readdir } from "node:fs/promises";
10
+ import * as path from "node:path";
11
+
12
+ import type { TeammateSpec } from "./types.js";
13
+ import { isThinkingLevel } from "./model-config.js";
14
+
15
+ const SPEC_DIRS = [".pi/teammates", ".claude/teammates"] as const;
16
+
17
+ /**
18
+ * Locate and parse a teammate spec by role name.
19
+ * Returns `null` if no matching spec file exists in either directory.
20
+ */
21
+ export async function loadTeammateSpec(
22
+ cwd: string,
23
+ role: string,
24
+ ): Promise<TeammateSpec | null> {
25
+ for (const dir of SPEC_DIRS) {
26
+ const filePath = path.join(cwd, dir, `${role}.md`);
27
+ const spec = await tryRead(filePath, role);
28
+ if (spec) return spec;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /** List all available teammate specs in the current project. Exported for tests and future /teammate specs command. */
34
+ export async function listTeammateSpecs(cwd: string): Promise<TeammateSpec[]> {
35
+ const specs: TeammateSpec[] = [];
36
+ const seen = new Set<string>();
37
+ for (const dir of SPEC_DIRS) {
38
+ const absDir = path.join(cwd, dir);
39
+ let entries: string[] = [];
40
+ try {
41
+ entries = await readdir(absDir);
42
+ } catch (err) {
43
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") continue;
44
+ throw err;
45
+ }
46
+ for (const entry of entries) {
47
+ if (!entry.endsWith(".md")) continue;
48
+ const role = entry.slice(0, -3);
49
+ if (seen.has(role)) continue;
50
+ const spec = await tryRead(path.join(absDir, entry), role);
51
+ if (spec) {
52
+ seen.add(role);
53
+ specs.push(spec);
54
+ }
55
+ }
56
+ }
57
+ return specs;
58
+ }
59
+
60
+ async function tryRead(filePath: string, role: string): Promise<TeammateSpec | null> {
61
+ let raw: string;
62
+ try {
63
+ raw = await readFile(filePath, "utf8");
64
+ } catch (err) {
65
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
66
+ throw err;
67
+ }
68
+ return parseSpec(raw, role, filePath);
69
+ }
70
+
71
+ /**
72
+ * Parse a markdown file with YAML-ish frontmatter.
73
+ *
74
+ * Frontmatter is delimited by `---` lines. Supports scalar values and simple
75
+ * comma-separated arrays (e.g. `tools: read, bash, grep`). We intentionally
76
+ * avoid a YAML dependency — the spec grammar is deliberately small.
77
+ */
78
+ export function parseSpec(raw: string, fallbackRole: string, sourcePath: string): TeammateSpec {
79
+ const lines = raw.split(/\r?\n/);
80
+ let body = raw;
81
+ const fm: Record<string, string> = {};
82
+
83
+ if (lines[0]?.trim() === "---") {
84
+ const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
85
+ if (end > 0) {
86
+ for (let i = 1; i < end; i++) {
87
+ const line = lines[i];
88
+ const match = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
89
+ if (match) {
90
+ fm[match[1]] = match[2].trim().replace(/^['"]|['"]$/g, "");
91
+ }
92
+ }
93
+ body = lines.slice(end + 1).join("\n").trim();
94
+ }
95
+ }
96
+
97
+ const tools = fm.tools ? fm.tools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
98
+
99
+ return {
100
+ name: fm.name || fallbackRole,
101
+ description: fm.description,
102
+ needsWorktree: parseBool(fm.needsWorktree),
103
+ hasMemory: parseBool(fm.hasMemory),
104
+ modelTier: fm.modelTier,
105
+ thinkingLevel: parseThinkingLevel(fm.thinkingLevel ?? fm.thinking),
106
+ tools,
107
+ systemPrompt: body,
108
+ sourcePath,
109
+ };
110
+ }
111
+
112
+ function parseThinkingLevel(value: string | undefined): TeammateSpec["thinkingLevel"] {
113
+ if (value === undefined) return undefined;
114
+ const v = value.toLowerCase();
115
+ return isThinkingLevel(v) ? v : undefined;
116
+ }
117
+
118
+ function parseBool(value: string | undefined): boolean | undefined {
119
+ if (value === undefined) return undefined;
120
+ const v = value.toLowerCase();
121
+ if (v === "true" || v === "yes" || v === "1") return true;
122
+ if (v === "false" || v === "no" || v === "0") return false;
123
+ return undefined;
124
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Pi Team-Mode — Core Types
3
+ *
4
+ * Flat peer-agent model mirroring Claude Code's team-mode semantics:
5
+ * named teammates are spawned and optionally grouped under teams; each
6
+ * teammate has its own resumable pi session.
7
+ */
8
+
9
+ export type TeammateStatus =
10
+ | "pending"
11
+ | "running"
12
+ | "completed"
13
+ | "failed"
14
+ | "stopped";
15
+
16
+ export type TeammateExitReason =
17
+ | "completed"
18
+ | "stopped"
19
+ | "failed"
20
+ | "wrapped_up"
21
+ | "aborted";
22
+
23
+ export type IsolationMode = "none" | "worktree";
24
+
25
+ export type ExecutionRuntime = "subprocess" | "transient";
26
+
27
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
28
+
29
+ /** Persistent record for a single named teammate. */
30
+ export type TeammateRecord = {
31
+ /** Stable id, used as pi session id. */
32
+ id: string;
33
+ /** Caller-supplied name (unique within a session). Falls back to id if omitted. */
34
+ name: string;
35
+ /** Optional team grouping (namespace). */
36
+ teamId?: string;
37
+ /** Role/spec name (e.g. "researcher", "reviewer"). */
38
+ subagentType?: string;
39
+ /** Bare model id passed to pi via `--model`. */
40
+ model?: string;
41
+ /** Provider passed to pi via `--provider`. */
42
+ provider?: string;
43
+ /** Thinking level passed to pi via `--thinking`. */
44
+ thinkingLevel?: ThinkingLevel;
45
+ /** Isolation strategy at spawn time. */
46
+ isolation: IsolationMode;
47
+ /** Working directory the teammate operates in. */
48
+ cwd: string;
49
+ /** Worktree branch, when isolation="worktree". */
50
+ worktreeBranch?: string;
51
+ /** Lifecycle status. */
52
+ status: TeammateStatus;
53
+ /** Pid of the currently-running pi subprocess (if any). */
54
+ pid?: number;
55
+ /** Whether the caller requested background execution. */
56
+ background: boolean;
57
+ /** ISO timestamp. */
58
+ createdAt: string;
59
+ /** ISO timestamp, updated on every turn completion. */
60
+ updatedAt: string;
61
+ /** Last final message emitted by the teammate. */
62
+ lastResult?: string;
63
+ /** Exit code of the last subprocess run. */
64
+ lastExitCode?: number;
65
+ /** Parent session id (the main pi session that owns this teammate). */
66
+ parentSessionId?: string;
67
+ };
68
+
69
+ /** Persistent record for a team grouping. */
70
+ export type TeamRecord = {
71
+ id: string;
72
+ name: string;
73
+ /** ISO timestamp. */
74
+ createdAt: string;
75
+ /** Default isolation for teammates spawned under this team. */
76
+ defaultIsolation: IsolationMode;
77
+ /** Base directory for worktrees created under this team. */
78
+ worktreeBase?: string;
79
+ /** Parent session id (the main pi session that created this team). */
80
+ parentSessionId?: string;
81
+ };
82
+
83
+ /** Arguments accepted by `agentManager.spawn()`. */
84
+ export type SpawnOpts = {
85
+ description: string;
86
+ prompt: string;
87
+ name?: string;
88
+ teamId?: string;
89
+ subagentType?: string;
90
+ model?: string;
91
+ thinkingLevel?: ThinkingLevel;
92
+ isolation?: IsolationMode;
93
+ background?: boolean;
94
+ /** Execution backend. Defaults to subprocess for durable/resumable workers. */
95
+ runtime?: ExecutionRuntime;
96
+ /** Override cwd (defaults to ctx.cwd or team's worktreeBase). */
97
+ cwd?: string;
98
+ };
99
+
100
+ export type LiveTeammateMetrics = {
101
+ turns: number;
102
+ maxTurns?: number;
103
+ toolUses: number;
104
+ tokens: number;
105
+ currentTool?: string;
106
+ currentToolStartedAt?: number;
107
+ activityHint?: string;
108
+ startedAt: number;
109
+ finishedAt?: number;
110
+ exitReason?: TeammateExitReason;
111
+ };
112
+
113
+ export type LiveTeammateSnapshot = {
114
+ record: TeammateRecord;
115
+ metrics: LiveTeammateMetrics;
116
+ description?: string;
117
+ transcriptPath: string;
118
+ };
119
+
120
+ /** Outcome of a spawn or send_message operation. */
121
+ export type TeammateRunResult = {
122
+ teammateId: string;
123
+ name: string;
124
+ /** Short task label passed at spawn. Used in the task-notification summary. */
125
+ description?: string;
126
+ status: TeammateStatus;
127
+ /** Final text message from the teammate, or a stub if background. */
128
+ result: string;
129
+ exitCode: number | null;
130
+ metrics?: LiveTeammateMetrics;
131
+ transcriptPath?: string;
132
+ provider?: string;
133
+ model?: string;
134
+ thinkingLevel?: ThinkingLevel;
135
+ modelRationale?: string;
136
+ worktree?: {
137
+ path: string;
138
+ branch: string;
139
+ };
140
+ background?: boolean;
141
+ durationMs?: number;
142
+ runtime?: ExecutionRuntime;
143
+ };
144
+
145
+ /** Teammate role spec loaded from `.pi/teammates/*.md` or `.claude/teammates/*.md`. */
146
+ export type TeammateSpec = {
147
+ name: string;
148
+ description?: string;
149
+ needsWorktree?: boolean;
150
+ hasMemory?: boolean;
151
+ modelTier?: string;
152
+ /** Default thinking level for this role. */
153
+ thinkingLevel?: ThinkingLevel;
154
+ /** Allowed tool names (forwarded via pi --tools). */
155
+ tools?: string[];
156
+ /** Markdown body becomes the teammate's system prompt. */
157
+ systemPrompt: string;
158
+ /** Path the spec was loaded from, for diagnostics. */
159
+ sourcePath: string;
160
+ };