pi-crew 0.1.45 → 0.1.46

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 (198) hide show
  1. package/README.md +5 -5
  2. package/agents/analyst.md +1 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/executor.md +1 -1
  5. package/agents/explorer.md +1 -1
  6. package/agents/planner.md +1 -1
  7. package/agents/reviewer.md +1 -1
  8. package/agents/security-reviewer.md +1 -1
  9. package/agents/test-engineer.md +1 -1
  10. package/agents/verifier.md +1 -1
  11. package/agents/writer.md +1 -1
  12. package/docs/next-upgrade-roadmap.md +733 -0
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-oh-my-pi-distillation.md +322 -0
  21. package/docs/research-optimization-plan.md +548 -548
  22. package/docs/research-phase10-distillation.md +198 -198
  23. package/docs/research-phase11-distillation.md +201 -201
  24. package/docs/research-pi-coding-agent.md +357 -357
  25. package/docs/research-source-pi-crew-reference.md +174 -174
  26. package/docs/runtime-flow.md +148 -148
  27. package/docs/source-runtime-refactor-map.md +107 -83
  28. package/docs/usage.md +3 -3
  29. package/index.ts +6 -6
  30. package/install.mjs +52 -8
  31. package/package.json +1 -1
  32. package/schema.json +2 -1
  33. package/skills/async-worker-recovery/SKILL.md +42 -0
  34. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  35. package/skills/delegation-patterns/SKILL.md +54 -0
  36. package/skills/mailbox-interactive/SKILL.md +40 -0
  37. package/skills/model-routing-context/SKILL.md +39 -0
  38. package/skills/multi-perspective-review/SKILL.md +58 -0
  39. package/skills/observability-reliability/SKILL.md +41 -0
  40. package/skills/ownership-session-security/SKILL.md +41 -0
  41. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  42. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  43. package/skills/resource-discovery-config/SKILL.md +41 -0
  44. package/skills/runtime-state-reader/SKILL.md +44 -0
  45. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  46. package/skills/state-mutation-locking/SKILL.md +42 -0
  47. package/skills/systematic-debugging/SKILL.md +67 -0
  48. package/skills/ui-render-performance/SKILL.md +39 -0
  49. package/skills/verification-before-done/SKILL.md +57 -0
  50. package/skills/worktree-isolation/SKILL.md +39 -0
  51. package/src/agents/agent-serializer.ts +34 -34
  52. package/src/agents/discover-agents.ts +12 -11
  53. package/src/config/config.ts +48 -24
  54. package/src/config/defaults.ts +14 -0
  55. package/src/extension/cross-extension-rpc.ts +82 -82
  56. package/src/extension/project-init.ts +62 -2
  57. package/src/extension/register.ts +11 -9
  58. package/src/extension/registration/commands.ts +32 -25
  59. package/src/extension/registration/compaction-guard.ts +125 -125
  60. package/src/extension/registration/subagent-helpers.ts +8 -0
  61. package/src/extension/registration/subagent-tools.ts +149 -148
  62. package/src/extension/registration/team-tool.ts +8 -6
  63. package/src/extension/run-bundle-schema.ts +89 -89
  64. package/src/extension/run-index.ts +13 -5
  65. package/src/extension/run-maintenance.ts +62 -43
  66. package/src/extension/team-tool/api.ts +25 -8
  67. package/src/extension/team-tool/cancel.ts +33 -4
  68. package/src/extension/team-tool/context.ts +5 -0
  69. package/src/extension/team-tool/handle-settings.ts +188 -188
  70. package/src/extension/team-tool/inspect.ts +41 -41
  71. package/src/extension/team-tool/lifecycle-actions.ts +91 -79
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/respond.ts +37 -17
  74. package/src/extension/team-tool/run.ts +52 -10
  75. package/src/extension/team-tool/status.ts +12 -1
  76. package/src/extension/team-tool-types.ts +2 -0
  77. package/src/extension/team-tool.ts +32 -11
  78. package/src/i18n.ts +184 -184
  79. package/src/observability/event-to-metric.ts +8 -1
  80. package/src/observability/exporters/otlp-exporter.ts +77 -77
  81. package/src/prompt/prompt-runtime.ts +72 -72
  82. package/src/runtime/agent-control.ts +63 -63
  83. package/src/runtime/agent-memory.ts +72 -72
  84. package/src/runtime/agent-observability.ts +114 -114
  85. package/src/runtime/async-marker.ts +26 -26
  86. package/src/runtime/attention-events.ts +28 -28
  87. package/src/runtime/background-runner.ts +59 -53
  88. package/src/runtime/cancellation.ts +51 -0
  89. package/src/runtime/child-pi.ts +457 -444
  90. package/src/runtime/completion-guard.ts +190 -190
  91. package/src/runtime/crash-recovery.ts +1 -0
  92. package/src/runtime/crew-agent-records.ts +38 -6
  93. package/src/runtime/deadletter.ts +1 -0
  94. package/src/runtime/delivery-coordinator.ts +46 -25
  95. package/src/runtime/direct-run.ts +35 -35
  96. package/src/runtime/effectiveness.ts +76 -0
  97. package/src/runtime/foreground-control.ts +82 -82
  98. package/src/runtime/green-contract.ts +46 -46
  99. package/src/runtime/group-join.ts +106 -106
  100. package/src/runtime/heartbeat-gradient.ts +28 -28
  101. package/src/runtime/heartbeat-watcher.ts +124 -124
  102. package/src/runtime/live-agent-control.ts +88 -87
  103. package/src/runtime/live-agent-manager.ts +103 -85
  104. package/src/runtime/live-control-realtime.ts +36 -36
  105. package/src/runtime/live-session-runtime.ts +309 -305
  106. package/src/runtime/manifest-cache.ts +17 -2
  107. package/src/runtime/model-fallback.ts +6 -4
  108. package/src/runtime/parallel-research.ts +44 -44
  109. package/src/runtime/pi-args.ts +18 -3
  110. package/src/runtime/pi-json-output.ts +111 -111
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/process-status.ts +5 -1
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/retry-executor.ts +81 -64
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/runtime-resolver.ts +22 -6
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/skill-instructions.ts +222 -0
  123. package/src/runtime/stale-reconciler.ts +4 -14
  124. package/src/runtime/subagent-manager.ts +3 -0
  125. package/src/runtime/supervisor-contact.ts +59 -59
  126. package/src/runtime/task-display.ts +38 -38
  127. package/src/runtime/task-output-context.ts +127 -127
  128. package/src/runtime/task-runner/capabilities.ts +78 -0
  129. package/src/runtime/task-runner/live-executor.ts +105 -101
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  132. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  133. package/src/runtime/task-runner/result-utils.ts +14 -14
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/task-runner.ts +44 -5
  136. package/src/runtime/team-runner.ts +78 -15
  137. package/src/runtime/worker-heartbeat.ts +21 -21
  138. package/src/runtime/worker-startup.ts +57 -57
  139. package/src/schema/config-schema.ts +1 -0
  140. package/src/schema/team-tool-schema.ts +3 -3
  141. package/src/state/active-run-registry.ts +165 -0
  142. package/src/state/contracts.ts +1 -1
  143. package/src/state/mailbox.ts +44 -4
  144. package/src/state/state-store.ts +8 -1
  145. package/src/state/task-claims.ts +44 -44
  146. package/src/state/types.ts +44 -2
  147. package/src/state/usage.ts +29 -29
  148. package/src/subagents/async-entry.ts +1 -1
  149. package/src/subagents/index.ts +3 -3
  150. package/src/subagents/live/control.ts +1 -1
  151. package/src/subagents/live/manager.ts +1 -1
  152. package/src/subagents/live/realtime.ts +1 -1
  153. package/src/subagents/live/session-runtime.ts +1 -1
  154. package/src/subagents/manager.ts +1 -1
  155. package/src/subagents/spawn.ts +1 -1
  156. package/src/teams/team-config.ts +1 -0
  157. package/src/teams/team-serializer.ts +38 -38
  158. package/src/types/diff.d.ts +18 -18
  159. package/src/ui/crew-footer.ts +101 -101
  160. package/src/ui/crew-select-list.ts +111 -111
  161. package/src/ui/crew-widget.ts +4 -3
  162. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  163. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  164. package/src/ui/dynamic-border.ts +25 -25
  165. package/src/ui/layout-primitives.ts +106 -106
  166. package/src/ui/loaders.ts +158 -158
  167. package/src/ui/render-diff.ts +119 -119
  168. package/src/ui/render-scheduler.ts +143 -143
  169. package/src/ui/run-snapshot-cache.ts +10 -2
  170. package/src/ui/snapshot-types.ts +2 -0
  171. package/src/ui/spinner.ts +17 -17
  172. package/src/ui/status-colors.ts +58 -58
  173. package/src/ui/syntax-highlight.ts +116 -116
  174. package/src/utils/atomic-write.ts +33 -33
  175. package/src/utils/completion-dedupe.ts +63 -63
  176. package/src/utils/frontmatter.ts +68 -68
  177. package/src/utils/git.ts +262 -262
  178. package/src/utils/ids.ts +12 -12
  179. package/src/utils/names.ts +27 -27
  180. package/src/utils/paths.ts +4 -2
  181. package/src/utils/redaction.ts +44 -44
  182. package/src/utils/safe-paths.ts +47 -47
  183. package/src/utils/sleep.ts +32 -32
  184. package/src/workflows/validate-workflow.ts +40 -40
  185. package/src/workflows/workflow-config.ts +1 -0
  186. package/src/worktree/branch-freshness.ts +45 -45
  187. package/teams/default.team.md +12 -12
  188. package/teams/fast-fix.team.md +11 -11
  189. package/teams/implementation.team.md +18 -18
  190. package/teams/parallel-research.team.md +14 -14
  191. package/teams/research.team.md +11 -11
  192. package/teams/review.team.md +12 -12
  193. package/workflows/default.workflow.md +29 -29
  194. package/workflows/fast-fix.workflow.md +22 -22
  195. package/workflows/implementation.workflow.md +38 -38
  196. package/workflows/parallel-research.workflow.md +46 -46
  197. package/workflows/research.workflow.md +22 -22
  198. package/workflows/review.workflow.md +30 -30
