@tea-agent/loop-agent 0.39.0-next.18 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 变更
6
+
7
+ - npm `next` 渠道发布改为提交数节流:main push 仅当距上次 next 发布的 source commit ≥ 5 个 commit 时才自动发布(新增 `scripts/next-publish-gate.mjs`,读已发布 tarball build-stamp 对比 HEAD);不足阈值时 workflow 绿色跳过,手动 `workflow_dispatch` 不受限(切 release 线前强制出包 / 紧急补发)。发布文档真源 §4.1/§5.1/§7/§12 同步。
8
+
5
9
  ### 新增
6
10
 
7
11
  - backend-test 适配 A–D:可配置 `backendTest` 布局(`testRoot`/`markdownRel`/`scriptRel`)、用户公共前置文档引用与模块顶部 `user_shared_setup`(不写 conftest)、查询轴 Scenario Partitions 确定性展开与 fail-closed 门禁、`mode=gap-fill` 13 节点增量 DAG。Slot ID 以声明的 Partition ID 为真源(`TP-<Partition ID>-...`)。
@@ -24,6 +28,7 @@
24
28
 
25
29
  ### 改进
26
30
 
31
+ - `frontend-plan-pi` 改为 runtime skeleton + editable RFC 7386 patch:`schemaVersion`、source binding、risk、writer target files 与生产 Mock 安全位由 DAG 冻结且不可被 plan/revision 覆盖;节点 validator 将补丁编译为 canonical 完整契约后再交给 review/prewrite,并在 `contracts/candidates/<nodeId>/` 保存每次 raw/extracted/normalized/validation 证据。任务提示与三类结构化重试(invalid-output / provider length 截断 / output-too-large)同步收敛为单一 patch + 唯一 `openspec-citations`;无法在声明文件中解析的可选 verification symbol 会被确定性移除并留下 normalization action,避免格式冲突、完整契约重复输出和说明性 symbol 延迟到 trace 才失败。
27
32
  - Operator Chat 恢复工具对齐真实能力:系统提示改用 `dagReport` / `dagRerunPlan` / `dagRerun` / `standaloneTaskRerun` / `workerTaskRetry` 真名,不再写 `operator_*` 或把 `dagResume` 当成模型可执行项;`dagDoctor` / `dagStatus` / `workerPoolDoctor` 改为 typed `runId`/`taskId`,不再让模型猜 CLI `args[]`。ADR 0013 自定义 base prompt 上限由 1900 统一调整为 2500(2026-08-18,废止 0.8×2498 比例),预算用于写清节点续跑 / 整单重生 / Worker 重排队分流,以及多轮迭代恢复纪律(先一句话总结上一轮错误、修根因不修症状、禁止原样重交、同错两轮后停下报告)。
28
33
 
29
34
  - Operator Chat 对齐 pi-web:建会话不再传封闭 `tools` 白名单(避免 SDK 把扩展 MCP 工具滤出 registry),创建后 `bindExtensions` 发 `session_start`,再钉激活集。扩展注册的 MCP 工具可被模型调用;`apply_patch` / `full-tools` / `shell` / `coding-chat` 仍排除。
@@ -59,6 +64,7 @@
59
64
  - 强化 backend-test correspondence、scenario-param、Markdown/pytest writer、collection repair、日志审计和 Windows pytest argfile 执行链,稳定区分 controller TestBug、generated TestBug 与真实 API ProductBug
60
65
 
61
66
  ### 改进
67
+
62
68
  - backend-test correspondence 只分析目标请求实际绑定的 body/payload,忽略 `scenario` 元数据与 setup-only payload;对 `scenario["body"]` 保留字段 shape,并把 intentional invalid enum 交由 scenario-param 门禁判断;同时识别下划线前缀 pytest 参数集合与带说明的 `Payload Contract: none(…)`,避免零 eligible 的 payload/parameter 误阻断
63
69
  - backend-test correspondence 继续解析模块内 transport wrapper 的位置 payload 与 wrapper 构造 body,并清理 `Payload Enum` 值后的中文说明,避免 PUT/lifecycle 合法载荷误报;Markdown/pytest writer 同步禁止臆测响应 ID 类型,并在路径 ID 类型未声明时使用 create-delete 派生的合法 missing ID,避免把测试资产缺陷误分类为产品 400/404 缺陷
64
70
  - backend-test correspondence 可还原 `_apply_patch(_base_body(...), patch)` 等 transport-body 组合表达式,并把 setup wrapper payload 限定为有 body 契约时的 fallback;traceability 接受经递归脱敏和有界输出的结构化 request/response 日志,scenario-param 支持带显式例值的 string wrong-type;pytest writer/repair 同步禁止把 path/query/control `id` 混入 DTO patch
