@tea-agent/loop-agent 0.38.0 → 0.38.1

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 (117) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/commands/init-upgrade.js +97 -19
  4. package/dist/commands/init.js +14 -2
  5. package/dist/executors/dag-pi-executor.js +1 -0
  6. package/dist/governance/manifest-types.js +2 -0
  7. package/dist/infrastructure/harness/atomic-write.js +2 -1
  8. package/dist/shared/openspec-spec.js +76 -0
  9. package/dist/shared/operator/capabilities.js +65 -8
  10. package/dist/task/config-types.js +21 -0
  11. package/dist/task/frontend-project-capability.js +292 -38
  12. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  13. package/dist/worker/console/chat/pi-runtime.js +155 -78
  14. package/dist/worker/console/chat/repo-walk.js +10 -1
  15. package/dist/worker/console/chat/routes.js +19 -8
  16. package/dist/worker/console/chat/semantic-activity.js +17 -5
  17. package/dist/worker/console/chat/turn-process.js +17 -30
  18. package/dist/worker/console/harness-pi-model-matrix.js +146 -0
  19. package/dist/worker/console/operator-actions.js +123 -0
  20. package/dist/worker/console/operator-user-error.js +19 -0
  21. package/dist/worker/console/pi-readiness.js +7 -1
  22. package/dist/worker/console/security.js +20 -1
  23. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-Hfsot6rP.js} +1 -1
  24. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-YGwEASin.js} +1 -1
  25. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-CmOM0F92.js} +1 -1
  26. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DDLuaHC5.js} +1 -1
  27. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-B0Um43W4.js} +1 -1
  28. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +1 -0
  29. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DvBN4JZc.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-B1UVgW4E.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-Cf0GgXzX.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-DbNA24sj.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-Bi_0Be06.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-C8pPGvRd.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BitQY8Z-.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-U67GCiEo.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-IoJwXPBA.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-YDZ_UHgE.js} +1 -1
  39. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +1 -0
  40. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +1 -0
  41. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-CUZgsiRC.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-COmCh4pT.js} +1 -1
  43. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-Ta6FSm68.js} +1 -1
  44. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-UdcR-VW9.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-DybcO3Et.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-wErDFWLw.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-DkVHZWqz.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DdqWs7QN.js} +1 -1
  49. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-C9dMVmDo.js} +1 -1
  50. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-B2T2WcBK.js} +1 -1
  51. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-Bt9rjDFq.js} +1 -1
  52. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-U8ic_tvi.js} +1 -1
  53. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-BfQJdQoO.js} +1 -1
  54. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CpMftH4O.js} +1 -1
  55. package/dist/worker/console/static/assets/index-BA5-N5ER.css +1 -0
  56. package/dist/worker/console/static/assets/index-DKPXBrdc.js +325 -0
  57. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-DzqWXNO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js} +1 -1
  59. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-ChPtKFLz.js} +1 -1
  60. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-PRe7wB5u.js} +1 -1
  61. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-DpKX6QNp.js} +1 -1
  62. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-Dnl76b7Y.js} +5 -5
  63. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-S9PCXc3J.js} +1 -1
  64. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-Du8e-FSX.js} +1 -1
  65. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-CY6OmHTc.js} +1 -1
  66. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-C65GPhpj.js} +1 -1
  67. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-U6IUiTCL.js} +1 -1
  68. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D-I_9Qhi.js} +1 -1
  69. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-B57Fhed0.js} +1 -1
  70. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-CJOtiw21.js} +1 -1
  71. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-FyX8zxSh.js} +1 -1
  72. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-eF-pbs9t.js} +1 -1
  73. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +1 -0
  74. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-CcfkA-Dq.js} +2 -2
  75. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +8 -0
  76. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-DH8FjnOK.js} +1 -1
  77. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-beoTagJg.js} +1 -1
  78. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CQSfw4cK.js} +1 -1
  79. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-CQgzQ6jn.js} +1 -1
  80. package/dist/worker/console/static/index.html +2 -2
  81. package/dist/worker/console/static-src/app/console-types.js +51 -0
  82. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +44 -21
  83. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  84. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  85. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +15 -0
  86. package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
  87. package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
  88. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  89. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  90. package/dist/workflows/dag/failure-routing.js +9 -4
  91. package/dist/workflows/dag/frontend-implementation-contract.js +158 -0
  92. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  93. package/dist/workflows/dag/frontend-prewrite-gate.js +411 -20
  94. package/dist/workflows/dag/init-hybrid.js +272 -27
  95. package/dist/workflows/dag/lifecycle.js +4 -0
  96. package/dist/workflows/dag/node-execution.js +51 -9
  97. package/dist/workflows/dag/report.js +6 -0
  98. package/dist/workflows/dag/retry-policy.js +15 -0
  99. package/dist/workflows/dag/types.js +50 -4
  100. package/dist/workflows/dag/validate.js +7 -2
  101. package/docs/templates/frontend-design-contract.md +40 -14
  102. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  103. package/docs/templates/frontend-task-constraints.md +40 -13
  104. package/docs/templates/frontend-task-requirement.md +54 -20
  105. package/docs/templates/harness.schema.json +8 -0
  106. package/harness.json +11 -2
  107. package/package.json +1 -1
  108. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  109. package/skills/frontend-implementation/references/design-spec.md +16 -19
  110. package/skills/frontend-implementation/references/node-contracts.md +15 -11
  111. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  114. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  115. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  116. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -7,9 +7,10 @@ import { assertValidDagSpec } from "./validate.js";