@@ -0,0 +1,165 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
4
+ import type { TeamRunManifest } from "./types.ts";
5
+ import { atomicWriteJson } from "./atomic-write.ts";
6
+ import { userCrewRoot } from "../utils/paths.ts";
7
+ import { isSafePathId } from "../utils/safe-paths.ts";
8
+
9
+ export interface ActiveRunRegistryEntry {
10
+ runId: string;
11
+ cwd: string;
12
+ stateRoot: string;
13
+ manifestPath: string;
14
+ updatedAt: string;
15
+ }
16
+
17
+ function registryPath(): string {
18
+ return path.join(userCrewRoot(), DEFAULT_PATHS.state.runsSubdir, "active-run-index.json");
19
+ }
20
+
21
+ function registryLockPath(): string {
22
+ return `${registryPath()}.lock`;
23
+ }
24
+
25
+ function sleepSync(ms: number): void {
26
+ try {
27
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
28
+ } catch {
29
+ const deadline = Date.now() + ms;
30
+ while (Date.now() < deadline) {
31
+ // Best-effort fallback for rare runtimes without Atomics.wait.
32
+ }
33
+ }
34
+ }
35
+
36
+ function lockCreatedAt(raw: string): number | undefined {
37
+ try {
38
+ const parsed = JSON.parse(raw) as { createdAt?: unknown };
39
+ if (typeof parsed.createdAt !== "string") return undefined;
40
+ const time = Date.parse(parsed.createdAt);
41
+ return Number.isNaN(time) ? undefined : time;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ function removeStaleRegistryLock(lockPath: string, staleMs: number): boolean {
48
+ try {
49
+ const stat = fs.statSync(lockPath);
50
+ const createdAt = lockCreatedAt(fs.readFileSync(lockPath, "utf-8")) ?? stat.mtimeMs;
51
+ if (Date.now() - createdAt <= staleMs) return false;
52
+ fs.rmSync(lockPath, { force: true });
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function withRegistryLock<T>(fn: () => T): T {
60
+ const filePath = registryLockPath();
61
+ const staleMs = 30_000;
62
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
+ let attempt = 0;
64
+ const deadline = Date.now() + staleMs * 2;
65
+ while (true) {
66
+ try {
67
+ const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644);
68
+ try {
69
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
70
+ } finally {
71
+ fs.closeSync(fd);
72
+ }
73
+ break;
74
+ } catch (error) {
75
+ const code = (error as NodeJS.ErrnoException).code;
76
+ if (code !== "EEXIST") throw error;
77
+ if (!removeStaleRegistryLock(filePath, staleMs) && Date.now() > deadline) throw new Error("Active-run registry is locked by another operation.");
78
+ sleepSync(Math.min(250, 25 * 2 ** attempt));
79
+ attempt += 1;
80
+ }
81
+ }
82
+ try {
83
+ return fn();
84
+ } finally {
85
+ try {
86
+ fs.rmSync(filePath, { force: true });
87
+ } catch {
88
+ // Best-effort cleanup.
89
+ }
90
+ }
91
+ }
92
+
93
+ function normalizeEntry(value: unknown): ActiveRunRegistryEntry | undefined {
94
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
95
+ const record = value as Record<string, unknown>;
96
+ const runId = typeof record.runId === "string" ? record.runId : undefined;
97
+ const cwd = typeof record.cwd === "string" ? record.cwd : undefined;
98
+ const stateRoot = typeof record.stateRoot === "string" ? record.stateRoot : undefined;
99
+ const manifestPath = typeof record.manifestPath === "string" ? record.manifestPath : undefined;
100
+ const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : undefined;
101
+ if (!runId || !isSafePathId(runId) || !cwd || !stateRoot || !manifestPath || !updatedAt) return undefined;
102
+ if (path.basename(stateRoot) !== runId) return undefined;
103
+ if (path.resolve(manifestPath) !== path.resolve(path.join(stateRoot, DEFAULT_PATHS.state.manifestFile))) return undefined;
104
+ return { runId, cwd, stateRoot, manifestPath, updatedAt };
105
+ }
106
+
107
+ export function readActiveRunRegistry(maxEntries = DEFAULT_CACHE.manifestMaxEntries): ActiveRunRegistryEntry[] {
108
+ let parsed: unknown;
109
+ try {
110
+ parsed = JSON.parse(fs.readFileSync(registryPath(), "utf-8"));
111
+ } catch {
112
+ return [];
113
+ }
114
+ const entries = Array.isArray(parsed) ? parsed.map(normalizeEntry).filter((entry): entry is ActiveRunRegistryEntry => entry !== undefined) : [];
115
+ const byId = new Map<string, ActiveRunRegistryEntry>();
116
+ for (const entry of entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))) {
117
+ if (!byId.has(entry.runId)) byId.set(entry.runId, entry);
118
+ }
119
+ return [...byId.values()].slice(0, Math.max(0, maxEntries));
120
+ }
121
+
122
+ function writeEntries(entries: ActiveRunRegistryEntry[]): void {
123
+ fs.mkdirSync(path.dirname(registryPath()), { recursive: true });
124
+ atomicWriteJson(registryPath(), entries.slice(0, DEFAULT_CACHE.manifestMaxEntries));
125
+ }
126
+
127
+ export function registerActiveRun(manifest: TeamRunManifest): void {
128
+ const entry: ActiveRunRegistryEntry = {
129
+ runId: manifest.runId,
130
+ cwd: manifest.cwd,
131
+ stateRoot: manifest.stateRoot,
132
+ manifestPath: path.join(manifest.stateRoot, DEFAULT_PATHS.state.manifestFile),
133
+ updatedAt: manifest.updatedAt,
134
+ };
135
+ withRegistryLock(() => {
136
+ writeEntries([entry, ...readActiveRunRegistry().filter((item) => item.runId !== manifest.runId)]);
137
+ });
138
+ }
139
+
140
+ export function unregisterActiveRun(runId: string): void {
141
+ if (!isSafePathId(runId)) return;
142
+ withRegistryLock(() => {
143
+ writeEntries(readActiveRunRegistry().filter((entry) => entry.runId !== runId));
144
+ });
145
+ }
146
+
147
+ export function activeRunEntries(): ActiveRunRegistryEntry[] {
148
+ const entries: ActiveRunRegistryEntry[] = [];
149
+ for (const entry of readActiveRunRegistry()) {
150
+ try {
151
+ if (!fs.existsSync(entry.stateRoot) || !fs.existsSync(entry.manifestPath)) continue;
152
+ if (fs.lstatSync(entry.stateRoot).isSymbolicLink()) continue;
153
+ const manifest = JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8")) as { status?: unknown };
154
+ if (manifest.status !== "queued" && manifest.status !== "planning" && manifest.status !== "running") continue;
155
+ entries.push(entry);
156
+ } catch {
157
+ // Ignore stale entries; callers filter active status from manifests.
158
+ }
159
+ }
160
+ return entries;
161
+ }
162
+
163
+ export function activeRunRoots(): string[] {
164
+ return [...new Set(activeRunEntries().map((entry) => path.dirname(entry.stateRoot)))];
165
+ }
@@ -20,7 +20,7 @@ export const TEAM_RUN_STATUS_TRANSITIONS: Readonly<Record<TeamRunStatus, readonl
20
20
  export const TEAM_TASK_STATUS_TRANSITIONS: Readonly<Record<TeamTaskStatus, readonly TeamTaskStatus[]>> = {
21
21
  queued: ["running", "cancelled", "skipped", "failed"],
22
22
  running: ["completed", "failed", "cancelled", "queued", "waiting"],
23
- waiting: ["running", "completed", "failed", "cancelled"],
23
+ waiting: ["running", "queued", "completed", "failed", "cancelled"],
24
24
  completed: ["queued"],
25
25
  failed: ["queued", "cancelled"],
26
26
  cancelled: ["queued"],
@@ -6,6 +6,9 @@ import { redactSecrets } from "../utils/redaction.ts";
6
6
 
7
7
  export type MailboxDirection = "inbox" | "outbox";
8
8
  export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
9
+ export type MailboxMessageKind = "message" | "steer" | "follow-up" | "response" | "group_join";
10
+ export type MailboxMessagePriority = "urgent" | "normal" | "low";
11
+ export type MailboxDeliveryMode = "interrupt" | "next_turn";
9
12
 
10
13
  export interface MailboxMessage {
11
14
  id: string;
@@ -16,6 +19,9 @@ export interface MailboxMessage {
16
19
  body: string;
17
20
  createdAt: string;
18
21
  status: MailboxMessageStatus;
22
+ kind?: MailboxMessageKind;
23
+ priority?: MailboxMessagePriority;
24
+ deliveryMode?: MailboxDeliveryMode;
19
25
  taskId?: string;
20
26
  acknowledgedAt?: string;
21
27
  data?: Record<string, unknown>;
@@ -130,12 +136,26 @@ function isStatus(value: unknown): value is MailboxMessageStatus {
130
136
  return value === "queued" || value === "delivered" || value === "acknowledged";
131
137
  }
132
138
 
139
+ function isKind(value: unknown): value is MailboxMessageKind {
140
+ return value === "message" || value === "steer" || value === "follow-up" || value === "response" || value === "group_join";
141
+ }
142
+
143
+ function isPriority(value: unknown): value is MailboxMessagePriority {
144
+ return value === "urgent" || value === "normal" || value === "low";
145
+ }
146
+
147
+ function isDeliveryMode(value: unknown): value is MailboxDeliveryMode {
148
+ return value === "interrupt" || value === "next_turn";
149
+ }
150
+
133
151
  function parseMailboxMessage(raw: unknown, expectedDirection: MailboxDirection): MailboxMessage | undefined {
134
152
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
135
153
  const obj = raw as Record<string, unknown>;
136
154
  if (typeof obj.id !== "string" || typeof obj.runId !== "string" || !isDirection(obj.direction) || typeof obj.from !== "string" || typeof obj.to !== "string" || typeof obj.body !== "string" || typeof obj.createdAt !== "string" || !isStatus(obj.status)) return undefined;
137
155
  if (obj.direction !== expectedDirection) return undefined;
138
- return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined, data: obj.data && typeof obj.data === "object" && !Array.isArray(obj.data) ? obj.data as Record<string, unknown> : undefined };
156
+ const data = obj.data && typeof obj.data === "object" && !Array.isArray(obj.data) ? obj.data as Record<string, unknown> : undefined;
157
+ const dataKind = data?.kind;
158
+ return { id: obj.id, runId: obj.runId, direction: obj.direction, from: obj.from, to: obj.to, body: obj.body, createdAt: obj.createdAt, status: obj.status, kind: isKind(obj.kind) ? obj.kind : isKind(dataKind) ? dataKind : undefined, priority: isPriority(obj.priority) ? obj.priority : undefined, deliveryMode: isDeliveryMode(obj.deliveryMode) ? obj.deliveryMode : undefined, taskId: typeof obj.taskId === "string" ? obj.taskId : undefined, acknowledgedAt: typeof obj.acknowledgedAt === "string" ? obj.acknowledgedAt : undefined, data };
139
159
  }
140
160
 
141
161
  function readMailboxFile(filePath: string, direction: MailboxDirection): MailboxMessage[] {
@@ -163,18 +183,22 @@ export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirect
163
183
  return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
164
184
  }
165
185
 
166
- function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
167
- const messages = [...safeReadMailboxFile(mailboxFile(manifest, "inbox"), "inbox")];
186
+ function readAllMessages(manifest: TeamRunManifest, direction: MailboxDirection): MailboxMessage[] {
187
+ const messages = [...safeReadMailboxFile(mailboxFile(manifest, direction), direction)];
168
188
  const tasksDir = safeMailboxTasksRoot(manifest);
169
189
  if (fs.existsSync(tasksDir)) {
170
190
  for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
171
191
  if (!entry.isDirectory()) continue;
172
- messages.push(...safeReadMailboxFile(mailboxFile(manifest, "inbox", entry.name), "inbox"));
192
+ messages.push(...safeReadMailboxFile(mailboxFile(manifest, direction, entry.name), direction));
173
193
  }
174
194
  }
175
195
  return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
176
196
  }
177
197
 
198
+ function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
199
+ return readAllMessages(manifest, "inbox");
200
+ }
201
+
178
202
  export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
179
203
  try {
180
204
  const raw = JSON.parse(fs.readFileSync(deliveryFile(manifest), "utf-8")) as unknown;
@@ -208,6 +232,9 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
208
232
  body: message.body,
209
233
  createdAt,
210
234
  status: message.status ?? "queued",
235
+ kind: message.kind,
236
+ priority: message.priority,
237
+ deliveryMode: message.deliveryMode,
211
238
  taskId: message.taskId,
212
239
  data: message.data,
213
240
  };
@@ -219,6 +246,19 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
219
246
  return complete;
220
247
  }