@@ -240,6 +246,7 @@
240
246
 
241
247
  - 修复「处理详情」思考折叠块背景引用未定义样式变量 --panel 的问题,改为确定性透明背景,消除 Console 样式变量完整性门禁对发布的拦截(视觉无变化)
242
248
  - 移除自指的 /help 命令项:输入 / 已弹出命令面板,不再单独提供重复的帮助入口
249
+
243
250
  ## [0.35.1] - 2026-08-15
244
251
 
245
252
  ### 重点更新
@@ -473,19 +480,23 @@
473
480
  ## [0.33.7-beta.2] - 2026-08-13
474
481
 
475
482
  ### Fixed
483
+
476
484
  - scenario-param 解析 `bound=LEN100`、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,识别 `"x" * _TITLE_MAX_LENGTH` 长度表达式
477
485
  - correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免假 PAYLOAD_SHAPE_MISMATCH
478
486
 
479
487
  ### Changed
488
+
480
489
  - my-webapp 五轮 backend-test 全流程 live campaign 归档(R1–R5 均 finished;R5 correspondence PASS)
481
490
 
482
491
  ## [0.33.7-beta.1] - 2026-08-12
483
492
 
484
493
  ### Fixed
494
+
485
495
  - scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
486
496
  - backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量;max/min/max+1 应用长度表达式/helper
487
497
 
488
498
  ### Changed
499
+
489
500
  - 归档 R108–R110 live campaign 证据与 residual 收紧结果
490
501
 
491
502
  ## [0.33.7-beta.0] - 2026-08-11
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "version": "0.37.0",
4
- "gitSha": "c311e93da8b34457472e44bd349ac0aa256a3c68",
5
- "builtAt": "2026-08-19T09:55:58.554Z"
4
+ "gitSha": "3a13db908ffc98610e2d1c7ea17929a7ef87d8f3",
5
+ "builtAt": "2026-08-19T14:12:38.827Z"
6
6
  }
@@ -3,7 +3,8 @@
3
3
  * its schemaId in the contract output registry. Imported once by the node
4
4
  * executor; keeps per-contract imports out of the executor.
5
5
  */
6
- import { validateFrontendContractNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
6
+ import { validateFrontendContractNodeOutput, validateFrontendPlanPatchNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
7
7
  import { registerStructuredContractValidator } from "./contract-output-registry.js";
8
8
  registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
9
+ registerStructuredContractValidator("frontend-implementation-contract-plan-patch-v1", validateFrontendPlanPatchNodeOutput);
9
10
  registerStructuredContractValidator("frontend-implementation-contract-revision-patch-v1", validateFrontendRevisionPatchNodeOutput);
@@ -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) {
@@ -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.",
@@ -129,6 +129,26 @@ export function buildNodePrompt(spec, task, upstream, options) {
129
129
  convergenceFeedback: options?.convergenceFeedback,
130
130
  });
131
131
  }
132
+ function frontendStructuredArtifactRetryGuidance(schemaId) {
133
+ if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
134
+ return [
135
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
136
+ "The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
137
+ "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
138
+ ];
139
+ }
140
+ if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
141
+ return [
142
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
143
+ "The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
144
+ "Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
145
+ ];
146
+ }
147
+ return [
148
+ "Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block conforming to frontend-implementation-contract-v1, immediately followed by exactly one fenced openspec-citations block.",
149
+ "The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
150
+ ];
151
+ }
132
152
  function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
133
153
  if (attemptNumber <= 1)
134
154
  return basePrompt;
@@ -144,27 +164,14 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
144
164
  if (previousFailureCategory === "invalid-output" &&
145
165
  task.structuredContractOutput &&
146
166
  previousProtocolReason) {
147
- // Revision nodes emit an RFC 7386 merge-patch delta (no schemaVersion /
148
- // targets), so the retry instruction must not push them toward a full
149
- // contract rewrite; plan nodes keep the strict v1 guidance.
150
- const guidance = task.structuredContractOutput.schemaId ===
151
- "frontend-implementation-contract-revision-patch-v1"
152
- ? [
153
- "Return exactly one fenced json object containing the RFC 7386 merge-patch delta against the original contract (null deletes a key; arrays and scalars replace; plain objects merge recursively) + exactly one openspec-citations block.",
154
- "The delta contains only the fields you change; do not emit a full contract or a schemaVersion/targets section.",
155
- ]
156
- : [
157
- "Return exactly one fenced json block conforming to the frontend-implementation-contract-v1 schema.",
158
- "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
159
- "The Markdown explanation may be omitted; prioritize a complete contract.",
160
- ];
161
167
  return [
162
168
  basePrompt,
163
169
  "",
164
170
  "<retry_instruction>",
165
171
  "Previous attempt produced an invalid frontend implementation contract:",
166
172
  previousProtocolReason,
167
- ...guidance,
173
+ ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
174
+ "Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
168
175
  "</retry_instruction>",
169
176
  ].join("\n");
170
177
  }
