@tea-agent/loop-agent 0.42.0-next.14 → 0.42.0-next.16

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 (113) hide show
  1. package/CHANGELOG.md +19 -3
  2. package/dist/application/task-lifecycle/advance.js +9 -4
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/commands/task-source-prepare.js +3 -1
  5. package/dist/executors/dag-pi-executor.js +1437 -154
  6. package/dist/executors/shell-executor.js +107 -35
  7. package/dist/shared/dag-failure-category.js +6 -0
  8. package/dist/task/contract/apply.js +36 -2
  9. package/dist/task/source-prepare/parse-intent.js +7 -0
  10. package/dist/worker/console/chat/pi-runtime.js +45 -2
  11. package/dist/worker/console/chat/resource-loader.js +4 -1
  12. package/dist/worker/console/chat/routes.js +8 -0
  13. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  14. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  15. package/dist/worker/console/chat/subagents/index.js +5 -0
  16. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  17. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  18. package/dist/worker/console/chat/subagents/types.js +13 -0
  19. package/dist/worker/console/chat/tool-preview.js +10 -0
  20. package/dist/worker/console/chat/tools.js +11 -1
  21. package/dist/worker/console/interview/tools.js +1 -0
  22. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BGMbAdAG.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
  23. package/dist/worker/console/static/assets/{arc-CJu1LND8.js → arc-DN59MZqN.js} +1 -1
  24. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-BmJh9gMQ.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
  25. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BWjaX3ZF.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
  26. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-CLEOAxLB.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
  27. package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
  28. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CydnyQUX.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-tjJ1IoZ1.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DR358Phd.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-B-LuWFZD.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-FG53fA6v.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bkt-w8SZ.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-WxHo0z2I.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CqHrBJhp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-D4MVae_f.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CamBh0Ll.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
  38. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
  39. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
  40. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-C3qheCuW.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DQlWfkS7.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
  42. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BAxa9rFb.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
  43. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-YM09nLJw.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-I4W9EJj2.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DH8eJ8Ff.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D0D0ntDW.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BIISKq1s.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
  48. package/dist/worker/console/static/assets/{diagram-UB23O5K3--xyAWj0w.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
  49. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B3sYXLJ4.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
  50. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-zRSYWhJP.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
  51. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BUYlie03.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
  52. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DP4kzzrM.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
  53. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CAPoUxTF.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
  54. package/dist/worker/console/static/assets/{index-CWhTyxvc.js → index-BmMi-Bve.js} +71 -71
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXPfjekV.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-aOI3pV6t.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DAPZuvi4.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-atwDzL3C.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-D5E09qji.js → linear-PTmQ9LkV.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-CE4idY-s.js → mermaid.core-B6Lxil0V.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CbZxtPca.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-De-FYEte.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bs0USobz.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bq3ZrGCJ.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bk8ojyIj.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CGhoyws1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DmVSQs3b.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-C80h1u0Q.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CcbksQVZ.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CFTTTQKj.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Cw7Jr7Tg.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CVcdTVa5.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BpLbmWp9.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-6PXfbwj-.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-Bjkcz4ih.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
  78. package/dist/worker/console/static/index.html +1 -1
  79. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  80. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  81. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  82. package/dist/workflows/dag/frontend-design-policy.js +100 -15
  83. package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
  84. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  85. package/dist/workflows/dag/frontend-risk.js +2 -0
  86. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  87. package/dist/workflows/dag/frontend-shape.js +16 -6
  88. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  89. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  90. package/dist/workflows/dag/frontend-verification-trace.js +32 -0
  91. package/dist/workflows/dag/init-hybrid.js +16 -13
  92. package/dist/workflows/dag/node-execution.js +178 -88
  93. package/dist/workflows/dag/rerun-feedback.js +1 -0
  94. package/dist/workflows/dag/rerun-plan.js +90 -4
  95. package/dist/workflows/dag/rerun-run.js +7 -0
  96. package/dist/workflows/dag/retry-policy.js +16 -10
  97. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  98. package/dist/workflows/dag/runner.js +67 -7
  99. package/dist/workflows/dag/structured-output-repair.js +4 -1
  100. package/dist/workflows/dag/validate.js +10 -8
  101. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  102. package/docs/operations/README.md +1 -0
  103. package/docs/templates/agent-dag.schema.json +2 -2
  104. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  105. package/package.json +2 -3
  106. package/skills/frontend-plan/SKILL.md +14 -1
  107. package/skills/frontend-plan/references/decision-contract.md +84 -10
  108. package/skills/frontend-plan/references/design-decisions.md +32 -0
  109. package/dist/worker/console/static/assets/channel-DOsaUk1k.js +0 -1
  110. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D4-D49vY.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D4-D49vY.js +0 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C4YCesmi.js +0 -1
  113. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-lN2U_DLl.js +0 -8
@@ -28,31 +28,37 @@ function sortFindings(findings) {
28
28
  * visual UI-state dimension (it cannot cover behavioural interactions). */
29
29
  export function checkUiDesignCoverage(contract) {
30
30
  const findings = [];
31
- const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
32
- const hasStyling = Boolean(contract.stylingStrategy?.trim());
33
- // Reuse of an existing component/pattern covers behavioural interactions
34
- // without forcing one uiComponentChoices row per interaction: the model
35
- // should not enumerate every click/hover on a reused Button. Only
36
- // interactions with no reuse and no explicit purpose are flagged.
37
- const hasReuseChoice = (contract.uiComponentChoices ?? []).some((choice) => choice.decision === "reuse-existing");
31
+ // Explicit per-id coverage (dogfood r-plan-quality): one choice covers an
32
+ // interaction/state when its purpose equals the name OR the choice lists
33
+ // the name in covers. The old wildcards are gone — a single reuse-existing
34
+ // row no longer passes every interaction, and a non-empty stylingStrategy
35
+ // no longer passes every state.
36
+ const covered = new Set();
37
+ for (const choice of contract.uiComponentChoices ?? []) {
38
+ if (choice.purpose.trim())
39
+ covered.add(choice.purpose);
40
+ for (const id of choice.covers ?? []) {
41
+ if (id.trim())
42
+ covered.add(id);
43
+ }
44
+ }
38
45
  for (const state of contract.uiStates) {
39
46
  if (!state.applicable)
40
47
  continue;
41
- const covered = purposes.has(state.name) || hasStyling;
42
- if (!covered) {
48
+ if (!covered.has(state.name)) {
43
49
  findings.push({
44
50
  code: "ui-design-coverage-missing",
45
- message: `applicable UI state "${state.name}" has no matching uiComponentChoices.purpose or stylingStrategy`,
51
+ message: `applicable UI state "${state.name}" is not covered by any uiComponentChoices purpose or covers entry`,
46
52
  path: state.name,
47
53
  });
48
54
  }
49
55
  }
50
56
  for (const interaction of contract.interactions) {
51
- if (purposes.has(interaction.name) || hasReuseChoice)
57
+ if (covered.has(interaction.name))
52
58
  continue;
53
59
  findings.push({
54
60
  code: "ui-design-coverage-missing",
55
- message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose and no reuse-existing choice covering it`,
61
+ message: `interaction "${interaction.name}" is not covered by any uiComponentChoices purpose or covers entry`,
56
62
  path: interaction.name,
57
63
  });
58
64
  }
@@ -158,7 +164,7 @@ export function checkMockStrategy(contract, allowedMockStrategies) {
158
164
  * A component specReference must still not point into clearly non-component
159
165
  * spec areas — templates and the ai_workspace governance subtree cannot be the
160
166
  * authoritative component definition. */
161
- export function checkComponentChoice(contract, componentSpecCandidatePaths) {
167
+ export function checkComponentChoice(contract, componentSpecCandidatePaths, options) {
162
168
  const findings = [];
163
169
  const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
164
170
  const isNonComponentSpecArea = (path) => {
@@ -200,6 +206,23 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
200
206
  });
201
207
  }
202
208
  }
209
+ else if (choice.decision === "reuse-existing") {
210
+ if (!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
211
+ findings.push({
212
+ code: "component-reuse-evidence-missing",
213
+ message: `reuse-existing component choice #${index} is missing evidencePath naming the existing repo file that proves the reuse; a greenfield path must use decision=new`,
214
+ path: choice.purpose,
215
+ });
216
+ }
217
+ else if (options?.hasFreshPathEvidence &&
218
+ !options.hasFreshPathEvidence(choice.evidencePath)) {
219
+ findings.push({
220
+ code: "component-reuse-path-not-fresh",
221
+ message: `reuse-existing component choice #${index} evidencePath "${choice.evidencePath}" has no fresh existence evidence (path not found); reuse requires an existing file — use decision=new for greenfield paths`,
222
+ path: choice.evidencePath,
223
+ });
224
+ }
225
+ }
203
226
  else if (choice.decision === "new") {
204
227
  if (!choice.specReference || !choice.specReference.path) {
205
228
  findings.push({
@@ -236,7 +259,7 @@ export function checkTargetPaths(contract, writeSetPatterns) {
236
259
  if (contract.targets.files.length > MAX_WRITE_SET_FILES) {
237
260
  findings.push({
238
261
  code: "write-set-too-large",
239
- message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task (record_split_proposal) so no single implement node exceeds the bound`,
262
+ message: `implementation writeSet has ${contract.targets.files.length} files (limit ${MAX_WRITE_SET_FILES}); split the task at the Contract boundary so no single implement node exceeds the bound; preserve all required files`,
240
263
  });
241
264
  }
242
265
  for (const file of contract.targets.files) {
@@ -371,6 +394,64 @@ export function checkDesignApproval(designApproval) {
371
394
  }
372
395
  return [];
373
396
  }
397
+ /**
398
+ * UX vocabulary collisions (item ③, dogfood dag-1788504923861-0f7b17a9):
399
+ * prioritize-planner-task / prioritizePlannerTask / prioritize-planner-tasks
400
+ * are one behavior renamed three ways. Case/separator-insensitive duplicates
401
+ * inside interactions or inside uiStates are blocking.
402
+ */
403
+ export function checkUxNameCollisions(contract) {
404
+ const findings = [];
405
+ const normalize = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
406
+ const collisions = (names, kind) => {
407
+ const byNormalized = new Map();
408
+ const found = [];
409
+ for (const name of names) {
410
+ const key = normalize(name);
411
+ if (!key)
412
+ continue;
413
+ const existing = byNormalized.get(key);
414
+ if (existing !== undefined && existing !== name) {
415
+ found.push({
416
+ code: "ux-name-collision",
417
+ message: `"${name}" and "${existing}" are the same ${kind} name up to case/separator — one behavior must keep one stable name`,
418
+ path: name,
419
+ });
420
+ }
421
+ else {
422
+ byNormalized.set(key, name);
423
+ }
424
+ }
425
+ return found;
426
+ };
427
+ findings.push(...collisions(contract.interactions.map((interaction) => interaction.name), "interaction"));
428
+ findings.push(...collisions(contract.uiStates.map((state) => state.name), "UI state"));
429
+ return findings;
430
+ }
431
+ /**
432
+ * Deliverable completeness (item ⑦): every file explicitly declared by Contract
433
+ * from frozen source fragments (projected into targets.requiredDeliverables)
434
+ * must be planned — bound by some requirement's implementationTargets or a
435
+ * verification target's file, or listed in targets.files. allowedPaths is a
436
+ * write boundary and never implies coverage.
437
+ */
438
+ export function checkRequiredDeliverables(contract) {
439
+ const required = contract.targets.requiredDeliverables ?? [];
440
+ if (required.length === 0)
441
+ return [];
442
+ const planned = new Set([
443
+ ...contract.targets.files,
444
+ ...contract.requirements.flatMap((requirement) => requirement.implementationTargets),
445
+ ...contract.verificationTargets.map((target) => target.file),
446
+ ]);
447
+ return required
448
+ .filter((path) => !planned.has(path))
449
+ .map((path) => ({
450
+ code: "required-deliverable-unplanned",
451
+ message: `Contract declares required deliverable "${path}" but no requirement implementationTarget, verification target, or targets.files entry plans it`,
452
+ path,
453
+ }));
454
+ }
374
455
  /** Aggregate the deterministic design-policy checks into a single
375
456
  * deterministic result. Any finding blocks; an empty finding list approves. */
376
457
  export function evaluateFrontendDesignPolicy(input) {
@@ -379,10 +460,14 @@ export function evaluateFrontendDesignPolicy(input) {
379
460
  ...checkUiDesignCoverage(input.contract),
380
461
  ...checkSourceFreshness(input.contract, input.sourceFreshness),
381
462
  ...checkMockStrategy(input.contract, input.allowedMockStrategies),
382
- ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths),
463
+ ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths, input.hasFreshPathEvidence
464
+ ? { hasFreshPathEvidence: input.hasFreshPathEvidence }
465
+ : undefined),
383
466
  ...checkTargetPaths(input.contract, input.writeSetPatterns),
384
467
  ...checkDependencies(input.contract, input.allowedDependencies),
385
468
  ...checkUiStateAttribution(input.contract),
469
+ ...checkUxNameCollisions(input.contract),
470
+ ...checkRequiredDeliverables(input.contract),
386
471
  ]);
387
472
  const classification = findings.length > 0 ? "blocked" : "approved";
388
473
  const result = {
@@ -9,10 +9,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
11
  import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
12
+ import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
13
+ export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
12
14
  // Cycle-safe value imports: the check functions are hoisted declarations used
13
15
  // only at call time, so the contract module and the policy module can depend
14
16
  // on each other without module-init ordering issues.
15
- import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
17
+ import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
16
18
  import { isDagSourceBindingV2 } from "./types.js";
17
19
  import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
18
20
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
@@ -600,12 +602,30 @@ const uiComponentChoiceSchema = z
600
602
  decision: z.enum(["specified", "reuse-existing", "new"]),
601
603
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
602
604
  // Optional: the decision enum already carries the semantic (specified →
603
- // specReference, new → task-source traceability, reuse-existing → no
604
- // rationale needed).
605
- // Omit for reuse-existing to keep plan output small.
605
+ // specReference, new → task-source traceability, reuse-existing →
606
+ // evidencePath). Omit for reuse-existing to keep plan output small.
606
607
  rationale: z.string().min(1).optional(),
608
+ // Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
609
+ // UI states/interactions explicitly instead of the old implicit
610
+ // "reuse-existing covers everything" wildcard.
611
+ covers: z.array(z.string().min(1)).optional(),
612
+ // reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
613
+ // reuse-existing rows cited conventions in a not-yet-written
614
+ // src/planner.ts): a repo-relative path to the existing file that
615
+ // proves the reuse. Enforced by superRefine + fresh-existence checks.
616
+ evidencePath: safePath.optional(),
607
617
  })
608
- .strict();
618
+ .strict()
619
+ .superRefine((choice, ctx) => {
620
+ if (choice.decision === "reuse-existing" &&
621
+ !(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
622
+ ctx.addIssue({
623
+ code: z.ZodIssueCode.custom,
624
+ path: ["evidencePath"],
625
+ message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
626
+ });
627
+ }
628
+ });
609
629
  const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
610
630
  /**
611
631
  * Normalize unstable model emissions before the strict schema sees them:
@@ -680,6 +700,11 @@ export const frontendImplementationContractSchema = z
680
700
  files: z.array(safePath).min(1),
681
701
  routes: z.array(z.string().startsWith("/")).optional(),
682
702
  publicApiChanges: z.array(z.string().min(1)).optional(),
703
+ // Runtime-owned deliverable contract (dogfood r-plan-quality):
704
+ // explicit, source-bound file inventory committed by Contract.
705
+ // The design-policy completeness gate checks ONLY these —
706
+ // allowedPaths is a write boundary, not a coverage obligation.
707
+ requiredDeliverables: z.array(safePath).optional(),
683
708
  })
684
709
  .strict(),
685
710
  requirements: z.array(z
@@ -1634,7 +1659,12 @@ export function compactFrontendImplementationContract(value) {
1634
1659
  const targets = asRecord(record.targets);
1635
1660
  if (targets) {
1636
1661
  const compactTargets = { ...targets };
1637
- for (const field of ["files", "routes", "publicApiChanges"]) {
1662
+ for (const field of [
1663
+ "files",
1664
+ "routes",
1665
+ "publicApiChanges",
1666
+ "requiredDeliverables",
1667
+ ]) {
1638
1668
  if (Object.hasOwn(compactTargets, field)) {
1639
1669
  compactTargets[field] = dedupeJsonArray(compactTargets[field]);
1640
1670
  }
@@ -2425,6 +2455,16 @@ export async function analyzeFrontendImplementationContract(input) {
2425
2455
  files: referencedConcretePaths,
2426
2456
  };
2427
2457
  }
2458
+ // Both Plan precheck and policy materialization consume this exact inventory.
2459
+ // Never trust a Plan patch or infer delivery obligations from prose.
2460
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
2461
+ const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
2462
+ .filter((record) => record.phase === "committed")
2463
+ .map((record) => record.fact));
2464
+ delete result.data.targets.requiredDeliverables;
2465
+ if (requiredDeliverables.length > 0) {
2466
+ result.data.targets.requiredDeliverables = requiredDeliverables;
2467
+ }
2428
2468
  const blockingGaps = [
2429
2469
  ...(result.data.evidenceGaps ?? []),
2430
2470
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
@@ -2632,6 +2672,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
2632
2672
  "/sourceBinding",
2633
2673
  "/riskLevel",
2634
2674
  "/targets/files",
2675
+ "/targets/requiredDeliverables",
2635
2676
  "/mockApi/productionDefaultOff",
2636
2677
  ];
2637
2678
  const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
@@ -2680,6 +2721,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
2680
2721
  const targets = asRecord(record.targets);
2681
2722
  if (targets && Object.hasOwn(targets, "files"))
2682
2723
  violations.push("/targets/files");
2724
+ if (targets && Object.hasOwn(targets, "requiredDeliverables"))
2725
+ violations.push("/targets/requiredDeliverables");
2683
2726
  const mockApi = asRecord(record.mockApi);
2684
2727
  if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
2685
2728
  violations.push("/mockApi/productionDefaultOff");
@@ -2864,6 +2907,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
2864
2907
  const policyPreFindings = [
2865
2908
  ...checkUiDesignCoverage(analysis.canonical),
2866
2909
  ...checkUiStateAttribution(analysis.canonical),
2910
+ ...checkRequiredDeliverables(analysis.canonical),
2911
+ ...checkUxNameCollisions(analysis.canonical),
2867
2912
  ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
2868
2913
  ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
2869
2914
  ];
@@ -3082,9 +3127,11 @@ export async function buildContractFromCommittedFacts(input) {
3082
3127
  }
3083
3128
  // b. Committed contract terminal fact (contract-finalized).
3084
3129
  const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
3085
- const contractKinds = new Set(contractRecords
3130
+ const committedContractFacts = contractRecords
3086
3131
  .filter((record) => record.phase === "committed")
3087
- .map((record) => record.fact.kind)
3132
+ .map((record) => record.fact);
3133
+ const contractKinds = new Set(committedContractFacts
3134
+ .map((fact) => fact?.kind)
3088
3135
  .filter((kind) => typeof kind === "string"));
3089
3136
  if (!contractKinds.has("contract-finalized")) {
3090
3137
  return {
@@ -3139,6 +3186,7 @@ export async function buildContractFromCommittedFacts(input) {
3139
3186
  runDir: input.runDir,
3140
3187
  rawContractText: serializeDeterministicJson(merged),
3141
3188
  sourceBinding: input.sourceBinding,
3189
+ contractNodeId,
3142
3190
  });
3143
3191
  return {
3144
3192
  ok: true,
@@ -12,8 +12,19 @@ function bulletList(items) {
12
12
  return "";
13
13
  return items.map((item) => `- ${item}`).join("\n");
14
14
  }
15
+ /** The runtime's deterministic fallback for an outcome the plan did not
16
+ * author. It is bookkeeping for the schema's non-empty constraint, not user
17
+ * prose — the renderer must never present it as requirement content. */
18
+ const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
15
19
  function requirementsCoverage(contract) {
16
- const lines = contract.requirements.map((requirement) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
20
+ const lines = contract.requirements.map((requirement) => {
21
+ const targets = requirement.implementationTargets.join(", ");
22
+ const verificationTargets = requirement.verificationTargetIds.join(", ");
23
+ const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
24
+ ? ""
25
+ : ` — ${requirement.expectedOutcome}`;
26
+ return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
27
+ });
17
28
  return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
18
29
  }
19
30
  function uiStateHandling(contract) {
@@ -98,7 +109,7 @@ export function renderFrontendPlanMarkdown(contract) {
98
109
  interactionNotes(contract),
99
110
  "",
100
111
  "## Mock & API Strategy",
101
- contract.mockApi.strategy,
112
+ contract.mockApi?.strategy || "_(not specified)_",
102
113
  "",
103
114
  "## Dependency Policy",
104
115
  contract.dependencyPolicy || "_(not specified)_",
@@ -70,8 +70,10 @@ function blobOf(input) {
70
70
  ].join("\n");
71
71
  }
72
72
  function pathSpreadScore(paths) {
73
+ const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
73
74
  const tops = new Set(paths
74
75
  .map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
76
+ .filter((root) => !auxiliaryRoot.test(root))
75
77
  .filter(Boolean));
76
78
  return tops.size;
77
79
  }
@@ -226,6 +226,7 @@ export function parseTargetSurfaceFacts(text) {
226
226
  }
227
227
  export const PLAN_LEDGER_FACT_KINDS = [
228
228
  "target-surface",
229
+ "state-registry",
229
230
  "component-choice",
230
231
  "state-flow",
231
232
  "data-flow",
@@ -484,7 +485,22 @@ export function assemblePlanPatchFromCommittedFacts(records, contractRequirement
484
485
  }
485
486
  const verificationTargets = verificationTargetOrder
486
487
  .map((id) => verificationTargetById.get(id))
487
- .filter((entry) => Boolean(entry));
488
+ .filter((entry) => Boolean(entry))
489
+ .map((entry) => {
490
+ // Coverage precedes UX. A later state-flow fact explicitly naming
491
+ // this VT establishes the reverse binding without another model call.
492
+ // No path/name heuristics: unbound or ambiguous decisions stay subject
493
+ // to canonical validation. Recompute from live states after removals.
494
+ const boundStates = [...stateByName.values()]
495
+ .filter((state) => state.applicable === true &&
496
+ asStringArray(state.verificationTargetIds).includes(asString(entry.id)))
497
+ .map((state) => asString(state.name));
498
+ if (boundStates.length === 0)
499
+ return entry;
500
+ return { ...entry, uiStates: [...new Set([
501
+ ...asStringArray(entry.uiStates), ...boundStates,
502
+ ])].sort() };
503
+ });
488
504
  if (verificationTargets.length > 0) {
489
505
  patch.verificationTargets = verificationTargets;
490
506
  }
@@ -46,8 +46,15 @@ const EXECUTABLE_SHAPE_RANK = {
46
46
  high: 3,
47
47
  };
48
48
  function pathSpreadScore(paths) {
49
+ // Verification-only roots are routinely authorized together with the
50
+ // implementation root (for example `src/**` + `test/**`). They do not make
51
+ // a UI change cross-domain, so exclude them from the topology spread signal.
52
+ // Keep product packages and source roots intact: `apps/**` + `packages/**`
53
+ // must still count as a real spread and prevent an unsafe small topology.
54
+ const auxiliaryRoot = /^(?:test|tests|__tests__|e2e|cypress|spec|specs|docs|documentation)$/i;
49
55
  const tops = new Set(paths
50
56
  .map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
57
+ .filter((root) => !auxiliaryRoot.test(root))
51
58
  .filter(Boolean));
52
59
  return tops.size;
53
60
  }
@@ -65,6 +72,13 @@ function collectEscalationSignals(blob, supervised) {
65
72
  }
66
73
  return [...new Set(signals)];
67
74
  }
75
+ /** Negation applies to the API mention itself, never to unrelated Mock policy. */
76
+ function hasFrontendRemoteSignal(blob) {
77
+ const remaining = blob
78
+ .replace(/\bno\s+api\b/gi, "")
79
+ .replace(/无\s*(?:api\b|接口)/gi, "");
80
+ return /\b(api|fetch|axios|graphql|endpoint)\b|远程|接口/i.test(remaining);
81
+ }
68
82
  function resolveBaselineFrontendTaskShape(input) {
69
83
  const blob = [
70
84
  input.requirementMarkdown ?? "",
@@ -84,9 +98,7 @@ function resolveBaselineFrontendTaskShape(input) {
84
98
  const paths = input.allowedPaths ?? [];
85
99
  const spread = pathSpreadScore(paths);
86
100
  const concentrated = spread <= 1 && paths.length <= 4;
87
- const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
88
- !(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
89
- /无\s*api|无\s*接口/i.test(blob));
101
+ const hasApiSignal = hasFrontendRemoteSignal(blob);
90
102
  const complexity = (input.complexity ?? "").toLowerCase();
91
103
  // split-required wins over everything: it is a termination/escalation
92
104
  // signal, not an executable topology.
@@ -266,9 +278,7 @@ export function deriveFrontendRuntimeShapeFacts(input) {
266
278
  const supervised = /\bsupervised\b/i.test(blob);
267
279
  const escalationSignals = collectEscalationSignals(blob, supervised);
268
280
  const paths = input.allowedPaths ?? [];
269
- const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
270
- !(/(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])/i.test(blob) ||
271
- /无\s*api|无\s*接口/i.test(blob));
281
+ const hasApiSignal = hasFrontendRemoteSignal(blob);
272
282
  const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
273
283
  return {
274
284
  splitRequired: input.splitRequired,
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ import { z } from "zod";
4
+ /** Observe supported frozen Vitest commands without changing their selection. */
5
+ export async function withFrontendTestReporter(input) {
6
+ // Compound commands/wrappers need their own adapter: never append flags to
7
+ // a different shell command, change selection, or override a user reporter.
8
+ if (/[;&|<>`\n\r$]/.test(input.command) || /--(?:reporter|outputFile)/.test(input.command))
9
+ return undefined;
10
+ const direct = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?vitest\s+run(?:\s|$)/.test(input.command.trim());
11
+ const npm = /^npm\s+(?:run\s+([\w:-]+)|(test))(?=\s|$)(.*)$/.exec(input.command.trim());
12
+ let separator = "";
13
+ if (!direct) {
14
+ if (!npm)
15
+ return undefined;
16
+ let manifest;
17
+ try {
18
+ manifest = JSON.parse(await readFile(path.join(input.cwd, "package.json"), "utf8"));
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ const script = manifest.scripts?.[npm[1] ?? "test"] ?? "";
24
+ if (!/^vitest\s+run(?:\s|$)/.test(script) || /[;&|<>`\n\r$]|--(?:reporter|outputFile)/.test(script))
25
+ return undefined;
26
+ if (npm[3]?.trim() && !/^--(?:\s|$)/.test(npm[3].trim()))
27
+ return undefined;
28
+ separator = npm[3]?.trim() ? "" : " --";
29
+ }
30
+ const quote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
31
+ return `${input.command}${separator} --reporter=json --outputFile=${quote(input.reportPath)}`;
32
+ }
33
+ const resultSchema = z.object({
34
+ testResults: z.array(z.object({
35
+ name: z.string().min(1),
36
+ assertionResults: z.array(z.object({
37
+ title: z.string(), fullName: z.string().optional(),
38
+ ancestorTitles: z.array(z.string()).optional(), status: z.string(),
39
+ })),
40
+ })),
41
+ });
42
+ export function parseFrontendTestExecutionReport(raw, cwd) {
43
+ return resultSchema.parse(raw).testResults.flatMap((suite) => suite.assertionResults.map((test) => ({
44
+ file: path.relative(cwd, path.resolve(cwd, suite.name)).replace(/\\/g, "/"),
45
+ title: test.fullName ?? [...(test.ancestorTitles ?? []), test.title].join(" "),
46
+ status: test.status,
47
+ })));
48
+ }
@@ -95,6 +95,8 @@ export const CONTRACT_FACT_KINDS = [
95
95
  "open-question",
96
96
  "split-proposal",
97
97
  "openspec-selection",
98
+ "ui-state-declaration",
99
+ "required-deliverables",
98
100
  "contract-finalized",
99
101
  ];
100
102
  export const CONTRACT_TERMINAL_FACT_KINDS = ["contract-finalized"];
@@ -124,6 +126,7 @@ export const PLAN_RECORD_FACT_KINDS = [
124
126
  "plan-requirement",
125
127
  "plan-verification-target",
126
128
  "plan-evidence-gap",
129
+ "state-registry",
127
130
  ];
128
131
  /**
129
132
  * A+B (AC-009): typed issue category shared by review and design change
@@ -152,6 +152,7 @@ export function extractFrontendTestTitles(content) {
152
152
  const tokens = lexFrontendTestSource(content);
153
153
  const titles = [];
154
154
  const configurationModifiers = new Set(["each", "runIf", "skipIf"]);
155
+ const nonExecutingModifiers = new Set(["skip", "todo", "fails"]);
155
156
  function closingParenIndex(openIndex) {
156
157
  let depth = 0;
157
158
  for (let cursor = openIndex; cursor < tokens.length; cursor += 1) {
@@ -177,12 +178,21 @@ export function extractFrontendTestTitles(content) {
177
178
  }
178
179
  let cursor = index + 1;
179
180
  let hasConfigurationModifier = false;
181
+ let hasNonExecutingModifier = false;
182
+ let conditionalModifier;
180
183
  while (tokens[cursor]?.type === "punctuation" &&
181
184
  tokens[cursor]?.value === "." &&
182
185
  tokens[cursor + 1]?.type === "identifier") {
183
186
  if (tokens[cursor + 1]?.type === "identifier" &&
184
187
  configurationModifiers.has(tokens[cursor + 1].value)) {
185
188
  hasConfigurationModifier = true;
189
+ if (["runIf", "skipIf"].includes(tokens[cursor + 1].value)) {
190
+ conditionalModifier = tokens[cursor + 1].value;
191
+ }
192
+ }
193
+ if (tokens[cursor + 1]?.type === "identifier" &&
194
+ nonExecutingModifiers.has(tokens[cursor + 1].value)) {
195
+ hasNonExecutingModifier = true;
186
196
  }
187
197
  cursor += 2;
188
198
  }
@@ -191,6 +201,13 @@ export function extractFrontendTestTitles(content) {
191
201
  continue;
192
202
  }
193
203
  if (hasConfigurationModifier) {
204
+ if (conditionalModifier && tokens[cursor + 1]?.type === "identifier" &&
205
+ tokens[cursor + 2]?.value === ")") {
206
+ const condition = tokens[cursor + 1].value;
207
+ if ((conditionalModifier === "runIf" && condition === "false") ||
208
+ (conditionalModifier === "skipIf" && condition === "true"))
209
+ hasNonExecutingModifier = true;
210
+ }
194
211
  const close = closingParenIndex(cursor);
195
212
  if (close === null ||
196
213
  tokens[close + 1]?.type !== "punctuation" ||
@@ -199,6 +216,11 @@ export function extractFrontendTestTitles(content) {
199
216
  }
200
217
  cursor = close + 1;
201
218
  }
219
+ if (hasNonExecutingModifier) {
220
+ // A skipped suite also skips every nested test title in its body.
221
+ index = closingParenIndex(cursor) ?? tokens.length;
222
+ continue;
223
+ }
202
224
  if (tokens[cursor + 1]?.type !== "string")
203
225
  continue;
204
226
  titles.push(tokens[cursor + 1].value);
@@ -302,6 +324,15 @@ export async function evaluateVerificationTargets(input) {
302
324
  target,
303
325
  });
304
326
  issues.push(...fileTrace.issues);
327
+ const executed = input.executedTests?.get(target.commandLabel);
328
+ if (target.mode !== "static" && executed !== undefined) {
329
+ // safePath permits "./" prefixes; reporter files are cwd-relative.
330
+ const targetFile = path.posix.normalize(target.file.replace(/\\/g, "/"));
331
+ const matching = executed.filter((test) => test.file === targetFile && traceKeyMatchesTitle(target.id, test.title));
332
+ if (matching.length === 0 || matching.some((test) => test.status !== "passed")) {
333
+ issues.push(`behavior target was not fully executed and passed: ${target.id} in ${target.file}`);
334
+ }
335
+ }
305
336
  const status = issues.length ? "failed" : "ok";
306
337
  if (issues.length)
307
338
  hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
@@ -473,6 +504,7 @@ export async function runFrontendVerificationTraceGate(input) {
473
504
  contract,
474
505
  labelOwners,
475
506
  workspaceRoot: input.workspaceRoot,
507
+ executedTests: input.evidence?.behavior.executedTests,
476
508
  });
477
509
  if (hardIssues.length) {
478
510
  throw new Error(`trace: ${hardIssues.join("; ")}`);