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,211 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import {
4
+ applyTemplate,
5
+ createChainDir,
6
+ expandCountedTasks,
7
+ isParallelChainStep,
8
+ readStepInputs,
9
+ type DelegateChainStep,
10
+ type DelegateTask,
11
+ writeStepOutput,
12
+ } from "../core/chain-utils.js";
13
+ import { aggregateParallelOutputs, DEFAULT_PARALLEL_CONCURRENCY, mapConcurrent } from "../core/parallel-utils.js";
14
+ import type { ExecutionRuntime, IsolationMode, TeammateRunResult } from "../core/types.js";
15
+ import type { AgentManager } from "./agent-manager.js";
16
+
17
+ const DEFAULT_MAX_PARALLEL = 8;
18
+
19
+ export type DelegationResult = {
20
+ mode: "parallel" | "chain";
21
+ output: string;
22
+ steps: number;
23
+ chainDir?: string;
24
+ };
25
+
26
+ export type RunParallelInput = {
27
+ tasks: DelegateTask[];
28
+ concurrency?: number;
29
+ isolation?: IsolationMode;
30
+ runtime?: ExecutionRuntime;
31
+ };
32
+
33
+ export type RunChainInput = {
34
+ task: string;
35
+ chain: DelegateChainStep[];
36
+ concurrency?: number;
37
+ isolation?: IsolationMode;
38
+ runtime?: ExecutionRuntime;
39
+ };
40
+
41
+ export class DelegationManager {
42
+ constructor(private readonly agents: AgentManager) {}
43
+
44
+ async runParallel(input: RunParallelInput): Promise<DelegationResult> {
45
+ const expanded = expandCountedTasks(input.tasks);
46
+ const maxParallel = readMaxParallel();
47
+ if (expanded.length === 0) throw new Error("delegate.tasks must include at least one task");
48
+ if (expanded.length > maxParallel) {
49
+ throw new Error(`delegate.tasks exceeds max tasks (${maxParallel})`);
50
+ }
51
+
52
+ const concurrency = boundedConcurrency(input.concurrency);
53
+ let launched = 0;
54
+ this.agents.setQueuedCount(Math.max(0, expanded.length - Math.min(expanded.length, concurrency)));
55
+ try {
56
+ const results = await mapConcurrent(expanded, concurrency, async (task, index) => {
57
+ launched += 1;
58
+ this.agents.setQueuedCount(Math.max(0, expanded.length - launched));
59
+ return this.runTask(task, index, input.isolation, input.runtime);
60
+ });
61
+ return {
62
+ mode: "parallel",
63
+ steps: expanded.length,
64
+ output: aggregateParallelOutputs(results.map(toParallelResult)),
65
+ };
66
+ } finally {
67
+ this.agents.setQueuedCount(0);
68
+ }
69
+ }
70
+
71
+ async runChain(input: RunChainInput): Promise<DelegationResult> {
72
+ if (input.chain.length === 0) throw new Error("delegate.chain must include at least one step");
73
+ const runId = `chain-${randomUUID().slice(0, 8)}`;
74
+ const chainDir = await createChainDir(runId);
75
+ let previous = "";
76
+ const topConcurrency = boundedConcurrency(input.concurrency);
77
+
78
+ for (let i = 0; i < input.chain.length; i += 1) {
79
+ const step = input.chain[i];
80
+ if (isParallelChainStep(step)) {
81
+ const expanded = expandCountedTasks(step.parallel);
82
+ const maxParallel = readMaxParallel();
83
+ if (expanded.length > maxParallel) {
84
+ throw new Error(`delegate.chain parallel step ${i + 1} exceeds max tasks (${maxParallel})`);
85
+ }
86
+ const concurrency = boundedConcurrency(step.concurrency ?? topConcurrency);
87
+ let launched = 0;
88
+ let failFastTriggered = false;
89
+ this.agents.setQueuedCount(Math.max(0, expanded.length - Math.min(expanded.length, concurrency)));
90
+ try {
91
+ const runs = await mapConcurrent(expanded, concurrency, async (task, index) => {
92
+ if (step.failFast && failFastTriggered) {
93
+ return skippedResult(task, index);
94
+ }
95
+ launched += 1;
96
+ this.agents.setQueuedCount(Math.max(0, expanded.length - launched));
97
+ const renderedTask = await withTemplatedTask(task, input.task, previous, chainDir);
98
+ const run = await this.runTask(
99
+ renderedTask,
100
+ index,
101
+ step.isolation ?? input.isolation,
102
+ step.runtime ?? input.runtime,
103
+ );
104
+ await writeStepOutput(chainDir, renderedTask.output, run.result);
105
+ if (step.failFast && run.status !== "completed") failFastTriggered = true;
106
+ return run;
107
+ });
108
+ previous = aggregateParallelOutputs(runs.map(toParallelResult));
109
+ if (step.failFast && runs.some((r) => r.status !== "completed")) {
110
+ break;
111
+ }
112
+ } finally {
113
+ this.agents.setQueuedCount(0);
114
+ }
115
+ continue;
116
+ }
117
+
118
+ const stepTask = await withTemplatedTask(step, input.task, previous, chainDir);
119
+ const result = await this.runTask(stepTask, i, input.isolation, input.runtime);
120
+ await writeStepOutput(chainDir, stepTask.output, result.result);
121
+ previous = result.result;
122
+ }
123
+
124
+ return {
125
+ mode: "chain",
126
+ steps: input.chain.length,
127
+ chainDir,
128
+ output: previous,
129
+ };
130
+ }
131
+
132
+ private async runTask(
133
+ task: DelegateTask,
134
+ index: number,
135
+ defaultIsolation?: IsolationMode,
136
+ defaultRuntime?: ExecutionRuntime,
137
+ ): Promise<TeammateRunResult> {
138
+ const name = task.name?.trim();
139
+ const result = await this.agents.spawn({
140
+ description: task.description,
141
+ prompt: task.prompt,
142
+ name,
143
+ teamId: task.teamId,
144
+ subagentType: task.subagentType,
145
+ model: task.model,
146
+ thinkingLevel: task.thinkingLevel,
147
+ isolation: task.isolation ?? defaultIsolation,
148
+ runtime: task.runtime ?? defaultRuntime,
149
+ background: false,
150
+ cwd: task.cwd,
151
+ });
152
+ return result;
153
+ }
154
+ }
155
+
156
+ type TemplatedTask = DelegateTask;
157
+
158
+ async function withTemplatedTask(
159
+ task: DelegateTask,
160
+ rootTask: string,
161
+ previous: string,
162
+ chainDir: string,
163
+ ): Promise<TemplatedTask> {
164
+ const readPrefix = await readStepInputs(chainDir, task.reads);
165
+ const taskTemplate = `${readPrefix}${task.prompt}`;
166
+ return {
167
+ ...task,
168
+ prompt: applyTemplate(taskTemplate, { task: rootTask, previous, chainDir }),
169
+ };
170
+ }
171
+
172
+ function toParallelResult(run: TeammateRunResult) {
173
+ return {
174
+ name: run.name,
175
+ output: run.result,
176
+ exitCode: run.exitCode,
177
+ error: run.status === "completed" ? undefined : run.result,
178
+ };
179
+ }
180
+
181
+ function skippedResult(task: DelegateTask, index: number): TeammateRunResult {
182
+ return {
183
+ teammateId: `skipped-${index + 1}`,
184
+ name: task.name ?? `task-${index + 1}`,
185
+ description: task.description,
186
+ status: "failed",
187
+ result: "[skipped due to failFast]",
188
+ exitCode: null,
189
+ };
190
+ }
191
+
192
+ function boundedConcurrency(input: number | undefined): number {
193
+ const parsed = Number.isFinite(input) ? Math.floor(input as number) : readDefaultConcurrency();
194
+ return Math.max(1, parsed);
195
+ }
196
+
197
+ function readDefaultConcurrency(): number {
198
+ const env = process.env.PI_TEAM_MATE_PARALLEL_CONCURRENCY;
199
+ if (!env) return DEFAULT_PARALLEL_CONCURRENCY;
200
+ const parsed = Number.parseInt(env, 10);
201
+ if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_PARALLEL_CONCURRENCY;
202
+ return parsed;
203
+ }
204
+
205
+ function readMaxParallel(): number {
206
+ const env = process.env.PI_TEAM_MATE_MAX_PARALLEL;
207
+ if (!env) return DEFAULT_MAX_PARALLEL;
208
+ const parsed = Number.parseInt(env, 10);
209
+ if (!Number.isFinite(parsed) || parsed < 1) return DEFAULT_MAX_PARALLEL;
210
+ return parsed;
211
+ }
@@ -0,0 +1,238 @@
1
+ // Pi Team-Mode — Task Manager
2
+ //
3
+ // TODO-list CRUD matching Claude Code's TaskCreate/TaskUpdate/TaskList/TaskGet
4
+ // semantics. Coordinator assigns via TaskUpdate({ owner }); there is no
5
+ // auto-claim. update() takes a filesystem lock + CAS version counter so
6
+ // concurrent edits from teammate subprocesses don't clobber each other.
7
+
8
+ import { spawn } from "node:child_process";
9
+ import { mkdir, open, unlink } from "node:fs/promises";
10
+ import * as path from "node:path";
11
+
12
+ import {
13
+ generateTaskId,
14
+ type TaskRecord,
15
+ type TaskStatus,
16
+ type TaskStore,
17
+ } from "../core/tasks.js";
18
+
19
+ const HOOK_TIMEOUT_MS = 120_000;
20
+ const LOCK_RETRY_DELAY_MS = 20;
21
+ const LOCK_MAX_ATTEMPTS = 100;
22
+ const LOCK_STALE_MS = 10_000;
23
+
24
+ export type TaskCreateOpts = {
25
+ subject: string;
26
+ description?: string;
27
+ activeForm?: string;
28
+ metadata?: Record<string, unknown>;
29
+ teamId?: string;
30
+ };
31
+
32
+ export type TaskUpdateOpts = {
33
+ expectedVersion?: number;
34
+ status?: TaskStatus;
35
+ owner?: string | null;
36
+ subject?: string;
37
+ description?: string;
38
+ activeForm?: string;
39
+ result?: string;
40
+ addBlocks?: string[];
41
+ addBlockedBy?: string[];
42
+ metadata?: Record<string, unknown>;
43
+ };
44
+
45
+ export type TaskManagerDeps = {
46
+ store: TaskStore;
47
+ getParentSessionId: () => string;
48
+ getTaskCompletedHook?: () => Promise<string | undefined> | string | undefined;
49
+ getCwd?: () => string;
50
+ };
51
+
52
+ export class TaskManager {
53
+ constructor(private readonly deps: TaskManagerDeps) {}
54
+
55
+ async create(opts: TaskCreateOpts): Promise<TaskRecord> {
56
+ const parentSessionId = this.deps.getParentSessionId();
57
+ const now = new Date().toISOString();
58
+ const record: TaskRecord = {
59
+ id: generateTaskId(opts.subject),
60
+ subject: opts.subject,
61
+ description: opts.description,
62
+ activeForm: opts.activeForm,
63
+ status: "pending",
64
+ owner: null,
65
+ blockedBy: [],
66
+ blocks: [],
67
+ metadata: opts.metadata,
68
+ parentSessionId,
69
+ teamId: opts.teamId,
70
+ createdAt: now,
71
+ updatedAt: now,
72
+ version: 1,
73
+ };
74
+ await this.deps.store.save(record);
75
+ return record;
76
+ }
77
+
78
+ async update(taskId: string, opts: TaskUpdateOpts): Promise<TaskRecord> {
79
+ const parentSessionId = this.deps.getParentSessionId();
80
+ return withTaskLock(this.deps.store.dir(parentSessionId), taskId, async () => {
81
+ const current = await this.deps.store.load(parentSessionId, taskId);
82
+ if (!current) throw new Error(`unknown task: ${taskId}`);
83
+ if (opts.expectedVersion !== undefined && current.version !== opts.expectedVersion) {
84
+ throw new VersionConflictError(current.version, opts.expectedVersion);
85
+ }
86
+
87
+ const nextStatus = opts.status ?? current.status;
88
+ const transitioningToCompleted =
89
+ current.status !== "completed" && nextStatus === "completed";
90
+
91
+ const updated: TaskRecord = {
92
+ ...current,
93
+ subject: opts.subject ?? current.subject,
94
+ description: opts.description ?? current.description,
95
+ activeForm: opts.activeForm ?? current.activeForm,
96
+ owner: opts.owner === undefined ? current.owner : opts.owner,
97
+ status: nextStatus,
98
+ result: opts.result ?? current.result,
99
+ blockedBy: opts.addBlockedBy
100
+ ? Array.from(new Set([...current.blockedBy, ...opts.addBlockedBy]))
101
+ : current.blockedBy,
102
+ blocks: opts.addBlocks
103
+ ? Array.from(new Set([...current.blocks, ...opts.addBlocks]))
104
+ : current.blocks,
105
+ metadata: opts.metadata
106
+ ? { ...(current.metadata ?? {}), ...opts.metadata }
107
+ : current.metadata,
108
+ updatedAt: new Date().toISOString(),
109
+ version: current.version + 1,
110
+ };
111
+
112
+ if (transitioningToCompleted) {
113
+ const hook = await this.deps.getTaskCompletedHook?.();
114
+ if (hook && hook.trim()) {
115
+ const hookResult = await this.runHook(hook, updated);
116
+ updated.hookOutput = hookResult.output;
117
+ if (hookResult.exitCode !== 0) {
118
+ updated.status = "failed";
119
+ updated.result = `${updated.result ? updated.result + "\n\n" : ""}[TaskCompleted hook failed, exit ${hookResult.exitCode}] ${hook}`;
120
+ }
121
+ }
122
+ }
123
+
124
+ await this.deps.store.save(updated);
125
+ return updated;
126
+ });
127
+ }
128
+
129
+ async get(taskId: string): Promise<TaskRecord | null> {
130
+ return this.deps.store.load(this.deps.getParentSessionId(), taskId);
131
+ }
132
+
133
+ async list(filter?: {
134
+ status?: TaskStatus;
135
+ owner?: string;
136
+ teamId?: string;
137
+ }): Promise<TaskRecord[]> {
138
+ const all = await this.deps.store.list(this.deps.getParentSessionId());
139
+ let result = all;
140
+ if (filter?.status) result = result.filter((t) => t.status === filter.status);
141
+ if (filter?.owner) result = result.filter((t) => t.owner === filter.owner);
142
+ if (filter?.teamId) result = result.filter((t) => t.teamId === filter.teamId);
143
+ return result;
144
+ }
145
+
146
+ async clear(): Promise<void> {
147
+ await this.deps.store.clear(this.deps.getParentSessionId());
148
+ }
149
+
150
+ private async runHook(
151
+ hook: string,
152
+ task: TaskRecord,
153
+ ): Promise<{ exitCode: number; output: string }> {
154
+ const cwd = this.deps.getCwd?.() ?? process.cwd();
155
+ return new Promise((resolve) => {
156
+ const proc = spawn("sh", ["-c", hook], {
157
+ cwd,
158
+ env: {
159
+ ...process.env,
160
+ PI_TEAM_MATE_TASK_ID: task.id,
161
+ PI_TEAM_MATE_TASK_SUBJECT: task.subject,
162
+ PI_TEAM_MATE_TASK_OWNER: task.owner ?? "",
163
+ },
164
+ stdio: ["ignore", "pipe", "pipe"],
165
+ });
166
+ let output = "";
167
+ const append = (chunk: Buffer) => {
168
+ if (output.length < 8192) output += chunk.toString("utf8");
169
+ };
170
+ proc.stdout.on("data", append);
171
+ proc.stderr.on("data", append);
172
+ const timer = setTimeout(() => {
173
+ proc.kill("SIGTERM");
174
+ output += `\n[hook timed out after ${HOOK_TIMEOUT_MS}ms]`;
175
+ }, HOOK_TIMEOUT_MS);
176
+ timer.unref();
177
+ proc.on("close", (code) => {
178
+ clearTimeout(timer);
179
+ resolve({ exitCode: code ?? 1, output: output.trim() });
180
+ });
181
+ proc.on("error", (err) => {
182
+ clearTimeout(timer);
183
+ resolve({ exitCode: 1, output: `[hook spawn error] ${err.message}` });
184
+ });
185
+ });
186
+ }
187
+ }
188
+
189
+ export class VersionConflictError extends Error {
190
+ constructor(public readonly actual: number, public readonly expected: number) {
191
+ super(`version conflict: expected ${expected}, got ${actual}`);
192
+ this.name = "VersionConflictError";
193
+ }
194
+ }
195
+
196
+ async function withTaskLock<T>(
197
+ taskDir: string,
198
+ taskId: string,
199
+ fn: () => Promise<T>,
200
+ ): Promise<T> {
201
+ await mkdir(taskDir, { recursive: true });
202
+ const lockPath = path.join(taskDir, `${taskId}.lock`);
203
+
204
+ for (let attempt = 0; attempt < LOCK_MAX_ATTEMPTS; attempt++) {
205
+ try {
206
+ const handle = await open(lockPath, "wx");
207
+ await handle.writeFile(String(process.pid));
208
+ await handle.close();
209
+ try {
210
+ return await fn();
211
+ } finally {
212
+ await unlink(lockPath).catch(() => {});
213
+ }
214
+ } catch (err) {
215
+ if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
216
+ if (await isStaleLock(lockPath)) {
217
+ await unlink(lockPath).catch(() => {});
218
+ continue;
219
+ }
220
+ await delay(LOCK_RETRY_DELAY_MS + Math.random() * LOCK_RETRY_DELAY_MS);
221
+ }
222
+ }
223
+ throw new Error(`could not acquire task lock after ${LOCK_MAX_ATTEMPTS} attempts: ${lockPath}`);
224
+ }
225
+
226
+ async function isStaleLock(lockPath: string): Promise<boolean> {
227
+ try {
228
+ const { stat } = await import("node:fs/promises");
229
+ const st = await stat(lockPath);
230
+ return Date.now() - st.mtimeMs > LOCK_STALE_MS;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ function delay(ms: number): Promise<void> {
237
+ return new Promise((r) => setTimeout(r, ms));
238
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pi Team-Mode — Team Manager
3
+ *
4
+ * Teams are lightweight namespaces that group teammates. Creating one returns
5
+ * a stable id; deleting one stops all its teammates and removes their records.
6
+ */
7
+
8
+ import type { TeamMateStore } from "../core/store.js";
9
+ import { generateTeamId } from "../core/store.js";
10
+ import type { IsolationMode, TeamRecord } from "../core/types.js";
11
+ import type { AgentManager } from "./agent-manager.js";
12
+
13
+ export type TeamCreateOpts = {
14
+ name: string;
15
+ defaultIsolation?: IsolationMode;
16
+ worktreeBase?: string;
17
+ };
18
+
19
+ export class TeamManager {
20
+ constructor(
21
+ private readonly store: TeamMateStore,
22
+ private readonly agents: AgentManager,
23
+ private readonly getParentSessionId: () => string,
24
+ ) {}
25
+
26
+ async create(opts: TeamCreateOpts): Promise<TeamRecord> {
27
+ const record: TeamRecord = {
28
+ id: generateTeamId(opts.name),
29
+ name: opts.name,
30
+ createdAt: new Date().toISOString(),
31
+ defaultIsolation: opts.defaultIsolation ?? "none",
32
+ worktreeBase: opts.worktreeBase,
33
+ parentSessionId: this.getParentSessionId(),
34
+ };
35
+ await this.store.saveTeam(record);
36
+ return record;
37
+ }
38
+
39
+ async delete(teamId: string): Promise<void> {
40
+ const team = await this.store.loadTeam(teamId);
41
+ if (!team) throw new Error(`unknown team: ${teamId}`);
42
+ const teammates = (await this.agents.list()).filter((t) => t.teamId === teamId);
43
+ await Promise.all(
44
+ teammates.map(async (t) => {
45
+ await this.agents.stop(t.id).catch(() => {});
46
+ await this.store.deleteTeammate(t.id);
47
+ }),
48
+ );
49
+ await this.store.deleteTeam(teamId);
50
+ }
51
+
52
+ async list(): Promise<TeamRecord[]> {
53
+ return this.store.listTeams();
54
+ }
55
+
56
+ async get(teamId: string): Promise<TeamRecord | null> {
57
+ return this.store.loadTeam(teamId);
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "pi-mono-team-mode",
3
+ "version": "2.3.1",
4
+ "description": "Pi extension for flat peer-agent orchestration — named, addressable teammates with resumable context (mirrors Claude Code's team-mate model)",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension"
9
+ ],
10
+ "scripts": {
11
+ "test": "npx tsx --test '__tests__/**/*.test.ts'"
12
+ },
13
+ "peerDependencies": {
14
+ "@mariozechner/pi-ai": "*",
15
+ "@mariozechner/pi-coding-agent": "*",
16
+ "@mariozechner/pi-tui": "*",
17
+ "@sinclair/typebox": "*"
18
+ },
19
+ "pi": {
20
+ "extensions": [
21
+ "./index.ts"
22
+ ]
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/emanuelcasco/pi-mono-extensions.git",
27
+ "directory": "extensions/team-mode"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/emanuelcasco/pi-mono-extensions/issues"
31
+ },
32
+ "homepage": "https://github.com/emanuelcasco/pi-mono-extensions#readme"
33
+ }