pi-crew 0.1.37 → 0.1.39

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 (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +5 -0
  4. package/agents/analyst.md +11 -11
  5. package/agents/critic.md +11 -11
  6. package/agents/executor.md +11 -11
  7. package/agents/explorer.md +11 -11
  8. package/agents/planner.md +11 -11
  9. package/agents/reviewer.md +11 -11
  10. package/agents/security-reviewer.md +11 -11
  11. package/agents/test-engineer.md +11 -11
  12. package/agents/verifier.md +11 -11
  13. package/agents/writer.md +11 -11
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-optimization-plan.md +548 -548
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/resource-formats.md +10 -8
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/docs/usage.md +6 -0
  27. package/index.ts +6 -6
  28. package/package.json +3 -3
  29. package/schema.json +2 -2
  30. package/src/agents/agent-serializer.ts +34 -34
  31. package/src/config/config.ts +8 -4
  32. package/src/extension/cross-extension-rpc.ts +82 -82
  33. package/src/extension/import-index.ts +18 -2
  34. package/src/extension/register.ts +11 -1
  35. package/src/extension/registration/compaction-guard.ts +125 -125
  36. package/src/extension/registration/subagent-helpers.ts +30 -6
  37. package/src/extension/registration/subagent-tools.ts +8 -3
  38. package/src/extension/result-watcher.ts +98 -98
  39. package/src/extension/run-import.ts +12 -2
  40. package/src/extension/run-index.ts +12 -2
  41. package/src/extension/run-maintenance.ts +24 -24
  42. package/src/extension/team-tool/api.ts +54 -14
  43. package/src/extension/team-tool/cancel.ts +31 -31
  44. package/src/extension/team-tool/doctor.ts +179 -179
  45. package/src/extension/team-tool/inspect.ts +41 -41
  46. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  47. package/src/extension/team-tool/plan.ts +19 -19
  48. package/src/extension/team-tool/status.ts +73 -73
  49. package/src/observability/correlation.ts +35 -35
  50. package/src/observability/event-to-metric.ts +54 -54
  51. package/src/observability/exporters/adapter.ts +24 -24
  52. package/src/observability/exporters/otlp-exporter.ts +65 -65
  53. package/src/observability/exporters/prometheus-exporter.ts +47 -47
  54. package/src/observability/metric-registry.ts +72 -72
  55. package/src/observability/metric-retention.ts +46 -46
  56. package/src/observability/metric-sink.ts +51 -51
  57. package/src/observability/metrics-primitives.ts +166 -166
  58. package/src/prompt/prompt-runtime.ts +68 -68
  59. package/src/runtime/agent-control.ts +64 -64
  60. package/src/runtime/agent-memory.ts +72 -72
  61. package/src/runtime/agent-observability.ts +114 -113
  62. package/src/runtime/async-marker.ts +26 -26
  63. package/src/runtime/background-runner.ts +53 -53
  64. package/src/runtime/crash-recovery.ts +56 -56
  65. package/src/runtime/crew-agent-records.ts +54 -9
  66. package/src/runtime/crew-agent-runtime.ts +58 -58
  67. package/src/runtime/deadletter.ts +36 -36
  68. package/src/runtime/direct-run.ts +35 -35
  69. package/src/runtime/foreground-control.ts +82 -82
  70. package/src/runtime/green-contract.ts +46 -46
  71. package/src/runtime/group-join.ts +88 -88
  72. package/src/runtime/heartbeat-gradient.ts +28 -28
  73. package/src/runtime/heartbeat-watcher.ts +80 -80
  74. package/src/runtime/live-agent-control.ts +87 -78
  75. package/src/runtime/live-agent-manager.ts +85 -85
  76. package/src/runtime/live-control-realtime.ts +36 -36
  77. package/src/runtime/live-session-runtime.ts +299 -299
  78. package/src/runtime/manifest-cache.ts +248 -212
  79. package/src/runtime/model-fallback.ts +261 -261
  80. package/src/runtime/parallel-research.ts +44 -44
  81. package/src/runtime/parallel-utils.ts +99 -99
  82. package/src/runtime/pi-json-output.ts +111 -111
  83. package/src/runtime/policy-engine.ts +78 -78
  84. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  85. package/src/runtime/process-status.ts +56 -56
  86. package/src/runtime/progress-event-coalescer.ts +43 -43
  87. package/src/runtime/recovery-recipes.ts +74 -74
  88. package/src/runtime/retry-executor.ts +59 -59
  89. package/src/runtime/role-permission.ts +39 -39
  90. package/src/runtime/session-usage.ts +79 -79
  91. package/src/runtime/sidechain-output.ts +28 -28
  92. package/src/runtime/subagent-manager.ts +80 -12
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -106
  95. package/src/runtime/task-runner/live-executor.ts +98 -98
  96. package/src/runtime/task-runner/progress.ts +111 -111
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/team-runner.ts +1 -1
  100. package/src/runtime/worker-heartbeat.ts +21 -21
  101. package/src/runtime/worker-startup.ts +57 -57
  102. package/src/schema/config-schema.ts +21 -21
  103. package/src/schema/team-tool-schema.ts +100 -100
  104. package/src/state/artifact-store.ts +122 -108
  105. package/src/state/contracts.ts +105 -105
  106. package/src/state/jsonl-writer.ts +77 -77
  107. package/src/state/mailbox.ts +67 -22
  108. package/src/state/state-store.ts +36 -5
  109. package/src/state/task-claims.ts +42 -42
  110. package/src/state/usage.ts +29 -29
  111. package/src/subagents/async-entry.ts +1 -1
  112. package/src/subagents/index.ts +3 -3
  113. package/src/subagents/live/control.ts +1 -1
  114. package/src/subagents/live/manager.ts +1 -1
  115. package/src/subagents/live/realtime.ts +1 -1
  116. package/src/subagents/live/session-runtime.ts +1 -1
  117. package/src/subagents/manager.ts +1 -1
  118. package/src/subagents/spawn.ts +1 -1
  119. package/src/teams/discover-teams.ts +27 -5
  120. package/src/teams/team-serializer.ts +38 -36
  121. package/src/types/diff.d.ts +18 -18
  122. package/src/ui/crew-footer.ts +101 -101
  123. package/src/ui/crew-select-list.ts +111 -111
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/mascot.ts +441 -441
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/run-dashboard.ts +5 -2
  131. package/src/ui/run-snapshot-cache.ts +19 -8
  132. package/src/ui/spinner.ts +17 -17
  133. package/src/ui/status-colors.ts +54 -54
  134. package/src/ui/syntax-highlight.ts +116 -116
  135. package/src/ui/transcript-viewer.ts +15 -1
  136. package/src/utils/completion-dedupe.ts +63 -63
  137. package/src/utils/file-coalescer.ts +84 -84
  138. package/src/utils/frontmatter.ts +36 -36
  139. package/src/utils/fs-watch.ts +31 -31
  140. package/src/utils/git.ts +262 -262
  141. package/src/utils/ids.ts +12 -12
  142. package/src/utils/names.ts +26 -26
  143. package/src/utils/paths.ts +3 -2
  144. package/src/utils/safe-paths.ts +34 -0
  145. package/src/utils/sleep.ts +32 -32
  146. package/src/utils/timings.ts +31 -31
  147. package/src/utils/visual.ts +159 -159
  148. package/src/workflows/discover-workflows.ts +30 -3
  149. package/src/workflows/validate-workflow.ts +40 -40
  150. package/src/worktree/branch-freshness.ts +45 -45
  151. package/teams/default.team.md +12 -12
  152. package/teams/fast-fix.team.md +11 -11
  153. package/teams/implementation.team.md +18 -18
  154. package/teams/parallel-research.team.md +14 -14
  155. package/teams/research.team.md +11 -11
  156. package/teams/review.team.md +12 -12
  157. package/workflows/default.workflow.md +29 -29
  158. package/workflows/fast-fix.workflow.md +22 -22
  159. package/workflows/implementation.workflow.md +38 -38
  160. package/workflows/parallel-research.workflow.md +46 -46
  161. package/workflows/research.workflow.md +22 -22
  162. package/workflows/review.workflow.md +30 -30
@@ -1,77 +1,77 @@
1
- import * as fs from "node:fs";
2
-
3
- export interface DrainableSource {
4
- pause(): void;
5
- resume(): void;
6
- }
7
-
8
- export interface JsonlWriteStream {
9
- write(chunk: string): boolean;
10
- once(event: "drain", listener: () => void): JsonlWriteStream;
11
- end(callback?: () => void): void;
12
- }
13
-
14
- const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
15
-
16
- export interface JsonlWriterDeps {
17
- createWriteStream?: (filePath: string) => JsonlWriteStream;
18
- maxBytes?: number;
19
- }
20
-
21
- export interface JsonlWriter {
22
- writeLine(line: string): void;
23
- close(): Promise<void>;
24
- }
25
-
26
- export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
27
- if (!filePath) {
28
- return {
29
- writeLine() {},
30
- async close() {},
31
- };
32
- }
33
-
34
- const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
35
- let stream: JsonlWriteStream | undefined;
36
- try {
37
- stream = createWriteStream(filePath);
38
- } catch {
39
- return {
40
- writeLine() {},
41
- async close() {},
42
- };
43
- }
44
-
45
- let backpressured = false;
46
- let closed = false;
47
- let bytesWritten = 0;
48
- const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
49
-
50
- return {
51
- writeLine(line: string) {
52
- if (!stream || closed || !line.trim()) return;
53
- const chunk = `${line}\n`;
54
- const chunkBytes = Buffer.byteLength(chunk, "utf-8");
55
- if (bytesWritten + chunkBytes > maxBytes) return;
56
- try {
57
- const ok = stream.write(chunk);
58
- bytesWritten += chunkBytes;
59
- if (!ok && !backpressured) {
60
- backpressured = true;
61
- source.pause();
62
- stream.once("drain", () => {
63
- backpressured = false;
64
- if (!closed) source.resume();
65
- });
66
- }
67
- } catch {}
68
- },
69
- async close() {
70
- if (!stream || closed) return;
71
- closed = true;
72
- const current = stream;
73
- stream = undefined;
74
- await new Promise<void>((resolve) => current.end(() => resolve()));
75
- },
76
- };
77
- }
1
+ import * as fs from "node:fs";
2
+
3
+ export interface DrainableSource {
4
+ pause(): void;
5
+ resume(): void;
6
+ }
7
+
8
+ export interface JsonlWriteStream {
9
+ write(chunk: string): boolean;
10
+ once(event: "drain", listener: () => void): JsonlWriteStream;
11
+ end(callback?: () => void): void;
12
+ }
13
+
14
+ const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
15
+
16
+ export interface JsonlWriterDeps {
17
+ createWriteStream?: (filePath: string) => JsonlWriteStream;
18
+ maxBytes?: number;
19
+ }
20
+
21
+ export interface JsonlWriter {
22
+ writeLine(line: string): void;
23
+ close(): Promise<void>;
24
+ }
25
+
26
+ export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
27
+ if (!filePath) {
28
+ return {
29
+ writeLine() {},
30
+ async close() {},
31
+ };
32
+ }
33
+
34
+ const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
35
+ let stream: JsonlWriteStream | undefined;
36
+ try {
37
+ stream = createWriteStream(filePath);
38
+ } catch {
39
+ return {
40
+ writeLine() {},
41
+ async close() {},
42
+ };
43
+ }
44
+
45
+ let backpressured = false;
46
+ let closed = false;
47
+ let bytesWritten = 0;
48
+ const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
49
+
50
+ return {
51
+ writeLine(line: string) {
52
+ if (!stream || closed || !line.trim()) return;
53
+ const chunk = `${line}\n`;
54
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
55
+ if (bytesWritten + chunkBytes > maxBytes) return;
56
+ try {
57
+ const ok = stream.write(chunk);
58
+ bytesWritten += chunkBytes;
59
+ if (!ok && !backpressured) {
60
+ backpressured = true;
61
+ source.pause();
62
+ stream.once("drain", () => {
63
+ backpressured = false;
64
+ if (!closed) source.resume();
65
+ });
66
+ }
67
+ } catch {}
68
+ },
69
+ async close() {
70
+ if (!stream || closed) return;
71
+ closed = true;
72
+ const current = stream;
73
+ stream = undefined;
74
+ await new Promise<void>((resolve) => current.end(() => resolve()));
75
+ },
76
+ };
77
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { TeamRunManifest } from "./types.ts";
4
+ import { resolveRealContainedPath } from "../utils/safe-paths.ts";
4
5
 
