pi-crew 0.1.44 → 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 (142) 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 +198 -198
  20. package/docs/research-phase11-distillation.md +201 -201
  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/extension/cross-extension-rpc.ts +82 -82
  29. package/src/extension/register.ts +8 -1
  30. package/src/extension/registration/commands.ts +18 -2
  31. package/src/extension/registration/compaction-guard.ts +125 -125
  32. package/src/extension/registration/subagent-tools.ts +148 -148
  33. package/src/extension/registration/team-tool.ts +26 -8
  34. package/src/extension/run-bundle-schema.ts +89 -89
  35. package/src/extension/run-maintenance.ts +43 -43
  36. package/src/extension/team-tool/cancel.ts +105 -102
  37. package/src/extension/team-tool/context.ts +1 -0
  38. package/src/extension/team-tool/handle-settings.ts +188 -188
  39. package/src/extension/team-tool/inspect.ts +41 -41
  40. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  41. package/src/extension/team-tool/plan.ts +19 -19
  42. package/src/extension/team-tool/respond.ts +83 -66
  43. package/src/extension/team-tool/run.ts +1 -0
  44. package/src/i18n.ts +184 -184
  45. package/src/observability/exporters/otlp-exporter.ts +77 -77
  46. package/src/prompt/prompt-runtime.ts +72 -72
  47. package/src/runtime/agent-control.ts +63 -63
  48. package/src/runtime/agent-memory.ts +72 -72
  49. package/src/runtime/agent-observability.ts +114 -114
  50. package/src/runtime/async-marker.ts +26 -26
  51. package/src/runtime/attention-events.ts +28 -28
  52. package/src/runtime/background-runner.ts +53 -53
  53. package/src/runtime/child-pi.ts +444 -444
  54. package/src/runtime/completion-guard.ts +190 -190
  55. package/src/runtime/crew-agent-records.ts +8 -0
  56. package/src/runtime/delivery-coordinator.ts +153 -142
  57. package/src/runtime/direct-run.ts +35 -35
  58. package/src/runtime/foreground-control.ts +82 -82
  59. package/src/runtime/green-contract.ts +46 -46
  60. package/src/runtime/group-join.ts +106 -106
  61. package/src/runtime/heartbeat-gradient.ts +28 -28
  62. package/src/runtime/heartbeat-watcher.ts +124 -124
  63. package/src/runtime/live-agent-control.ts +87 -87
  64. package/src/runtime/live-agent-manager.ts +85 -85
  65. package/src/runtime/live-control-realtime.ts +36 -36
  66. package/src/runtime/live-session-runtime.ts +305 -305
  67. package/src/runtime/overflow-recovery.ts +175 -156
  68. package/src/runtime/parallel-research.ts +44 -44
  69. package/src/runtime/pi-json-output.ts +111 -111
  70. package/src/runtime/policy-engine.ts +79 -79
  71. package/src/runtime/progress-event-coalescer.ts +43 -43
  72. package/src/runtime/recovery-recipes.ts +74 -74
  73. package/src/runtime/retry-executor.ts +64 -64
  74. package/src/runtime/role-permission.ts +39 -39
  75. package/src/runtime/session-resources.ts +25 -25
  76. package/src/runtime/session-snapshot.ts +59 -59
  77. package/src/runtime/session-usage.ts +79 -79
  78. package/src/runtime/sidechain-output.ts +29 -29
  79. package/src/runtime/stale-reconciler.ts +199 -179
  80. package/src/runtime/supervisor-contact.ts +59 -59
  81. package/src/runtime/task-display.ts +38 -38
  82. package/src/runtime/task-output-context.ts +127 -127
  83. package/src/runtime/task-runner/live-executor.ts +101 -101
  84. package/src/runtime/task-runner/progress.ts +119 -119
  85. package/src/runtime/task-runner/result-utils.ts +14 -14
  86. package/src/runtime/task-runner/state-helpers.ts +22 -22
  87. package/src/runtime/team-runner.ts +13 -4
  88. package/src/runtime/worker-heartbeat.ts +21 -21
  89. package/src/runtime/worker-startup.ts +57 -57
  90. package/src/state/state-store.ts +43 -0
  91. package/src/state/task-claims.ts +44 -44
  92. package/src/state/types.ts +2 -0
  93. package/src/state/usage.ts +29 -29
  94. package/src/subagents/async-entry.ts +1 -1
  95. package/src/subagents/index.ts +3 -3
  96. package/src/subagents/live/control.ts +1 -1
  97. package/src/subagents/live/manager.ts +1 -1
  98. package/src/subagents/live/realtime.ts +1 -1
  99. package/src/subagents/live/session-runtime.ts +1 -1
  100. package/src/subagents/manager.ts +1 -1
  101. package/src/subagents/spawn.ts +1 -1
  102. package/src/teams/team-serializer.ts +38 -38
  103. package/src/types/diff.d.ts +18 -18
  104. package/src/ui/crew-footer.ts +101 -101
  105. package/src/ui/crew-select-list.ts +111 -111
  106. package/src/ui/crew-widget.ts +5 -1
  107. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  108. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  109. package/src/ui/dynamic-border.ts +25 -25
  110. package/src/ui/layout-primitives.ts +106 -106
  111. package/src/ui/loaders.ts +158 -158
  112. package/src/ui/powerbar-publisher.ts +1 -1
  113. package/src/ui/render-diff.ts +119 -119
  114. package/src/ui/render-scheduler.ts +143 -143
  115. package/src/ui/run-snapshot-cache.ts +56 -37
  116. package/src/ui/snapshot-types.ts +5 -0
  117. package/src/ui/spinner.ts +17 -17
  118. package/src/ui/status-colors.ts +58 -58
  119. package/src/ui/syntax-highlight.ts +116 -116
  120. package/src/utils/atomic-write.ts +33 -33
  121. package/src/utils/completion-dedupe.ts +63 -63
  122. package/src/utils/frontmatter.ts +68 -68
  123. package/src/utils/git.ts +262 -262
  124. package/src/utils/ids.ts +12 -12
  125. package/src/utils/names.ts +27 -27
  126. package/src/utils/redaction.ts +44 -44
  127. package/src/utils/safe-paths.ts +47 -47
  128. package/src/utils/sleep.ts +32 -32
  129. package/src/workflows/validate-workflow.ts +40 -40
  130. package/src/worktree/branch-freshness.ts +45 -45
  131. package/teams/default.team.md +12 -12
  132. package/teams/fast-fix.team.md +11 -11
  133. package/teams/implementation.team.md +18 -18
  134. package/teams/parallel-research.team.md +14 -14
  135. package/teams/research.team.md +11 -11
  136. package/teams/review.team.md +12 -12
  137. package/workflows/default.workflow.md +29 -29
  138. package/workflows/fast-fix.workflow.md +22 -22
  139. package/workflows/implementation.workflow.md +38 -38
  140. package/workflows/parallel-research.workflow.md +46 -46
  141. package/workflows/research.workflow.md +22 -22
  142. package/workflows/review.workflow.md +30 -30
