@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.
Files changed (49) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +1 -1
  4. package/bin/loop-agent.js +37 -1
  5. package/dist/build-stamp.json +6 -0
  6. package/dist/cli/program.js +2 -2
  7. package/dist/executors/dag-pi-executor.js +44 -0
  8. package/dist/shared/package-metadata.js +42 -0
  9. package/dist/worker/console/chat/assistant-content.js +11 -0
  10. package/dist/worker/console/chat/pi-runtime.js +6 -2
  11. package/dist/worker/console/chat/turn-process.js +17 -9
  12. package/dist/worker/console/chat/workspace-landing.js +1 -1
  13. package/dist/worker/console/static/assets/index-DuVLjCIT.js +57 -0
  14. package/dist/worker/console/static/index.html +1 -1
  15. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -0
  16. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +15 -3
  17. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  18. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  19. package/dist/worker/console/static-src/operator-chat/useChatThread.js +1 -0
  20. package/dist/worker/loop-agent/loop-agent-client.js +17 -3
  21. package/dist/worker/observability/read-model.js +20 -0
  22. package/dist/worker/observe/spec-evidence.js +3 -8
  23. package/dist/worker/observe/static/views/dag-inspector.js +6 -71
  24. package/dist/worker/preflight.js +2 -1
  25. package/dist/workflows/dag/backend-test-scenario-param.js +33 -23
  26. package/dist/workflows/dag/contract-output-registry.js +14 -0
  27. package/dist/workflows/dag/contract-validator-registrations.js +8 -0
  28. package/dist/workflows/dag/dynamic-runtime/shared.js +9 -1
  29. package/dist/workflows/dag/failure-routing.js +9 -4
  30. package/dist/workflows/dag/frontend-implementation-contract.js +233 -39
  31. package/dist/workflows/dag/frontend-prewrite-gate.js +364 -61
  32. package/dist/workflows/dag/frontend-recovery-plan.js +73 -0
  33. package/dist/workflows/dag/frontend-recovery-root-manifest.js +123 -0
  34. package/dist/workflows/dag/frontend-recovery-run.js +539 -0
  35. package/dist/workflows/dag/frontend-repair.js +219 -18
  36. package/dist/workflows/dag/frontend-verification-trace.js +47 -32
  37. package/dist/workflows/dag/frontend-writer-recovery.js +106 -0
  38. package/dist/workflows/dag/frontend-writer-rollback.js +821 -0
  39. package/dist/workflows/dag/init-hybrid.js +49 -24
  40. package/dist/workflows/dag/lifecycle.js +4 -0
  41. package/dist/workflows/dag/node-execution.js +89 -0
  42. package/dist/workflows/dag/recovery-recommendation.js +58 -0
  43. package/dist/workflows/dag/report.js +6 -0
  44. package/dist/workflows/dag/runner.js +245 -11
  45. package/dist/workflows/dag/scheduler.js +257 -3
  46. package/dist/workflows/dag/types.js +130 -2
  47. package/docs/templates/frontend-task-constraints.md +13 -7
  48. package/package.json +4 -3
  49. 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
- role: "implementer",
2720
- executor: "pi",
2721
- toolProfile: "write",
2722
- complexity: resolveWriterComplexity(taskConfig),
2723
- writePolicy: "exclusive",
2724
- writeSet: implementPaths.writeSet,
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
- role: "implementer",
2785
- executor: "pi",
2786
- toolProfile: "write",
2787
- complexity: resolveWriterComplexity(taskConfig),
2788
- writePolicy: "exclusive",
2789
- writeSet: implementPaths.writeSet,
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"