221
248
 
249
+ export function appendSteeringMessage(manifest: TeamRunManifest, input: { taskId: string; body: string; from?: string; to?: string; priority?: MailboxMessagePriority; status?: MailboxMessageStatus; data?: Record<string, unknown> }): MailboxMessage {
250
+ return appendMailboxMessage(manifest, { direction: "inbox", from: input.from ?? "leader", to: input.to ?? input.taskId, taskId: input.taskId, body: input.body, kind: "steer", priority: input.priority ?? "urgent", deliveryMode: "interrupt", status: input.status, data: { ...(input.data ?? {}), kind: "steer" } });
251
+ }
252
+
253
+ export function appendFollowUpMessage(manifest: TeamRunManifest, input: { taskId: string; body: string; from?: string; to?: string; priority?: MailboxMessagePriority; status?: MailboxMessageStatus; data?: Record<string, unknown> }): MailboxMessage {
254
+ return appendMailboxMessage(manifest, { direction: "inbox", from: input.from ?? "leader", to: input.to ?? input.taskId, taskId: input.taskId, body: input.body, kind: "follow-up", priority: input.priority ?? "normal", deliveryMode: "next_turn", status: input.status, data: { ...(input.data ?? {}), kind: "follow-up" } });
255
+ }
256
+
257
+ export function listMailboxByKind(manifest: TeamRunManifest, kind: MailboxMessageKind, direction?: MailboxDirection): MailboxMessage[] {
258
+ const messages = direction ? readAllMessages(manifest, direction) : [...readAllMessages(manifest, "inbox"), ...readAllMessages(manifest, "outbox")].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
259
+ return messages.filter((message) => message.kind === kind || message.data?.kind === kind);
260
+ }
261
+
222
262
  export function findMailboxMessageByRequestId(manifest: TeamRunManifest, requestId: string): MailboxMessage | undefined {
223
263
  return readMailbox(manifest).find((message) => message.data?.requestId === requestId);
224
264
  }
