pi-crew 0.1.43 → 0.1.45

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 (158) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +199 -0
  20. package/docs/research-phase11-distillation.md +201 -0
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/agents/discover-agents.ts +1 -0
  29. package/src/config/config.ts +19 -0
  30. package/src/extension/cross-extension-rpc.ts +82 -82
  31. package/src/extension/register.ts +134 -8
  32. package/src/extension/registration/commands.ts +18 -2
  33. package/src/extension/registration/compaction-guard.ts +125 -125
  34. package/src/extension/registration/subagent-tools.ts +148 -148
  35. package/src/extension/registration/team-tool.ts +27 -8
  36. package/src/extension/run-bundle-schema.ts +89 -89
  37. package/src/extension/run-index.ts +19 -0
  38. package/src/extension/run-maintenance.ts +43 -43
  39. package/src/extension/team-tool/api.ts +1 -1
  40. package/src/extension/team-tool/cancel.ts +79 -4
  41. package/src/extension/team-tool/context.ts +2 -0
  42. package/src/extension/team-tool/handle-settings.ts +188 -188
  43. package/src/extension/team-tool/inspect.ts +41 -41
  44. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  45. package/src/extension/team-tool/plan.ts +19 -19
  46. package/src/extension/team-tool/respond.ts +84 -0
  47. package/src/extension/team-tool/run.ts +3 -2
  48. package/src/extension/team-tool/status.ts +7 -1
  49. package/src/extension/team-tool-types.ts +4 -0
  50. package/src/extension/team-tool.ts +2 -0
  51. package/src/i18n.ts +184 -184
  52. package/src/observability/event-to-metric.ts +6 -0
  53. package/src/observability/exporters/otlp-exporter.ts +77 -77
  54. package/src/prompt/prompt-runtime.ts +72 -72
  55. package/src/runtime/agent-control.ts +63 -63
  56. package/src/runtime/agent-memory.ts +72 -72
  57. package/src/runtime/agent-observability.ts +114 -114
  58. package/src/runtime/async-marker.ts +26 -26
  59. package/src/runtime/attention-events.ts +28 -28
  60. package/src/runtime/background-runner.ts +53 -53
  61. package/src/runtime/child-pi.ts +444 -444
  62. package/src/runtime/completion-guard.ts +87 -0
  63. package/src/runtime/crash-recovery.ts +30 -0
  64. package/src/runtime/crew-agent-records.ts +8 -0
  65. package/src/runtime/crew-agent-runtime.ts +2 -1
  66. package/src/runtime/delivery-coordinator.ts +154 -0
  67. package/src/runtime/direct-run.ts +35 -35
  68. package/src/runtime/foreground-control.ts +82 -82
  69. package/src/runtime/green-contract.ts +46 -46
  70. package/src/runtime/group-join.ts +106 -106
  71. package/src/runtime/heartbeat-gradient.ts +28 -28
  72. package/src/runtime/heartbeat-watcher.ts +124 -124
  73. package/src/runtime/live-agent-control.ts +87 -87
  74. package/src/runtime/live-agent-manager.ts +85 -85
  75. package/src/runtime/live-control-realtime.ts +36 -36
  76. package/src/runtime/live-session-runtime.ts +305 -305
  77. package/src/runtime/model-fallback.ts +5 -2
  78. package/src/runtime/overflow-recovery.ts +176 -0
  79. package/src/runtime/parallel-research.ts +44 -44
  80. package/src/runtime/pi-json-output.ts +111 -111
  81. package/src/runtime/policy-engine.ts +79 -79
  82. package/src/runtime/process-status.ts +1 -1
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/retry-executor.ts +64 -64
  86. package/src/runtime/role-permission.ts +39 -39
  87. package/src/runtime/session-resources.ts +25 -0
  88. package/src/runtime/session-snapshot.ts +59 -0
  89. package/src/runtime/session-usage.ts +79 -79
  90. package/src/runtime/sidechain-output.ts +29 -29
  91. package/src/runtime/stale-reconciler.ts +199 -0
  92. package/src/runtime/supervisor-contact.ts +59 -0
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -127
  95. package/src/runtime/task-runner/live-executor.ts +101 -101
  96. package/src/runtime/task-runner/progress.ts +119 -119
  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/task-runner.ts +14 -0
  100. package/src/runtime/team-runner.ts +19 -8
  101. package/src/runtime/worker-heartbeat.ts +21 -21
  102. package/src/runtime/worker-startup.ts +57 -57
  103. package/src/schema/config-schema.ts +1 -0
  104. package/src/schema/team-tool-schema.ts +6 -1
  105. package/src/state/contracts.ts +6 -2
  106. package/src/state/state-store.ts +43 -0
  107. package/src/state/task-claims.ts +44 -44
  108. package/src/state/types.ts +2 -0
  109. package/src/state/usage.ts +29 -29
  110. package/src/subagents/async-entry.ts +1 -1
  111. package/src/subagents/index.ts +3 -3
  112. package/src/subagents/live/control.ts +1 -1
  113. package/src/subagents/live/manager.ts +1 -1
  114. package/src/subagents/live/realtime.ts +1 -1
  115. package/src/subagents/live/session-runtime.ts +1 -1
  116. package/src/subagents/manager.ts +1 -1
  117. package/src/subagents/spawn.ts +1 -1
  118. package/src/teams/team-serializer.ts +38 -38
  119. package/src/types/diff.d.ts +18 -18
  120. package/src/ui/crew-footer.ts +101 -101
  121. package/src/ui/crew-select-list.ts +111 -111
  122. package/src/ui/crew-widget.ts +10 -5
  123. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  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/powerbar-publisher.ts +4 -4
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/render-scheduler.ts +143 -143
  131. package/src/ui/run-snapshot-cache.ts +310 -17
  132. package/src/ui/snapshot-types.ts +5 -0
  133. package/src/ui/spinner.ts +17 -17
  134. package/src/ui/status-colors.ts +58 -54
  135. package/src/ui/syntax-highlight.ts +116 -116
  136. package/src/utils/atomic-write.ts +33 -0
  137. package/src/utils/completion-dedupe.ts +63 -63
  138. package/src/utils/frontmatter.ts +68 -68
  139. package/src/utils/git.ts +262 -262
  140. package/src/utils/ids.ts +12 -12
  141. package/src/utils/names.ts +27 -27
  142. package/src/utils/redaction.ts +44 -44
  143. package/src/utils/safe-paths.ts +47 -47
  144. package/src/utils/sleep.ts +32 -32
  145. package/src/workflows/validate-workflow.ts +40 -40
  146. package/src/worktree/branch-freshness.ts +45 -45
  147. package/teams/default.team.md +12 -12
  148. package/teams/fast-fix.team.md +11 -11
  149. package/teams/implementation.team.md +18 -18
  150. package/teams/parallel-research.team.md +14 -14
  151. package/teams/research.team.md +11 -11
  152. package/teams/review.team.md +12 -12
  153. package/workflows/default.workflow.md +29 -29
  154. package/workflows/fast-fix.workflow.md +22 -22
  155. package/workflows/implementation.workflow.md +38 -38
  156. package/workflows/parallel-research.workflow.md +46 -46
  157. package/workflows/research.workflow.md +22 -22
  158. package/workflows/review.workflow.md +30 -30
