@tea-agent/loop-agent 0.36.4-beta.0 → 0.37.1-beta.0

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 (118) hide show
  1. package/AGENTS.md +2 -6
  2. package/CHANGELOG.md +72 -163
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +1 -37
  5. package/dist/application/task-lifecycle/advance.js +0 -1
  6. package/dist/application/task-lifecycle/observe.js +0 -15
  7. package/dist/application/task-lifecycle/plan-transitions.js +0 -15
  8. package/dist/cli/command-definitions.js +0 -7
  9. package/dist/cli/program.js +3 -8
  10. package/dist/commands/init-upgrade.js +19 -351
  11. package/dist/commands/init.js +67 -14
  12. package/dist/commands/run-dag-progress.js +0 -14
  13. package/dist/commands/task-advance.js +3 -33
  14. package/dist/executors/dag-pi-executor.js +30 -47
  15. package/dist/executors/pi-sdk-executor.js +0 -34
  16. package/dist/executors/shell-executor.js +95 -6
  17. package/dist/infrastructure/harness/atomic-write.js +1 -2
  18. package/dist/shared/operator/capabilities.js +13 -200
  19. package/dist/shared/package-metadata.js +0 -42
  20. package/dist/task/frontend-project-capability.js +12 -162
  21. package/dist/task/source-prepare/completeness.js +0 -17
  22. package/dist/task/source-prepare/parse-intent.js +1 -15
  23. package/dist/task/source-prepare/semantic-intake.js +23 -144
  24. package/dist/worker/console/chat/artifact-card.js +1 -8
  25. package/dist/worker/console/chat/chat-event-store.js +0 -61
  26. package/dist/worker/console/chat/human-gate-card.js +1 -9
  27. package/dist/worker/console/chat/operation-card.js +2 -71
  28. package/dist/worker/console/chat/pi-runtime.js +62 -86
  29. package/dist/worker/console/chat/routes.js +8 -36
  30. package/dist/worker/console/chat/session-store.js +0 -3
  31. package/dist/worker/console/chat/shortcuts.js +7 -9
  32. package/dist/worker/console/chat/turn-process.js +23 -97
  33. package/dist/worker/console/chat/workspace-landing.js +1 -2
  34. package/dist/worker/console/operation-runner.js +6 -155
  35. package/dist/worker/console/operator-actions.js +13 -373
  36. package/dist/worker/console/operator-user-error.js +0 -4
  37. package/dist/worker/console/recovery-cta.js +3 -50
  38. package/dist/worker/console/static/assets/index-HX1pbOyl.css +1 -0
  39. package/dist/worker/console/static/assets/index-M0BLEBfh.js +56 -0
  40. package/dist/worker/console/static/index.html +2 -2
  41. package/dist/worker/console/static-src/app/console-types.js +9 -13
  42. package/dist/worker/console/static-src/app/useOperatorActions.js +2 -4
  43. package/dist/worker/console/static-src/app/useRecoveryActions.js +56 -65
  44. package/dist/worker/console/static-src/app/useRecoveryConsole.js +1 -73
  45. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +30 -89
  46. package/dist/worker/console/static-src/operator-chat/refs.js +0 -3
  47. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  48. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +11 -30
  49. package/dist/worker/console/static-src/operator-chat/useChatThread.js +6 -12
  50. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -45
  51. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -7
  52. package/dist/worker/loop-agent/loop-agent-client.js +3 -17
  53. package/dist/worker/observability/read-model.js +0 -20
  54. package/dist/worker/observe/spec-evidence.js +8 -3
  55. package/dist/worker/observe/static/operator-chrome.d.ts +0 -1
  56. package/dist/worker/observe/static/operator-chrome.js +1 -6
  57. package/dist/worker/observe/static/views/dag-inspector.js +71 -6
  58. package/dist/worker/observe/static/views/dag.js +0 -12
  59. package/dist/worker/preflight.js +1 -2
  60. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +707 -37
  61. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  62. package/dist/workflows/dag/backend-test-markdown-workflow.js +109 -7
  63. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  64. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  65. package/dist/workflows/dag/backend-test-scenario-param.js +890 -164
  66. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  67. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  68. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -9
  69. package/dist/workflows/dag/failure-routing.js +4 -9
  70. package/dist/workflows/dag/frontend-implementation-contract.js +39 -233
  71. package/dist/workflows/dag/frontend-prewrite-gate.js +62 -394
  72. package/dist/workflows/dag/frontend-repair.js +18 -219
  73. package/dist/workflows/dag/frontend-verification-trace.js +32 -47
  74. package/dist/workflows/dag/init-hybrid.js +86 -238
  75. package/dist/workflows/dag/lifecycle.js +0 -4
  76. package/dist/workflows/dag/node-execution.js +6 -105
  77. package/dist/workflows/dag/recovery-recommendation.js +0 -58
  78. package/dist/workflows/dag/report.js +0 -6
  79. package/dist/workflows/dag/retry-policy.js +0 -11
  80. package/dist/workflows/dag/runner.js +12 -314
  81. package/dist/workflows/dag/scheduler.js +3 -257
  82. package/dist/workflows/dag/types.js +9 -132
  83. package/dist/workflows/dag/validate.js +2 -7
  84. package/docs/README.md +3 -3
  85. package/docs/architecture/evolution.md +67 -102
  86. package/docs/templates/backend-test-dag.json +50 -32
  87. package/docs/templates/frontend-design-contract.md +14 -34
  88. package/docs/templates/frontend-task-constraints.md +13 -33
  89. package/docs/templates/frontend-task-requirement.md +20 -54
  90. package/docs/templates/init-managed-agents.md +2 -5
  91. package/harness.json +2 -2
  92. package/package.json +3 -4
  93. package/skills/loop-agent/SKILL.md +0 -1
  94. package/skills/loop-agent/references/command-reference.md +0 -3
  95. package/dist/build-stamp.json +0 -6
  96. package/dist/commands/dag-request-interrupt.js +0 -20
  97. package/dist/worker/console/chat/assistant-content.js +0 -121
  98. package/dist/worker/console/chat/semantic-activity.js +0 -471
  99. package/dist/worker/console/operation-run-facts.js +0 -190
  100. package/dist/worker/console/operation-wait.js +0 -355
  101. package/dist/worker/console/recovery-error-copy.js +0 -198
  102. package/dist/worker/console/static/assets/index-D83DYAFG.css +0 -1
  103. package/dist/worker/console/static/assets/index-IXm7oYjL.js +0 -59
  104. package/dist/worker/console/static-src/operator-chat/activity-journey.js +0 -125
  105. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  106. package/dist/worker/console/static-src/operator-chat/activity-references.js +0 -20
  107. package/dist/worker/console/static-src/operator-chat/slash-palette-layout.js +0 -24
  108. package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +0 -141
  109. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
  110. package/dist/worker/observability/interrupt-eligibility.js +0 -264
  111. package/dist/workflows/dag/contract-output-registry.js +0 -14
  112. package/dist/workflows/dag/contract-validator-registrations.js +0 -8
  113. package/dist/workflows/dag/frontend-recovery-plan.js +0 -73
  114. package/dist/workflows/dag/frontend-recovery-root-manifest.js +0 -123
  115. package/dist/workflows/dag/frontend-recovery-run.js +0 -539
  116. package/dist/workflows/dag/frontend-writer-recovery.js +0 -106
  117. package/dist/workflows/dag/frontend-writer-rollback.js +0 -821
  118. package/dist/workflows/dag/interrupt-request.js +0 -559
