@tea-agent/loop-agent 0.39.0-next.17 → 0.39.0-next.19

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 (80) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/build-stamp.json +2 -2
  3. package/dist/commands/init.js +2 -14
  4. package/dist/governance/manifest-types.js +0 -2
  5. package/dist/worker/console/harness-pi-model-matrix.js +2 -11
  6. package/dist/worker/console/pi-readiness.js +1 -0
  7. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_oUmMOh.js → abnfDiagram-N423BO3Z-DwiNHTdC.js} +1 -1
  8. package/dist/worker/console/static/assets/{arc-ftlOud1c.js → arc-CqaEjHku.js} +1 -1
  9. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DjW1pNpm.js → architectureDiagram-T3A2C74G-BoztYl-Q.js} +1 -1
  10. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BIKIhuQ2.js → blockDiagram-VBNYF7ZC-BR6kEDU-.js} +1 -1
  11. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-gQPyScnQ.js → c4Diagram-5PPSVZJV-x8RjYRwv.js} +1 -1
  12. package/dist/worker/console/static/assets/channel-1N7RyL5T.js +1 -0
  13. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DbYJpklA.js → chunk-2GRJ4B5K-_HKgW9Gv.js} +1 -1
  14. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B2EimXhx.js → chunk-2Q5K7J3B-BF0MICQ6.js} +1 -1
  15. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CRl4qEu8.js → chunk-5RXB4S5H-CwMHVJ8a.js} +1 -1
  16. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-aCdt3uIm.js → chunk-5VM5RSS4-BsAqLAeh.js} +1 -1
  17. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-upQU37XN.js → chunk-6Q2QTUOP-IdBfnQF8.js} +1 -1
  18. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CTNubrNg.js → chunk-GF5L2VYU-CmnTA6PM.js} +1 -1
  19. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CzhqBe80.js → chunk-JWPE2WC7-DDC1y-tm.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-51xTGTO2.js → chunk-KBJHAD2P-Df-jjFHX.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CCYvtwPn.js → chunk-RYQCIY6F-DXHlHej7.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ct6GraPo.js → chunk-XXDRQBXY-BJUMTtaD.js} +1 -1
  23. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BEne8l1a.js +1 -0
  24. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BEne8l1a.js +1 -0
  25. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DB-ntLzq.js → cose-bilkent-JH36ORCC-DvfSBnQA.js} +1 -1
  26. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BHujm5FN.js → cynefin-VYW2F7L2-BXe-YrAg.js} +1 -1
  27. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cx5wZ3Lb.js → cynefinDiagram-MW4NZA55-C25KAXmE.js} +1 -1
  28. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CZspWvZB.js → dagre-VZM6K2ZE-DvTVWxGT.js} +1 -1
  29. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B8eB7kJZ.js → diagram-7IWD3JNH-e_4e-pmQ.js} +1 -1
  30. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-IPXv9g8R.js → diagram-B4RE2ZJO-vG09145o.js} +1 -1
  31. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BGnjGtYW.js → diagram-LBJQPF4R-DlqSYoTs.js} +1 -1
  32. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-hEQoyS1K.js → diagram-Q27KOJAE-BI-4PyfE.js} +1 -1
  33. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C5mG8Xwz.js → diagram-UB23O5K3-C9dLNlyM.js} +1 -1
  34. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D3xfcbnF.js → ebnfDiagram-BXEA7PRR-CuqayYwb.js} +1 -1
  35. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CcLL1SM0.js → erDiagram-JOGREHBK-Cjt_WoXR.js} +1 -1
  36. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CSuAfPiR.js → flowDiagram-UKHOOZJN-DI5LAkdd.js} +1 -1
  37. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DLqDAAke.js → ganttDiagram-PKOTCBZU-BNulKWJe.js} +1 -1
  38. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DAZsLAah.js → gitGraphDiagram-DS77QQ5N-BjWpiSad.js} +1 -1
  39. package/dist/worker/console/static/assets/{index-BTNvk20w.js → index-DAAlqKAz.js} +57 -57
  40. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DXSlZAx8.js → infoDiagram-6WML65LV-C2z-HyGf.js} +1 -1
  41. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DhBkRCya.js → ishikawaDiagram-WSZJBQD7-BqiVeYz0.js} +1 -1
  42. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C-RZUWu7.js → journeyDiagram-NVQOT4AX-jEOZ2ROo.js} +1 -1
  43. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-B2kKlMxE.js → kanban-definition-27J2QSJJ-CArokfSN.js} +1 -1
  44. package/dist/worker/console/static/assets/{linear-DMke1BBn.js → linear-Bq9c9mgI.js} +1 -1
  45. package/dist/worker/console/static/assets/{mermaid.core-DeA6yB6l.js → mermaid.core-CD1Q-FZj.js} +5 -5
  46. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BZ6xfhel.js → mindmap-definition-FAOFIHXS-C3_TRMVq.js} +1 -1
  47. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DTAzpGRC.js → pegDiagram-VL7TDLO6-d6-3C5eP.js} +1 -1
  48. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BX_T9L06.js → pieDiagram-7S7Q4E2Y-rBMZEugn.js} +1 -1
  49. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cj4UGUTS.js → quadrantDiagram-CIZ2JOQS-B1oYGuOW.js} +1 -1
  50. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Hcvjh9D4.js → railroadDiagram-AXF67PYL-CduEVeL8.js} +1 -1
  51. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-PEuuqrN2.js → requirementDiagram-LRYGKXZP-BX1CnUrJ.js} +1 -1
  52. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Mb_IOENk.js → sankeyDiagram-W5VNT64P-Cf5q1crB.js} +1 -1
  53. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DGbthBV4.js → sequenceDiagram-SI44F4Z6-D84Udu8j.js} +1 -1
  54. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Ctp2tKYQ.js → sizeCapture-X5ZJPWSS-BGbReoBj.js} +1 -1
  55. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CHTwWXQq.js → stateDiagram-OKZ733FA-CW1DYgTf.js} +1 -1
  56. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BPT5G0Dj.js +1 -0
  57. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-MfdGy8FB.js → swimlanes-SLNWSIFB-B0Ps2tSZ.js} +2 -2
  58. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BGMpptSy.js +8 -0
  59. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-oVHobrn4.js → timeline-definition-Z64GVDOM-CAvdUBWu.js} +1 -1
  60. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CJ-FRNVZ.js → vennDiagram-T6HMQDX7-Cc5Qi527.js} +1 -1
  61. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-HTiTkCdT.js → wardleyDiagram-T6FBY63Y-m_CGFXYN.js} +1 -1
  62. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DiuS-VLK.js → xychartDiagram-ELKLHX3M-DNfoFR4W.js} +1 -1
  63. package/dist/worker/console/static/index.html +1 -1
  64. package/dist/worker/console/static-src/app/console-types.js +0 -12
  65. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  66. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  67. package/dist/workflows/dag/frontend-implementation-contract.js +236 -8
  68. package/dist/workflows/dag/frontend-prewrite-gate.js +89 -32
  69. package/dist/workflows/dag/init-hybrid.js +20 -30
  70. package/dist/workflows/dag/node-execution.js +45 -18
  71. package/dist/workflows/dag/types.js +18 -0
  72. package/docs/templates/harness.schema.json +0 -8
  73. package/harness.json +1 -8
  74. package/package.json +1 -1
  75. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  76. package/dist/worker/console/static/assets/channel-Kf0dWmvg.js +0 -1
  77. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DvMFMDx0.js +0 -1
  78. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DvMFMDx0.js +0 -1
  79. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-GwsFii7W.js +0 -1
  80. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D603UkUt.js +0 -8