@@ -1,143 +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
- }
10
-
11
- export interface DeliveryCoordinatorDeps {
12
- /** Emit an event to the active Pi event bus. */
13
- emit?: (event: string, data: unknown) => void;
14
- /** Send a follow-up message to the active session (for notifications). */
15
- sendFollowUp?: (title: string, body: string) => void;
16
- /** Send a wake-up message to the active session (for async results). */
17
- sendWakeUp?: (message: string) => void;
18
- }
19
-
20
- const PENDING_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
21
-
22
- export class DeliveryCoordinator {
23
- private ownerSessionId: string | undefined;
24
- private active = false;
25
- private pending: PendingDelivery[] = [];
26
- private readonly deps: DeliveryCoordinatorDeps;
27
- private ttlTimer: ReturnType<typeof setInterval> | undefined;
28
-
29
- constructor(deps: DeliveryCoordinatorDeps) {
30
- this.deps = deps;
31
- this.ttlTimer = setInterval(() => this.evictExpired(), 60_000);
32
- this.ttlTimer.unref();
33
- }
34
-
35
- activate(sessionId: string): void {
36
- this.ownerSessionId = sessionId;
37
- this.active = true;
38
- this.flushQueuedResults();
39
- }
40
-
41
- deactivate(): void {
42
- this.active = false;
43
- this.ownerSessionId = undefined;
44
- }
45
-
46
- isActive(): boolean {
47
- return this.active;
48
- }
49
-
50
- getPendingCount(): number {
51
- return this.pending.length;
52
- }
53
-
54
- deliverResult(runId: string, result: unknown): void {
55
- if (this.active && this.deps.emit) {
56
- try {
57
- this.deps.emit("pi-crew:run-result", result);
58
- } catch (error) {
59
- logInternalError("delivery-coordinator.deliverResult", error, `runId=${runId}`);
60
- }
61
- return;
62
- }
63
- this.enqueue({ runId, payload: result, timestamp: Date.now(), type: "result" });
64
- }
65
-
66
- deliverNotification(notification: NotificationDescriptor): void {
67
- if (this.active && this.deps.sendFollowUp) {
68
- try {
69
- this.deps.sendFollowUp(notification.title, notification.body ?? "");
70
- } catch (error) {
71
- logInternalError("delivery-coordinator.deliverNotification", error, `id=${notification.id}`);
72
- }
73
- if (this.deps.emit) {
74
- try {
75
- this.deps.emit("pi-crew:notification", notification);
76
- } catch { /* secondary delivery, ignore errors */ }
77
- }
78
- return;
79
- }
80
- this.enqueue({ runId: notification.runId ?? "", payload: notification, timestamp: Date.now(), type: "notification" });
81
- }
82
-
83
- deliverSteer(runId: string, message: string): void {
84
- if (this.active && this.deps.sendWakeUp) {
85
- try {
86
- this.deps.sendWakeUp(message);
87
- } catch (error) {
88
- logInternalError("delivery-coordinator.deliverSteer", error, `runId=${runId}`);
89
- }
90
- return;
91
- }
92
- this.enqueue({ runId, payload: message, timestamp: Date.now(), type: "steer" });
93
- }
94
-
95
- flushQueuedResults(): void {
96
- if (!this.active || this.pending.length === 0) return;
97
- const batch = this.pending.splice(0);
98
- for (const delivery of batch) {
99
- try {
100
- switch (delivery.type) {
101
- case "result":
102
- this.deliverResult(delivery.runId, delivery.payload);
103
- break;
104
- case "notification": {
105
- const notification = delivery.payload as NotificationDescriptor;
106
- this.deliverNotification(notification);
107
- break;
108
- }
109
- case "steer": {
110
- const message = typeof delivery.payload === "string" ? delivery.payload : String(delivery.payload);
111
- this.deliverSteer(delivery.runId, message);
112
- break;
113
- }
114
- }
115
- } catch (error) {
116
- logInternalError("delivery-coordinator.flush", error, `runId=${delivery.runId} type=${delivery.type}`);
117
- }
118
- }
119
- }
120
-
121
- dispose(): void {
122
- this.deactivate();
123
- this.pending.length = 0;
124
- if (this.ttlTimer) {
125
- clearInterval(this.ttlTimer);
126
- this.ttlTimer = undefined;
127
- }
128
- }
129
-
130
- private enqueue(delivery: PendingDelivery): void {
131
- this.pending.push(delivery);
132
- }
133
-
134
- private evictExpired(): void {
135
- const cutoff = Date.now() - PENDING_TTL_MS;
136
- const before = this.pending.length;
137
- this.pending = this.pending.filter((d) => d.timestamp > cutoff);
138
- const evicted = before - this.pending.length;
139
- if (evicted > 0) {
140
- logInternalError("delivery-coordinator.evict", undefined, `evicted=${evicted} remaining=${this.pending.length}`);
141
- }
142
- }
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
+ }
143
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
+ }