@@ -196,7 +196,12 @@ export async function saveRunTasksAsync(manifest: TeamRunManifest, tasks: TeamTa
196
196
  invalidateRunCache(manifest.stateRoot);
197
197
  }
198
198
 
199
- export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string): TeamRunManifest {
199
+ export interface UpdateRunStatusOptions {
200
+ data?: Record<string, unknown>;
201
+ metadata?: Parameters<typeof appendEvent>[1]["metadata"];
202
+ }
203
+
204
+ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManifest["status"], summary?: string, options: UpdateRunStatusOptions = {}): TeamRunManifest {
200
205
  if (!canTransitionRunStatus(manifest.status, status)) {
201
206
  throw new Error(`Invalid run status transition: ${manifest.status} -> ${status}`);
202
207
  }
@@ -206,11 +211,13 @@ export function updateRunStatus(manifest: TeamRunManifest, status: TeamRunManife
206
211
  type: `run.${status}`,
207
212
  runId: updated.runId,
208
213
  message: summary,
214
+ ...(options.data ? { data: options.data } : {}),
209
215
  metadata: {
210
216
  provenance: "team_runner",
211
217
  sessionIdentity: { title: updated.team, workspace: updated.cwd, purpose: updated.goal },
212
218
  ownership: { owner: updated.team, workflowScope: updated.workflow ?? "manual", watcherAction: "act" },
213
219
  confidence: "high",
220
+ ...options.metadata,
214
221
  },
215
222
  });