@@ -175,9 +182,8 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
175
182
  "",
176
183
  "<retry_instruction>",
177
184
  "Previous attempt was truncated by the provider (stopReason=length) before the JSON contract was completed.",
178
- "This attempt: output ONLY the single fenced json contract block, immediately followed by exactly one openspec-citations block.",
179
- "Do not emit any Markdown explanation, evidence excerpts, or duplicated upstream context.",
180
- "The contract JSON must be complete; the trailing Markdown explanation may be omitted entirely.",
185
+ ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
186
+ "The JSON artifact must be complete; omit evidence excerpts and duplicated upstream context.",
181
187
  "</retry_instruction>",
182
188
  ].join("\n");
183
189
  }
@@ -246,6 +252,17 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
246
252
  return basePrompt;
247
253
  }
248
254
  if (task.outputMode === "structured-required") {
255
+ if (task.structuredContractOutput) {
256
+ return [
257
+ basePrompt,
258
+ "",
259
+ "<retry_instruction>",
260
+ "Previous attempt exceeded the structured output size limit.",
261
+ ...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
262
+ "Keep every required field but make values concise; omit evidence excerpts and duplicated upstream context.",
263
+ "</retry_instruction>",
264
+ ].join("\n");
265
+ }
249
266
  return [
250
267
  basePrompt,
251
268
  "",
@@ -792,6 +809,9 @@ export async function executeDagNode(input) {
792
809
  runDir,
793
810
  text: contractText,
794
811
  sourceBinding: spec.sourceBinding,
812
+ nodeId,
813
+ attempt: attemptNumber,
814
+ structuredContractOutput: task.structuredContractOutput,
795
815
  });
796
816
  if (!contractCheck.ok) {
797
817
  // A provider stopReason=length means the response was cut before the
@@ -811,6 +831,13 @@ export async function executeDagNode(input) {
811
831
  previousProtocolReason = contractCheck.reason;
812
832
  }
813
833
  else {
834
+ if (contractCheck.normalizedText) {
835
+ result = {
836
+ ...result,
837
+ assistantText: contractCheck.normalizedText,
838
+ stdout: contractCheck.normalizedText,
839
+ };
840
+ }
814
841
  previousProtocolReason = undefined;
815
842
  }
816
843
  }
@@ -713,6 +713,7 @@ export const dagConvergenceSpecSchema = z
713
713
  */
714
714
  export const structuredContractOutputSchemaIds = [
715
715
  "frontend-implementation-contract-v1",
716
+ "frontend-implementation-contract-plan-patch-v1",
716
717
  "frontend-implementation-contract-revision-patch-v1",
717
718
  ];
718
719
  export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/, "task id must be kebab-case"),
@@ -775,8 +776,25 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
775
776
  .object({
776
777
  schemaId: z.enum(structuredContractOutputSchemaIds),
777
778
  retryOnInvalid: z.boolean().default(true),
779
+ /**
780
+ * Optional deterministic base object for patch-producing structured
781
+ * nodes. The frontend plan patch validator applies the model delta to
782
+ * this run-owned skeleton before strict contract validation.
783
+ */
784
+ skeleton: z.record(z.string(), z.unknown()).optional(),
778
785
  })
779
786
  .strict()
787
+ .superRefine((value, ctx) => {
788
+ if (value.schemaId ===
789
+ "frontend-implementation-contract-plan-patch-v1" &&
790
+ !value.skeleton) {
791
+ ctx.addIssue({
792
+ code: "custom",
793
+ message: "frontend plan patch output requires a runtime skeleton",
794
+ path: ["skeleton"],
795
+ });
796
+ }
797
+ })
780
798
  .optional(),
