agentic-engineering-harness 0.6.13 → 0.6.15

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 (68) hide show
  1. package/dist/agents/outputContracts.d.ts +44 -1
  2. package/dist/agents/outputContracts.js +12 -2
  3. package/dist/agents/outputContracts.js.map +1 -1
  4. package/dist/agents/reviewLifecycle.js +54 -14
  5. package/dist/agents/reviewLifecycle.js.map +1 -1
  6. package/dist/agents/waveExecutor.d.ts +2 -13
  7. package/dist/audit/run.d.ts +7 -0
  8. package/dist/audit/run.js +198 -32
  9. package/dist/audit/run.js.map +1 -1
  10. package/dist/core/run.js +103 -28
  11. package/dist/core/run.js.map +1 -1
  12. package/dist/main.js +95 -29
  13. package/dist/main.js.map +1 -1
  14. package/dist/operations/artifacts.d.ts +13 -0
  15. package/dist/operations/artifacts.js +45 -0
  16. package/dist/operations/artifacts.js.map +1 -0
  17. package/dist/operations/change.d.ts +11 -0
  18. package/dist/operations/change.js +144 -0
  19. package/dist/operations/change.js.map +1 -0
  20. package/dist/operations/completion.d.ts +4 -1
  21. package/dist/operations/completion.js +69 -89
  22. package/dist/operations/completion.js.map +1 -1
  23. package/dist/operations/controller.d.ts +8 -5
  24. package/dist/operations/controller.js +270 -131
  25. package/dist/operations/controller.js.map +1 -1
  26. package/dist/operations/leadBinding.d.ts +2 -0
  27. package/dist/operations/leadBinding.js +22 -0
  28. package/dist/operations/leadBinding.js.map +1 -0
  29. package/dist/operations/liveness.d.ts +1 -0
  30. package/dist/operations/liveness.js +2 -0
  31. package/dist/operations/liveness.js.map +1 -0
  32. package/dist/operations/livenessV2.d.ts +34 -0
  33. package/dist/operations/livenessV2.js +339 -0
  34. package/dist/operations/livenessV2.js.map +1 -0
  35. package/dist/operations/mcp.d.ts +1 -0
  36. package/dist/operations/mcp.js +84 -57
  37. package/dist/operations/mcp.js.map +1 -1
  38. package/dist/operations/monitorProcess.d.ts +8 -0
  39. package/dist/operations/monitorProcess.js +33 -0
  40. package/dist/operations/monitorProcess.js.map +1 -0
  41. package/dist/operations/portfolio.d.ts +35 -0
  42. package/dist/operations/portfolio.js +158 -0
  43. package/dist/operations/portfolio.js.map +1 -0
  44. package/dist/operations/state.d.ts +174 -17
  45. package/dist/operations/state.js +178 -163
  46. package/dist/operations/state.js.map +1 -1
  47. package/dist/operations/supervisor.d.ts +38 -0
  48. package/dist/operations/supervisor.js +248 -0
  49. package/dist/operations/supervisor.js.map +1 -0
  50. package/dist/operations/wakeBudget.d.ts +15 -0
  51. package/dist/operations/wakeBudget.js +142 -0
  52. package/dist/operations/wakeBudget.js.map +1 -0
  53. package/dist/paseo/launchSpec.d.ts +4 -0
  54. package/dist/paseo/launchSpec.js +31 -37
  55. package/dist/paseo/launchSpec.js.map +1 -1
  56. package/dist/paseo/sdk.d.ts +5 -0
  57. package/dist/paseo/sdk.js +35 -87
  58. package/dist/paseo/sdk.js.map +1 -1
  59. package/dist/paseo/start.d.ts +1 -1
  60. package/dist/paseo/start.js +38 -87
  61. package/dist/paseo/start.js.map +1 -1
  62. package/dist/workers/agentPrompt.d.ts +2 -0
  63. package/dist/workers/agentPrompt.js +135 -190
  64. package/dist/workers/agentPrompt.js.map +1 -1
  65. package/docs/OPERATION_SUPERVISION.md +198 -0
  66. package/package.json +1 -1
  67. package/presets/agents/orchestration.jsonc +14 -4
  68. package/skills/engineering-workflow/SKILL.md +149 -104
@@ -1,5 +1,8 @@
1
- export type OperationKind = "audit" | "run";
1
+ export type OperationKind = "audit" | "run" | "change";
2
2
  export type OperationStatus = "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "CANCELLED";