216
223
  return updated;
@@ -1,44 +1,44 @@
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
- const parsed = Date.parse(claim.leasedUntil);
17
- // Corrupt or invalid date strings produce NaN — treat as expired immediately.
18
- return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
19
- }
20
-
21
- export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
22
- return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
23
- }
24
-
25
- export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
26
- if (task.claim && !isTaskClaimExpired(task.claim, now)) {
27
- throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
28
- }
29
- return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
30
- }
31
-
32
- export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
33
- if (!canUseTaskClaim(task, owner, token, now)) {
34
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
35
- }
36
- return { ...task, claim: undefined };
37
- }
38
-
39
- export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
40
- if (!canUseTaskClaim(task, owner, token, now)) {
41
- throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
42
- }
43
- return { ...task, status };
44
- }
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
+ const parsed = Date.parse(claim.leasedUntil);
17
+ // Corrupt or invalid date strings produce NaN — treat as expired immediately.
18
+ return Number.isFinite(parsed) ? parsed <= now.getTime() : true;
19
+ }
20
+
21
+ export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
22
+ return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
23
+ }
24
+
25
+ export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
26
+ if (task.claim && !isTaskClaimExpired(task.claim, now)) {
27
+ throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
28
+ }
29
+ return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
30
+ }
31
+
32
+ export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
33
+ if (!canUseTaskClaim(task, owner, token, now)) {
34
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
35
+ }
36
+ return { ...task, claim: undefined };
37
+ }
38
+
39
+ export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
40
+ if (!canUseTaskClaim(task, owner, token, now)) {
41
+ throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
42
+ }
43
+ return { ...task, status };
44
+ }
@@ -55,8 +55,8 @@ export interface TaskPacket {
55
55
  verification: VerificationContract;
56
56
  }