781
799
  /**
782
800
  * Fail-closed outcome/diff consistency contract for bounded Pi writers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.39.0-next.18",
3
+ "version": "0.39.0-next.19",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -6,14 +6,14 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
6
6
 
7
7
  - **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`; no guessed requirements.
8
8
  - **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets; fact vs inference vs gap. Search+read `openspec/schemas/`, `openspec/project-specs/`, `ai_workspace/` first. Output stack, styling, conventions, state/data, test entry points, reuse, risks.
9
- - **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; steps, in-bound files, UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap. **JSON-only**: one-line lead-in + **exactly one** fenced `json` block (`frontend-implementation-contract-v1`) + one `openspec-citations` block. No plan prose; plan.md is rendered by the gate. `frontend-contract-json-pi`/`frontend-contract-json-validate-shell` don't exist; IDs+paths behavior semantics. Use `uiStates: []` for logic-only changes; don't invent UI states. Applicable states need behavior/implementation/verification; non-applicable need reasons. Non-`not-needed` needs frozen Mock verify commands (`frontendMock.verifyCommands`/`package.json` mock script/capability seed); `auto` with absent/ambiguous capability or no command → `not-needed`; `required` without a command → generation-time blocked. Initial pass = original plan; only `request-revision` runs revision+final review; small-risk = one review.
9
+ - **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; include steps, in-bound files, UI states, reuse, deps, Mock/API, frozen verify entrypoints and integration gaps. DAG freezes a protected skeleton (`schemaVersion`, `sourceBinding`, `riskLevel`, `targets.files`, `mockApi.productionDefaultOff`). Model returns a **JSON-only** editable RFC 7386 patch (`frontend-implementation-contract-plan-patch-v1`) plus one `openspec-citations` block. Runtime compiles canonical `frontend-implementation-contract-v1` and archives raw/extracted/normalized/validation candidates; unresolved optional symbols are dropped and audited. No plan prose; gate renders plan.md. IDs+paths ≠ behavior semantics. Applicable UI states need behavior/implementation/verification; non-applicable states need reasons. Active Mock needs frozen commands; `auto` without confirmed capability/command → `not-needed`; `required` without command → generation-time blocked. Only `request-revision` runs revision+final review; small-risk gets one review.
10
10
  - `uiComponentChoices[]` (optional): `{purpose, component, decision, specReference, rationale}`; `specified` needs `specReference{path,section,line}`; `new` needs null ref+rationale.
11
11
  - **`frontend-prewrite-gate-shell`**: sole write authorization. Resolve effective plan (revised if revision ran, else original) + review; require exact pass; retain every REQ/BR/AC id; enforce Mock policy; validate schema/source binding+writeSet containment; materialize `contracts/frontend-implementation-contract.json` from the **single** fenced contract block (multiple candidates fail closed `invalid-output`). Fallback only when conditional primary absent; malformed primary fails closed. Generation-time blocked Mock yields one blocking shell node, no writer.
12
12
  - `verificationTarget.commandLabel` must be a **frozen command label** from `verifyEvidence.commandLabels`; others fail closed (`invalid-output`). Empty frozen set (no `run.json`) skips check.
13
13
  - `mockApi.strategy !== "not-needed"` fails closed when `mockCommandLabels` empty (`no authorized Mock verification commands`).
14
14
  - OpenSpec policy: `task.json.frontendOpenspec.policy` (default `cited`); candidates = `requiredReadPaths` ∪ task-source citations; effective plan must append one fenced `openspec-citations` block (one JSON `{"path","section","line"}` per line). Candidates non-empty but block missing/unparseable → `openspec-citation-block-unparseable`; candidate not cited → `openspec-not-cited`; cited path without read event → `openspec-citation-not-read` (anti-fabrication). Empty candidates skip enforcement. `scan-strict` keeps full-read semantics (`openspec-not-read`); both modes keep `candidate-missing-drift`.
15
15
  - Component conformance: missing `uiComponentChoices` (frozen bucket non-empty + UI-visible work) → `component-choices-missing`; bad/uncited `specReference.path` → `component-spec-reference-invalid` / `component-spec-not-cited`.
16
- - revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original contract node (`planFallbackFromNodeIds[0]`); legacy DAGs keep full-contract semantics. Size guard: canonical serialization >64KB → `retryable-invalid` + `contract-too-large`.
16
+ - revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original canonical contract node (`planFallbackFromNodeIds[0]`); protected skeleton paths remain immutable. Legacy DAGs keep full-contract semantics. Size guard: canonical serialization >64KB → `retryable-invalid` + `contract-too-large`.
17
17
  - **`frontend-implement-pi`**: sole regular exclusive writer; uses `frontend-bounded-implement` (not this skill). Stay in `writeSet`; real requests default-on. Atomic handler/intercept/adapter with tests. Stop on forbidden paths/guesses. First line `IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked`; runtime checks against attributed diff. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. `not-needed` keeps real integration pending unless real backend has fresh evidence.
18
18
 
19
19
  ## Contract / trace / stages (M1–M2)