@tea-agent/loop-agent 0.35.2 → 0.35.4-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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +43 -1
- package/README.md +1 -1
- package/bin/loop-agent.js +37 -1
- package/dist/build-stamp.json +6 -0
- package/dist/cli/program.js +2 -2
- package/dist/executors/dag-pi-executor.js +44 -0
- package/dist/shared/package-metadata.js +42 -0
- package/dist/worker/console/chat/assistant-content.js +11 -0
- package/dist/worker/console/chat/pi-runtime.js +6 -2
- package/dist/worker/console/chat/turn-process.js +17 -9
- package/dist/worker/console/chat/workspace-landing.js +1 -1
- package/dist/worker/console/static/assets/index-DuVLjCIT.js +57 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +15 -3
- package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +1 -0
- package/dist/worker/loop-agent/loop-agent-client.js +17 -3
- package/dist/worker/observability/read-model.js +20 -0
- package/dist/worker/observe/spec-evidence.js +3 -8
- package/dist/worker/observe/static/views/dag-inspector.js +6 -71
- package/dist/worker/preflight.js +2 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +33 -23
- package/dist/workflows/dag/contract-output-registry.js +14 -0
- package/dist/workflows/dag/contract-validator-registrations.js +8 -0
- package/dist/workflows/dag/dynamic-runtime/shared.js +9 -1
- package/dist/workflows/dag/failure-routing.js +9 -4
- package/dist/workflows/dag/frontend-implementation-contract.js +233 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +364 -61
- package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
- package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
- package/dist/workflows/dag/frontend-recovery-run.js +539 -0
- package/dist/workflows/dag/frontend-repair.js +219 -18
- package/dist/workflows/dag/frontend-verification-trace.js +47 -32
- package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
- package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
- package/dist/workflows/dag/init-hybrid.js +49 -24
- package/dist/workflows/dag/lifecycle.js +4 -0
- package/dist/workflows/dag/node-execution.js +89 -0
- package/dist/workflows/dag/recovery-recommendation.js +58 -0
- package/dist/workflows/dag/report.js +6 -0
- package/dist/workflows/dag/runner.js +245 -11
- package/dist/workflows/dag/scheduler.js +257 -3
- package/dist/workflows/dag/types.js +130 -2
- package/docs/templates/frontend-task-constraints.md +13 -7
- package/package.json +4 -3
- package/dist/worker/console/static/assets/index-gVHrlqI9.js +0 -56
|
@@ -2199,6 +2199,26 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2199
2199
|
return { ...task, depends_on };
|
|
2200
2200
|
});
|
|
2201
2201
|
}
|
|
2202
|
+
/**
|
|
2203
|
+
* AC-1: shared frontend writer node defaults. frontend-implement-pi and
|
|
2204
|
+
* frontend-repair-pi share one prompt/contract/writeSet-guard surface and an
|
|
2205
|
+
* identical writeSet; only id, depends_on, runIf, outputContract, and
|
|
2206
|
+
* subtask_prompt differ per phase.
|
|
2207
|
+
*/
|
|
2208
|
+
function buildFrontendWriterNodeDefaults(input) {
|
|
2209
|
+
return {
|
|
2210
|
+
role: "implementer",
|
|
2211
|
+
executor: "pi",
|
|
2212
|
+
toolProfile: "write",
|
|
2213
|
+
complexity: input.complexity,
|
|
2214
|
+
writePolicy: "exclusive",
|
|
2215
|
+
writeSet: input.writeSet,
|
|
2216
|
+
allowedPaths: input.allowedPaths,
|
|
2217
|
+
forbiddenPaths: input.forbiddenPaths,
|
|
2218
|
+
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2219
|
+
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2202
2222
|
async function buildFrontendHybridDagFromTask(sources) {
|
|
2203
2223
|
const { taskConfig } = sources;
|
|
2204
2224
|
const mockCapability = sources.frontendMockCapability ?? {
|
|
@@ -2339,6 +2359,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2339
2359
|
`Every requirement MUST have a non-empty expectedOutcome. Every interaction MUST have non-empty trigger and expectedBehavior. UI states with applicable=true MUST have non-empty expectedBehavior. Empty strings or omitted fields for these will cause contract rejection.`,
|
|
2340
2360
|
].join("\n")
|
|
2341
2361
|
: "";
|
|
2362
|
+
const verificationTargetFileInstruction = [
|
|
2363
|
+
`## Verification target file semantics`,
|
|
2364
|
+
`verificationTargets[].file is the code file that the target verifies (the file the writer changes), NOT where the command is defined.`,
|
|
2365
|
+
`Non-static targets (type unit/component/integration/mock) MUST set file to a concrete code file inside the implementation writeSet (task allowedPaths); the prewrite gate rejects any non-static target whose file falls outside the writeSet.`,
|
|
2366
|
+
`Command-level checks that run project-wide (all tests, typecheck, build, governance) MUST use type "static" and must NOT be bound as non-static targets with file=package.json/tsconfig.json/vite.config.ts/scripts/*. Static targets are exempt from the writeSet containment check.`,
|
|
2367
|
+
].join("\n");
|
|
2342
2368
|
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
2343
2369
|
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
2344
2370
|
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
@@ -2532,6 +2558,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2532
2558
|
writePolicy: "read-only",
|
|
2533
2559
|
outputMode: "structured-required",
|
|
2534
2560
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2561
|
+
structuredContractOutput: {
|
|
2562
|
+
schemaId: "frontend-implementation-contract-v1",
|
|
2563
|
+
retryOnInvalid: true,
|
|
2564
|
+
},
|
|
2535
2565
|
allowedPaths: readOnlyPaths,
|
|
2536
2566
|
forbiddenPaths,
|
|
2537
2567
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
@@ -2545,6 +2575,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2545
2575
|
"End with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response; the plan text must not contain other balanced JSON objects.",
|
|
2546
2576
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2547
2577
|
requirementCoverageInstruction,
|
|
2578
|
+
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2579
|
+
verificationTargetFileInstruction,
|
|
2548
2580
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
2549
2581
|
fixedVerificationContext,
|
|
2550
2582
|
sourceContext,
|
|
@@ -2584,6 +2616,10 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2584
2616
|
writePolicy: "read-only",
|
|
2585
2617
|
outputMode: "structured-required",
|
|
2586
2618
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2619
|
+
structuredContractOutput: {
|
|
2620
|
+
schemaId: "frontend-implementation-contract-v1",
|
|
2621
|
+
retryOnInvalid: true,
|
|
2622
|
+
},
|
|
2587
2623
|
allowedPaths: readOnlyPaths,
|
|
2588
2624
|
forbiddenPaths,
|
|
2589
2625
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
@@ -2598,6 +2634,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2598
2634
|
"End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. This node is the single contract JSON producer when the design review requests revision: the fenced block is the authoritative contract the prewrite gate materializes. Do not emit any raw JSON, JSON in prose, or a second fenced block anywhere in the response. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
|
|
2599
2635
|
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
|
|
2600
2636
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2637
|
+
verificationTargetFileInstruction,
|
|
2601
2638
|
fixedVerificationContext,
|
|
2602
2639
|
sourceContext,
|
|
2603
2640
|
frontendContractSchemaBlock,
|
|
@@ -2716,18 +2753,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2716
2753
|
? ["frontend-lint-baseline-shell"]
|
|
2717
2754
|
: []),
|
|
2718
2755
|
],
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
allowedPaths: implementPaths.allowedPaths,
|
|
2726
|
-
forbiddenPaths,
|
|
2727
|
-
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2728
|
-
writerOutcomePolicy: {
|
|
2729
|
-
type: "implementation-outcome-v1",
|
|
2730
|
-
},
|
|
2756
|
+
...buildFrontendWriterNodeDefaults({
|
|
2757
|
+
complexity: resolveWriterComplexity(taskConfig),
|
|
2758
|
+
writeSet: implementPaths.writeSet,
|
|
2759
|
+
allowedPaths: implementPaths.allowedPaths,
|
|
2760
|
+
forbiddenPaths,
|
|
2761
|
+
}),
|
|
2731
2762
|
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a Markdown delivery summary with Contract Ref (path/schema/hash), Changed Files, Requirements Implemented, UI States, Tests Changed, Verification Attempts, Deviations, and Residual Risks. Follow fixed stages: contract confirm → tests → component/state → API/Mock → focused checks → diff cleanup.",
|
|
2732
2763
|
subtask_prompt: [
|
|
2733
2764
|
"Implement against the validated run-owned Frontend Implementation Contract from frontend-prewrite-gate-shell (path/schema/hash). Do not rebuild the contract from Markdown alone.",
|
|
@@ -2781,18 +2812,12 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2781
2812
|
id: "frontend-repair-pi",
|
|
2782
2813
|
depends_on: ["frontend-verify-assess-shell", implementId],
|
|
2783
2814
|
runIf: "$.nodes['frontend-verify-assess-shell'].json.eligible == true",
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
allowedPaths: implementPaths.allowedPaths,
|
|
2791
|
-
forbiddenPaths,
|
|
2792
|
-
skills: FRONTEND_BOUNDED_IMPLEMENT_SKILLS,
|
|
2793
|
-
writerOutcomePolicy: {
|
|
2794
|
-
type: "implementation-outcome-v1",
|
|
2795
|
-
},
|
|
2815
|
+
...buildFrontendWriterNodeDefaults({
|
|
2816
|
+
complexity: resolveWriterComplexity(taskConfig),
|
|
2817
|
+
writeSet: implementPaths.writeSet,
|
|
2818
|
+
allowedPaths: implementPaths.allowedPaths,
|
|
2819
|
+
forbiddenPaths,
|
|
2820
|
+
}),
|
|
2796
2821
|
outputContract: "First non-empty line must be exactly one of: IMPLEMENTATION_OUTCOME: changed; IMPLEMENTATION_OUTCOME: already-satisfied; IMPLEMENTATION_OUTCOME: blocked. Then a repair summary for an eligible repairable assessment. Must not expand writeSet, re-interpret requirements, skip tests, or enable Mock by default.",
|
|
2797
2822
|
subtask_prompt: [
|
|
2798
2823
|
"Read contracts/frontend-repair-assessment.json and the validated frontend implementation contract.",
|
|
@@ -743,6 +743,7 @@ function findDoctorFailureNode(state) {
|
|
|
743
743
|
nodeId: state.pausedByNodeId,
|
|
744
744
|
status: node?.status,
|
|
745
745
|
rawFailureCategory: node?.failureCategory,
|
|
746
|
+
skippedReason: node?.skippedReason,
|
|
746
747
|
};
|
|
747
748
|
}
|
|
748
749
|
const errorEntry = Object.entries(state.nodes).find(([, node]) => node.status === "ERROR");
|
|
@@ -758,6 +759,7 @@ function findDoctorFailureNode(state) {
|
|
|
758
759
|
nodeId: selected[0],
|
|
759
760
|
status: selected[1].status,
|
|
760
761
|
rawFailureCategory: selected[1].failureCategory,
|
|
762
|
+
skippedReason: selected[1].skippedReason,
|
|
761
763
|
};
|
|
762
764
|
}
|
|
763
765
|
async function readRunOwnedBackendTestClassification(runDir) {
|
|
@@ -786,6 +788,7 @@ async function resolveDoctorFailureRouting(input) {
|
|
|
786
788
|
normalizedFailureCategory: "unknown",
|
|
787
789
|
nodeId: input.nodeId,
|
|
788
790
|
productLineFailureCategory: classifiedCategory,
|
|
791
|
+
skippedReason: input.skippedReason,
|
|
789
792
|
});
|
|
790
793
|
}
|
|
791
794
|
return routeDagFailure(input);
|
|
@@ -807,6 +810,7 @@ async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
|
807
810
|
rawFailureCategory,
|
|
808
811
|
normalizedFailureCategory: normalizedCategory,
|
|
809
812
|
nodeId: failure.nodeId,
|
|
813
|
+
skippedReason: failure.skippedReason,
|
|
810
814
|
});
|
|
811
815
|
const evidence = failure.nodeId
|
|
812
816
|
? path.join(located.runDir, failure.nodeId, "result.summary.md")
|
|
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
6
6
|
import { recordDecisionEnvelopeForNode, shouldPauseOnHumanEscalation, writeHumanEscalationArtifacts, } from "./decision-envelope.js";
|
|
7
|
+
import { FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT, FRONTEND_WRITER_NODE_IDS, isFrontendWriterAuthorized, readFrontendPrewriteResult, } from "./scheduler.js";
|
|
7
8
|
import { writeNodeRecord, writeNodeSkillArtifacts } from "./run-store.js";
|
|
8
9
|
import { resolveContextPolicy } from "./context-policy.js";
|
|
9
10
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
@@ -12,6 +13,8 @@ import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry,
|
|
|
12
13
|
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
13
14
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
14
15
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
16
|
+
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
17
|
+
import "./contract-validator-registrations.js";
|
|
15
18
|
import { writeDagNodeJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
16
19
|
import { buildProjectGovernanceContext, readCompletedWriterChangeManifests, writeProjectGovernanceContext, } from "./project-governance-context.js";
|
|
17
20
|
import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillSnapshotIntegrityError, readSkillSnapshot, } from "./skill-snapshot.js";
|
|
@@ -138,6 +141,20 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
138
141
|
buildProtocolRetryInstruction(task.outputProtocol, previousProtocolReason),
|
|
139
142
|
].join("\n");
|
|
140
143
|
}
|
|
144
|
+
if (previousFailureCategory === "invalid-output" &&
|
|
145
|
+
task.structuredContractOutput &&
|
|
146
|
+
previousProtocolReason) {
|
|
147
|
+
return [
|
|
148
|
+
basePrompt,
|
|
149
|
+
"",
|
|
150
|
+
"<retry_instruction>",
|
|
151
|
+
"Previous attempt produced an invalid frontend implementation contract:",
|
|
152
|
+
previousProtocolReason,
|
|
153
|
+
"Return exactly one fenced json block conforming to the frontend-implementation-contract-v1 schema.",
|
|
154
|
+
"Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
|
|
155
|
+
"</retry_instruction>",
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
141
158
|
if (previousFailureCategory === "writer-empty-diff") {
|
|
142
159
|
const maxAttempts = task.retryPolicy?.maxAttempts ?? 3;
|
|
143
160
|
// When a completeness progress exists for this writer, fold the concrete
|
|
@@ -395,6 +412,21 @@ export async function executeDagNode(input) {
|
|
|
395
412
|
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
396
413
|
};
|
|
397
414
|
const failSkillSnapshot = (error) => failBeforePrompt(error, "skill-snapshot-integrity");
|
|
415
|
+
const skipFrontendWriter = async (admission) => {
|
|
416
|
+
const skippedAt = new Date().toISOString();
|
|
417
|
+
node.startedAt ??= skippedAt;
|
|
418
|
+
node.frontendWriterAdmission = admission;
|
|
419
|
+
node.status = "SKIPPED";
|
|
420
|
+
node.skippedReason = "frontend-prewrite-not-authorized";
|
|
421
|
+
node.finishedAt = skippedAt;
|
|
422
|
+
node.lastActivityAt = skippedAt;
|
|
423
|
+
node.durationMs = durationBetween(node.startedAt, node.finishedAt);
|
|
424
|
+
node.timing = { retryBackoffMs: 0, settlementCleanupMs: 0 };
|
|
425
|
+
state.nodes[nodeId].nodeRecordPath = path.join(runDir, `${nodeId}.json`);
|
|
426
|
+
await writeNodeRecord(runDir, nodeId, state.nodes[nodeId]);
|
|
427
|
+
await input.persistState();
|
|
428
|
+
await notifyNodeObserver(input.observer, "onNodeFinish", nodeId, state);
|
|
429
|
+
};
|
|
398
430
|
if (task.finalWriteSetApproval) {
|
|
399
431
|
const authorization = parseAndValidateFinalWriteSetApproval({ task, spec, state });
|
|
400
432
|
if (!authorization.ok) {
|
|
@@ -423,6 +455,30 @@ export async function executeDagNode(input) {
|
|
|
423
455
|
effectiveWriteSet: [...authorization.effectiveWriteSet],
|
|
424
456
|
};
|
|
425
457
|
}
|
|
458
|
+
if (FRONTEND_WRITER_NODE_IDS.includes(nodeId)) {
|
|
459
|
+
const admission = await readFrontendPrewriteResult(runDir);
|
|
460
|
+
if (!admission.ok) {
|
|
461
|
+
await skipFrontendWriter(undefined);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const decision = isFrontendWriterAuthorized(admission.result);
|
|
465
|
+
const record = {
|
|
466
|
+
schemaVersion: 1,
|
|
467
|
+
writerNodeId: nodeId,
|
|
468
|
+
decision,
|
|
469
|
+
sourceArtifact: FRONTEND_PREWRITE_RESULT_SOURCE_ARTIFACT,
|
|
470
|
+
artifactHash: admission.artifactHash,
|
|
471
|
+
checkedAt: new Date().toISOString(),
|
|
472
|
+
reason: decision === "denied"
|
|
473
|
+
? `classification: ${admission.result.classification}`
|
|
474
|
+
: null,
|
|
475
|
+
};
|
|
476
|
+
if (decision === "denied") {
|
|
477
|
+
await skipFrontendWriter(record);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
node.frontendWriterAdmission = record;
|
|
481
|
+
}
|
|
426
482
|
let projectGovernanceContext;
|
|
427
483
|
if (task.governanceStandardReview) {
|
|
428
484
|
try {
|
|
@@ -689,6 +745,39 @@ export async function executeDagNode(input) {
|
|
|
689
745
|
previousProtocolReason = undefined;
|
|
690
746
|
}
|
|
691
747
|
}
|
|
748
|
+
// R1: structured contract nodes self-validate their output so schema,
|
|
749
|
+
// typo, and null violations surface as retryable invalid-output at the
|
|
750
|
+
// producing node instead of failing the whole run at the prewrite gate.
|
|
751
|
+
if (result.ok && task.structuredContractOutput) {
|
|
752
|
+
const validator = getStructuredContractValidator(task.structuredContractOutput.schemaId);
|
|
753
|
+
if (validator) {
|
|
754
|
+
const contractText = canonicalNodeOutput(result);
|
|
755
|
+
const contractCheck = await validator({
|
|
756
|
+
runDir,
|
|
757
|
+
text: contractText,
|
|
758
|
+
sourceBinding: spec.sourceBinding,
|
|
759
|
+
});
|
|
760
|
+
if (!contractCheck.ok) {
|
|
761
|
+
result = {
|
|
762
|
+
...result,
|
|
763
|
+
ok: false,
|
|
764
|
+
failureCategory: "invalid-output",
|
|
765
|
+
stderr: [result.stderr, contractCheck.reason]
|
|
766
|
+
.filter(Boolean)
|
|
767
|
+
.join("\n"),
|
|
768
|
+
};
|
|
769
|
+
previousProtocolReason = contractCheck.reason;
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
previousProtocolReason = undefined;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
// DAG spec validation rejects unknown schemaIds; this is a
|
|
777
|
+
// defensive fallback for a registry that has not been populated.
|
|
778
|
+
console.warn(`[run-dag] warning: no structured contract validator registered for schemaId ${task.structuredContractOutput.schemaId}; skipping node self-check`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
692
781
|
const attemptFinishedAt = new Date().toISOString();
|
|
693
782
|
const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
|
|
694
783
|
totalAttemptWallDurationMs += attemptWallDurationMs;
|
|
@@ -269,3 +269,61 @@ export function planDagRecovery(input) {
|
|
|
269
269
|
}
|
|
270
270
|
return planByNormalizedCategory(input);
|
|
271
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Phase 6: route a frontend recovery outcome to a recovery CTA (read-only; never
|
|
274
|
+
* mutates a run's status). The six outcomes map to the existing recovery actions
|
|
275
|
+
* so the Console recovery page reuses the current CTA surface.
|
|
276
|
+
*/
|
|
277
|
+
export function recommendFrontendRecoveryOutcome(outcome) {
|
|
278
|
+
switch (outcome) {
|
|
279
|
+
case "none":
|
|
280
|
+
return {
|
|
281
|
+
action: "none",
|
|
282
|
+
summary: "未发生前端恢复;按普通终态展示。",
|
|
283
|
+
reason: "该 frontend-implementation 根没有触发自动恢复。",
|
|
284
|
+
humanRequired: false,
|
|
285
|
+
autoRetryEligible: false,
|
|
286
|
+
};
|
|
287
|
+
case "recovered":
|
|
288
|
+
return {
|
|
289
|
+
action: "none",
|
|
290
|
+
summary: "自动恢复成功;child 已重新实现并通过验证。",
|
|
291
|
+
reason: "parent 的 transient 失败已由 child 恢复,全链按 root 计一次成功。",
|
|
292
|
+
humanRequired: false,
|
|
293
|
+
autoRetryEligible: false,
|
|
294
|
+
};
|
|
295
|
+
case "candidate-contract-invalid":
|
|
296
|
+
return {
|
|
297
|
+
action: "manual-review",
|
|
298
|
+
summary: "方案输出无效:candidate JSON 无法解析/推导,重试用尽。",
|
|
299
|
+
reason: "建议人工检查需求或重新生成方案后再跑。",
|
|
300
|
+
humanRequired: true,
|
|
301
|
+
autoRetryEligible: false,
|
|
302
|
+
};
|
|
303
|
+
case "prewrite-blocked":
|
|
304
|
+
return {
|
|
305
|
+
action: "manual-review",
|
|
306
|
+
summary: "治理门禁阻断:source stale / writeSet 越界 / Mock policy / contract 违规。",
|
|
307
|
+
reason: "需人工修正任务边界/事实后重跑,不自动重试。",
|
|
308
|
+
humanRequired: true,
|
|
309
|
+
autoRetryEligible: false,
|
|
310
|
+
};
|
|
311
|
+
case "repair-exhausted":
|
|
312
|
+
return {
|
|
313
|
+
action: "rerun-after-fix",
|
|
314
|
+
summary: "自动修复未通过:verify 失败,1 次 repair 后 reverify 仍失败。",
|
|
315
|
+
reason: "建议 `dag rerun` 或人工排查。",
|
|
316
|
+
humanRequired: true,
|
|
317
|
+
autoRetryEligible: false,
|
|
318
|
+
commandHint: "loop-agent dag rerun --run-id <runId>",
|
|
319
|
+
};
|
|
320
|
+
case "auto-recovery-blocked":
|
|
321
|
+
return {
|
|
322
|
+
action: "manual-review",
|
|
323
|
+
summary: "自动恢复被阻止:rollback 无法证明完整或存在外部/越界修改。",
|
|
324
|
+
reason: "必须人工介入,禁止自动覆盖用户文件。",
|
|
325
|
+
humanRequired: true,
|
|
326
|
+
autoRetryEligible: false,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -441,6 +441,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
441
441
|
normalizedFailureCategory,
|
|
442
442
|
nodeId,
|
|
443
443
|
executor: node.executor,
|
|
444
|
+
skippedReason: node.skippedReason,
|
|
444
445
|
});
|
|
445
446
|
const followUp = node.status === "ERROR" || node.status === "SKIPPED"
|
|
446
447
|
? recommendFollowUpForFailureCategory(node.failureCategory)
|
|
@@ -505,6 +506,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
505
506
|
normalizedFailureCategory: normalizeDagFailureCategory(pausedNode.failureCategory, pausedNode.status),
|
|
506
507
|
failureCategory: pausedNode.failureCategory,
|
|
507
508
|
nodeId: input.state.pausedByNodeId,
|
|
509
|
+
skippedReason: pausedNode.skippedReason,
|
|
508
510
|
}
|
|
509
511
|
: firstActionableNode
|
|
510
512
|
? {
|
|
@@ -513,6 +515,7 @@ export async function buildDagRunReportEntry(input) {
|
|
|
513
515
|
normalizeDagFailureCategory(firstActionableNode.failureCategory, firstActionableNode.status),
|
|
514
516
|
failureCategory: firstActionableNode.failureCategory,
|
|
515
517
|
nodeId: firstActionableNode.nodeId,
|
|
518
|
+
skippedReason: input.state.nodes[firstActionableNode.nodeId]?.skippedReason,
|
|
516
519
|
}
|
|
517
520
|
: {
|
|
518
521
|
status: input.state.status,
|
|
@@ -529,6 +532,9 @@ export async function buildDagRunReportEntry(input) {
|
|
|
529
532
|
rawFailureCategory: runRecoverySource.failureCategory,
|
|
530
533
|
normalizedFailureCategory: runRecoverySource.normalizedFailureCategory,
|
|
531
534
|
nodeId: "nodeId" in runRecoverySource ? runRecoverySource.nodeId : undefined,
|
|
535
|
+
skippedReason: "skippedReason" in runRecoverySource
|
|
536
|
+
? runRecoverySource.skippedReason
|
|
537
|
+
: undefined,
|
|
532
538
|
});
|
|
533
539
|
const runFollowUp = input.state.status === "failed" ||
|
|
534
540
|
input.state.status === "partial_failed"
|