@@ -13,6 +13,7 @@ export const frontendNormalizationActionSchema = z.enum([
13
13
  "strip-comments",
14
14
  "canonicalize-requirement-ids",
15
15
  "canonicalize-verification-alias",
16
+ "drop-unresolved-verification-symbols",
16
17
  "inject-source-binding",
17
18
  ]);
18
19
  export function sha256Hex(input) {
@@ -102,6 +103,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
102
103
  return [...labels];
103
104
  }
104
105
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
106
+ export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
107
+ /**
108
+ * Build the deterministic, run-owned portion of the frontend contract. The
109
+ * planner is allowed to fill semantic fields with an RFC 7386 merge patch but
110
+ * cannot redefine task identity, risk, or the implementation write boundary.
111
+ */
112
+ export function buildFrontendImplementationContractSkeleton(input) {
113
+ return {
114
+ schemaVersion: 1,
115
+ sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
116
+ riskLevel: input.riskLevel,
117
+ targets: { files: [...input.targetFiles] },
118
+ mockApi: { productionDefaultOff: true },
119
+ };
120
+ }
105
121
  /**
106
122
  * Load the canonical frontend-implementation-contract-v1 JSON Schema from the
107
123
  * installed loop-agent package docs/templates/ path. Package-root discovery
@@ -499,9 +515,9 @@ function isPlainObject(value) {
499
515
  * ever materialized, so a patch that removes a required field still fails
500
516
  * closed at the gate.
501
517
  */