7
7
  import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1, DAG_RUNTIME_CONTRACT_SCHEMA_VERSION, DEFAULT_DAG_OUTPUT_LANGUAGE, DEFAULT_DAG_EXECUTOR_MODELS, parseDagSpec, } from "./types.js";
8
8
  import { planMavenVerification, } from "../../verification/maven/index.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
+ import { extractTaskSourceOpenspecPaths } from "../../shared/openspec-spec.js";
10
11
  import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
11
12
  import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
12
- import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
13
+ import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
13
14
  import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
14
15
  import { resolveAdapter } from "../../adapters/index.js";
15
16
  import { loadHarnessManifest } from "../../governance/harness.js";
@@ -19,6 +20,7 @@ import { applySddEmbeddedEnhancements, probeRepoLocalSddSkills, } from "./sdd-em
19
20
  import { discoverProjectGovernancePresence } from "./project-governance-context.js";
20
21
  import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
21
22
  import { materializeTaskReferenceDocs } from "../../task/source-references.js";
23
+ import { REQUIREMENT_FACT_ROLES } from "../../task/source-prepare/artifact-meta.js";
22
24
  import { observeTaskContract } from "../../task/contract/observe.js";
23
25
  import { dagHasWriterExecution } from "./task-contract-binding.js";
24
26
  import { DEFAULT_VERIFY_TIMEOUT_MS, resolveVerifyPreset, } from "../../executors/shell-verification.js";
@@ -558,6 +560,39 @@ export function hasApiDependency(sources) {
558
560
  .some((clause) => !negationPatterns.some((pattern) => pattern.test(clause)) &&
559
561
  dependencyPatterns.some((pattern) => pattern.test(clause)));
560
562
  }
563
+ /**
564
+ * Generation-time heuristic: does the task source mention interface/API/Mock
565
+ * needs anywhere (requirement or constraints)?
566
+ *
567
+ * Mirrors hasApiDependency's clause-level negation filtering so explicit
568
+ * "not involved / not implementing" clauses and out-of-scope sections do not
569
+ * become positive evidence. Used to emit a machine-visible advisory when auto
570
+ * mode narrows Mock to not-needed despite the source mentioning such needs.
571
+ */
572
+ export function frontendSourceMentionsMock(sources) {
573
+ const text = [sources.requirementMarkdown, sources.constraintMarkdown]
574
+ .filter((entry) => typeof entry === "string" && entry.trim().length > 0)
575
+ .map((entry) => normalizeTaskRequirementText(entry).replace(/`[^`\n]*`/g, " "))
576
+ .join("\n");
577
+ const signalPatterns = [
578
+ /mock/i,
579
+ /模拟服务/,
580
+ /接口桩/,
581
+ /\bmsw\b/i,
582
+ /(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
583
+ /接口/,
584
+ ];
585
+ const negationPatterns = [
586
+ /(?:不涉及|无需|不需要|不依赖|不调用|不请求|不实现|没有|禁止|不得).{0,16}(?:接口|后端|服务端|远程数据|异步数据|API|mock|模拟|桩)/i,
587
+ /\b(?:no|without|does\s+not|do\s+not|must\s+not)\b.{0,24}\b(?:api|endpoint|request|backend|server|mock)\b/i,
588
+ ];
589
+ return text
590
+ .split(/[。!?!?;;,,\r\n]+/)
591
+ .map((clause) => clause.trim())
592
+ .filter(Boolean)
593
+ .some((clause) => !negationPatterns.some((pattern) => pattern.test(clause)) &&
594
+ signalPatterns.some((pattern) => pattern.test(clause)));
595
+ }
561
596
  /**
562
597
  * Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
563
598
  *
@@ -1465,11 +1500,27 @@ function buildSourceContextBlock(sources) {
1465
1500
  .relative(path.join(sources.taskDir, "source"), reference.path)
1466
1501
  .replaceAll(path.sep, "/");
1467
1502
  const referenceRef = toDagSourcePath(sources, reference.path);
1468
- const referenceExcerpt = excerptMarkdown(reference.markdown, {
1469
- sourceRef: referenceRef,
1470
- });
1471
- boundReadPaths.push(`- reference ${relativePath}: ${referenceRef}`);
1472
- parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, referenceExcerpt.text);
1503
+ // Fact-source roles (requirement/acceptance) carry the fields contracts
1504
+ // depend on (AC/scope/columns). Inject them in full so a fixed excerpt
1505
+ // cannot silently drop definitions; archival roles stay excerpted.
1506
+ const isFactRole = reference.role !== undefined &&
1507
+ REQUIREMENT_FACT_ROLES.has(reference.role);
1508
+ const referenceExcerpt = isFactRole
1509
+ ? {
1510
+ text: reference.markdown.trim(),
1511
+ truncated: false,
1512
+ originalChars: reference.markdown.trim().length,
1513
+ maxChars: Number.POSITIVE_INFINITY,
1514
+ }
1515
+ : excerptMarkdown(reference.markdown, {
1516
+ sourceRef: referenceRef,
1517
+ });
1518
+ boundReadPaths.push(`- reference ${relativePath}${isFactRole ? " (full source)" : ""}: ${referenceRef}`);
1519
+ parts.push(`## Task source reference: ${relativePath}`, `Bound readPath (use for Pi read-tool calls): ${referenceRef}`, ...(isFactRole
1520
+ ? [
1521
+ `Full source injected (role: ${reference.role}) — complete and authoritative; no excerpt truncation applied.`,
1522
+ ]
1523
+ : []), referenceExcerpt.text);
1473
1524
  }