3
+ export type OperationStageStatus = "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "BLOCKED" | "SKIPPED";
4
+ export type OperationParticipantStatus = "REGISTERED" | "IDLE" | "RUNNING" | "COMPLETED" | "FAILED" | "BLOCKED" | "CANCELLED";
5
+ export type SupervisorGenerationStatus = "ACTIVE" | "DRAINING" | "ARCHIVED" | "FAILED";
3
6
  export interface AuditOperationPayload {
4
7
  request: string;
5
8
  files?: string[];
@@ -10,59 +13,213 @@ export interface AuditOperationPayload {
10
13
  export interface RunOperationPayload {
11
14
  taskId: string;
12
15
  profile?: string;
16
+ priority?: number;
13
17
  }
14
- export type OperationPayload = AuditOperationPayload | RunOperationPayload;
18
+ export interface ChangeOperationPayload {
19
+ request: string;
20
+ title?: string;
21
+ taskId?: string;
22
+ files?: string[];
23
+ domains?: string[];
24
+ acceptance?: string[];
25
+ risk?: "low" | "medium" | "high";
26
+ profile?: string;
27
+ priority?: number;
28
+ }
29
+ export type OperationPayload = AuditOperationPayload | RunOperationPayload | ChangeOperationPayload;
15
30
  export interface OperationAgentRecord {
16
31
  id: string;
17
32
  role?: string;
18
33
  phase?: string;
19
34
  workspaceId?: string;
20
- transport?: "sdk" | "cli";
35
+ transport?: string;
36
+ registeredAt: string;
37
+ }
38
+ export interface OperationLeadBinding {
39
+ agentId: string;
40
+ source?: string;
41
+ generation: number;
42
+ boundAt: string;
43
+ acknowledgedRevision: number;
44
+ acknowledgedAt?: string;
45
+ }
46
+ export interface OperationSupervisorGeneration {
47
+ generation: number;
48
+ agentId?: string;
49
+ status: SupervisorGenerationStatus;
50
+ createdAt: string;
51
+ activatedAt?: string;
52
+ drainingAt?: string;
53
+ archivedAt?: string;
54
+ checkpointArtifact?: string;
55
+ contextRatio?: number;
56
+ error?: string;
57
+ }
58
+ export interface OperationSupervisionState {
59
+ required: boolean;
60
+ materialized: boolean;
61
+ activeGeneration?: number;
62
+ generations: OperationSupervisorGeneration[];
63
+ latestConsolidationRevision?: number;
64
+ latestConsolidationArtifact?: string;
65
+ }
66
+ export interface OperationStageRecord {
67
+ name: string;
68
+ status: OperationStageStatus;
69
+ revision: number;
70
+ startedAt?: string;
71
+ finishedAt?: string;
72
+ message?: string;
73
+ artifact?: string;
74
+ }
75
+ export interface OperationParticipantRecord {
76
+ id: string;
77
+ logicalAgent?: string;
78
+ role?: string;
79
+ stage?: string;
80
+ phase?: string;
81
+ parentSupervisorGeneration?: number;
82
+ parentAgentId?: string;
83
+ workspaceId?: string;
84
+ transport?: string;
85
+ status: OperationParticipantStatus;
21
86
  registeredAt: string;
87
+ startedAt?: string;
88
+ finishedAt?: string;
89
+ resultArtifact?: string;
90
+ error?: string;
91
+ }
92
+ export interface OperationProgress {
93
+ expected: number;
94
+ registered: number;
95
+ running: number;
96
+ completed: number;
97
+ failed: number;
98
+ blocked: number;
22
99
  }
23
- export interface OperationRecord {
100
+ export interface OperationNotificationState {
101
+ lastLeadWakeRevision: number;
102
+ lastLeadWakeAt?: string;
103
+ lastLeadWakeReason?: string;
104
+ terminalDelivered: boolean;
105
+ attempts: number;
106
+ lastError?: string;
107
+ }
108
+ export interface OperationIntentState {
109
+ request?: string;
110
+ classification?: "AUDIT" | "CHANGE" | "RUN";
111
+ mode?: "quick" | "spec";
112
+ risk?: "low" | "medium" | "high";
113
+ priority?: number;
114
+ }
115
+ export interface OperationRecordV1 {
24
116
  version: 1;
25
117
  id: string;
118
+ kind: "audit" | "run";
119
+ status: OperationStatus;
120
+ phase: string;
121
+ root: string;
122
+ payload: AuditOperationPayload | RunOperationPayload;
123
+ createdAt: string;
124
+ updatedAt: string;
125
+ startedAt?: string;
126
+ finishedAt?: string;
127
+ pid?: number;
128
+ workspaceId?: string;
129
+ workspaceWarning?: string;
130
+ agents?: OperationAgentRecord[];
131
+ cleanupWarnings?: string[];
132
+ result?: Record<string, unknown>;
133
+ error?: string;
134
+ }
135
+ export interface OperationRecordV2 {
136
+ version: 2;
137
+ id: string;
26
138
  kind: OperationKind;
27
139
  status: OperationStatus;
28
140
  phase: string;
29
141
  root: string;
142
+ workspaceRoot?: string;
30
143
  payload: OperationPayload;
144
+ revision: number;
31
145
  createdAt: string;
32
146
  updatedAt: string;
147
+ lastProgressAt: string;
33
148
  startedAt?: string;
34
149
  finishedAt?: string;
35
150
  pid?: number;
36
151
  workspaceId?: string;
37
152
  workspaceWarning?: string;
153
+ intent?: OperationIntentState;
154
+ lead?: OperationLeadBinding;
155
+ supervision: OperationSupervisionState;
156
+ stages: Record<string, OperationStageRecord>;
157
+ participants: Record<string, OperationParticipantRecord>;
158
+ progress: OperationProgress;
159
+ notification: OperationNotificationState;
38
160
  agents?: OperationAgentRecord[];
39
161
  cleanupWarnings?: string[];
40
162
  result?: Record<string, unknown>;
41
163
  error?: string;
42
164
  }
165
+ export type OperationRecord = OperationRecordV1 | OperationRecordV2;
43
166
  export interface TerminalOperationTransition {
44
- record: OperationRecord;
167
+ record: OperationRecordV2;
45
168
  transitioned: boolean;
46
169
  }
170
+ export interface OperationEvent {
171
+ version: 1;
172
+ operationId: string;
173
+ revision: number;
174
+ at: string;
175
+ type: string;
176
+ status: OperationStatus;
177
+ phase: string;
178
+ changed?: string[];
179
+ details?: Record<string, unknown>;
180
+ }
181
+ export declare function resolveOperationStateRoot(root: string): string;
47
182
  export declare function operationFile(root: string, operationId: string): string;
48
- export declare function loadOperation(root: string, operationId: string): Promise<OperationRecord>;
183
+ export declare function operationArtifactDir(root: string, operationId: string): string;
184
+ export declare function operationEventsFile(root: string, operationId: string): string;
185
+ export declare function loadOperation(root: string, operationId: string): Promise<OperationRecordV2>;
49
186
  export declare function saveOperation(root: string, record: OperationRecord): Promise<void>;
50
- export declare function patchOperation(root: string, operationId: string, patch: Partial<OperationRecord>): Promise<OperationRecord>;
51
- /**
52
- * Atomically grants exactly one controller path ownership of a non-terminal ->
53
- * terminal transition. Callers must perform one-shot terminal side effects
54
- * (for example the managed-lead completion callback) only when transitioned is
55
- * true. A concurrent cancel/execute path receives the already-terminal record
56
- * without becoming a second side-effect owner.
57
- */
58
- export declare function transitionOperationToTerminal(root: string, operationId: string, patch: Partial<OperationRecord> & {
187
+ export declare function patchOperation(root: string, operationId: string, patch: Partial<OperationRecordV2>): Promise<OperationRecordV2>;
188
+ export declare function patchOperationMetadata(root: string, operationId: string, patch: Partial<OperationRecordV2>): Promise<OperationRecordV2>;
189
+ export declare function transitionOperationToTerminal(root: string, operationId: string, patch: Partial<OperationRecordV2> & {
59
190
  status: "SUCCEEDED" | "FAILED" | "CANCELLED";
60
191
  }): Promise<TerminalOperationTransition>;
61
- export declare function registerOperationAgent(root: string, operationId: string, agent: Omit<OperationAgentRecord, "registeredAt">): Promise<OperationRecord>;
62
- export declare function registerCurrentOperationAgent(root: string, agent: Omit<OperationAgentRecord, "registeredAt">): Promise<void>;
192
+ export declare function bindOperationLead(root: string, operationId: string, agentId: string, source?: string): Promise<OperationRecordV2>;
193
+ export declare function acknowledgeOperationLead(root: string, operationId: string, revision: number, reason?: string): Promise<OperationRecordV2>;
194
+ export declare function markTerminalDelivered(root: string, operationId: string, attempts: number, error?: string): Promise<OperationRecordV2>;
195
+ export declare function setOperationStage(root: string, operationId: string, name: string, status: OperationStageStatus, options?: {
196
+ message?: string;
197
+ artifact?: string;
198
+ }): Promise<OperationRecordV2>;
199
+ export declare function registerSupervisorGeneration(root: string, operationId: string, input: {
200
+ agentId?: string;
201
+ materialized: boolean;
202
+ checkpointArtifact?: string;
203
+ }): Promise<OperationRecordV2>;
204
+ export declare function updateSupervisorGeneration(root: string, operationId: string, generation: number, patch: Partial<OperationSupervisorGeneration>): Promise<OperationRecordV2>;
205
+ export declare function activeOperationSupervisor(record: OperationRecordV2): OperationSupervisorGeneration | undefined;
206
+ export declare function registerOperationAgent(root: string, operationId: string, agent: Omit<OperationAgentRecord, "registeredAt"> & {
207
+ logicalAgent?: string;
208
+ parentAgentId?: string;
209
+ parentSupervisorGeneration?: number;
210
+ }): Promise<OperationRecordV2>;
211
+ export declare function updateOperationParticipant(root: string, operationId: string, agentId: string, patch: Partial<Omit<OperationParticipantRecord, "id" | "registeredAt">>): Promise<OperationRecordV2>;
212
+ export declare function registerCurrentOperationAgent(root: string, agent: Omit<OperationAgentRecord, "registeredAt"> & {
213
+ logicalAgent?: string;
214
+ parentAgentId?: string;
215
+ parentSupervisorGeneration?: number;
216
+ }): Promise<void>;
63
217
  export declare function currentOperationContext(): {
64
218
  id?: string;
65
219
  kind?: string;
66
220
  workspaceId?: string;
221
+ controlRoot?: string;
67
222
  };
68
223
  export declare function updateCurrentOperationPhase(root: string, phase: string): Promise<void>;
224
+ export declare function normalizeOperationRecord(record: OperationRecord): OperationRecordV2;
225
+ export declare function isTerminalOperation(status: OperationStatus): boolean;
@@ -5,205 +5,220 @@ const OPERATIONS_DIR = ".harness/operations";
5
5
  const LOCK_RETRY_MS = 20;
6
6
  const LOCK_TIMEOUT_MS = 10_000;
7
7
  const STALE_LOCK_MS = 30_000;
8
- export function operationFile(root, operationId) {
9
- return path.resolve(root, OPERATIONS_DIR, `${safeId(operationId)}.json`);
8
+ export function resolveOperationStateRoot(root) {
9
+ const configured = process.env.AEH_OPERATION_ID?.trim() ? process.env.AEH_CONTROL_ROOT?.trim() : undefined;
10
+ return path.resolve(configured || root);
10
11
  }
12
+ export function operationFile(root, operationId) { return path.resolve(resolveOperationStateRoot(root), OPERATIONS_DIR, `${safeId(operationId)}.json`); }
13
+ export function operationArtifactDir(root, operationId) { return path.resolve(resolveOperationStateRoot(root), OPERATIONS_DIR, safeId(operationId)); }
14
+ export function operationEventsFile(root, operationId) { return path.join(operationArtifactDir(root, operationId), "events.ndjson"); }
11
15
  export async function loadOperation(root, operationId) {
12
- return JSON.parse(await fs.readFile(operationFile(root, operationId), "utf8"));
16
+ return normalizeOperationRecord(JSON.parse(await fs.readFile(operationFile(root, operationId), "utf8")));
13
17
  }
14
18
  export async function saveOperation(root, record) {
15
- const file = operationFile(root, record.id);
19
+ const normalized = normalizeOperationRecord(record);
20
+ const stateRoot = resolveOperationStateRoot(root);
21
+ const file = operationFile(stateRoot, normalized.id);
16
22
  await fs.mkdir(path.dirname(file), { recursive: true });
17
- await withOperationLock(file, async () => writeRecord(file, record));
23
+ await withOperationLock(file, async () => { await writeRecord(file, normalized); await appendOperationEvent(stateRoot, normalized, "operation.created", ["status", "phase"]); });
18
24
  }
19
- export async function patchOperation(root, operationId, patch) {
20
- return mutateOperation(root, operationId, (current) => {
21
- const guardedPatch = guardTerminalTransition(current, patch);
22
- return { ...current, ...guardedPatch };
23
- });
24
- }
25
- /**
26
- * Atomically grants exactly one controller path ownership of a non-terminal ->
27
- * terminal transition. Callers must perform one-shot terminal side effects
28
- * (for example the managed-lead completion callback) only when transitioned is
29
- * true. A concurrent cancel/execute path receives the already-terminal record
30
- * without becoming a second side-effect owner.
31
- */
25
+ export async function patchOperation(root, operationId, patch) { return mutateOperation(root, operationId, patch, true, "operation.updated"); }
26
+ export async function patchOperationMetadata(root, operationId, patch) { return mutateOperation(root, operationId, patch, false, "operation.metadata"); }
32
27
  export async function transitionOperationToTerminal(root, operationId, patch) {
33
- const file = operationFile(root, operationId);
28
+ const stateRoot = resolveOperationStateRoot(root);
29
+ const file = operationFile(stateRoot, operationId);
34
30
  await fs.mkdir(path.dirname(file), { recursive: true });
35
31
  return withOperationLock(file, async () => {
36
- const current = JSON.parse(await fs.readFile(file, "utf8"));
32
+ const current = normalizeOperationRecord(JSON.parse(await fs.readFile(file, "utf8")));
37
33
  if (isTerminal(current.status))
38
34
  return { record: current, transitioned: false };
39
- const next = {
40
- ...current,
41
- ...patch,
42
- id: current.id,
43
- kind: current.kind,
44
- version: 1,
45
- updatedAt: new Date().toISOString()
46
- };
35
+ const now = new Date().toISOString();
36
+ const revision = current.revision + 1;
37
+ const next = normalizeOperationRecord({ ...current, ...patch, version: 2, id: current.id, kind: current.kind, revision, updatedAt: now, lastProgressAt: now, stages: { ...current.stages, finished: { name: "finished", status: terminalStageStatus(patch.status), revision, startedAt: now, finishedAt: now } } });
47
38
  await writeRecord(file, next);
39
+ await appendOperationEvent(stateRoot, next, "operation.terminal", ["status", "phase"]);
48
40
  return { record: next, transitioned: true };
49
41
  });
50
42
  }
43
+ export async function bindOperationLead(root, operationId, agentId, source) {
44
+ return mutateOperation(root, operationId, {}, true, "operation.lead.bound", (current, revision, now) => ({ ...current, revision, updatedAt: now, lastProgressAt: now, lead: { agentId: requiredId(agentId), source, generation: (current.lead?.generation ?? 0) + 1, boundAt: now, acknowledgedRevision: revision, acknowledgedAt: now }, notification: { ...current.notification, lastLeadWakeRevision: revision, lastLeadWakeAt: now, lastLeadWakeReason: "operation-started" } }));
45
+ }
46
+ export async function acknowledgeOperationLead(root, operationId, revision, reason) {
47
+ const current = await loadOperation(root, operationId);
48
+ const now = new Date().toISOString();
49
+ return patchOperationMetadata(root, operationId, { lead: current.lead ? { ...current.lead, acknowledgedRevision: Math.max(current.lead.acknowledgedRevision, revision), acknowledgedAt: now } : undefined, notification: { ...current.notification, lastLeadWakeRevision: Math.max(current.notification.lastLeadWakeRevision, revision), lastLeadWakeAt: now, lastLeadWakeReason: reason ?? current.notification.lastLeadWakeReason } });
50
+ }
51
+ export async function markTerminalDelivered(root, operationId, attempts, error) {
52
+ const current = await loadOperation(root, operationId);
53
+ return patchOperationMetadata(root, operationId, { notification: { ...current.notification, lastLeadWakeRevision: error ? current.notification.lastLeadWakeRevision : current.revision, lastLeadWakeAt: error ? current.notification.lastLeadWakeAt : new Date().toISOString(), lastLeadWakeReason: error ? current.notification.lastLeadWakeReason : "terminal", terminalDelivered: !error, attempts, lastError: error } });
54
+ }
55
+ export async function setOperationStage(root, operationId, name, status, options = {}) {
56
+ return mutateOperation(root, operationId, {}, true, "operation.stage", (current, revision, now) => {
57
+ const previous = current.stages[name];
58
+ const terminal = ["COMPLETED", "FAILED", "BLOCKED", "SKIPPED"].includes(status);
59
+ const stage = { name, status, revision, startedAt: previous?.startedAt ?? (status === "RUNNING" ? now : undefined), finishedAt: terminal ? now : undefined, message: options.message ?? previous?.message, artifact: options.artifact ?? previous?.artifact };
60
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, phase: name, stages: { ...current.stages, [name]: stage } };
61
+ });
62
+ }
63
+ export async function registerSupervisorGeneration(root, operationId, input) {
64
+ return mutateOperation(root, operationId, {}, true, "operation.supervisor.registered", (current, revision, now) => {
65
+ const generations = current.supervision.generations.map((item) => item.status === "ACTIVE" ? { ...item, status: "DRAINING", drainingAt: item.drainingAt ?? now } : item);
66
+ const generation = Math.max(0, ...generations.map((item) => item.generation)) + 1;
67
+ generations.push({ generation, agentId: input.agentId, status: "ACTIVE", createdAt: now, activatedAt: now, checkpointArtifact: input.checkpointArtifact });
68
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, supervision: { ...current.supervision, required: true, materialized: current.supervision.materialized || input.materialized, activeGeneration: generation, generations } };
69
+ });
70
+ }
71
+ export async function updateSupervisorGeneration(root, operationId, generation, patch) {
72
+ return mutateOperation(root, operationId, {}, true, "operation.supervisor.updated", (current, revision, now) => {
73
+ const generations = current.supervision.generations.map((item) => item.generation === generation ? { ...item, ...patch } : item);
74
+ const active = generations.find((item) => item.status === "ACTIVE");
75
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, supervision: { ...current.supervision, activeGeneration: active?.generation, generations } };
76
+ });
77
+ }
78
+ export function activeOperationSupervisor(record) { const generation = record.supervision.activeGeneration; return generation === undefined ? undefined : record.supervision.generations.find((item) => item.generation === generation && item.status === "ACTIVE"); }
51
79
  export async function registerOperationAgent(root, operationId, agent) {
52
- return mutateOperation(root, operationId, (current) => {
53
- if (isTerminal(current.status))
54
- return current;
55
- const existing = current.agents ?? [];
56
- const previous = existing.find((item) => item.id === agent.id);
57
- const next = {
58
- ...previous,
59
- ...agent,
60
- registeredAt: previous?.registeredAt ?? new Date().toISOString()
61
- };
62
- return {
63
- ...current,
64
- agents: [...existing.filter((item) => item.id !== agent.id), next]
65
- };
80
+ return mutateOperation(root, operationId, {}, true, "operation.participant.registered", (current, revision, now) => {
81
+ const existingAgents = current.agents ?? [];
82
+ const previousAgent = existingAgents.find((item) => item.id === agent.id);
83
+ const compatibility = { ...previousAgent, id: agent.id, role: agent.role ?? previousAgent?.role, phase: agent.phase ?? previousAgent?.phase, workspaceId: agent.workspaceId ?? previousAgent?.workspaceId, transport: agent.transport ?? previousAgent?.transport, registeredAt: previousAgent?.registeredAt ?? now };
84
+ const agents = [...existingAgents.filter((item) => item.id !== agent.id), compatibility];
85
+ const logicalAgent = agent.logicalAgent ?? agent.role;
86
+ if (logicalAgent === "operation-supervisor")
87
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, agents };
88
+ const previous = current.participants[agent.id];
89
+ const participant = { ...previous, id: agent.id, logicalAgent: logicalAgent ?? previous?.logicalAgent, role: agent.role ?? previous?.role, stage: agent.phase ?? previous?.stage, phase: agent.phase ?? previous?.phase, parentSupervisorGeneration: agent.parentSupervisorGeneration ?? previous?.parentSupervisorGeneration ?? current.supervision.activeGeneration, parentAgentId: agent.parentAgentId ?? previous?.parentAgentId ?? activeOperationSupervisor(current)?.agentId, workspaceId: agent.workspaceId ?? previous?.workspaceId, transport: agent.transport ?? previous?.transport, status: previous?.status ?? "REGISTERED", registeredAt: previous?.registeredAt ?? now };
90
+ const participants = { ...current.participants, [agent.id]: participant };
91
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, agents, participants, progress: deriveProgress(participants) };
66
92
  });
67
93
  }
68
- export async function registerCurrentOperationAgent(root, agent) {
69
- const operationId = currentOperationContext().id;
70
- if (!operationId)
71
- return;
72
- try {
73
- await registerOperationAgent(root, operationId, agent);
74
- }
75
- catch {
76
- // Direct/non-controller execution may carry stale environment state. Agent
77
- // registration is observability/cleanup metadata, not execution authority.
78
- }
94
+ export async function updateOperationParticipant(root, operationId, agentId, patch) {
95
+ return mutateOperation(root, operationId, {}, true, "operation.participant.updated", (current, revision, now) => {
96
+ const previous = current.participants[agentId] ?? { id: agentId, status: "REGISTERED", registeredAt: now };
97
+ const next = { ...previous, ...patch, id: agentId, registeredAt: previous.registeredAt, startedAt: patch.status === "RUNNING" ? previous.startedAt ?? now : patch.startedAt ?? previous.startedAt, finishedAt: ["COMPLETED", "FAILED", "CANCELLED"].includes(patch.status ?? "") ? patch.finishedAt ?? now : patch.finishedAt ?? previous.finishedAt };
98
+ const participants = { ...current.participants, [agentId]: next };
99
+ return { ...current, revision, updatedAt: now, lastProgressAt: now, participants, progress: deriveProgress(participants) };
100
+ });
79
101
  }
80
- export function currentOperationContext() {
81
- return {
82
- id: process.env.AEH_OPERATION_ID?.trim() || undefined,
83
- kind: process.env.AEH_OPERATION_KIND?.trim() || undefined,
84
- workspaceId: process.env.AEH_OPERATION_WORKSPACE_ID?.trim() || undefined
85
- };
86
- }
87
- export async function updateCurrentOperationPhase(root, phase) {
88
- const operationId = currentOperationContext().id;
89
- if (!operationId)
90
- return;
91
- try {
92
- await patchOperation(root, operationId, { phase });
102
+ export async function registerCurrentOperationAgent(root, agent) { const operationId = currentOperationContext().id; if (!operationId)
103
+ return; try {
104
+ await registerOperationAgent(resolveOperationStateRoot(root), operationId, agent);
105
+ }
106
+ catch { /* stale direct execution metadata is non-authoritative */ } }
107
+ export function currentOperationContext() { return { id: process.env.AEH_OPERATION_ID?.trim() || undefined, kind: process.env.AEH_OPERATION_KIND?.trim() || undefined, workspaceId: process.env.AEH_OPERATION_WORKSPACE_ID?.trim() || undefined, controlRoot: process.env.AEH_CONTROL_ROOT?.trim() || undefined }; }
108
+ export async function updateCurrentOperationPhase(root, phase) { const operationId = currentOperationContext().id; if (!operationId)
109
+ return; try {
110
+ await setOperationStage(resolveOperationStateRoot(root), operationId, phase, "RUNNING");
111
+ }
112
+ catch { /* direct/non-controller */ } }
113
+ export function normalizeOperationRecord(record) {
114
+ if (record.version === 2) {
115
+ const participants = record.participants ?? {};
116
+ return { ...record, version: 2, revision: Math.max(1, record.revision || 1), lastProgressAt: record.lastProgressAt || record.updatedAt, supervision: record.supervision ?? defaultSupervision(record.kind), stages: record.stages ?? {}, participants, progress: record.progress ?? deriveProgress(participants), notification: record.notification ?? defaultNotification() };
93
117
  }
94
- catch {
95
- // Direct/non-controller execution has no operation state to update.
118
+ const participants = {};
119
+ for (const agent of record.agents ?? []) {
120
+ if (agent.role === "operation-supervisor")
121
+ continue;
122
+ participants[agent.id] = { id: agent.id, logicalAgent: agent.role, role: agent.role, stage: agent.phase, phase: agent.phase, workspaceId: agent.workspaceId, transport: agent.transport, status: "REGISTERED", registeredAt: agent.registeredAt };
96
123
  }
124
+ return { ...record, version: 2, kind: record.kind, payload: record.payload, revision: 1, lastProgressAt: record.updatedAt, intent: inferIntent(record.kind, record.payload), supervision: defaultSupervision(record.kind), stages: record.phase ? { [record.phase]: { name: record.phase, status: isTerminal(record.status) ? terminalStageStatus(record.status) : "RUNNING", revision: 1, startedAt: record.startedAt, finishedAt: record.finishedAt } } : {}, participants, progress: deriveProgress(participants), notification: defaultNotification() };
97
125
  }
98
- async function mutateOperation(root, operationId, mutate) {
99
- const file = operationFile(root, operationId);
126
+ async function mutateOperation(root, operationId, patch, touchRevision, eventType, custom) {
127
+ const stateRoot = resolveOperationStateRoot(root);
128
+ const file = operationFile(stateRoot, operationId);
100
129
  await fs.mkdir(path.dirname(file), { recursive: true });
101
130
  return withOperationLock(file, async () => {
102
- const current = JSON.parse(await fs.readFile(file, "utf8"));
103
- const candidate = mutate(current);
104
- const next = {
105
- ...candidate,
106
- id: current.id,
107
- kind: current.kind,
108
- version: 1,
109
- updatedAt: new Date().toISOString()
110
- };
131
+ const current = normalizeOperationRecord(JSON.parse(await fs.readFile(file, "utf8")));
132
+ const guardedPatch = guardTerminalTransition(current, patch);
133
+ const now = new Date().toISOString();
134
+ const revision = touchRevision ? current.revision + 1 : current.revision;
135
+ const candidate = custom ? custom(current, revision, now) : { ...current, ...guardedPatch, version: 2, id: current.id, kind: current.kind, revision, updatedAt: now, lastProgressAt: touchRevision ? now : current.lastProgressAt };
136
+ const next = normalizeOperationRecord(candidate);
111
137
  await writeRecord(file, next);
138
+ await appendOperationEvent(stateRoot, next, eventType, Object.keys(patch));
112
139
  return next;
113
140
  });
114
141
  }
115
- async function writeRecord(file, record) {
116
- const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
117
- await fs.writeFile(temp, `${JSON.stringify(record, null, 2)}\n`);
118
- try {
119
- await fs.rename(temp, file);
120
- }
121
- finally {
122
- await fs.rm(temp, { force: true }).catch(() => undefined);
123
- }
142
+ async function appendOperationEvent(root, record, type, changed, details) { const file = operationEventsFile(root, record.id); await fs.mkdir(path.dirname(file), { recursive: true }); const event = { version: 1, operationId: record.id, revision: record.revision, at: new Date().toISOString(), type, status: record.status, phase: record.phase, changed, details }; await fs.appendFile(file, `${JSON.stringify(event)}\n`); }
143
+ async function writeRecord(file, record) { const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`; await fs.writeFile(temp, `${JSON.stringify(record, null, 2)}\n`); try {
144
+ await fs.rename(temp, file);
124
145
  }
125
- async function withOperationLock(file, action) {
126
- const lock = `${file}.lock`;
127
- const deadline = Date.now() + LOCK_TIMEOUT_MS;
128
- for (;;) {
129
- let handle;
146
+ finally {
147
+ await fs.rm(temp, { force: true }).catch(() => undefined);
148
+ } }
149
+ async function withOperationLock(file, action) { const lock = `${file}.lock`; const deadline = Date.now() + LOCK_TIMEOUT_MS; for (;;) {
150
+ let handle;
151
+ try {
152
+ handle = await fs.open(lock, "wx");
130
153
  try {
131
- handle = await fs.open(lock, "wx");
132
- try {
133
- await handle.writeFile(`${process.pid}\n`);
134
- return await action();
135
- }
136
- finally {
137
- await handle.close().catch(() => undefined);
138
- await fs.rm(lock, { force: true }).catch(() => undefined);
139
- }
154
+ await handle.writeFile(`${process.pid}\n`);
155
+ return await action();
140
156
  }
141
- catch (error) {
142
- if (handle) {
143
- await handle.close().catch(() => undefined);
144
- await fs.rm(lock, { force: true }).catch(() => undefined);
145
- throw error;
146
- }
147
- if (!isAlreadyExists(error))
148
- throw error;
149
- if (await canRecoverLock(lock)) {
150
- await fs.rm(lock, { force: true }).catch(() => undefined);
151
- continue;
152
- }
153
- if (Date.now() >= deadline) {
154
- throw new Error(`Timed out acquiring operation state lock for ${path.basename(file)}.`);
155
- }
156
- await delay(LOCK_RETRY_MS);
157
+ finally {
158
+ await handle.close().catch(() => undefined);
159
+ await fs.rm(lock, { force: true }).catch(() => undefined);
157
160
  }
158
161
  }
159
- }
160
- async function canRecoverLock(lock) {
161
- try {
162
- const [rawPid, stat] = await Promise.all([
163
- fs.readFile(lock, "utf8").catch(() => ""),
164
- fs.stat(lock)
165
- ]);
166
- const ownerPid = Number.parseInt(rawPid.trim(), 10);
167
- if (Number.isInteger(ownerPid) && ownerPid > 0 && !processAlive(ownerPid))
168
- return true;
169
- return Date.now() - stat.mtimeMs > STALE_LOCK_MS;
170
- }
171
- catch {
172
- return true;
173
- }
174
- }
175
- function processAlive(pid) {
176
- try {
177
- process.kill(pid, 0);
178
- return true;
179
- }
180
162
  catch (error) {
181
- return !/ESRCH/.test(String(error));
163
+ if (handle) {
164
+ await handle.close().catch(() => undefined);
165
+ await fs.rm(lock, { force: true }).catch(() => undefined);
166
+ throw error;
167
+ }
168
+ if (!isAlreadyExists(error))
169
+ throw error;
170
+ if (await canRecoverLock(lock)) {
171
+ await fs.rm(lock, { force: true }).catch(() => undefined);
172
+ continue;
173
+ }
174
+ if (Date.now() >= deadline)
175
+ throw new Error(`Timed out acquiring operation state lock for ${path.basename(file)}.`);
176
+ await delay(LOCK_RETRY_MS);
182
177
  }
183
- }
178
+ } }
179
+ async function canRecoverLock(lock) { try {
180
+ const [rawPid, stat] = await Promise.all([fs.readFile(lock, "utf8").catch(() => ""), fs.stat(lock)]);
181
+ const ownerPid = Number.parseInt(rawPid.trim(), 10);
182
+ if (Number.isInteger(ownerPid) && ownerPid > 0 && !processAlive(ownerPid))
183
+ return true;
184
+ return Date.now() - stat.mtimeMs > STALE_LOCK_MS;
185
+ }
186
+ catch {
187
+ return true;
188
+ } }
189
+ function processAlive(pid) { try {
190
+ process.kill(pid, 0);
191
+ return true;
192
+ }
193
+ catch {
194
+ return false;
195
+ } }
184
196
  function guardTerminalTransition(current, patch) {
185
197
  if (!isTerminal(current.status))
186
198
  return patch;
187
- const guarded = {};
188
- if (patch.cleanupWarnings)
189
- guarded.cleanupWarnings = patch.cleanupWarnings;
190
- return guarded;
191
- }
192
- function isTerminal(status) {
193
- return status === "SUCCEEDED" || status === "FAILED" || status === "CANCELLED";
194
- }
195
- function isAlreadyExists(error) {
196
- return Boolean(error &&
197
- typeof error === "object" &&
198
- "code" in error &&
199
- error.code === "EEXIST");
200
- }
201
- function delay(ms) {
202
- return new Promise((resolve) => setTimeout(resolve, ms));
203
- }
204
- function safeId(value) {
205
- if (!/^[A-Za-z0-9._-]+$/.test(value))
206
- throw new Error(`Invalid operation id '${value}'.`);
207
- return value;
208
- }
199
+ // Once one controller owns terminalization, late execution/cancellation paths
200
+ // may still append cleanup/notification metadata but cannot rewrite the
201
+ // terminal outcome, phase, result, error or finish timestamp.
202
+ const { status: _status, phase: _phase, result: _result, error: _error, finishedAt: _finishedAt, ...metadata } = patch;
203
+ return metadata;
204
+ }
205
+ function deriveProgress(participants) { const values = Object.values(participants); return { expected: values.length, registered: values.filter((item) => item.status === "REGISTERED" || item.status === "IDLE").length, running: values.filter((item) => item.status === "RUNNING").length, completed: values.filter((item) => item.status === "COMPLETED").length, failed: values.filter((item) => item.status === "FAILED" || item.status === "CANCELLED").length, blocked: values.filter((item) => item.status === "BLOCKED").length }; }
206
+ function defaultSupervision(kind) { return { required: kind === "audit" || kind === "change", materialized: false, generations: [] }; }
207
+ function defaultNotification() { return { lastLeadWakeRevision: 0, terminalDelivered: false, attempts: 0 }; }
208
+ function inferIntent(kind, payload) { if (kind === "audit") {
209
+ const audit = payload;
210
+ return { request: audit.request, classification: "AUDIT", risk: audit.risk };
211
+ } if (kind === "change") {
212
+ const change = payload;
213
+ return { request: change.request, classification: "CHANGE", risk: change.risk, priority: change.priority };
214
+ } return { classification: "RUN", priority: payload.priority }; }
215
+ function terminalStageStatus(status) { return status === "SUCCEEDED" ? "COMPLETED" : status === "CANCELLED" ? "SKIPPED" : "FAILED"; }
216
+ export function isTerminalOperation(status) { return isTerminal(status); }
217
+ function isTerminal(status) { return status === "SUCCEEDED" || status === "FAILED" || status === "CANCELLED"; }
218
+ function requiredId(value) { const trimmed = value.trim(); if (!trimmed)
219
+ throw new Error("agent id is required"); return trimmed; }
220
+ function safeId(value) { if (!/^[A-Za-z0-9._-]+$/.test(value))
221
+ throw new Error(`Invalid operation id '${value}'.`); return value; }
222
+ function isAlreadyExists(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST"); }
223
+ function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
209
224
  //# sourceMappingURL=state.js.map