@@ -1,264 +0,0 @@
1
- import { hostname as localHostname } from "node:os";
2
- import { DagInterruptError, INTERRUPT_ERROR_MESSAGES, projectedInterruptStatus, readInterruptRequest, runnerIdentitiesMatch, runnerIdentityFromState, } from "../../workflows/dag/interrupt-request.js";
3
- import { assessDagRunLiveness, locateDagRun, readDagRunState, } from "../../workflows/dag/lifecycle.js";
4
- function operationControllerFingerprint(operation) {
5
- const identity = operation.controllerIdentity;
6
- if (!identity)
7
- return undefined;
8
- if ("fingerprint" in identity && typeof identity.fingerprint === "string") {
9
- const value = identity.fingerprint.trim();
10
- return value || undefined;
11
- }
12
- if ("packageFingerprint" in identity &&
13
- identity.packageFingerprint &&
14
- typeof identity.packageFingerprint === "object" &&
15
- "value" in identity.packageFingerprint &&
16
- typeof identity.packageFingerprint.value === "string") {
17
- const value = identity.packageFingerprint.value.trim();
18
- return value || undefined;
19
- }
20
- return undefined;
21
- }
22
- function ineligible(partial) {
23
- const code = partial.reasonCodes[0] ?? "RUN_NOT_ACTIVE";
24
- return {
25
- ...partial,
26
- eligible: false,
27
- message: partial.message ?? INTERRUPT_ERROR_MESSAGES[code],
28
- };
29
- }
30
- function bindLiveOperations(input) {
31
- return input.operations.filter((operation) => {
32
- if (operation.state !== "running" && operation.state !== "starting") {
33
- return false;
34
- }
35
- if (operation.pid !== input.runner.pid)
36
- return false;
37
- if (operation.dagRunId && operation.dagRunId !== input.runId)
38
- return false;
39
- const fingerprint = operationControllerFingerprint(operation);
40
- if (!fingerprint || fingerprint !== input.runner.controllerFingerprint) {
41
- return false;
42
- }
43
- return true;
44
- });
45
- }
46
- export async function evaluateConsoleDagInterruptEligibility(input) {
47
- const runId = input.runId.trim();
48
- if (!runId) {
49
- return ineligible({
50
- runId: "",
51
- ownerKind: "unknown",
52
- reasonCodes: ["INVALID_INPUT"],
53
- interruptStatus: "none",
54
- });
55
- }
56
- const located = await locateDagRun(input.repoRoot, runId);
57
- if (!located) {
58
- return ineligible({
59
- runId,
60
- ownerKind: "unknown",
61
- reasonCodes: ["RUN_NOT_ACTIVE"],
62
- interruptStatus: "none",
63
- });
64
- }
65
- let state;
66
- try {
67
- state = await readDagRunState(located.runDir);
68
- }
69
- catch {
70
- return ineligible({
71
- runId,
72
- lifecycle: located.lifecycle,
73
- ownerKind: "unknown",
74
- reasonCodes: ["RUN_NOT_ACTIVE"],
75
- interruptStatus: "none",
76
- });
77
- }
78
- let interrupt;
79
- try {
80
- interrupt = await readInterruptRequest(located.runDir);
81
- }
82
- catch {
83
- interrupt = undefined;
84
- }
85
- const interruptStatus = projectedInterruptStatus(interrupt);
86
- if (state.workerAssociation) {
87
- return ineligible({
88
- runId,
89
- lifecycle: located.lifecycle,
90
- dagStatus: state.status,
91
- ownerKind: "worker-managed",
92
- reasonCodes: ["RUN_NOT_CONSOLE_OWNED"],
93
- interruptStatus,
94
- interrupt,
95
- });
96
- }
97
- if (located.lifecycle !== "active" || state.status !== "running") {
98
- const terminal = located.lifecycle === "completed" ||
99
- located.lifecycle === "paused" ||
100
- state.status === "paused" ||
101
- state.status === "finished" ||
102
- state.status === "failed" ||
103
- state.status === "partial_failed" ||
104
- state.status === "superseded" ||
105
- state.status === "abandoned";
106
- return ineligible({
107
- runId,
108
- lifecycle: located.lifecycle,
109
- dagStatus: state.status,
110
- ownerKind: "external-local",
111
- reasonCodes: [terminal ? "RUN_ALREADY_TERMINAL" : "RUN_NOT_ACTIVE"],
112
- interruptStatus,
113
- interrupt,
114
- });
115
- }
116
- const runner = runnerIdentityFromState(state);
117
- if (!runner) {
118
- return ineligible({
119
- runId,
120
- lifecycle: located.lifecycle,
121
- dagStatus: state.status,
122
- ownerKind: "unknown",
123
- reasonCodes: ["RUNNER_IDENTITY_MISMATCH"],
124
- interruptStatus,
125
- interrupt,
126
- });
127
- }
128
- if (runner.hostname !== (input.hostname ?? localHostname())) {
129
- return ineligible({
130
- runId,
131
- lifecycle: located.lifecycle,
132
- dagStatus: state.status,
133
- ownerKind: "external-local",
134
- reasonCodes: ["RUN_NOT_CONSOLE_OWNED"],
135
- interruptStatus,
136
- interrupt,
137
- });
138
- }
139
- const liveness = assessDagRunLiveness({
140
- state,
141
- hostname: input.hostname ?? localHostname(),
142
- });
143
- if (liveness.status === "orphaned" || liveness.status === "unknown-host") {
144
- return ineligible({
145
- runId,
146
- lifecycle: located.lifecycle,
147
- dagStatus: state.status,
148
- ownerKind: "unknown",
149
- reasonCodes: ["RUN_NOT_ACTIVE"],
150
- interruptStatus,
151
- interrupt,
152
- });
153
- }
154
- const liveOps = await input.operations.list();
155
- const matches = bindLiveOperations({
156
- operations: liveOps,
157
- runId,
158
- runner,
159
- });
160
- const targetFromInterrupt = interrupt?.targetOperationId
161
- ? (liveOps.find((op) => op.operationId === interrupt.targetOperationId) ??
162
- (await input.operations.get(interrupt.targetOperationId)))
163
- : undefined;
164
- const targetOperationState = (matches.length === 1 ? matches[0]?.state : undefined) ??
165
- targetFromInterrupt?.state;
166
- const expected = {
167
- pid: runner.pid,
168
- hostname: runner.hostname,
169
- startedAt: runner.startedAt,
170
- controllerFingerprint: runner.controllerFingerprint,
171
- };
172
- if (interrupt &&
173
- (interrupt.status === "requested" || interrupt.status === "acknowledged")) {
174
- return ineligible({
175
- runId,
176
- lifecycle: located.lifecycle,
177
- dagStatus: state.status,
178
- failureCategory: state.failureCategory,
179
- ownerKind: "console-operation",
180
- targetOperationId: interrupt.targetOperationId,
181
- targetOperationState,
182
- expectedRunnerIdentity: runner,
183
- reasonCodes: ["INTERRUPT_ALREADY_PENDING"],
184
- interruptStatus,
185
- interrupt,
186
- });
187
- }
188
- if (interrupt?.status === "needs-reconcile") {
189
- return ineligible({
190
- runId,
191
- lifecycle: located.lifecycle,
192
- dagStatus: state.status,
193
- failureCategory: state.failureCategory,
194
- ownerKind: "console-operation",
195
- targetOperationId: interrupt.targetOperationId,
196
- targetOperationState,
197
- expectedRunnerIdentity: runner,
198
- reasonCodes: ["INTERRUPT_NEEDS_RECONCILE"],
199
- interruptStatus,
200
- interrupt,
201
- });
202
- }
203
- if (matches.length === 0) {
204
- return ineligible({
205
- runId,
206
- lifecycle: located.lifecycle,
207
- dagStatus: state.status,
208
- failureCategory: state.failureCategory,
209
- ownerKind: "external-local",
210
- reasonCodes: ["RUN_NOT_CONSOLE_OWNED"],
211
- interruptStatus,
212
- interrupt,
213
- });
214
- }
215
- if (matches.length > 1) {
216
- return ineligible({
217
- runId,
218
- lifecycle: located.lifecycle,
219
- dagStatus: state.status,
220
- failureCategory: state.failureCategory,
221
- ownerKind: "unknown",
222
- reasonCodes: ["RUNNER_IDENTITY_MISMATCH"],
223
- message: "找到多个可能持有该运行的 Console operation,无法无歧义绑定。请用 Doctor 查看,不要猜测。",
224
- interruptStatus,
225
- interrupt,
226
- });
227
- }
228
- const target = matches[0];
229
- if (target.pid !== expected.pid ||
230
- !runnerIdentitiesMatch(expected, runner)) {
231
- return ineligible({
232
- runId,
233
- lifecycle: located.lifecycle,
234
- dagStatus: state.status,
235
- failureCategory: state.failureCategory,
236
- ownerKind: "unknown",
237
- reasonCodes: ["RUNNER_IDENTITY_MISMATCH"],
238
- interruptStatus,
239
- interrupt,
240
- });
241
- }
242
- return {
243
- eligible: true,
244
- runId,
245
- lifecycle: located.lifecycle,
246
- dagStatus: state.status,
247
- failureCategory: state.failureCategory,
248
- ownerKind: "console-operation",
249
- targetOperationId: target.operationId,
250
- targetOperationState: target.state,
251
- expectedRunnerIdentity: expected,
252
- reasonCodes: [],
253
- message: "可以由当前 Console 协作中止此运行。",
254
- interruptStatus,
255
- interrupt,
256
- };
257
- }
258
- export function interruptEligibilityError(eligibility) {
259
- const code = eligibility.reasonCodes[0] ?? "RUN_NOT_ACTIVE";
260
- return new DagInterruptError(code, eligibility.message);
261
- }
262
- // Console-side re-exports: keep the console layer free of direct workflows/dag
263
- // kernel imports (see test/worker/console/security-and-tools.test.ts).
264
- export { INTERRUPT_ERROR_MESSAGES, isInterruptReasonCode, normalizeInterruptReasonDetail, } from "../../workflows/dag/interrupt-request.js";
@@ -1,14 +0,0 @@
1
- import { structuredContractOutputSchemaIds, } from "./types.js";
2
- const validators = new Map();
3
- export function registerStructuredContractValidator(schemaId, validator) {
4
- if (validators.has(schemaId)) {
5
- throw new Error(`structured contract validator already registered for ${schemaId}`);
6
- }
7
- validators.set(schemaId, validator);
8
- }
9
- export function getStructuredContractValidator(schemaId) {
10
- if (structuredContractOutputSchemaIds.includes(schemaId)) {
11
- return validators.get(schemaId);
12
- }
13
- return undefined;
14
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Side-effect module: registers every structured contract validator against
3
- * its schemaId in the contract output registry. Imported once by the node
4
- * executor; keeps per-contract imports out of the executor.
5
- */
6
- import { validateFrontendContractNodeOutput } from "./frontend-implementation-contract.js";
7
- import { registerStructuredContractValidator } from "./contract-output-registry.js";
8
- registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
@@ -1,73 +0,0 @@
1
- import { computeResetClosure } from "./rerun-plan.js";
2
- /**
3
- * Frontend-only candidate-continuation recovery planning (AC-5).
4
- *
5
- * Distinct from the generic `dag rerun` reset closure: the failure source is
6
- * derived from the prewrite-result's `selectedPlanNodeId` rather than an
7
- * operator-selected node, and the closure starts at the specific frontend
8
- * candidate producer that failed.
9
- */
10
- /** Idempotency namespace for requestId-keyed recovery intents (D2). */
11
- export const FRONTEND_RECOVERY_INTENT_REL_DIR = ".runtime/frontend-recovery";
12
- /** Import manifest path inside the child run dir; hashed into the activation marker. */
13
- export const FRONTEND_RECOVERY_IMPORT_MANIFEST_REL_PATH = ".runtime/import-manifest.json";
14
- /** Staging directory prefix under `active/` for in-flight child materialization. */
15
- export const FRONTEND_RECOVERY_STAGING_PREFIX = ".frontend-recovery-staging-";
16
- export function deriveFrontendRecoveryFailureSource(result) {
17
- if (result.selectedPlanNodeId === "frontend-plan-revision-pi") {
18
- return "frontend-plan-revision-pi";
19
- }
20
- if (result.selectedPlanNodeId === "frontend-plan-pi") {
21
- return "frontend-plan-pi";
22
- }
23
- return "frontend-prewrite-gate-shell";
24
- }
25
- /**
26
- * Compute the frontend candidate-continuation reset partition:
27
- * - `frontend-plan-pi` failure → reset plan + design-review + prewrite (+ descendants);
28
- * - `frontend-plan-revision-pi` failure → reset revision + final-review + prewrite (+ descendants);
29
- * - prewrite-only materialization → reset prewrite (+ descendants), reuse verified plan/review.
30
- *
31
- * Everything upstream of the failure source is imported as verified parent facts.
32
- */
33
- export function computeFrontendRecoveryPlan(input) {
34
- const failureSource = deriveFrontendRecoveryFailureSource(input.result);
35
- return computeFrontendRecoveryPlanForSource({
36
- spec: input.spec,
37
- parentRunId: input.parentRunId,
38
- requestId: input.requestId,
39
- failureSource,
40
- });
41
- }
42
- /**
43
- * Phase 5: writer transient partial-write recovery plan. The writer itself
44
- * failed after some bounded writes, so the child re-runs `frontend-implement-pi`
45
- * (and its descendants) while importing the verified contract/scout/plan/prewrite
46
- * facts. Everything upstream of the writer is imported; the writer + descendants
47
- * are reset.
48
- */
49
- export function computeFrontendWriterRecoveryPlan(input) {
50
- return computeFrontendRecoveryPlanForSource({
51
- spec: input.spec,
52
- parentRunId: input.parentRunId,
53
- requestId: input.requestId,
54
- failureSource: "frontend-implement-pi",
55
- });
56
- }
57
- function computeFrontendRecoveryPlanForSource(input) {
58
- const resetNodeIds = computeResetClosure(input.spec, input.failureSource);
59
- const resetSet = new Set(resetNodeIds);
60
- const importedNodeIds = input.spec.tasks
61
- .map((task) => task.id)
62
- .filter((nodeId) => !resetSet.has(nodeId))
63
- .sort();
64
- return {
65
- schemaVersion: 1,
66
- parentRunId: input.parentRunId,
67
- requestId: input.requestId,
68
- failureSource: input.failureSource,
69
- resetNodeIds,
70
- importedNodeIds,
71
- };
72
- }
73
- export { computeFrontendRecoveryPlanForSource };
@@ -1,123 +0,0 @@
1
- import { mkdir, readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
5
- /**
6
- * Phase 6: frontend recovery root manifest — the single final read model for a
7
- * frontend recovery chain. A "root" is the first physical run of a
8
- * frontend-implementation attempt; its parent/child lineage is counted once
9
- * under `recoveryRootRunId`. report/Console read the manifest for dedup and
10
- * final-status display instead of scanning parent/child fields (P1-4).
11
- */
12
- export const FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID = "frontend-recovery-root-manifest-v1";
13
- export const frontendRecoveryRootManifestSchema = z
14
- .object({
15
- schemaVersion: z.literal(1),
16
- schemaId: z.literal(FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID),
17
- recoveryRootRunId: z.string().min(1),
18
- /** Terminal logical status for the whole chain (finished/partial_failed/failed). */
19
- status: z.enum(["finished", "partial_failed", "failed"]),
20
- outcome: z.enum([
21
- "none",
22
- "recovered",
23
- "candidate-contract-invalid",
24
- "prewrite-blocked",
25
- "repair-exhausted",
26
- "auto-recovery-blocked",
27
- ]),
28
- attempts: z.array(z
29
- .object({
30
- runId: z.string().min(1),
31
- role: z.enum(["root", "child"]),
32
- attemptIndex: z.number().int().min(0),
33
- outcome: z.enum([
34
- "none",
35
- "recovered",
36
- "candidate-contract-invalid",
37
- "prewrite-blocked",
38
- "repair-exhausted",
39
- "auto-recovery-blocked",
40
- ]),
41
- status: z.enum(["finished", "partial_failed", "failed"]),
42
- })
43
- .strict()),
44
- evidenceRefs: z.array(z
45
- .object({
46
- runId: z.string().min(1),
47
- relativePath: z.string().min(1),
48
- sha256: z.string().min(1),
49
- })
50
- .strict()),
51
- createdAt: z.string().min(1),
52
- })
53
- .strict();
54
- /** Relative path of the root manifest inside a run dir. */
55
- export const FRONTEND_RECOVERY_ROOT_MANIFEST_REL = "contracts/frontend-recovery-root-manifest.json";
56
- export function frontendRecoveryRootManifestAbsPath(runDir) {
57
- return path.join(runDir, FRONTEND_RECOVERY_ROOT_MANIFEST_REL);
58
- }
59
- export async function writeFrontendRecoveryRootManifest(runDir, manifest) {
60
- const abs = frontendRecoveryRootManifestAbsPath(runDir);
61
- await mkdir(path.dirname(abs), { recursive: true });
62
- await writeJsonAtomic(abs, manifest);
63
- return abs;
64
- }
65
- export async function readFrontendRecoveryRootManifest(runDir) {
66
- try {
67
- const raw = JSON.parse(await readFile(frontendRecoveryRootManifestAbsPath(runDir), "utf8"));
68
- const parsed = frontendRecoveryRootManifestSchema.safeParse(raw);
69
- return parsed.success ? parsed.data : undefined;
70
- }
71
- catch {
72
- return undefined;
73
- }
74
- }
75
- /**
76
- * Build a root manifest from a converged root run's lineage. The manifest is the
77
- * single authority for final status/outcome: parent keeps its raw failure status,
78
- * the chain's logical status is only `finished` when a child finished and the
79
- * outcome is `recovered`.
80
- */
81
- export function buildFrontendRecoveryRootManifest(input) {
82
- const status = input.outcome === "recovered" ? "finished" : "failed";
83
- return {
84
- schemaVersion: 1,
85
- schemaId: FRONTEND_RECOVERY_ROOT_MANIFEST_SCHEMA_ID,
86
- recoveryRootRunId: input.recoveryRootRunId,
87
- status,
88
- outcome: input.outcome,
89
- attempts: input.attempts,
90
- evidenceRefs: input.evidenceRefs ?? [],
91
- createdAt: input.createdAt ?? new Date().toISOString(),
92
- };
93
- }
94
- /**
95
- * Dedup a set of DAG runs by recovery root (P1-4): each root contributes exactly
96
- * one summary; parent/child physical runs are folded into the root's attempts.
97
- * A run with no `frontendRecoveryState` is its own single-attempt root.
98
- */
99
- export function aggregateFrontendRecoveryRoots(runs) {
100
- const byRoot = new Map();
101
- for (const run of runs) {
102
- const rootId = run.frontendRecoveryState?.recoveryRootRunId ?? run.runId;
103
- const list = byRoot.get(rootId) ?? [];
104
- list.push(run);
105
- byRoot.set(rootId, list);
106
- }
107
- const result = [];
108
- for (const [rootId, list] of byRoot) {
109
- const sorted = [...list].sort((a, b) => (a.frontendRecoveryState?.attemptIndex ?? 0) -
110
- (b.frontendRecoveryState?.attemptIndex ?? 0));
111
- // The root's displayed status is the last attempt's status, unless the
112
- // root run itself finished (a child recovered only surfaces `finished`
113
- // via the manifest, not via the raw root run status).
114
- const last = sorted[sorted.length - 1];
115
- result.push({
116
- recoveryRootRunId: rootId,
117
- status: last.status,
118
- attemptCount: sorted.length,
119
- runIds: sorted.map((run) => run.runId),
120
- });
121
- }
122
- return result;
123
- }