1474
1525
  parts.push("## Bound source read paths", ...boundReadPaths, "Use these repository-readable paths for any Pi read-tool calls. Bound files under `.harness/tasks/<taskId>/source/**` are read-only inputs: reading them is allowed even though writing `.harness/**` is forbidden.", "Never resolve task-relative citations such as `source/需求.md` or `source/references/*` against the repository root, invent `source/<taskId>/...`, search for substitutes, or fall back to `docs/**` when a bound read fails.", "## Task config summary", `- taskId: ${sources.taskConfig.taskId}`, `- flow: ${sources.taskConfig.flow}`, `- complexity: ${sources.taskConfig.complexity}`, `- contextProfile: ${sources.taskConfig.contextProfile}`, `- allowedPaths: ${sources.taskConfig.allowedPaths.join(", ") || "(none — review before execute)"}`, `- forbiddenPaths: ${sources.taskConfig.forbiddenPaths.join(", ") || "(none)"}`, '- Pi DAG nodes are read-only unless toolProfile="write" is explicitly selected for a bounded writer node.', "- Agent DAG read-only nodes must not write root artifacts/**; root artifacts/ is not a per-node scratchpad.", `- Derived execution contract and immutable references live under the Bound source read paths above (not as repo-root \`source/...\`).`);
1475
1526
  if (sources.taskConfig.hardConstraints.length > 0) {
@@ -1507,10 +1558,49 @@ async function loadMaterializedSourceReferences(sourceDir) {
1507
1558
  }
1508
1559
  await collect(referenceDir);
1509
1560
  referencePaths.sort((left, right) => left.localeCompare(right));
1510
- return Promise.all(referencePaths.map(async (filePath) => ({
1511
- path: filePath,
1512
- markdown: await readFile(filePath, "utf-8"),
1513
- })));
1561
+ const roleByPath = await loadReferenceRoles(sourceDir);
1562
+ return Promise.all(referencePaths.map(async (filePath) => {
1563
+ const relative = path
1564
+ .relative(sourceDir, filePath)
1565
+ .replaceAll(path.sep, "/");
1566
+ const role = roleByPath.get(relative);
1567
+ return {
1568
+ path: filePath,
1569
+ markdown: await readFile(filePath, "utf-8"),
1570
+ ...(role !== undefined ? { role } : {}),
1571
+ };
1572
+ }));
1573
+ }
1574
+ /**
1575
+ * Reads `source-manifest.json` into a materializedPath → role map so the DAG
1576
+ * generator can tell fact-source references (requirement/acceptance) apart
1577
+ * from archival ones (analysis/clarification/design). A missing or malformed
1578
+ * manifest yields an empty map; such references keep the bounded-excerpt
1579
+ * treatment instead of being injected in full.
1580
+ */
1581
+ async function loadReferenceRoles(sourceDir) {
1582
+ const roleByPath = new Map();
1583
+ const manifestPath = path.join(sourceDir, "source-manifest.json");
1584
+ let raw;
1585
+ try {
1586
+ raw = await readFile(manifestPath, "utf-8");
1587
+ }
1588
+ catch {
1589
+ return roleByPath;
1590
+ }
1591
+ try {
1592
+ const manifest = JSON.parse(raw);
1593
+ for (const document of manifest.documents ?? []) {
1594
+ if (typeof document.materializedPath === "string" &&
1595
+ typeof document.role === "string") {
1596
+ roleByPath.set(document.materializedPath.replaceAll(path.sep, "/"), document.role);
1597
+ }
1598
+ }
1599
+ }
1600
+ catch {
1601
+ // A malformed manifest must never break reference injection.
1602
+ }
1603
+ return roleByPath;
1514
1604
  }