5
6
  export type MailboxDirection = "inbox" | "outbox";
6
7
  export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
@@ -43,33 +44,78 @@ function mailboxDir(manifest: TeamRunManifest): string {
43
44
  return path.join(manifest.stateRoot, "mailbox");
44
45
  }
45
46
 
46
- function taskMailboxDir(manifest: TeamRunManifest, taskId: string): string {
47
- return path.join(mailboxDir(manifest), "tasks", taskId);
47
+ function safeMailboxDir(manifest: TeamRunManifest, create = false): string {
48
+ const dir = mailboxDir(manifest);
49
+ if (create) fs.mkdirSync(dir, { recursive: true });
50
+ if (!fs.existsSync(dir)) return dir;
51
+ if (fs.lstatSync(dir).isSymbolicLink()) throw new Error(`Invalid mailbox directory: ${dir}`);
52
+ return resolveRealContainedPath(manifest.stateRoot, "mailbox");
48
53
  }
49
54
 
50
- function mailboxPath(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string): string {
51
- return taskId ? path.join(taskMailboxDir(manifest, taskId), `${direction}.jsonl`) : path.join(mailboxDir(manifest), `${direction}.jsonl`);
55
+ function safeTaskId(taskId: string): string {
56
+ if (!/^[\w.-]+$/.test(taskId) || taskId.includes("..") || path.isAbsolute(taskId)) throw new Error(`Invalid mailbox task id: ${taskId}`);
57
+ return taskId;
52
58
  }
53
59
 
54
- function deliveryPath(manifest: TeamRunManifest): string {
55
- return path.join(mailboxDir(manifest), "delivery.json");
60
+ function safeMailboxTasksRoot(manifest: TeamRunManifest, create = false): string {
61
+ const root = path.join(safeMailboxDir(manifest, create), "tasks");
62
+ if (create) fs.mkdirSync(root, { recursive: true });
63
+ if (!fs.existsSync(root)) return root;
64
+ if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid mailbox tasks directory: ${root}`);
65
+ return resolveRealContainedPath(safeMailboxDir(manifest), "tasks");
66
+ }
67
+
68
+ function taskMailboxDir(manifest: TeamRunManifest, taskId: string, create = false): string {
69
+ const tasksRoot = safeMailboxTasksRoot(manifest, create);
70
+ const normalizedTaskId = safeTaskId(taskId);
71
+ const resolved = path.resolve(tasksRoot, normalizedTaskId);
72
+ const relative = path.relative(tasksRoot, resolved);
73
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Invalid mailbox task id: ${taskId}`);
74
+ if (create) fs.mkdirSync(resolved, { recursive: true });
75
+ if (!fs.existsSync(resolved)) return resolved;
76
+ if (fs.lstatSync(resolved).isSymbolicLink()) throw new Error(`Invalid mailbox task directory: ${resolved}`);
77
+ return resolveRealContainedPath(tasksRoot, normalizedTaskId);
78
+ }
79
+
80
+ function mailboxPath(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string, create = false): string {
81
+ return taskId ? path.join(taskMailboxDir(manifest, taskId, create), `${direction}.jsonl`) : path.join(safeMailboxDir(manifest, create), `${direction}.jsonl`);
82
+ }
83
+
84
+ function deliveryPath(manifest: TeamRunManifest, create = false): string {
85
+ return path.join(safeMailboxDir(manifest, create), "delivery.json");
86
+ }
87
+
88
+ function safeMailboxFile(filePath: string, parentDir: string): string {
89
+ if (!fs.existsSync(filePath)) return filePath;
90
+ if (fs.lstatSync(filePath).isSymbolicLink()) throw new Error(`Invalid mailbox file: ${filePath}`);
91
+ return resolveRealContainedPath(parentDir, path.basename(filePath));
92
+ }
93
+
94
+ function mailboxFile(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string, create = false): string {
95
+ const parent = taskId ? taskMailboxDir(manifest, taskId, create) : safeMailboxDir(manifest, create);
96
+ return safeMailboxFile(path.join(parent, `${direction}.jsonl`), parent);
97
+ }
98
+
99
+ function deliveryFile(manifest: TeamRunManifest, create = false): string {
100
+ const parent = safeMailboxDir(manifest, create);
101
+ return safeMailboxFile(path.join(parent, "delivery.json"), parent);
56
102
  }
57
103
 
58
104
  function ensureRunMailbox(manifest: TeamRunManifest): void {
59
- fs.mkdirSync(mailboxDir(manifest), { recursive: true });
105
+ safeMailboxDir(manifest, true);
60
106
  for (const direction of ["inbox", "outbox"] as const) {
61
- const filePath = mailboxPath(manifest, direction);
107
+ const filePath = mailboxFile(manifest, direction, undefined, true);
62
108
  if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
63
109
  }
64
- const delivery = deliveryPath(manifest);
110
+ const delivery = deliveryFile(manifest, true);
65
111
  if (!fs.existsSync(delivery)) fs.writeFileSync(delivery, `${JSON.stringify({ messages: {}, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf-8");
66
112
  }
67
113
 
68
114
  function ensureTaskMailbox(manifest: TeamRunManifest, taskId: string): void {
69
115
  ensureRunMailbox(manifest);
70
- fs.mkdirSync(taskMailboxDir(manifest, taskId), { recursive: true });
116
+ taskMailboxDir(manifest, taskId, true);
71
117
  for (const direction of ["inbox", "outbox"] as const) {
72
- const filePath = mailboxPath(manifest, direction, taskId);
118
+ const filePath = mailboxFile(manifest, direction, taskId, true);
73
119
  if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
74
120
  }
75
121
  }
@@ -112,25 +158,24 @@ function safeReadMailboxFile(filePath: string, direction: MailboxDirection): Mai
112
158
 
113
159
  export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string): MailboxMessage[] {
114
160
  const directions = direction ? [direction] : ["inbox", "outbox"] as const;
115
- return directions.flatMap((item) => safeReadMailboxFile(mailboxPath(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
161
+ return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
116
162
  }
117
163
 
118
164
  function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
119
- const messages = [...safeReadMailboxFile(mailboxPath(manifest, "inbox"), "inbox")];
120
- const tasksDir = path.join(mailboxDir(manifest), "tasks");
165
+ const messages = [...safeReadMailboxFile(mailboxFile(manifest, "inbox"), "inbox")];
166
+ const tasksDir = safeMailboxTasksRoot(manifest);
121
167
  if (fs.existsSync(tasksDir)) {
122
168
  for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
123
169
  if (!entry.isDirectory()) continue;
124
- messages.push(...safeReadMailboxFile(mailboxPath(manifest, "inbox", entry.name), "inbox"));
170
+ messages.push(...safeReadMailboxFile(mailboxFile(manifest, "inbox", entry.name), "inbox"));
125
171
  }
126
172
  }
127
173
  return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
128
174
  }
129
175
 
130
176
  export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
131
- ensureRunMailbox(manifest);
132
177
  try {
133
- const raw = JSON.parse(fs.readFileSync(deliveryPath(manifest), "utf-8")) as unknown;
178
+ const raw = JSON.parse(fs.readFileSync(deliveryFile(manifest), "utf-8")) as unknown;
134
179
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid delivery state.");
135
180
  const obj = raw as Record<string, unknown>;
136
181
  const messages: Record<string, MailboxMessageStatus> = {};
@@ -145,7 +190,7 @@ export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliverySta
145
190
 
146
191
  function writeDeliveryState(manifest: TeamRunManifest, state: MailboxDeliveryState): void {
147
192
  ensureRunMailbox(manifest);
148
- fs.writeFileSync(deliveryPath(manifest), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
193
+ fs.writeFileSync(deliveryFile(manifest, true), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
149
194
  }
150
195
 
151
196
  export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<MailboxMessage, "id" | "runId" | "createdAt" | "status"> & { id?: string; status?: MailboxMessageStatus }): MailboxMessage {
@@ -163,7 +208,7 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
163
208
  status: message.status ?? "queued",
164
209
  taskId: message.taskId,
165
210
  };
166
- fs.appendFileSync(mailboxPath(manifest, complete.direction, complete.taskId), `${JSON.stringify(complete)}\n`, "utf-8");
211
+ fs.appendFileSync(mailboxFile(manifest, complete.direction, complete.taskId), `${JSON.stringify(complete)}\n`, "utf-8");
167
212
  const delivery = readDeliveryState(manifest);
168
213
  delivery.messages[complete.id] = complete.status;
169
214
  delivery.updatedAt = createdAt;
@@ -196,7 +241,7 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
196
241
  const issues: MailboxValidationIssue[] = [];
197
242
  const repaired: string[] = [];
198
243
  for (const direction of ["inbox", "outbox"] as const) {
199
- const filePath = mailboxPath(manifest, direction);
244
+ const filePath = mailboxFile(manifest, direction);
200
245
  const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean);
201
246
  const validLines: string[] = [];
202
247
  for (const line of lines) {
@@ -217,12 +262,12 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
217
262
  }
218
263
  const delivery = readDeliveryState(manifest);
219
264
  const allMessages = readMailbox(manifest);
220
- for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryPath(manifest), message: `Missing delivery entry for ${message.id}.` });
265
+ for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryFile(manifest), message: `Missing delivery entry for ${message.id}.` });
221
266
  if (options.repair) {
222
267
  for (const message of allMessages) delivery.messages[message.id] ??= message.status;
223
268
  delivery.updatedAt = new Date().toISOString();
224
269
  writeDeliveryState(manifest, delivery);
225
- repaired.push(deliveryPath(manifest));
270
+ repaired.push(deliveryFile(manifest));
226
271
  }
227
272
  return { issues, repaired };
228
273
  }
@@ -7,6 +7,7 @@ import { appendEvent } from "./event-log.ts";
7
7
  import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
8
8
  import { createRunId, createTaskId } from "../utils/ids.ts";
9
9
  import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
10
+ import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
10
11
  import type { TeamConfig } from "../teams/team-config.ts";
11
12
  import type { WorkflowConfig } from "../workflows/workflow-config.ts";
12
13
 
@@ -53,14 +54,40 @@ function scopeBaseRoot(cwd: string): string {
53
54
  }
54
55
 
55
56
  function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
56
- const scopedPath = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir, runId);
57
- return fs.existsSync(scopedPath) ? scopedPath : undefined;
57
+ assertSafePathId("runId", runId);
58
+ const runsRoot = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
59
+ const scopedPath = resolveContainedRelativePath(runsRoot, runId, "runId");
60
+ if (!fs.existsSync(scopedPath)) return undefined;
61
+ try {
62
+ if (fs.lstatSync(scopedPath).isSymbolicLink()) return undefined;
63
+ resolveRealContainedPath(runsRoot, runId);
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ return scopedPath;
68
+ }
69
+
70
+ function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunManifest, stateRoot: string, tasksPath: string): boolean {
71
+ if (manifest.runId !== runId || manifest.stateRoot !== stateRoot || manifest.tasksPath !== tasksPath || manifest.eventsPath !== path.join(stateRoot, "events.jsonl")) return false;
72
+ const artifactsParent = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir);
73
+ const expectedArtifactsRoot = resolveContainedRelativePath(artifactsParent, runId, "runId");
74
+ if (manifest.artifactsRoot !== expectedArtifactsRoot) return false;
75
+ if (fs.existsSync(expectedArtifactsRoot)) {
76
+ try {
77
+ if (fs.lstatSync(expectedArtifactsRoot).isSymbolicLink()) return false;
78
+ resolveRealContainedPath(artifactsParent, runId);
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ return true;
58
84
  }
59
85
 
60
86
  export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
87
+ assertSafePathId("runId", runId);
61
88
  const baseRoot = scopeBaseRoot(cwd);
62
- const stateRoot = path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir, runId);
63
- const artifactsRoot = path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir, runId);
89
+ const stateRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir), runId, "runId");
90
+ const artifactsRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
64
91
  return {
65
92
  runId,
66
93
  stateRoot,
@@ -222,11 +249,15 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
222
249
  && cached.tasksMtimeMs === tasksMtimeMs
223
250
  && cached.tasksSize === (tasksStat?.size ?? 0)
224
251
  ) {
252
+ if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
253
+ manifestCache.delete(stateRoot);
254
+ return undefined;
255
+ }
225
256
  return { manifest: cached.manifest, tasks: cached.tasks };
226
257
  }
227
258
 
228
259
  const manifest = readJsonFile<TeamRunManifest>(manifestPath);
229
- if (!manifest) return undefined;
260
+ if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
230
261
  const tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
231
262
  setManifestCache(stateRoot, {
232
263
  manifest,
@@ -1,42 +1,42 @@
1
- import { randomUUID } from "node:crypto";
2
- import type { TeamTaskState } from "./types.ts";
3
-
4
- export interface TaskClaimState {
5
- owner: string;
6
- token: string;
7
- leasedUntil: string;
8
- }
9
-
10
- export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
11
- return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
12
- }
13
-
14
- export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
15
- if (!claim) return false;
16
- return Date.parse(claim.leasedUntil) <= now.getTime();
17
- }
18
-
19
- export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
20
- return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
21
- }
22
-
23
- export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
24
- if (task.claim && !isTaskClaimExpired(task.claim, now)) {
25
- throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
26
- }
27
- return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
28
- }
29
-
30
- export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
31
- if (!canUseTaskClaim(task, owner, token, now)) {
32
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
33
- }
34
- return { ...task, claim: undefined };
35
- }
36
-
37
- export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
38
- if (!canUseTaskClaim(task, owner, token, now)) {
39
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
40
- }
41
- return { ...task, status };
42
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import type { TeamTaskState } from "./types.ts";
3
+
4
+ export interface TaskClaimState {
5
+ owner: string;
6
+ token: string;
7
+ leasedUntil: string;
8
+ }
9
+
10
+ export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
11
+ return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
12
+ }
13
+
14
+ export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
15
+ if (!claim) return false;
16
+ return Date.parse(claim.leasedUntil) <= now.getTime();
17
+ }
18
+
19
+ export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
20
+ return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
21
+ }
22
+
23
+ export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
24
+ if (task.claim && !isTaskClaimExpired(task.claim, now)) {
25
+ throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
26
+ }
27
+ return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
28
+ }
29
+
30
+ export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
31
+ if (!canUseTaskClaim(task, owner, token, now)) {
32
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
33
+ }
34
+ return { ...task, claim: undefined };
35
+ }
36
+
37
+ export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
38
+ if (!canUseTaskClaim(task, owner, token, now)) {
39
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
40
+ }
41
+ return { ...task, status };
42
+ }
@@ -1,29 +1,29 @@
1
- import type { TeamTaskState, UsageState } from "./types.ts";
2
-
3
- export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
4
- const total: UsageState = {};
5
- let found = false;
6
- for (const task of tasks) {
7
- if (!task.usage) continue;
8
- found = true;
9
- total.input = (total.input ?? 0) + (task.usage.input ?? 0);
10
- total.output = (total.output ?? 0) + (task.usage.output ?? 0);
11
- total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
12
- total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
13
- total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
14
- total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
15
- }
16
- return found ? total : undefined;
17
- }
18
-
19
- export function formatUsage(usage: UsageState | undefined): string {
20
- if (!usage) return "(none)";
21
- const parts: string[] = [];
22
- if (usage.input !== undefined) parts.push(`input=${usage.input}`);
23
- if (usage.output !== undefined) parts.push(`output=${usage.output}`);
24
- if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
25
- if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
26
- if (usage.cost !== undefined) parts.push(`cost=${usage.cost.toFixed(6)}`);
27
- if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
28
- return parts.join(", ") || "(none)";
29
- }
1
+ import type { TeamTaskState, UsageState } from "./types.ts";
2
+
3
+ export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
4
+ const total: UsageState = {};
5
+ let found = false;
6
+ for (const task of tasks) {
7
+ if (!task.usage) continue;
8
+ found = true;
9
+ total.input = (total.input ?? 0) + (task.usage.input ?? 0);
10
+ total.output = (total.output ?? 0) + (task.usage.output ?? 0);
11
+ total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
12
+ total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
13
+ total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
14
+ total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
15
+ }
16
+ return found ? total : undefined;
17
+ }
18
+
19
+ export function formatUsage(usage: UsageState | undefined): string {
20
+ if (!usage) return "(none)";
21
+ const parts: string[] = [];
22
+ if (usage.input !== undefined) parts.push(`input=${usage.input}`);
23
+ if (usage.output !== undefined) parts.push(`output=${usage.output}`);
24
+ if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
25
+ if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
26
+ if (usage.cost !== undefined) parts.push(`cost=${usage.cost.toFixed(6)}`);
27
+ if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
28
+ return parts.join(", ") || "(none)";
29
+ }
@@ -1 +1 @@
1
- export * from "../runtime/async-runner.ts";
1
+ export * from "../runtime/async-runner.ts";
@@ -1,3 +1,3 @@
1
- export * from "./spawn.ts";
2
- export * from "./manager.ts";
3
- export * from "./async-entry.ts";
1
+ export * from "./spawn.ts";
2
+ export * from "./manager.ts";
3
+ export * from "./async-entry.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-agent-control.ts";
1
+ export * from "../../runtime/live-agent-control.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-agent-manager.ts";
1
+ export * from "../../runtime/live-agent-manager.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-control-realtime.ts";
1
+ export * from "../../runtime/live-control-realtime.ts";
@@ -1 +1 @@
1
- export * from "../../runtime/live-session-runtime.ts";
1
+ export * from "../../runtime/live-session-runtime.ts";
@@ -1 +1 @@
1
- export * from "../runtime/subagent-manager.ts";
1
+ export * from "../runtime/subagent-manager.ts";
@@ -1 +1 @@
1
- export * from "../runtime/child-pi.ts";
1
+ export * from "../runtime/child-pi.ts";
@@ -12,19 +12,41 @@ export interface TeamDiscoveryResult {
12
12
  project: TeamConfig[];
13
13
  }
14
14
 
15
+ function parseRoleSkills(value: string | undefined): string[] | false | undefined {
16
+ if (!value) return undefined;
17
+ if (value === "false") return false;
18
+ const skills = value.split(",").map((entry) => entry.trim()).filter(Boolean);
19
+ return skills.length ? skills : undefined;
20
+ }
21
+
15
22
  function parseRoleLine(line: string): TeamRole | undefined {
16
23
  const trimmed = line.trim();
17
24
  if (!trimmed.startsWith("-")) return undefined;
18
25
  const value = trimmed.slice(1).trim();
19
26
  if (!value) return undefined;
20
- const [namePart, restPart] = value.split(":", 2);
21
- const name = namePart?.trim();
27
+ const separator = value.indexOf(":");
28
+ const namePart = separator >= 0 ? value.slice(0, separator) : value;
29
+ const restPart = separator >= 0 ? value.slice(separator + 1) : "";
30
+ const name = namePart.trim();
22
31
  if (!name) return undefined;
23
- const agentMatch = restPart?.match(/agent\s*=\s*([\w-]+)/);
32
+ const metadata: Record<string, string> = {};
33
+ let descriptionSource = restPart.replace(/\bskills\s*=\s*([\w-]+(?:\s*,\s*[\w-]+)*)/g, (_match, raw: string) => {
34
+ metadata.skills = raw.replace(/\s*,\s*/g, ",").trim();
35
+ return "";
36
+ });
37
+ descriptionSource = descriptionSource.replace(/\b(agent|model|maxConcurrency)\s*=\s*(\S+)/g, (_match, key: string, raw: string) => {
38
+ metadata[key] = raw.trim();
39
+ return "";
40
+ });
41
+ const description = descriptionSource.replace(/\s+/g, " ").trim() || undefined;
42
+ const maxConcurrency = metadata.maxConcurrency ? Number.parseInt(metadata.maxConcurrency, 10) : undefined;
24
43
  return {
25
44
  name,
26
- agent: agentMatch?.[1] ?? name,
27
- description: restPart?.replace(/agent\s*=\s*[\w-]+/, "").trim() || undefined,
45
+ agent: metadata.agent ?? name,
46
+ description,
47
+ model: metadata.model,
48
+ skills: parseRoleSkills(metadata.skills),
49
+ maxConcurrency: maxConcurrency && maxConcurrency > 0 ? maxConcurrency : undefined,
28
50
  };
29
51
  }
30
52