57
57
 
58
- export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout";
59
- export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch";
58
+ export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout" | "fail";
59
+ export type PolicyDecisionReason = "task_failed" | "worker_stale" | "green_unsatisfied" | "limit_exceeded" | "run_complete" | "mailbox_timeout" | "review_rejected" | "branch_stale" | "scope_mismatch" | "ineffective_worker";
60
60
 
61
61
  export interface PolicyDecision {
62
62
  action: PolicyDecisionAction;
@@ -81,6 +81,39 @@ export interface AsyncRunState {
81
81
  spawnedAt: string;
82
82
  }
83
83
 
84
+ export interface RuntimeResolutionState {
85
+ kind: "scaffold" | "child-process" | "live-session";
86
+ requestedMode: "auto" | "scaffold" | "child-process" | "live-session";
87
+ safety: "trusted" | "explicit_dry_run" | "blocked";
88
+ available: boolean;
89
+ fallback?: "scaffold" | "child-process" | "live-session";
90
+ reason?: string;
91
+ resolvedAt: string;
92
+ }
93
+
94
+ export interface WorkerExitStatus {
95
+ exitCode: number | null;
96
+ cancelled: boolean;
97
+ timedOut: boolean;
98
+ killed: boolean;
99
+ signal?: string;
100
+ cleanupErrors: string[];
101
+ finalDrainMs: number;
102
+ }
103
+
104
+ export interface OperationTerminalEvidence {
105
+ operation: "worker" | "tool" | "model";
106
+ status: "cancelled" | "failed" | "completed";
107
+ startedAt?: string;
108
+ finishedAt: string;
109
+ attemptId?: string;
110
+ reason?: {
111
+ code: string;
112
+ message: string;
113
+ };
114
+ exitStatus?: WorkerExitStatus;
115
+ }
116
+
84
117
  export interface PlanApprovalState {
85
118
  required: boolean;
86
119
  status: "pending" | "approved" | "cancelled";
@@ -125,6 +158,12 @@ export interface TeamRunManifest {
125
158
  planApproval?: PlanApprovalState;
126
159
  /** Pi session that created the run, when available. Used to prevent cross-session destructive actions. */
127
160
  ownerSessionId?: string;
161
+ /** pi-crew skill override selected when the run was created. false disables injected skill instructions. */
162
+ skillOverride?: string[] | false;
163
+ /** Resolved runtime/safety mode used for execution. Optional for backward compatibility with older manifests. */
164
+ runtimeResolution?: RuntimeResolutionState;
165
+ /** Effective run config snapshot used by async background workers. Optional for backward compatibility. */
166
+ runConfig?: unknown;
128
167
  summary?: string;
129
168
  policyDecisions?: PolicyDecision[];
130
169
  }
@@ -166,6 +205,7 @@ export interface TaskCheckpointState {
166
205
  }
167
206
 
168
207
  export interface TaskAttemptState {
208
+ attemptId?: string;
169
209
  startedAt: string;
170
210
  endedAt?: string;
171
211
  error?: string;
@@ -200,6 +240,8 @@ export interface TeamTaskState {
200
240
  heartbeat?: WorkerHeartbeatState;
201
241
  checkpoint?: TaskCheckpointState;
202
242
  attempts?: TaskAttemptState[];
243
+ workerExitStatus?: WorkerExitStatus;
244
+ terminalEvidence?: OperationTerminalEvidence[];
203
245
  taskPacket?: TaskPacket;
204
246
  verification?: VerificationEvidence;
205
247
  graph?: TaskGraphNode;
@@ -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 && Number.isFinite(usage.cost)) 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 && Number.isFinite(usage.cost)) 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";
@@ -5,6 +5,7 @@ export interface TeamRole {
5
5
  agent: string;
6
6
  description?: string;
7
7
  model?: string;
8
+ /** Additional skills for this role; false disables role-default injected skills for tasks using this role. */
8
9
  skills?: string[] | false;
9
10
  maxConcurrency?: number;
10
11
  }