1515
1605
  export async function loadTaskHybridSources(repoRoot, taskId) {
1516
1606
  const paths = getTaskPaths(repoRoot, taskId);
@@ -2111,6 +2201,7 @@ function resolveFrontendMockContextBlock(sources) {
2111
2201
  parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed when Mock is intentionally skipped, or select a safe Mock strategy if project evidence supports one.");
2112
2202
  if (frontendMockStrategyMustBeNotNeeded(sources)) {
2113
2203
  parts.push('Auto mode has no confirmed project Mock capability or no deterministic Mock verification command. The structured contract must set mockApi.strategy to "not-needed". Do not add Mock files or dependencies; keep the real request path as default and record any unproved backend behavior as Real Integration Gap.');
2204
+ parts.push('HARD CONSTRAINT (frozen at generation time): this DAG allows only mockApi.strategy "not-needed"; the prewrite gate rejects any other strategy. If project governance (openspec / ai_workspace / decision records, e.g. a DEC rule requiring native) demands Mock-backed verification, that is a generation-time contract gap, not a plan-revision defect: declare frontendMock.verifyCommands (or policy: "required") in task.json and regenerate the DAG. Do not emit any mockApi.strategy outside the allowlist and do not add Mock files or dependencies within this run.');
2114
2205
  }
2115
2206
  }
2116
2207
  if (mode === "blocked") {
@@ -2129,6 +2220,41 @@ function frontendMockStrategyMustBeNotNeeded(sources) {
2129
2220
  capabilityStatus === "ambiguous" ||
2130
2221
  !hasDeterministicMockVerification));
2131
2222
  }