@@ -1,4 +1,10 @@
1
1
  import * as fs from "node:fs";
2
+ import type { TeamTaskState, TeamRunManifest } from "../state/types.ts";
3
+
4
+ // ============================================================================
5
+ // Phase 1.2: Completion Mutation Guard — detects tasks that claim success but
6
+ // made no observable mutations. Used by task-runner.ts.
7
+ // ============================================================================
2
8
 
3
9
  export interface CompletionMutationGuardInput {
4
10
  role: string;
@@ -101,3 +107,84 @@ export function evaluateCompletionMutationGuard(input: CompletionMutationGuardIn
101
107
  ...(expectedMutation && !observedMutation ? { reason: "no_mutation_observed" as const } : {}),
102
108
  };
103
109
  }
110
+
111
+ // ============================================================================
112
+ // Phase 11a: Artifact-based Completion Verification — a second layer that
113
+ // checks whether a completed task actually produced meaningful artifacts.
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Guard against false-positive task completions.
118
+ *
119
+ * Checks whether a task that claims success actually produced meaningful output.
120
+ * Returns a verification result with the green level (0-3) and any warnings.
121
+ */
122
+ export interface CompletionVerifyResult {
123
+ /** 0 = no output, 1 = minimal, 2 = moderate, 3 = strong */
124
+ greenLevel: number;
125
+ /** Warnings about potentially incomplete work */
126
+ warnings: string[];
127
+ }
128
+
129
+ const MAX_OUTPUT_PREVIEW = 200;
130
+
131
+ function isTrivialError(error: string | undefined): boolean {
132
+ if (!error) return false;
133
+ return error.trim().length === 0;
134
+ }
135
+
136
+ export function verifyTaskCompletion(
137
+ task: TeamTaskState,
138
+ manifest: TeamRunManifest,
139
+ ): CompletionVerifyResult {
140
+ const warnings: string[] = [];
141
+ let greenLevel = 0;
142
+
143
+ // Check 1: Has an error?
144
+ if (task.error && !isTrivialError(task.error)) {
145
+ return { greenLevel: 0, warnings: [`Task has error: ${task.error}`] };
146
+ }
147
+
148
+ // Check 2: Has result artifact?
149
+ if (task.resultArtifact) {
150
+ greenLevel += 1;
151
+ }
152
+
153
+ // Check 3: Has transcript?
154
+ if (task.transcriptArtifact) {
155
+ greenLevel += 1;
156
+ }
157
+
158
+ // Check 4: For implementation tasks, verify artifacts were actually produced
159
+ const runArtifacts = manifest.artifacts.filter(
160
+ (a) => a.producer === task.id || a.producer === task.agent,
161
+ );
162
+ if (runArtifacts.length > 0) {
163
+ greenLevel += 1;
164
+ } else if (greenLevel < 3) {
165
+ warnings.push("No run-level artifacts produced by this task");
166
+ }
167
+
168
+ // Check 5: Usage tracking — did the task actually consume tokens?
169
+ if (task.usage) {
170
+ const totalTokens = (task.usage.input ?? 0) + (task.usage.output ?? 0);
171
+ if (totalTokens === 0 && greenLevel < 3) {
172
+ warnings.push("Task reports zero token usage — may not have executed");
173
+ }
174
+ }
175
+
176
+ return {
177
+ greenLevel: Math.min(greenLevel, 3),
178
+ warnings,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Format a preview of task output for diagnostic display.
184
+ */
185
+ export function formatOutputPreview(output: string | undefined): string {
186
+ if (!output) return "(no output)";
187
+ const trimmed = output.trim();
188
+ if (trimmed.length <= MAX_OUTPUT_PREVIEW) return trimmed;
189
+ return trimmed.slice(0, MAX_OUTPUT_PREVIEW) + "...";
190
+ }
@@ -1,11 +1,13 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import type { MetricRegistry } from "../observability/metric-registry.ts";
3
3
  import { appendEvent, scanSequence } from "../state/event-log.ts";
4
+ import { withRunLockSync } from "../state/locks.ts";
4
5
  import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../state/state-store.ts";
5
6
  import type { TeamTaskState } from "../state/types.ts";
6
7
  import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
7
8
  import type { ManifestCache } from "./manifest-cache.ts";
8
9
  import { checkProcessLiveness } from "./process-status.ts";
10
+ import { reconcileStaleRun, type ReconcileResult } from "./stale-reconciler.ts";
9
11
 
10
12
  export interface RecoveryPlan {
11
13
  runId: string;
@@ -55,3 +57,31 @@ export function declineRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionConte
55
57
  appendEvent(loaded.manifest.eventsPath, { type: "crew.run.recovery_declined", runId: plan.runId, message: "Interrupted run was not resumed.", data: { recoveredFromSeq: plan.lastEventSeq } });
56
58
  updateRunStatus(loaded.manifest, "cancelled", "interrupted-not-resumed");
57
59
  }
60
+
61
+ /**
62
+ * Run 3-phase stale reconciliation on all active runs.
63
+ * Returns results for each reconciled run.
64
+ */
65
+ export function reconcileAllStaleRuns(cwd: string, manifestCache: ManifestCache, now = Date.now()): ReconcileResult[] {
66
+ const results: ReconcileResult[] = [];
67
+ for (const manifest of manifestCache.list(50)) {
68
+ if (manifest.status !== "running") continue;
69
+ const loaded = loadRunManifestById(cwd, manifest.runId);
70
+ if (!loaded) continue;
71
+ // Use lock to prevent race with cancel/status handlers modifying the same run
72
+ withRunLockSync(loaded.manifest, () => {
73
+ // Re-read inside lock to get freshest data
74
+ const fresh = loadRunManifestById(cwd, manifest.runId);
75
+ if (!fresh || fresh.manifest.status !== "running") return;
76
+ const result = reconcileStaleRun(fresh.manifest, fresh.tasks, now);
77
+ if (result.repaired) {
78
+ updateRunStatus(fresh.manifest, "failed", `Stale run reconciled: ${result.detail}`);
79
+ appendEvent(fresh.manifest.eventsPath, { type: "crew.run.reconciled_stale", runId: manifest.runId, message: result.detail, data: { verdict: result.verdict } });
80
+ }
81
+ if (result.verdict !== "healthy") {
82
+ results.push(result);
83
+ }
84
+ });
85
+ }
86
+ return results;
87
+ }
@@ -70,6 +70,14 @@ export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
70
70
  }
71
71
  }