502
- export function applyFrontendImplementationContractPatch(target, patch) {
518
+ export function applyFrontendContractMergePatch(target, patch) {
503
519
  if (!isPlainObject(patch)) {
504
- throw new Error("frontend revision patch must be a JSON object");
520
+ throw new Error("frontend contract patch must be a JSON object");
505
521
  }
506
522
  const merge = (base, delta) => {
507
523
  if (delta === null)
@@ -526,6 +542,9 @@ export function applyFrontendImplementationContractPatch(target, patch) {
526
542
  };
527
543
  return merge(target, patch);
528
544
  }
545
+ export function applyFrontendImplementationContractPatch(target, patch) {
546
+ return applyFrontendContractMergePatch(target, patch);
547
+ }
529
548
  export async function assertFrontendSourceBindingFresh(input) {
530
549
  for (const source of input.binding.sources) {
531
550
  const absolute = resolveDagTaskSourcePath({
@@ -1608,6 +1627,206 @@ export async function writeFrontendImplementationContractArtifact(input) {
1608
1627
  schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1609
1628
  };
1610
1629
  }
1630
+ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
1631
+ "/schemaVersion",
1632
+ "/sourceBinding",
1633
+ "/riskLevel",
1634
+ "/targets/files",
1635
+ "/mockApi/productionDefaultOff",
1636
+ ];
1637
+ function frontendPlanPatchProtectedPathViolations(patch) {
1638
+ const record = asRecord(patch);
1639
+ if (!record)
1640
+ return ["/"];
1641
+ const violations = [];
1642
+ for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
1643
+ if (Object.hasOwn(record, key))
1644
+ violations.push(`/${key}`);
1645
+ }
1646
+ const targets = asRecord(record.targets);
1647
+ if (targets && Object.hasOwn(targets, "files"))
1648
+ violations.push("/targets/files");
1649
+ const mockApi = asRecord(record.mockApi);
1650
+ if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
1651
+ violations.push("/mockApi/productionDefaultOff");
1652
+ return violations;
1653
+ }
1654
+ function workspaceRootFromDagRunDir(runDir) {
1655
+ const absolute = path.resolve(runDir);
1656
+ const segments = absolute.split(path.sep);
1657
+ const harnessIndex = segments.lastIndexOf(".harness");
1658
+ if (harnessIndex < 0 ||
1659
+ segments[harnessIndex + 1] !== "dag-runs" ||
1660
+ harnessIndex + 3 >= segments.length) {
1661
+ return null;
1662
+ }
1663
+ const joined = segments.slice(0, harnessIndex).join(path.sep);
1664
+ return joined || path.parse(absolute).root;
1665
+ }
1666
+ function verificationSymbolMatchesFile(symbol, content) {
1667
+ if (symbol.trim().toLowerCase() === "all describe blocks")
1668
+ return content.includes("describe(");
1669
+ const symbols = symbol.includes(" / ")
1670
+ ? symbol
1671
+ .split(/[((]/, 1)[0]
1672
+ .split("/")
1673
+ .map((item) => item.trim())
1674
+ .filter(Boolean)
1675
+ : [symbol];
1676
+ return symbols.every((item) => {
1677
+ const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
1678
+ return [item, describeTitle, describeTitle ? "describe(" : undefined]
1679
+ .filter((candidate) => Boolean(candidate))
1680
+ .some((candidate) => content.includes(candidate));
1681
+ });
1682
+ }
1683
+ async function dropUnresolvedVerificationSymbols(input) {
1684
+ const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1685
+ const targets = asRecord(input.contract)?.verificationTargets;
1686
+ if (!workspaceRoot || !Array.isArray(targets))
1687
+ return false;
1688
+ let changed = false;
1689
+ for (const targetValue of targets) {
1690
+ const target = asRecord(targetValue);
1691
+ const file = asString(target?.file);
1692
+ const symbol = asString(target?.symbol);
1693
+ if (!target || !file || !symbol)
1694
+ continue;
1695
+ const absolute = path.resolve(workspaceRoot, file);
1696
+ const relative = path.relative(workspaceRoot, absolute);
1697
+ if (relative.startsWith("..") ||
1698
+ path.isAbsolute(relative) ||
1699
+ relative.includes("..")) {
1700
+ continue;
1701
+ }
1702
+ let content;
1703
+ try {
1704
+ content = await readFile(absolute, "utf8");
1705
+ }
1706
+ catch {
1707
+ // Missing files remain a trace-time hard failure; do not hide them by
1708
+ // changing the optional symbol during candidate normalization.
1709
+ continue;
1710
+ }
1711
+ if (verificationSymbolMatchesFile(symbol, content))
1712
+ continue;
1713
+ delete target.symbol;
1714
+ changed = true;
1715
+ }
1716
+ return changed;
1717
+ }
1718
+ function extractSingleOpenspecCitationsBlock(text) {
1719
+ const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
1720
+ if (blocks.length !== 1) {
1721
+ throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1722
+ }
1723
+ return blocks[0];
1724
+ }
1725
+ async function writeFrontendPlanCandidateRaw(input) {
1726
+ const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1727
+ await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
1728
+ return relativePath;
1729
+ }
1730
+ /**
1731
+ * Compile the planner's editable RFC 7386 patch against the deterministic
1732
+ * runtime skeleton. Every attempt is preserved under contracts/candidates so
1733
+ * format/schema failures can be inspected and replayed without trusting model
1734
+ * prose. A successful result returns canonical full-contract text for all
1735
+ * downstream review and prewrite nodes.
1736
+ */
1737
+ export async function validateFrontendPlanPatchNodeOutput(input) {
1738
+ const nodeId = input.nodeId ?? "frontend-plan-pi";
1739
+ const attempt = input.attempt ?? 1;
1740
+ const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1741
+ const rawPath = await writeFrontendPlanCandidateRaw({
1742
+ runDir: input.runDir,
1743
+ nodeId,
1744
+ attempt,
1745
+ text: input.text,
1746
+ });
1747
+ const reportBase = {
1748
+ schemaVersion: 1,
1749
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
1750
+ nodeId,
1751
+ attempt,
1752
+ rawPath,
1753
+ protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
1754
+ candidateRawSha256: sha256Hex(input.text),
1755
+ };
1756
+ let extractedPatchPath;
1757
+ let extractedPatchSha256;
1758
+ const writeReport = async (value) => {
1759
+ await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
1760
+ ...reportBase,
1761
+ ...(extractedPatchPath
1762
+ ? { extractedPatchPath, extractedPatchSha256 }
1763
+ : {}),
1764
+ ...value,
1765
+ });
1766
+ };
1767
+ try {
1768
+ if (!input.sourceBinding)
1769
+ throw new Error("frontend plan patch validator requires DAG sourceBinding");
1770
+ const skeleton = input.structuredContractOutput?.skeleton;
1771
+ if (!skeleton)
1772
+ throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1773
+ const patch = extractFrontendImplementationJson(input.text);
1774
+ if (!isPlainObject(patch))
1775
+ throw new Error("frontend plan patch must extract to one JSON object");
1776
+ extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1777
+ const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1778
+ extractedPatchSha256 = extractedArtifact.sha256;
1779
+ const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1780
+ const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1781
+ if (protectedViolations.length > 0)
1782
+ throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1783
+ const merged = applyFrontendContractMergePatch(skeleton, patch);
1784
+ const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
1785
+ runDir: input.runDir,
1786
+ contract: merged,
1787
+ });
1788
+ const analysis = await analyzeFrontendImplementationContract({
1789
+ runDir: input.runDir,
1790
+ rawContractText: serializeDeterministicJson(merged),
1791
+ sourceBinding: input.sourceBinding,
1792
+ });
1793
+ const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1794
+ await writeReport({
1795
+ classification: "accepted-normalized",
1796
+ normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
1797
+ normalizedContractSha256: normalizedArtifact.sha256,
1798
+ normalizationActions: [
1799
+ "apply-runtime-skeleton",
1800
+ ...(droppedUnresolvedSymbols
1801
+ ? ["drop-unresolved-verification-symbols"]
1802
+ : []),
1803
+ ...analysis.normalizationActions,
1804
+ ],
1805
+ errors: [],
1806
+ });
1807
+ return {
1808
+ ok: true,
1809
+ contract: analysis.canonical,
1810
+ normalizedText: [
1811
+ "Frontend implementation contract compiled from the runtime skeleton.",
1812
+ "",
1813
+ "```json",
1814
+ serializeDeterministicJson(analysis.canonical),
1815
+ "```",
1816
+ citationsBlock,
1817
+ ].join("\n"),
1818
+ };
1819
+ }
1820
+ catch (error) {
1821
+ const reason = error instanceof Error ? error.message : String(error);
1822
+ await writeReport({
1823
+ classification: "retryable-invalid",
1824
+ normalizationActions: [],
1825
+ errors: [{ path: "$", code: "invalid-output", message: reason }],
1826
+ });
1827
+ return { ok: false, reason };
1828
+ }
1829
+ }
1611
1830
  /**
1612
1831
  * Node-output self-check for plan nodes that produce the implementation
1613
1832
  * contract. Mirrors the prewrite gate validation so schema/typo/null violations
@@ -1635,7 +1854,7 @@ export async function validateFrontendContractNodeOutput(input) {
1635
1854
  /**
1636
1855
  * Node-output self-check for the plan revision node, which emits an RFC 7386
1637
1856
  * merge-patch delta against the original contract instead of a full contract.
1638
- * Deliberately loose and format-level: the output must contain at least one
1857
+ * Deliberately loose and format-level: the output must contain exactly one
1639
1858
  * ```openspec-citations fenced block and must extract (after the citations
1640
1859
  * blocks are stripped) to exactly one JSON object. The delta carries no
1641
1860
  * schemaVersion/targets, so no full-contract schema/semantic checks run here;
@@ -1643,17 +1862,19 @@ export async function validateFrontendContractNodeOutput(input) {
1643
1862
  * the prewrite gate, which remains the only authority.
1644
1863
  */
1645
1864
  export async function validateFrontendRevisionPatchNodeOutput(input) {
1646
- // Mirror the prewrite gate's block-presence semantics (a fenced
1647
- // ```openspec-citations block with a newline-terminated body).
1648
- if (!/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/.test(input.text)) {
1865
+ let citationsBlock;
1866
+ try {
1867
+ citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1868
+ }
1869
+ catch (error) {
1649
1870
  return {
1650
1871
  ok: false,
1651
- reason: "revision patch output must include at least one ```openspec-citations fenced block",
1872
+ reason: error instanceof Error ? error.message : String(error),
1652
1873
  };
1653
1874
  }
1654
1875
  // Strip the citation blocks before extraction: their per-row JSON objects
1655
1876
  // would otherwise count as competing balanced objects in the extractor.
1656
- const textWithoutCitations = input.text.replace(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g, "");
1877
+ const textWithoutCitations = input.text.replace(citationsBlock, "");
1657
1878
  let patch;
1658
1879
  try {
1659
1880
  patch = extractFrontendImplementationJson(textWithoutCitations);
@@ -1670,6 +1891,13 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
1670
1891
  reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
1671
1892
  };
1672
1893
  }
1894
+ const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1895
+ if (protectedViolations.length > 0) {
1896
+ return {
1897
+ ok: false,
1898
+ reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
1899
+ };
1900
+ }
1673
1901
  return { ok: true, contract: patch };
1674
1902
  }
1675
1903
  export async function materializeFrontendImplementationContract(input) {
@@ -8,6 +8,7 @@ import { analyzeFrontendImplementationContract, writeFrontendImplementationContr
8
8
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
9
9
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
10
10
  import { pathMatchesPattern } from "../../shared/git-progress.js";
11
+ import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
11
12
  export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
12
13
  export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
13
14
  /** Canonical contract serialization must stay at or below this byte size. */
@@ -39,6 +40,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
39
40
  "component-spec-not-cited",
40
41
  "component-choices-missing",
41
42
  "contract-too-large",
43
+ "revision-patch-unmergeable",
42
44
  ]);
43
45
  export const frontendPrewriteResultV1Schema = z
44
46
  .object({
@@ -171,16 +173,53 @@ async function readNodeRawText(runDir, nodeId) {
171
173
  const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
172
174
  return record.assistantText ?? record.stdout ?? "";
173
175
  }
176
+ /**
177
+ * Same shape predicate as the internal `isContract` inside
178
+ * `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
179
+ * schemaVersion 1 + a plain `targets` object + `requirements` and
180
+ * `verificationTargets` arrays. A merge-patch delta does not carry these
181
+ * fields, so this is the deterministic boundary between a full contract and a
182
+ * patch. Keep the two predicates in sync.
183
+ */
184
+ function isFullContractShape(value) {
185
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
186
+ return false;
187
+ }
188
+ const record = value;
189
+ const targets = record.targets;
190
+ return (record.schemaVersion === 1 &&
191
+ targets !== null &&
192
+ typeof targets === "object" &&
193
+ !Array.isArray(targets) &&
194
+ Array.isArray(record.requirements) &&
195
+ Array.isArray(record.verificationTargets));
196
+ }
197
+ /**
198
+ * Shared merge pipeline for revisionPatch mode and content-driven
199
+ * compatibility: read the original contract node, extract the original
200
+ * contract and the effective node's patch, apply the RFC 7386 merge-patch, and
201
+ * serialize deterministically. Throws on any failure; callers map the error to
202
+ * their own classification (retryable-invalid for explicit revisionPatch mode,
203
+ * blocked for content-driven fallback).
204
+ */
205
+ async function mergeRevisionPatch(runDir, planText, originalNodeId) {
206
+ const originalText = await readNodeText(runDir, originalNodeId);
207
+ const originalValue = extractFrontendImplementationJson(originalText);
208
+ const patchValue = extractFrontendImplementationJson(planText);
209
+ const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
210
+ return serializeDeterministicJson(merged);
211
+ }
174
212
  function firstNonEmptyVerdictLine(text) {
175
213
  const lines = text
176
214
  .split(/\r?\n/)
177
215
  .map((line) => line.trim())
178
216
  .filter(Boolean);
179
- const normalize = (line) => {
180
- const emphasized = line.match(/^(?:`{1,3}|\*{1,3})\s*(VERDICT:[^`*]+?)\s*(?:`{1,3}|\*{1,3})$/);
181
- return (emphasized?.[1] ?? line).trim();
182
- };
183
- const normalizedLines = lines.map(normalize);
217
+ // Emphasis stripping (backtick + asterisk) is shared with the runIf
218
+ // selector and node-level output-protocol validation via
219
+ // normalizeVerdictCandidateLine. The gate keeps its own loose
220
+ // pass/request-revision normalization below (prefix match + trailing
221
+ // prose tolerance) for deterministic fail-closed behavior.
222
+ const normalizedLines = lines.map(normalizeVerdictCandidateLine);
184
223
  const first = normalizedLines[0] ?? "";
185
224
  // The prompt requires VERDICT to be the first non-empty line, but model
186
225
  // output can still prepend a summary. Keep the protocol strict in the
@@ -459,9 +498,8 @@ export async function runFrontendPrewriteGate(input) {
459
498
  let effectiveContractText = null;
460
499
  if (revisionPatchMode) {
461
500
  const originalNodeId = input.config.planFallbackFromNodeIds[0];
462
- let originalText;
463
501
  try {
464
- originalText = await readNodeText(input.runDir, originalNodeId);
502
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
465
503
  }
466
504
  catch (error) {
467
505
  return finalizePrewrite(input, {
@@ -472,35 +510,54 @@ export async function runFrontendPrewriteGate(input) {
472
510
  failureCode: "contract-invalid",
473
511
  });
474
512
  }
475
- let originalValue;
476
- let patchValue;
513
+ }
514
+ else {
515
+ // Content-driven compatibility (Bug 1): legacy DAGs without
516
+ // revisionPatch can still receive a merge-patch delta from the revision
517
+ // node. Detect the output shape instead of trusting the DAG flag alone.
518
+ // - Full contract shape → keep legacy full-contract semantics unchanged.
519
+ // - Patch shape with a usable original contract node → auto-merge and
520
+ // run the existing analysis/materialization pipeline.
521
+ // - Patch shape but unmergeable → blocked + revision-patch-unmergeable
522
+ // (no retryable loop).
523
+ let planJsonValue = null;
477
524
  try {
478
- originalValue = extractFrontendImplementationJson(originalText);
479
- patchValue = extractFrontendImplementationJson(planText);
525
+ planJsonValue = extractFrontendImplementationJson(planText);
480
526
  }
481
- catch (error) {
482
- return finalizePrewrite(input, {
483
- ...basePending,
484
- verdict,
485
- classification: "retryable-invalid",
486
- failureReason: `frontend prewrite gate revisionPatch mode failed to parse contract/patch: ${error instanceof Error ? error.message : String(error)}`,
487
- failureCode: "contract-invalid",
488
- });
527
+ catch {
528
+ // Not machine-readable JSON: fall back to the legacy full-contract
529
+ // path where analyzeFrontendImplementationContract surfaces the
530
+ // original diagnostic (fail-closed without a new failure mode).
489
531
  }
490
- let merged;
491
- try {
492
- merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
493
- }
494
- catch (error) {
495
- return finalizePrewrite(input, {
496
- ...basePending,
497
- verdict,
498
- classification: "retryable-invalid",
499
- failureReason: `frontend prewrite gate revisionPatch apply failed: ${error instanceof Error ? error.message : String(error)}`,
500
- failureCode: "contract-invalid",
501
- });
532
+ if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
533
+ const originalNodeId = input.config.planFallbackFromNodeIds[0];
534
+ if (!originalNodeId || originalNodeId === planNodeId) {
535
+ return finalizePrewrite(input, {
536
+ ...basePending,
537
+ verdict,
538
+ classification: "blocked",
539
+ failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
540
+ failureCode: "revision-patch-unmergeable",
541
+ });
542
+ }
543
+ try {
544
+ const originalText = await readNodeText(input.runDir, originalNodeId);
545
+ const originalValue = extractFrontendImplementationJson(originalText);
546
+ if (!isFullContractShape(originalValue)) {
547
+ throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
548
+ }
549
+ effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
550
+ }
551
+ catch (error) {
552
+ return finalizePrewrite(input, {
553
+ ...basePending,
554
+ verdict,
555
+ classification: "blocked",
556
+ failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
557
+ failureCode: "revision-patch-unmergeable",
558
+ });
559
+ }
502
560
  }
503
- effectiveContractText = serializeDeterministicJson(merged);
504
561
  }
505
562
  const requirementIdSearchText = effectiveContractText ?? planText;
506
563
  const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
@@ -33,7 +33,7 @@ import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js"
33
33
  import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
34
34
  import { classifyFrontendRisk, } from "./frontend-risk.js";
35
35
  import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
36
- import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
36
+ import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
37
37
  import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
38
38
  const REQUIREMENT_FILE = "需求.md";
39
39
  const CONSTRAINT_FILE = "执行约束.md";
@@ -2418,33 +2418,21 @@ async function buildFrontendHybridDagFromTask(sources) {
2418
2418
  complexity: taskConfig.complexity,
2419
2419
  });
2420
2420
  const frontendSourceBinding = buildDagSourceBinding(sources);
2421
+ const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
2422
+ sourceBinding: frontendSourceBinding,
2423
+ riskLevel: frontendRisk.selectedRisk,
2424
+ targetFiles: implementPaths.writeSet,
2425
+ });
2421
2426
  const frontendContractSchemaBlock = (() => {
2422
2427
  const schema = loadFrontendImplementationContractJsonSchema();
2423
- const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
2424
- if (!requirement) {
2425
- throw new Error("frontend implementation contract context requires a bound requirement source");
2426
- }
2427
- const referencePaths = frontendSourceBinding.sources
2428
- .filter((s) => s.kind === "reference")
2429
- .map((s) => s.path);
2430
- const fixedFields = {
2431
- schemaVersion: 1,
2432
- sourceBinding: {
2433
- taskId: frontendSourceBinding.taskId,
2434
- requirementPath: requirement.path,
2435
- requirementSha256: requirement.sha256,
2436
- referencePaths,
2437
- requirementIds: frontendSourceBinding.requirementIds,
2438
- },
2439
- riskLevel: frontendRisk.selectedRisk,
2440
- targets: { files: implementPaths.writeSet },
2441
- };
2442
2428
  return [
2443
- `## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative; do not guess fields)`,
2429
+ `## Final ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative after runtime merge)`,
2444
2430
  schema,
2445
2431
  "",
2446
- "## Fixed contract fields (deterministic; copy exactly and do not modify)",
2447
- JSON.stringify(fixedFields),
2432
+ "## Runtime contract skeleton (deterministic and protected)",
2433
+ JSON.stringify(frontendContractSkeleton),
2434
+ "",
2435
+ "The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then replaces the planner output with canonical full-contract JSON for downstream review.",
2448
2436
  "",
2449
2437
  "## Forbidden fields (these are NOT in the schema; do not emit)",
2450
2438
  "- schemaId",
@@ -2796,20 +2784,22 @@ async function buildFrontendHybridDagFromTask(sources) {
2796
2784
  outputMode: "structured-required",
2797
2785
  retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
2798
2786
  structuredContractOutput: {
2799
- schemaId: "frontend-implementation-contract-v1",
2787
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
2800
2788
  retryOnInvalid: true,
2789
+ skeleton: frontendContractSkeleton,
2801
2790
  },
2802
2791
  allowedPaths: readOnlyPaths,
2803
2792
  forbiddenPaths,
2804
2793
  skills: FRONTEND_IMPLEMENTATION_SKILLS,
2805
- 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.",
2794
+ outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.",
2806
2795
  subtask_prompt: [
2807
- "Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
2808
- "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.",
2796
+ "Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
2797
+ "The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
2798
+ "Select the Mock / API strategy only in the patch. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined editable fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
2809
2799
  "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.",
2810
2800
  "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.",
2811
2801
  "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.",
2812
- "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.",
2802
+ "Output in this exact order: (1) exactly one fenced json object containing the editable plan patch; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
2813
2803
  "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.",
2814
2804
  requirementCoverageInstruction,
2815
2805
  "verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
@@ -2837,7 +2827,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2837
2827
  outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
2838
2828
  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.",
2839
2829
  subtask_prompt: [
2840
- "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.",
2830
+ "Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
2841
2831
  "First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
2842
2832
  "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.",
2843
2833
  "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.",
@@ -2870,7 +2860,7 @@ async function buildFrontendHybridDagFromTask(sources) {
2870
2860
  subtask_prompt: [
2871
2861
  "Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
2872
2862
  "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.",
2873
- "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.",
2863
+ "The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. 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.",
2874
2864
  requirementCoverageInstruction,
2875
2865
  "Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
2876
2866
  "Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",