2223
+ function resolveFrontendOpenspecGateConfig(sources) {
2224
+ const taskConfig = sources.taskConfig;
2225
+ const policy = taskConfig.frontendOpenspec?.policy ?? "cited";
2226
+ const declared = taskConfig.frontendOpenspec?.requiredReadPaths ?? [];
2227
+ const taskSourceCited = extractTaskSourceOpenspecPaths([sources.requirementMarkdown, sources.constraintMarkdown ?? ""].join("\n"));
2228
+ const scanStrict = sources.frontendProjectCapability?.designEvidence.normativePaths ?? [];
2229
+ const dedupeSorted = (paths) => [...new Set(paths)].sort();
2230
+ const openspecCandidateSources = {
2231
+ declared: dedupeSorted(declared),
2232
+ taskSourceCited: dedupeSorted(taskSourceCited),
2233
+ scanStrict: dedupeSorted(scanStrict),
2234
+ };
2235
+ const openspecCandidatePaths = policy === "cited"
2236
+ ? dedupeSorted([...declared, ...taskSourceCited])
2237
+ : openspecCandidateSources.scanStrict;
2238
+ return {
2239
+ openspecPolicy: policy,
2240
+ openspecCandidatePaths,
2241
+ openspecCandidateSources,
2242
+ };
2243
+ }
2244
+ const openspecCitationInstruction = [
2245
+ "OpenSpec 引用块(citation block):在 fenced json 契约块之后,追加**恰好一个** ```openspec-citations 围栏代码块(三反引号 + openspec-citations)。",
2246
+ '该块内每行一个 JSON 对象 {"path":"<repo 相对 openspec 路径>","section":"<命中章节或空串>","line":<int 或 null>},必须逐条列出你在本计划中实际读取并应用的每个 openspec 规范文件。',
2247
+ "prewrite gate 会用真实 read 事件核验每条引用:引用存在但无成功 read 事件 → openspec-citation-not-read;契约冻结的必读候选未被引用 → openspec-not-cited;两者都 fail-closed。不要引用未读取的路径。",
2248
+ ].join("\n");
2249
+ const frontendComponentConformanceInstruction = [
2250
+ "## Component Selection conformance (uiComponentChoices; hard rule)",
2251
+ "每个 UI 用途必须在契约的 uiComponentChoices[] 中声明组件选型:{ purpose, component, decision, specReference, rationale }。",
2252
+ "- decision=specified:前端规范(候选组件/主题桶 + 任务源显式引用)已定义该用途组件 → 必须使用该组件,并给精确 specReference { path, section, line }(path 必须是 openspec/ai_workspace 受支持规范路径)。",
2253
+ "- decision=reuse-existing:复用仓库既有组件/惯例(规范未点名)→ specReference 可为 null,rationale 说明复用的现有组件与依据。",
2254
+ "- decision=new:规范与既有代码均无合适组件 → specReference 必须为 null,rationale 必须说明偏差理由(design-review 审,最终 review 复核)。",
2255
+ "不得静默替换规范组件或自创组件而无偏差声明;spec 已定义该用途组件时不得改选其它组件。",
2256
+ "prewrite gate 确定性交叉校验:specReference.path 非法 → component-spec-reference-invalid;未在 openspec-citations 引用块中引用或未真实读取 → component-spec-not-cited;候选桶非空且契约有 UI 可见工作而 uiComponentChoices 缺失/空 → component-choices-missing。",
2257
+ ].join("\n");
2132
2258
  function resolveFrontendCapabilityContextBlock(sources) {
2133
2259
  const risk = sources.frontendRisk;
2134
2260
  const capability = sources.frontendProjectCapability;
@@ -2141,9 +2267,34 @@ function resolveFrontendCapabilityContextBlock(sources) {
2141
2267
  }
2142
2268
  if (capability) {
2143
2269
  parts.push("", capability.adapterGuidance);
2144
- if (capability.designEvidence.normativePaths.length > 0) {
2145
- parts.push(`openspec normative candidates: ${capability.designEvidence.normativePaths.slice(0, 12).join(", ")}`);
2270
+ const classified = capability.designEvidence.classified;
2271
+ const bucketLines = [];
2272
+ const pushBucket = (label, paths) => {
2273
+ if (paths.length > 0)
2274
+ bucketLines.push(`${label}: ${paths.join(", ")}`);
2275
+ };
2276
+ pushBucket("schemas", classified.schemas);
2277
+ pushBucket("code-template", classified.codeTemplate);
2278
+ pushBucket("rule.api", classified.rule.api);
2279
+ pushBucket("rule.mock", classified.rule.mock);
2280
+ pushBucket("rule.router", classified.rule.router);
2281
+ pushBucket("rule.hooks", classified.rule.hooks);
2282
+ pushBucket("rule.utils", classified.rule.utils);
2283
+ pushBucket("rule.components", classified.rule.components);
2284
+ pushBucket("rule.other", classified.rule.other);
2285
+ pushBucket("theme", classified.theme);
2286
+ pushBucket("component", classified.component);
2287
+ pushBucket("ui-other", classified.uiOther);
2288
+ pushBucket("advisory-other", classified.advisoryOther);
2289
+ parts.push("## Classified openspec specification paths (role semantics)");
2290
+ if (bucketLines.length > 0) {
2291
+ parts.push(...bucketLines);
2292
+ }
2293
+ else {
2294
+ parts.push("(no openspec specification paths discovered — greenfield)");
2146
2295
  }
2296
+ parts.push("component / theme / rule.components 是「组件/主题规范」必读语义桶:规范已定义某用途组件时必须使用它(uiComponentChoices 用 decision=specified + 精确 path/section/line),不得静默替换为自认更合适的组件;无规范定义时才允许 reuse-existing 或 new(new 必须声明偏差 rationale)。这些路径在生成期冻结为 prewrite gate 的 componentSpecCandidatePaths。");
2297
+ parts.push("Each consuming node MUST report in its output: applicable rules, the hit path/section/line number for every applied specification, and any conflicts or missing specifications. Missing or conflicting required specifications must fail closed rather than silently substituting nearby repository conventions.");
2147
2298
  parts.push(`A11y capability: ${capability.a11y.status}` +
2148
2299
  (capability.a11y.tools.length
2149
2300
  ? ` (${capability.a11y.tools.join(", ")})`
@@ -2198,6 +2349,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
2198
2349
  planFallbackFromNodeIds: [],
2199
2350
  reviewFromNodeId: "frontend-design-review-pi",
2200
2351
  reviewFallbackFromNodeIds: [],
2352
+ revisionPatch: false,
2201
2353
  },
2202
2354
  }
2203
2355
  : task.shell,
@@ -2344,8 +2496,38 @@ async function buildFrontendHybridDagFromTask(sources) {
2344
2496
  "",
2345
2497
  "### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
2346
2498
  '{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
2499
+ "",
2500
+ "### optional plan fields - GOOD (all optional; omit when absent):",
2501
+ '{"implementationSteps":["confirm contract","sync tests"],"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
2502
+ "",
2503
+ "### optional plan fields - BAD (present-but-empty strings are rejected):",
2504
+ '{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
2505
+ "",
2506
+ "### uiComponentChoices - GOOD (specified with precise spec hit):",
2507
+ '{"purpose":"primary action button","component":"Button","decision":"specified","specReference":{"path":"openspec/schemas/button.md","section":"Variants","line":12},"rationale":"spec mandates Button for primary actions"}',
2508
+ "",
2509
+ "### uiComponentChoices - GOOD (new with deviation rationale; specReference null):",
2510
+ '{"purpose":"loading skeleton","component":"SkeletonCard","decision":"new","specReference":null,"rationale":"no spec or existing component covers skeleton; deviation pending design-review approval"}',
2511
+ "",
2512
+ "### uiComponentChoices - BAD (specified without specReference, or new with specReference):",
2513
+ '{"purpose":"primary action","component":"MyButton","decision":"specified","specReference":null,"rationale":"..."} <-- REJECTED: specified requires specReference',
2514
+ '{"purpose":"primary action","component":"MyButton","decision":"new","specReference":{"path":"openspec/schemas/button.md","section":"","line":null},"rationale":"..."} <-- REJECTED: new must not carry specReference',
2347
2515
  ].join("\n");
2348
2516
  })();
2517
+ const frontendContractFieldSummary = [
2518
+ "## Contract field summary (authoritative JSON; no plan prose)",
2519
+ "The plan/revision node emits only a fenced json contract — there is no Markdown plan explanation to read. Review these fields:",
2520
+ "- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
2521
+ "- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
2522
+ "- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
2523
+ "- targets: files, routes, publicApiChanges",
2524
+ "- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
2525
+ "- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
2526
+ "- designEvidence: source, paths, conflicts; evidenceGaps[]",
2527
+ "- optional: implementationSteps[], stylingStrategy, uiComponentChoices[], dependencyPolicy, residualRisks[], realIntegrationGap",
2528
+ "- uiComponentChoices[]: purpose, component, decision (specified|reuse-existing|new), specReference { path, section, line } | null, rationale",
2529
+ "Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
2530
+ ].join("\n");
2349
2531
  const sourceContext = [
2350
2532
  buildSourceContextBlock(sources),
2351
2533
  capabilityContextBlock,
@@ -2378,6 +2560,11 @@ async function buildFrontendHybridDagFromTask(sources) {
2378
2560
  `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.`,
2379
2561
  `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.`,
2380
2562
  ].join("\n");
2563
+ const mandatorySourceReadInstruction = [
2564
+ "## Mandatory full source read before contracting",
2565
+ "Before producing this contract/plan, use the Pi read tool to read the FULL bound source files (需求.md, 执行约束.md, and every `references/*` Bound readPath listed above) — the inline copies above may be truncated excerpts, and requirement/acceptance references are authoritative only in their full form.",
2566
+ "Do not drop scope fields, acceptance criteria, non-goals, UI states, or column/field definitions that exist in the full sources but are absent from the inline excerpts; if a field appears in the full source, it belongs in the contract.",
2567
+ ].join("\n");
2381
2568
  const strategy = resolveDagVerifyStrategy(taskConfig);
2382
2569
  const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
2383
2570
  const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
@@ -2509,6 +2696,40 @@ async function buildFrontendHybridDagFromTask(sources) {
2509
2696
  `- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
2510
2697
  ...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
2511
2698
  ].join("\n");
2699
+ const advisories = [];
2700
+ if (frontendSourceMentionsMock(frontendSources) &&
2701
+ frontendMockStrategyMustBeNotNeeded(frontendSources)) {
2702
+ advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
2703
+ }
2704
+ const openspecGate = resolveFrontendOpenspecGateConfig(sources);
2705
+ // Generation-frozen component/theme specification bucket (ADR 0016). Derived
2706
+ // from the classified component/theme/rule.components buckets; the prewrite
2707
+ // gate consumes it to enforce uiComponentChoices presence and specReference
2708
+ // cross-checking. `src/task/**` classification logic is intentionally
2709
+ // unchanged — these buckets already partition normativePaths.
2710
+ const classified = sources.frontendProjectCapability?.designEvidence.classified;
2711
+ const componentSpecCandidatePaths = [
2712
+ ...(classified?.component ?? []),
2713
+ ...(classified?.theme ?? []),
2714
+ ...(classified?.rule.components ?? []),
2715
+ ].filter((value, index, array) => array.indexOf(value) === index).sort();
2716
+ if (openspecGate.openspecPolicy === "cited") {
2717
+ if (openspecGate.openspecCandidatePaths.length === 0) {
2718
+ advisories.push("openspec 策略 cited:契约声明的 requiredReadPaths 与任务源引用均为空,prewrite gate 不强制读取 openspec;如需增强规范门禁,请在 task.json.frontendOpenspec.requiredReadPaths 声明必读路径或在任务源中显式引用 openspec 文件。");
2719
+ }
2720
+ else {
2721
+ advisories.push(`openspec 策略 cited:候选 ${openspecGate.openspecCandidatePaths.length} 个(declared ${openspecGate.openspecCandidateSources.declared.length} / task-source-cited ${openspecGate.openspecCandidateSources.taskSourceCited.length}),plan/review 必须在 openspec-citations 引用块中逐条引用并真实读取。`);
2722
+ }
2723
+ }
2724
+ else {
2725
+ const openspecDiscovery = sources.frontendProjectCapability?.openspecDiscovery;
2726
+ if (openspecDiscovery) {
2727
+ advisories.push(`openspec 策略 scan-strict:候选来源 auto-discovered,共 ${openspecDiscovery.candidateCount} 个,排除治理目录 ${openspecDiscovery.governanceRoot} 下 ${openspecDiscovery.excludedGovernanceCount} 个文件`);
2728
+ if (openspecDiscovery.truncated) {
2729
+ advisories.push(`openspec 候选命中安全上限 ${openspecDiscovery.maxCandidates},已截断(非静默);请缩小 spec 范围或提高上限`);
2730
+ }
2731
+ }
2732
+ }
2512
2733
  const spec = {
2513
2734
  version: 3,
2514
2735
  title: `Frontend implementation DAG: ${taskConfig.title}`,
@@ -2523,6 +2744,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2523
2744
  skillsByRole: FRONTEND_SKILLS_BY_ROLE,
2524
2745
  executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
2525
2746
  verifyStrategy: resolveDagVerifyStrategy(taskConfig),
2747
+ advisories: advisories.length > 0 ? advisories : undefined,
2526
2748
  tasks: [
2527
2749
  {
2528
2750
  id: "frontend-contract-pi",
@@ -2539,6 +2761,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2539
2761
  "Read task source and produce a concise frontend implementation contract.",
2540
2762
  "Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
2541
2763
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2764
+ mandatorySourceReadInstruction,
2542
2765
  sourceContext,
2543
2766
  ].join("\n\n"),
2544
2767
  },
@@ -2578,23 +2801,26 @@ async function buildFrontendHybridDagFromTask(sources) {
2578
2801
  allowedPaths: readOnlyPaths,
2579
2802
  forbiddenPaths,
2580
2803
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2581
- outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks, ending with exactly ONE fenced json object (```json ... ```) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once and must not contain or be followed by any raw JSON or extra fenced block. No file writes.",
2804
+ outputContract: "One-line lead-in, then exactly ONE fenced json object (```json ... ```) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a Markdown plan explanation or any prose after the contract JSON the output is JSON-only (the gate renders plan.md deterministically). Apart from the contract JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
2582
2805
  subtask_prompt: [
2583
2806
  "Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
2584
2807
  "Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
2585
- "Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2808
+ "Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the contract JSON fields. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
2586
2809
  "Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
2587
2810
  "Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
2588
- "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.",
2811
+ "Output in this exact order: (1) exactly one fenced json object conforming to frontend-implementation-contract-v1 this fenced block is the single authoritative contract the prewrite gate materializes; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a Markdown plan explanation or any prose after the contract JSON — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the single contract JSON fenced block and the openspec-citations block, do not emit any other fenced block.",
2589
2812
  "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.",
2590
2813
  requirementCoverageInstruction,
2591
2814
  "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2592
2815
  verificationTargetFileInstruction,
2593
2816
  "Read-only: do not modify code, docs, artifacts, or repository files.",
2817
+ mandatorySourceReadInstruction,
2594
2818
  fixedVerificationContext,
2595
2819
  sourceContext,
2596
2820
  mockContextBlock,
2597
2821
  frontendContractSchemaBlock,
2822
+ frontendComponentConformanceInstruction,
2823
+ openspecCitationInstruction,
2598
2824
  ].join("\n\n"),
2599
2825
  },
2600
2826
  {
@@ -2610,13 +2836,16 @@ async function buildFrontendHybridDagFromTask(sources) {
2610
2836
  outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
2611
2837
  outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
2612
2838
  subtask_prompt: [
2613
- "Audit the frontend plan before implementation.",
2839
+ "Audit the frontend plan before implementation. Consume the contract JSON (the single fenced json block from frontend-plan-pi); there is no separate plan prose.",
2614
2840
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2615
2841
  "Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
2616
2842
  "Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
2843
+ "Component selection conformance is a hard blocking condition: VERDICT: request-revision when the frontend spec (component/theme/rule.components bucket) already defines a component for a purpose but the plan selects another or self-invents one without a declared deviation; when uiComponentChoices is missing/empty for UI-visible work while the frozen component/theme bucket is non-empty; or when any uiComponentChoices specReference.path is not cited in the openspec-citations block or has no successful read event.",
2617
2844
  "Read-only: do not modify repository files.",
2618
2845
  fixedVerificationContext,
2619
2846
  sourceContext,
2847
+ frontendContractFieldSummary,
2848
+ mockContextBlock,
2620
2849
  ].join("\n\n"),
2621
2850
  },
2622
2851
  {
@@ -2630,27 +2859,30 @@ async function buildFrontendHybridDagFromTask(sources) {
2630
2859
  outputMode: "structured-required",
2631
2860
  retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2632
2861
  structuredContractOutput: {
2633
- schemaId: "frontend-implementation-contract-v1",
2862
+ schemaId: "frontend-implementation-contract-revision-patch-v1",
2634
2863
  retryOnInvalid: true,
2635
2864
  },
2636
2865
  allowedPaths: readOnlyPaths,
2637
2866
  forbiddenPaths,
2638
2867
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2639
- outputContract: "When the initial design review requests revision, return a complete Markdown revision plan followed by exactly ONE fenced json object (```json ... ```) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once and must not contain or be followed by any raw JSON or extra fenced block. Do NOT include multiple fenced JSON blocks; only the single authoritative contract JSON block is accepted. No file writes.",
2868
+ outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose the output is JSON-only; the gate applies the patch on the original contract and renders plan.md deterministically. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
2640
2869
  subtask_prompt: [
2641
- "Consume frontend-plan-pi (original plan) and frontend-design-review-pi (first design review findings).",
2642
- "This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
2643
- "The revised plan must include Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks.",
2870
+ "Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
2871
+ "This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
2872
+ "The patch delta may update any of these contract fields: requirements, implementationSteps, targets, uiStates, interactions, mockApi, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, realIntegrationGap. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
2644
2873
  requirementCoverageInstruction,
2645
2874
  "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
2646
2875
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
2647
- "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.",
2648
- "Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the revised contract; do not reduce behavior semantics to IDs and paths.",
2876
+ "Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta this is the only plan output the prewrite gate applies on the original contract; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a full contract, Markdown explanation, or prose the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block. Do not include secrets or unsafe paths.",
2877
+ "Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
2649
2878
  "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
2650
2879
  verificationTargetFileInstruction,
2651
2880
  fixedVerificationContext,
2652
2881
  sourceContext,
2653
2882
  frontendContractSchemaBlock,
2883
+ mockContextBlock,
2884
+ frontendComponentConformanceInstruction,
2885
+ openspecCitationInstruction,
2654
2886
  ].join("\n\n"),
2655
2887
  },
2656
2888
  {
@@ -2672,15 +2904,18 @@ async function buildFrontendHybridDagFromTask(sources) {
2672
2904
  outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
2673
2905
  outputContract: "For the effective frontend plan, return plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
2674
2906
  subtask_prompt: [
2675
- "Audit the revised frontend plan before implementation. This node runs only after request-revision and consumes frontend-plan-revision-pi.",
2907
+ "Audit the revised frontend plan before implementation. This node runs only after request-revision and consumes the merge-patch delta from frontend-plan-revision-pi applied on the original frontend-plan-pi contract JSON — there is no separate plan prose.",
2676
2908
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2677
2909
  "Verify that every Required Plan Correction from the initial design review has been fully addressed.",
2678
2910
  "Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
2679
2911
  "Review every explicit REQ-/BR-/AC- mapping; the downstream prewrite gate also checks identifier retention deterministically.",
2680
2912
  "Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
2913
+ "Also request revision for component selection non-conformance: spec-defined components silently replaced or self-invented without a declared deviation, uiComponentChoices missing for UI-visible work, or a uiComponentChoices specReference.path not cited in the openspec-citations block / not actually read.",
2681
2914
  "Read-only: do not modify repository files.",
2682
2915
  fixedVerificationContext,
2683
2916
  sourceContext,
2917
+ frontendContractFieldSummary,
2918
+ mockContextBlock,
2684
2919
  ].join("\n\n"),
2685
2920
  },
2686
2921
  {
@@ -2723,10 +2958,18 @@ async function buildFrontendHybridDagFromTask(sources) {
2723
2958
  ],
2724
2959
  artifactName: "frontend-implementation-contract.json",
2725
2960
  outputDir: "contracts",
2961
+ revisionPatch: true,
2962
+ planMdArtifactName: "frontend-plan.md",
2726
2963
  requireSourceFreshness: true,
2727
2964
  implementationWriteSet: implementPaths.writeSet,
2728
- openspecCandidatePaths: sources.frontendProjectCapability?.designEvidence
2729
- .normativePaths ?? [],
2965
+ openspecPolicy: openspecGate.openspecPolicy,
2966
+ openspecCandidateSources: {
2967
+ declared: openspecGate.openspecCandidateSources.declared,
2968
+ taskSourceCited: openspecGate.openspecCandidateSources.taskSourceCited,
2969
+ scanStrict: openspecGate.openspecCandidateSources.scanStrict,
2970
+ },
2971
+ openspecCandidatePaths: openspecGate.openspecCandidatePaths,
2972
+ componentSpecCandidatePaths,
2730
2973
  },
2731
2974
  cwd: ".",
2732
2975
  timeoutMs: 60000,
@@ -5783,7 +6026,9 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
5783
6026
  if (isSafeReadOnlyPiRetryCandidate(task)) {
5784
6027
  task.retryPolicy = task.outputProtocol
5785
6028
  ? PROTOCOL_AWARE_PI_RETRY_POLICY
5786
- : DEFAULT_READ_ONLY_PI_RETRY_POLICY;
6029
+ : task.role === "planner"
6030
+ ? PLANNER_OUTPUT_LIMIT_RETRY_POLICY
6031
+ : DEFAULT_READ_ONLY_PI_RETRY_POLICY;
5787
6032
  continue;
5788
6033
  }
5789
6034
  if (isWriterTransportRetryCandidate(task)) {
@@ -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")