72
72
 
73
+ export async function readCrewAgentsAsync(manifest: TeamRunManifest): Promise<CrewAgentRecord[]> {
74
+ try {
75
+ return JSON.parse(await fs.promises.readFile(agentsPath(manifest), "utf-8")) as CrewAgentRecord[];
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
73
81
  export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentRecord[]): void {
74
82
  fs.mkdirSync(manifest.stateRoot, { recursive: true });
75
83
  atomicWriteJson(agentsPath(manifest), redactSecrets(records));
@@ -2,7 +2,7 @@ import type { TeamTaskStatus } from "../state/contracts.ts";
2
2
  import type { CrewActivityState, ModelRoutingState, UsageState } from "../state/types.ts";
3
3
 
4
4
  export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
5
- export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
5
+ export type CrewAgentStatus = "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "stopped";
6
6
 
7
7
  export interface CrewAgentRecentTool {
8
8
  tool: string;
@@ -54,5 +54,6 @@ export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus
54
54
  if (status === "failed") return "failed";
55
55
  if (status === "cancelled" || status === "skipped") return "cancelled";
56
56
  if (status === "running") return "running";
57
+ if (status === "waiting") return "waiting";
57
58
  return "queued";
58
59
  }
@@ -0,0 +1,154 @@
1
+ import type { NotificationDescriptor } from "../extension/notification-router.ts";
2
+ import { logInternalError } from "../utils/internal-error.ts";
3
+
4
+ export interface PendingDelivery {
5
+ runId: string;
6
+ payload: unknown;
7
+ timestamp: number;
8
+ type: "result" | "notification" | "steer";
9
+ generation?: number;
10
+ }
11
+
12
+ export interface DeliveryCoordinatorDeps {
13
+ /** Emit an event to the active Pi event bus. */
14
+ emit?: (event: string, data: unknown) => void;
15
+ /** Send a follow-up message to the active session (for notifications). */
16
+ sendFollowUp?: (title: string, body: string) => void;
17
+ /** Send a wake-up message to the active session (for async results). */
18
+ sendWakeUp?: (message: string) => void;
19
+ }
20
+
21
+ const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
22
+
23
+ export class DeliveryCoordinator {
24
+ private ownerSessionId: string | undefined;
25
+ private active = false;
26
+ private generation = 0;
27
+ private pending: PendingDelivery[] = [];
28
+ private readonly deps: DeliveryCoordinatorDeps;
29
+ private ttlTimer: ReturnType<typeof setInterval> | undefined;
30
+
31
+ constructor(deps: DeliveryCoordinatorDeps) {
32
+ this.deps = deps;
33
+ this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
34
+ this.ttlTimer.unref();
35
+ }
36
+
37
+ activate(sessionId: string): void {
38
+ this.ownerSessionId = sessionId;
39
+ this.active = true;
40
+ this.flushQueuedResults();
41
+ }
42
+
43
+ deactivate(): void {
44
+ this.active = false;
45
+ this.ownerSessionId = undefined;
46
+ this.generation += 1;
47
+ }
48
+
49
+ isActive(): boolean {
50
+ return this.active;
51
+ }
52
+
53
+ getPendingCount(): number {
54
+ return this.pending.length;
55
+ }
56
+
57
+ deliverResult(runId: string, result: unknown): void {
58
+ if (this.active && this.deps.emit) {
59
+ try {
60
+ this.deps.emit("pi-crew:run-result", result);
61
+ return;
62
+ } catch (error) {
63
+ logInternalError("delivery-coordinator.deliverResult", error, `runId=${runId}`);
64
+ }
65
+ }
66
+ this.enqueue({ runId, payload: result, timestamp: Date.now(), type: "result" });
67
+ }
68
+
69
+ deliverNotification(notification: NotificationDescriptor): void {
70
+ let delivered = false;
71
+ if (this.active && this.deps.sendFollowUp) {
72
+ try {
73
+ this.deps.sendFollowUp(notification.title, notification.body ?? "");
74
+ delivered = true;
75
+ } catch (error) {
76
+ logInternalError("delivery-coordinator.deliverNotification", error, `id=${notification.id}`);
77
+ }
78
+ }
79
+ if (delivered) {
80
+ if (this.deps.emit) {
81
+ try {
82
+ this.deps.emit("pi-crew:notification", notification);
83
+ } catch { /* secondary delivery, ignore errors */ }
84
+ }
85
+ return;
86
+ }
87
+ this.enqueue({ runId: notification.runId ?? "", payload: notification, timestamp: Date.now(), type: "notification" });
88
+ }
89
+
90
+ deliverSteer(runId: string, message: string): void {
91
+ if (this.active && this.deps.sendWakeUp) {
92
+ try {
93
+ this.deps.sendWakeUp(message);
94
+ return;
95
+ } catch (error) {
96
+ logInternalError("delivery-coordinator.deliverSteer", error, `runId=${runId}`);
97
+ }
98
+ }
99
+ this.enqueue({ runId, payload: message, timestamp: Date.now(), type: "steer" });
100
+ }
101
+
102
+ flushQueuedResults(): void {
103
+ if (!this.active || this.pending.length === 0) return;
104
+ const batch = this.pending.splice(0);
105
+ for (const delivery of batch) {
106
+ if (delivery.generation !== undefined && delivery.generation !== this.generation) {
107
+ logInternalError("delivery-coordinator.flush.stale", undefined, `runId=${delivery.runId} type=${delivery.type}`);
108
+ continue;
109
+ }
110
+ try {
111
+ switch (delivery.type) {
112
+ case "result":
113
+ this.deliverResult(delivery.runId, delivery.payload);
114
+ break;
115
+ case "notification": {
116
+ const notification = delivery.payload as NotificationDescriptor;
117
+ this.deliverNotification(notification);
118
+ break;
119
+ }
120
+ case "steer": {
121
+ const message = typeof delivery.payload === "string" ? delivery.payload : String(delivery.payload);
122
+ this.deliverSteer(delivery.runId, message);
123
+ break;
124
+ }
125
+ }
126
+ } catch (error) {
127
+ logInternalError("delivery-coordinator.flush", error, `runId=${delivery.runId} type=${delivery.type}`);
128
+ }
129
+ }
130
+ }
131
+
132
+ dispose(): void {
133
+ this.deactivate();
134
+ this.pending.length = 0;
135
+ if (this.ttlTimer) {
136
+ clearInterval(this.ttlTimer);
137
+ this.ttlTimer = undefined;
138
+ }
139
+ }
140
+
141
+ private enqueue(delivery: PendingDelivery): void {
142
+ this.pending.push({ ...delivery, generation: this.generation });
143
+ }
144
+
145
+ private evictExpired(): void {
146
+ const cutoff = Date.now() - PENDING_TTL_MS;
147
+ const before = this.pending.length;
148
+ this.pending = this.pending.filter((d) => d.timestamp > cutoff);
149
+ const evicted = before - this.pending.length;
150
+ if (evicted > 0) {
151
+ logInternalError("delivery-coordinator.evict", undefined, `evicted=${evicted} remaining=${this.pending.length}`);
152
+ }
153
+ }
154
+ }
@@ -1,35 +1,35 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
- import type { TeamConfig } from "../teams/team-config.ts";
4
- import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
-
6
- export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
- return manifest.workflow === "direct-agent";
8
- }
9
-
10
- export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
- if (!isDirectRun(manifest)) return undefined;
12
- const firstTask = tasks[0];
13
- const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
- const agent = agents.find((candidate) => candidate.name === agentName);
15
- const role = firstTask?.role ?? "agent";
16
- const stepId = firstTask?.stepId ?? "01_agent";
17
- return {
18
- team: {
19
- name: manifest.team,
20
- description: `Direct subagent run for ${agentName}`,
21
- source: "builtin",
22
- filePath: "<generated>",
23
- roles: [{ name: role, agent: agentName, description: agent?.description }],
24
- defaultWorkflow: "direct-agent",
25
- workspaceMode: manifest.workspaceMode,
26
- },
27
- workflow: {
28
- name: manifest.workflow ?? "direct-agent",
29
- description: `Direct task for ${agentName}`,
30
- source: "builtin",
31
- filePath: "<generated>",
32
- steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
- },
34
- };
35
- }
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
+
6
+ export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
+ return manifest.workflow === "direct-agent";
8
+ }
9
+
10
+ export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
+ if (!isDirectRun(manifest)) return undefined;
12
+ const firstTask = tasks[0];
13
+ const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
+ const agent = agents.find((candidate) => candidate.name === agentName);
15
+ const role = firstTask?.role ?? "agent";
16
+ const stepId = firstTask?.stepId ?? "01_agent";
17
+ return {
18
+ team: {
19
+ name: manifest.team,
20
+ description: `Direct subagent run for ${agentName}`,
21
+ source: "builtin",
22
+ filePath: "<generated>",
23
+ roles: [{ name: role, agent: agentName, description: agent?.description }],
24
+ defaultWorkflow: "direct-agent",
25
+ workspaceMode: manifest.workspaceMode,
26
+ },
27
+ workflow: {
28
+ name: manifest.workflow ?? "direct-agent",
29
+ description: `Direct task for ${agentName}`,
30
+ source: "builtin",
31
+ filePath: "<generated>",
32
+ steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
+ },
34
+ };
35
+ }
@@ -1,82 +1,82 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { appendEvent } from "../state/event-log.ts";
4
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
- import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
- import { readCrewAgents } from "./crew-agent-records.ts";
7
-
8
- export type ForegroundControlRequestType = "interrupt" | "status";
9
-
10
- export interface ForegroundControlStatus {
11
- runId: string;
12
- status: TeamRunManifest["status"];
13
- active: boolean;
14
- asyncPid?: number;
15
- asyncAlive?: boolean;
16
- runningTasks: string[];
17
- runningAgents: string[];
18
- controlPath: string;
19
- lastRequest?: ForegroundControlRequest;
20
- }
21
-
22
- export interface ForegroundControlRequest {
23
- id: string;
24
- type: ForegroundControlRequestType;
25
- createdAt: string;
26
- reason: string;
27
- acknowledged: boolean;
28
- }
29
-
30
- export function foregroundControlPath(manifest: TeamRunManifest): string {
31
- return path.join(manifest.stateRoot, "foreground-control.json");
32
- }
33
-
34
- function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
- if (!fs.existsSync(controlPath)) return undefined;
36
- try {
37
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
- return parsed.requests?.at(-1);
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
- const controlPath = foregroundControlPath(manifest);
46
- const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
- return {
48
- runId: manifest.runId,
49
- status: manifest.status,
50
- active: isActiveRunStatus(manifest.status),
51
- asyncPid: manifest.async?.pid,
52
- asyncAlive,
53
- runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
- runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
- controlPath,
56
- lastRequest: readLastRequest(controlPath),
57
- };
58
- }
59
-
60
- export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
- const controlPath = foregroundControlPath(manifest);
62
- let requests: ForegroundControlRequest[] = [];
63
- if (fs.existsSync(controlPath)) {
64
- try {
65
- const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
- requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
- } catch {
68
- requests = [];
69
- }
70
- }
71
- const request: ForegroundControlRequest = {
72
- id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
- type: "interrupt",
74
- createdAt: new Date().toISOString(),
75
- reason,
76
- acknowledged: false,
77
- };
78
- fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
- fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
- appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
- return request;
82
- }
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { appendEvent } from "../state/event-log.ts";
4
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
+ import { checkProcessLiveness, isActiveRunStatus } from "./process-status.ts";
6
+ import { readCrewAgents } from "./crew-agent-records.ts";
7
+
8
+ export type ForegroundControlRequestType = "interrupt" | "status";
9
+
10
+ export interface ForegroundControlStatus {
11
+ runId: string;
12
+ status: TeamRunManifest["status"];
13
+ active: boolean;
14
+ asyncPid?: number;
15
+ asyncAlive?: boolean;
16
+ runningTasks: string[];
17
+ runningAgents: string[];
18
+ controlPath: string;
19
+ lastRequest?: ForegroundControlRequest;
20
+ }
21
+
22
+ export interface ForegroundControlRequest {
23
+ id: string;
24
+ type: ForegroundControlRequestType;
25
+ createdAt: string;
26
+ reason: string;
27
+ acknowledged: boolean;
28
+ }
29
+
30
+ export function foregroundControlPath(manifest: TeamRunManifest): string {
31
+ return path.join(manifest.stateRoot, "foreground-control.json");
32
+ }
33
+
34
+ function readLastRequest(controlPath: string): ForegroundControlRequest | undefined {
35
+ if (!fs.existsSync(controlPath)) return undefined;
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
38
+ return parsed.requests?.at(-1);
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function readForegroundControlStatus(manifest: TeamRunManifest, tasks: TeamTaskState[]): ForegroundControlStatus {
45
+ const controlPath = foregroundControlPath(manifest);
46
+ const asyncAlive = manifest.async?.pid !== undefined ? checkProcessLiveness(manifest.async.pid).alive : undefined;
47
+ return {
48
+ runId: manifest.runId,
49
+ status: manifest.status,
50
+ active: isActiveRunStatus(manifest.status),
51
+ asyncPid: manifest.async?.pid,
52
+ asyncAlive,
53
+ runningTasks: tasks.filter((task) => task.status === "running").map((task) => task.id),
54
+ runningAgents: readCrewAgents(manifest).filter((agent) => agent.status === "running").map((agent) => agent.id),
55
+ controlPath,
56
+ lastRequest: readLastRequest(controlPath),
57
+ };
58
+ }
59
+
60
+ export function writeForegroundInterruptRequest(manifest: TeamRunManifest, reason = "User requested foreground interrupt."): ForegroundControlRequest {
61
+ const controlPath = foregroundControlPath(manifest);
62
+ let requests: ForegroundControlRequest[] = [];
63
+ if (fs.existsSync(controlPath)) {
64
+ try {
65
+ const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: ForegroundControlRequest[] };
66
+ requests = Array.isArray(parsed.requests) ? parsed.requests : [];
67
+ } catch {
68
+ requests = [];
69
+ }
70
+ }
71
+ const request: ForegroundControlRequest = {
72
+ id: `fg_${Date.now().toString(36)}_${Math.random().toString(16).slice(2, 10)}`,
73
+ type: "interrupt",
74
+ createdAt: new Date().toISOString(),
75
+ reason,
76
+ acknowledged: false,
77
+ };
78
+ fs.mkdirSync(path.dirname(controlPath), { recursive: true });
79
+ fs.writeFileSync(controlPath, `${JSON.stringify({ requests: [...requests, request] }, null, 2)}\n`, "utf-8");
80
+ appendEvent(manifest.eventsPath, { type: "foreground.interrupt_requested", runId: manifest.runId, message: reason, data: { requestId: request.id, controlPath } });
81
+ return request;
82
+ }
@@ -1,46 +1,46 @@
1
- import type { GreenLevel, VerificationContract, VerificationEvidence } from "../state/types.ts";
2
-
3
- const GREEN_ORDER: Record<GreenLevel, number> = {
4
- none: 0,
5
- targeted: 1,
6
- package: 2,
7
- workspace: 3,
8
- merge_ready: 4,
9
- };
10
-
11
- export interface GreenContractOutcome {
12
- requiredGreenLevel: GreenLevel;
13
- observedGreenLevel: GreenLevel;
14
- satisfied: boolean;
15
- }
16
-
17
- export function greenLevelSatisfies(observed: GreenLevel, required: GreenLevel): boolean {
18
- return GREEN_ORDER[observed] >= GREEN_ORDER[required];
19
- }
20
-
21
- export function evaluateGreenContract(contract: VerificationContract, evidence?: VerificationEvidence): GreenContractOutcome {
22
- const observedGreenLevel = evidence?.observedGreenLevel ?? "none";
23
- return {
24
- requiredGreenLevel: contract.requiredGreenLevel,
25
- observedGreenLevel,
26
- satisfied: greenLevelSatisfies(observedGreenLevel, contract.requiredGreenLevel),
27
- };
28
- }
29
-
30
- export function inferGreenLevelFromTask(success: boolean, contract: VerificationContract): GreenLevel {
31
- if (!success) return "none";
32
- if (contract.requiredGreenLevel === "none") return "none";
33
- return contract.allowManualEvidence ? contract.requiredGreenLevel : "targeted";
34
- }
35
-
36
- export function createVerificationEvidence(contract: VerificationContract, success: boolean, notes: string): VerificationEvidence {
37
- const observedGreenLevel = inferGreenLevelFromTask(success, contract);
38
- const outcome = evaluateGreenContract(contract, { requiredGreenLevel: contract.requiredGreenLevel, observedGreenLevel, satisfied: false, commands: [], notes });
39
- return {
40
- requiredGreenLevel: contract.requiredGreenLevel,
41
- observedGreenLevel,
42
- satisfied: outcome.satisfied,
43
- commands: contract.commands.map((cmd) => ({ cmd, status: "not_run" as const })),
44
- notes,
45
- };
46
- }
1
+ import type { GreenLevel, VerificationContract, VerificationEvidence } from "../state/types.ts";
2
+
3
+ const GREEN_ORDER: Record<GreenLevel, number> = {
4
+ none: 0,
5
+ targeted: 1,
6
+ package: 2,
7
+ workspace: 3,
8
+ merge_ready: 4,
9
+ };
10
+
11
+ export interface GreenContractOutcome {
12
+ requiredGreenLevel: GreenLevel;
13
+ observedGreenLevel: GreenLevel;
14
+ satisfied: boolean;
15
+ }
16
+
17
+ export function greenLevelSatisfies(observed: GreenLevel, required: GreenLevel): boolean {
18
+ return GREEN_ORDER[observed] >= GREEN_ORDER[required];
19
+ }
20
+
21
+ export function evaluateGreenContract(contract: VerificationContract, evidence?: VerificationEvidence): GreenContractOutcome {
22
+ const observedGreenLevel = evidence?.observedGreenLevel ?? "none";
23
+ return {
24
+ requiredGreenLevel: contract.requiredGreenLevel,
25
+ observedGreenLevel,
26
+ satisfied: greenLevelSatisfies(observedGreenLevel, contract.requiredGreenLevel),
27
+ };
28
+ }
29
+
30
+ export function inferGreenLevelFromTask(success: boolean, contract: VerificationContract): GreenLevel {
31
+ if (!success) return "none";
32
+ if (contract.requiredGreenLevel === "none") return "none";
33
+ return contract.allowManualEvidence ? contract.requiredGreenLevel : "targeted";
34
+ }
35
+
36
+ export function createVerificationEvidence(contract: VerificationContract, success: boolean, notes: string): VerificationEvidence {
37
+ const observedGreenLevel = inferGreenLevelFromTask(success, contract);
38
+ const outcome = evaluateGreenContract(contract, { requiredGreenLevel: contract.requiredGreenLevel, observedGreenLevel, satisfied: false, commands: [], notes });
39
+ return {
40
+ requiredGreenLevel: contract.requiredGreenLevel,
41
+ observedGreenLevel,
42
+ satisfied: outcome.satisfied,
43
+ commands: contract.commands.map((cmd) => ({ cmd, status: "not_run" as const })),
44
+ notes,
45
+ };
46
+ }