@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3

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 (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -3,13 +3,13 @@
3
3
  * Strong evidence only: package.json deps, config files, source imports.
4
4
  * Lockfile-only or directory-name-only must NOT activate adapters.
5
5
  */
6
- import { readFile, readdir, stat } from "node:fs/promises";
6
+ import { lstat, readFile, readdir, realpath, stat } from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import { z } from "zod";
9
- import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, isOpenspecSpecFilePath, } from "../shared/openspec-spec.js";
9
+ import { DEFAULT_OPENSPEC_GOVERNANCE_ROOT, MAX_OPENSPEC_CANDIDATE_PATHS, OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE, isGovernanceSubtreePath, } from "../shared/openspec-spec.js";
10
10
  const classifiedPathSchema = z
11
11
  .string()
12
- .refine((candidate) => isOpenspecSpecFilePath(candidate), "classified path must be a repo-relative supported spec file under openspec/schemas/, openspec/project-specs/, or ai_workspace/");
12
+ .refine((candidate) => OPENSPEC_SPEC_EXT_RE.test(candidate), "classified path must be a repo-relative supported spec file");
13
13
  /**
14
14
  * Stable semantic partition of `designEvidence.normativePaths`. Keys are a
15
15
  * generic directory convention (not project-specific file names); the same
@@ -81,6 +81,34 @@ async function exists(filePath) {
81
81
  return false;
82
82
  }
83
83
  }
84
+ /**
85
+ * Resolve only unambiguous symlink-root aliases. The physical target is never
86
+ * emitted as a candidate path: it merely lets task-source wording such as
87
+ * `ai_doc/rules.md` resolve to the stable repo-local path `ai_workspace/rules.md`.
88
+ */
89
+ export async function resolveFrontendSpecRootAliases(repoRoot, specRoots) {
90
+ const candidates = [];
91
+ for (const logicalRoot of specRoots) {
92
+ const full = path.join(repoRoot, logicalRoot);
93
+ try {
94
+ if (!(await lstat(full)).isSymbolicLink())
95
+ continue;
96
+ const alias = path.basename(await realpath(full));
97
+ if (alias && alias !== path.basename(logicalRoot)) {
98
+ candidates.push({ alias, logicalRoot });
99
+ }
100
+ }
101
+ catch {
102
+ // Missing/broken links are handled by normal candidate discovery drift.
103
+ }
104
+ }
105
+ const counts = new Map();
106
+ for (const item of candidates)
107
+ counts.set(item.alias, (counts.get(item.alias) ?? 0) + 1);
108
+ return candidates
109
+ .filter((item) => counts.get(item.alias) === 1)
110
+ .sort((a, b) => a.alias.localeCompare(b.alias));
111
+ }
84
112
  /**
85
113
  * Resolve the governance subtree root from harness.json.governanceRoot.
86
114
  * Falls back to DEFAULT_OPENSPEC_GOVERNANCE_ROOT when the manifest is missing,
@@ -110,22 +138,20 @@ export async function listOpenspecDiscovery(repoRoot, options = {}) {
110
138
  return true;
111
139
  return extraExcludedPrefixes.some((prefix) => rel === prefix || rel.startsWith(prefix + "/"));
112
140
  };
113
- for (const specDir of OPENSPEC_SPEC_DIRS) {
141
+ for (const specDir of options.specRoots ?? [...OPENSPEC_SPEC_DIRS]) {
114
142
  const root = path.join(repoRoot, specDir);
115
143
  if (!(await exists(root)))
116
144
  continue;
117
145
  await walk(root, 0);
118
146
  }
119
- // openspec/schemas/** and openspec/project-specs/** sort first; remaining
120
- // ai_workspace/** business files follow. Dedupe preserves discovery order.
147
+ // Keep the established default-root priority for deterministic compatibility;
148
+ // configured extra roots are appended in lexical order.
121
149
  const unique = [...new Set(collected)];
122
- const openspecPaths = unique
123
- .filter((candidate) => candidate.startsWith("openspec/"))
124
- .sort();
125
- const aiWorkspacePaths = unique
126
- .filter((candidate) => candidate.startsWith("ai_workspace/"))
150
+ const defaultPaths = OPENSPEC_SPEC_DIRS.flatMap((root) => unique.filter((candidate) => candidate.startsWith(root + "/")).sort());
151
+ const customPaths = unique
152
+ .filter((candidate) => !defaultPaths.includes(candidate))
127
153
  .sort();
128
- const deduped = [...openspecPaths, ...aiWorkspacePaths];
154
+ const deduped = [...defaultPaths, ...customPaths];
129
155
  const truncated = deduped.length > MAX_OPENSPEC_CANDIDATE_PATHS;
130
156
  const paths = truncated
131
157
  ? deduped.slice(0, MAX_OPENSPEC_CANDIDATE_PATHS)
@@ -260,7 +286,7 @@ function classifyOpenspecPaths(paths) {
260
286
  return "other";
261
287
  };
262
288
  for (const candidate of paths) {
263
- if (!isOpenspecSpecFilePath(candidate))
289
+ if (!OPENSPEC_SPEC_EXT_RE.test(candidate))
264
290
  continue;
265
291
  const segments = candidate.split("/");
266
292
  const basename = segments[segments.length - 1] ?? "";
@@ -361,13 +387,13 @@ export function buildAdapterGuidance(capability) {
361
387
  * Discover frontend project capability from repo root.
362
388
  * Strong evidence: package.json direct deps + config/bootstrap files.
363
389
  */
364
- export async function discoverFrontendProjectCapability(repoRoot) {
390
+ export async function discoverFrontendProjectCapability(repoRoot, options = {}) {
365
391
  const evidencePaths = [];
366
392
  const reasons = [];
367
393
  const pkgPath = path.join(repoRoot, "package.json");
368
394
  const pkgRaw = await readJson(pkgPath);
369
395
  if (!pkgRaw || typeof pkgRaw !== "object") {
370
- const discovery = await listOpenspecDiscovery(repoRoot);
396
+ const discovery = await listOpenspecDiscovery(repoRoot, options);
371
397
  const designEvidence = buildDesignEvidence(discovery.paths);
372
398
  const base = {
373
399
  schemaVersion: 1,
@@ -540,7 +566,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
540
566
  router = router ?? "next-router";
541
567
  if (hasDep(deps, "vue-router"))
542
568
  router = "vue-router";
543
- const discovery = await listOpenspecDiscovery(repoRoot);
569
+ const discovery = await listOpenspecDiscovery(repoRoot, options);
544
570
  const designEvidence = buildDesignEvidence(discovery.paths);
545
571
  if (discovery.paths.length)
546
572
  evidencePaths.push(...discovery.paths.slice(0, 5));
@@ -35,8 +35,15 @@ const HEADING_ALIASES = {
35
35
  assumptions: new Set(["假设", "assumptions"]),
36
36
  };
37
37
  const OBJECTIVE_MAX_CHARS = 1200;
38
- /** Product Requirement V3/V4 style embedded AC heading, e.g. AC-FE-001 / AC-BE-12 / AC-001. */
39
- const EMBEDDED_AC_HEADING = /^(AC(?:[-_](?:FE|BE|UI|API))?[-_]?\d+(?:[-_]\d+)?)\s*[::]?\s*(.*)$/iu;
38
+ /**
39
+ * Product Requirement V3/V4 style embedded AC heading, e.g. AC-FE-001 /
40
+ * AC-BE-12 / AC-001 — and catalog-qualified feature segments such as
41
+ * AC-FE-INS-001 / AC-FE-CHAT-005 (uppercase letter/digit segments).
42
+ * Catalog ids embedded in bold (“**AC-FE-INS-001**:…”) must bind as-is;
43
+ * falling through to a minted AC-00N historically produced dual ids and
44
+ * acceptanceCoverage.missing noise (2026-08-23 fe-test loop finding 5).
45
+ */
46
+ const EMBEDDED_AC_HEADING = /^(AC(?:[-_](?:FE|BE|UI|API))?[-_]?[A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/u;
40
47
  function stripFencedAndComments(text) {
41
48
  // Remove fenced code blocks and HTML comments so headings inside them are ignored.
42
49
  return text
@@ -52,12 +59,25 @@ function normalizeAcceptanceId(raw) {
52
59
  .replace(/^(AC)(FE|BE|UI|API)(?=-|\d)/u, "$1-$2");
53
60
  }
54
61
  function parseEmbeddedAcHeading(headingText) {
55
- const normalized = headingText.trim().replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "");
62
+ const normalized = headingText
63
+ .trim()
64
+ // Strip leading section numbers ("2.1、" style) and bold/emphasis
65
+ // wrappers ("**AC-FE-INS-001**") so embedded catalog ids bind.
66
+ .replace(/^(?:\d+(?:\.\d+)*[..、]?\s*)+/u, "")
67
+ .replace(/^\*\*|\*\*$/g, "")
68
+ .replace(/^__|__$/g, "")
69
+ .trim();
56
70
  const match = EMBEDDED_AC_HEADING.exec(normalized);
57
71
  if (!match)
58
72
  return null;
59
73
  const id = normalizeAcceptanceId(match[1]);
60
- const text = match[2]?.trim() || id;
74
+ // Bold-wrapped ids leave their closing marker inside the captured text
75
+ // ("**AC-FE-INS-001**:xxx" captures "**:xxx"); strip emphasis remnants
76
+ // and leading separators so the bound text is clean prose.
77
+ const text = (match[2] ?? "")
78
+ .replace(/^[*_]+\s*/, "")
79
+ .replace(/^\s*[::]\s*/, "")
80
+ .trim() || id;
61
81
  return { id, text };
62
82
  }
63
83
  function normalizeHeadingText(raw) {
@@ -5,6 +5,8 @@ import { ContractMutationError, applyTaskContract, logicalRevisionForState, obse
5
5
  import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
6
6
  import { getTaskContractPaths } from "../contract/paths.js";
7
7
  import { loadTaskConfig } from "../runtime.js";
8
+ import { resolveFrontendSpecRootAliases } from "../frontend-project-capability.js";
9
+ import { DEFAULT_FRONTEND_SPEC_ROOTS, extractDeclaredFrontendSpecRoots, } from "../../shared/openspec-spec.js";
8
10
  import { buildPrepareDraft } from "./build-draft.js";
9
11
  import { hasBlockingGaps, hasBlockingRisks, isFreshLegacyUnversioned, listPrepareGaps, } from "./completeness.js";
10
12
  import { detectProductArtifactMeta, isIntakeSoftGapCode, } from "./artifact-meta.js";
@@ -591,6 +593,40 @@ export async function prepareTaskSource(input) {
591
593
  flags: input.flags,
592
594
  references,
593
595
  });
596
+ // A user can explicitly name a non-default normative directory in the task
597
+ // prompt (for example “规范目录 `design-system`”). Freeze that root into
598
+ // the managed task config before the DAG exists; runtime selectors never
599
+ // mutate task.json.
600
+ if (built.draft.taskKind === "frontend-implementation") {
601
+ // An imported PRD is materialized under sourceIntegrity rather than copied
602
+ // into intent.text. Use both inputs so a `--prd` invocation can declare a
603
+ // task-owned spec root just like supplemental free-form text can.
604
+ const sourceText = [
605
+ ...(sourceIntegrity?.documents ?? []).flatMap((document) => document.content ? [document.content] : []),
606
+ ...(input.intent.kind === "facts" && input.intent.text
607
+ ? [input.intent.text]
608
+ : []),
609
+ ].join("\n");
610
+ const inferredRoots = extractDeclaredFrontendSpecRoots(sourceText);
611
+ if (inferredRoots.length > 0) {
612
+ const current = existingTaskConfig?.frontendOpenspec;
613
+ const configuredRoots = current?.specRoots ?? [...DEFAULT_FRONTEND_SPEC_ROOTS];
614
+ // Include roots declared by this source before resolving physical-target
615
+ // aliases. A newly declared root may itself be a symlink (for example
616
+ // `project-specs -> .../ai_doc`), so resolving aliases against only the
617
+ // previous config cannot map `ai_doc/rules.md` back to project-specs.
618
+ const provisionalRoots = [
619
+ ...new Set([...configuredRoots, ...inferredRoots]),
620
+ ].sort();
621
+ const aliases = await resolveFrontendSpecRootAliases(repoRoot, provisionalRoots);
622
+ const aliasMap = new Map(aliases.map(({ alias, logicalRoot }) => [alias, logicalRoot]));
623
+ built.draft.frontendOpenspec = {
624
+ policy: current?.policy ?? "cited",
625
+ specRoots: [...new Set([...configuredRoots, ...inferredRoots.map((root) => aliasMap.get(root) ?? root)])].sort(),
626
+ requiredReadPaths: current?.requiredReadPaths ?? [],
627
+ };
628
+ }
629
+ }
594
630
  const { gaps, risks } = listPrepareGaps({
595
631
  draft: built.draft,
596
632
  state,
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { Command } from "commander";
7
+ import { createNpmUpdateClient } from "../shared/update/npm-client.js";
8
+ import { runConsoleDailyUpdateCheck } from "../shared/update/console-notifier.js";
6
9
  import { readPackageVersion } from "../shared/package-metadata.js";
7
10
  import YAML from "yaml";
8
11
  import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
@@ -838,10 +841,33 @@ function parseOptionalTimeoutMs(raw) {
838
841
  return value;
839
842
  }
840
843
  /** Console HTTP entry for bare `agent-worker console` (listen first, then optional browser open). */
841
- async function runConsoleServe(options) {
844
+ export async function runConsoleServe(options) {
842
845
  const { createConsoleServer } = await import("./console/server.js");
843
846
  const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
844
847
  try {
848
+ try {
849
+ const packageRoot = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
850
+ const currentVersion = readPackageVersion(packageRoot) ?? "0.0.0";
851
+ await runConsoleDailyUpdateCheck({
852
+ currentVersion,
853
+ packageRoot,
854
+ homeDir: os.homedir(),
855
+ cwd: options.repoRoot,
856
+ env: process.env,
857
+ stdinIsTTY: process.stdin.isTTY === true,
858
+ stderrIsTTY: process.stderr.isTTY === true,
859
+ client: createNpmUpdateClient({
860
+ currentVersion,
861
+ packageRoot,
862
+ cwd: options.repoRoot,
863
+ env: process.env,
864
+ }),
865
+ });
866
+ }
867
+ catch (error) {
868
+ const detail = error instanceof Error ? error.message : String(error);
869
+ process.stderr.write(`loop-agent 更新检查已跳过:${detail}\n`);
870
+ }
845
871
  const server = await createConsoleServer({
846
872
  repoRoot: options.repoRoot,
847
873
  host: options.host,
@@ -857,6 +883,10 @@ async function runConsoleServe(options) {
857
883
  noOpen: options.noOpen === true,
858
884
  serverUrl: server.url,
859
885
  });
886
+ if (options.holdUntilSignal === false) {
887
+ await server.close();
888
+ return;
889
+ }
860
890
  await new Promise((resolve) => {
861
891
  const shutdown = () => resolve();
862
892
  process.once("SIGINT", shutdown);
@@ -1,6 +1,13 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { open, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ /** 新建模型时,未显式提供的字段使用的默认值(仅含 6 个定义参数)。 */
5
+ const DEFAULT_MODEL_FIELDS = {
6
+ contextWindow: 32000,
7
+ maxTokens: 8192,
8
+ reasoning: false,
9
+ input: ["text"],
10
+ };
4
11
  const revision = (raw) => createHash("sha256").update(raw).digest("hex");
5
12
  export async function readModelsConfig(agentDir) {
6
13
  let raw = "{}";
@@ -29,6 +36,18 @@ export async function readModelsConfig(agentDir) {
29
36
  return {
30
37
  id: String(item.id ?? ""),
31
38
  name: typeof item.name === "string" ? item.name : undefined,
39
+ contextWindow: typeof item.contextWindow === "number"
40
+ ? item.contextWindow
41
+ : undefined,
42
+ maxTokens: typeof item.maxTokens === "number"
43
+ ? item.maxTokens
44
+ : undefined,
45
+ input: Array.isArray(item.input)
46
+ ? item.input.filter((entry) => typeof entry === "string")
47
+ : undefined,
48
+ reasoning: typeof item.reasoning === "boolean"
49
+ ? item.reasoning
50
+ : undefined,
32
51
  };
33
52
  })
34
53
  .filter((model) => model.id)
@@ -81,19 +100,25 @@ export async function patchModelsConfig(agentDir, input) {
81
100
  code: "INVALID_INPUT",
82
101
  status: 400,
83
102
  });
103
+ const fields = validateModelFields(input.model);
104
+ const existingIndex = models.findIndex((model) => model.id === input.model.id);
105
+ const merged = {
106
+ // id/name 在前;新建模型用默认字段补齐(保证定义完整),更新保留已有值。
107
+ id: input.model.id,
108
+ ...(fields.name !== undefined ? { name: fields.name } : {}),
109
+ ...(existingIndex >= 0 ? models[existingIndex] : DEFAULT_MODEL_FIELDS),
110
+ ...fields,
111
+ };
84
112
  provider.models =
85
113
  input.action === "remove-model"
86
114
  ? models.filter((model) => model.id !== input.model.id)
87
- : [
88
- ...models.filter((model) => model.id !== input.model.id),
89
- {
90
- ...(models.find((model) => model.id === input.model.id) ?? {}),
91
- id: input.model.id,
92
- ...(input.model.name !== undefined
93
- ? { name: input.model.name }
94
- : {}),
95
- },
96
- ];
115
+ : existingIndex >= 0
116
+ ? [
117
+ ...models.slice(0, existingIndex),
118
+ merged,
119
+ ...models.slice(existingIndex + 1),
120
+ ]
121
+ : [...models, merged];
97
122
  }
98
123
  const body = `${JSON.stringify(parsed, null, 2)}\n`;
99
124
  const tmp = path.join(agentDir, `.models.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
@@ -108,6 +133,52 @@ export async function patchModelsConfig(agentDir, input) {
108
133
  await rename(tmp, file);
109
134
  return readModelsConfig(agentDir);
110
135
  }
136
+ /** Whitelist + validate model fields carried by a client upsert. Never accept
137
+ * arbitrary keys; only known numeric/boolean/string-array shape fields. */
138
+ function validateModelFields(model) {
139
+ const fields = {};
140
+ if (model.name !== undefined)
141
+ fields.name = String(model.name);
142
+ if (model.contextWindow !== undefined) {
143
+ if (typeof model.contextWindow !== "number" || model.contextWindow < 0)
144
+ throw invalid("contextWindow must be a non-negative number");
145
+ fields.contextWindow = model.contextWindow;
146
+ }
147
+ if (model.maxTokens !== undefined) {
148
+ if (typeof model.maxTokens !== "number" || model.maxTokens < 0)
149
+ throw invalid("maxTokens must be a non-negative number");
150
+ fields.maxTokens = model.maxTokens;
151
+ }
152
+ if (model.reasoning !== undefined) {
153
+ if (typeof model.reasoning !== "boolean")
154
+ throw invalid("reasoning must be a boolean");
155
+ fields.reasoning = model.reasoning;
156
+ }
157
+ if (model.input !== undefined) {
158
+ if (!Array.isArray(model.input) ||
159
+ !model.input.every((item) => typeof item === "string"))
160
+ throw invalid("input must be an array of strings");
161
+ fields.input = model.input.map((item) => String(item));
162
+ }
163
+ if (model.cost !== undefined) {
164
+ if (!model.cost || typeof model.cost !== "object")
165
+ throw invalid("cost must be an object");
166
+ const cost = {};
167
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
168
+ const value = model.cost[key];
169
+ if (value !== undefined) {
170
+ if (typeof value !== "number" || value < 0)
171
+ throw invalid(`cost.${key} must be a non-negative number`);
172
+ cost[key] = value;
173
+ }
174
+ }
175
+ fields.cost = cost;
176
+ }
177
+ return fields;
178
+ }
179
+ function invalid(message) {
180
+ return Object.assign(new Error(message), { code: "INVALID_INPUT", status: 400 });
181
+ }
111
182
  /** Max bytes of a package.json we are willing to read (read-only, bounded). */
112
183
  export const MAX_PACKAGE_JSON_BYTES = 64 * 1024;
113
184
  /** Max directory levels walked upward from an owned resolved resource path. */
@@ -270,6 +270,7 @@ export function parseChatImages(value) {
270
270
  }
271
271
  function gateMutation(req, deps) {
272
272
  const failure = evaluateMutationGate(req, {
273
+ cookieName: deps.bootToken.cookieName,
273
274
  bootToken: deps.bootToken.value,
274
275
  confirmationToken: deps.bootToken.confirmationToken,
275
276
  consoleOrigin: deps.getConsoleOrigin(),
@@ -35,6 +35,32 @@ import { hashDagBytes, } from "./dag-confirmation.js";
35
35
  * framework invocations that don't embed the word "verify".
36
36
  */
37
37
  export const STRUCTURED_VERIFICATION_COMMAND_RE = /\b(vitest|jest|playwright|cypress|pytest|py-test|go\s+test|cargo\s+test|npm\s+run\s+(lint|typecheck|test|verify)|npm\s+(test|run\s+test)|check-repo\.sh|loop-agent-standard-verify)\b/i;
38
+ /**
39
+ * Deterministic shell preset keys whose executors are themselves structured
40
+ * verification/gate materializers: their shell.commands[] is empty and the
41
+ * preset alone carries the verification authority (hash-bound artifact
42
+ * gates, evidence validators, report finalize). Mirrors the named-shell
43
+ * handler registry in shell-executor.ts so preset-only workflows
44
+ * (frontend-test finalize/reports, backend-test pipeline, verify-evidence
45
+ * gates) pass the G2 autonomous execution gate without depending on node-id
46
+ * naming conventions.
47
+ */
48
+ export const STRUCTURED_VERIFICATION_SHELL_PRESETS = new Set([
49
+ "frontendTestCaseChecklist",
50
+ "frontendTestCaseManifest",
51
+ "frontendTestEvidenceValidation",
52
+ "frontendTestResultFinalize",
53
+ "frontendTestL5Report",
54
+ "frontendTestHtmlReport",
55
+ "frontendTestReports",
56
+ "backendTestPipeline",
57
+ "verifyEvidence",
58
+ "repairArtifactGate",
59
+ "jacocoCoverage",
60
+ "requirementCoverageGate",
61
+ "jsonArtifactGate",
62
+ "verdictGate",
63
+ ]);
38
64
  export const DEFAULT_EXECUTION_RECEIPT_TTL_MS = 30 * 60 * 1000;
39
65
  function normalizePathEntry(entry) {
40
66
  return entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
@@ -157,6 +183,13 @@ export function deriveEligibilityFactsFromDagSpec(input) {
157
183
  return false;
158
184
  if (/verify|verification/i.test(task.id ?? ""))
159
185
  return true;
186
+ // Preset-only verification shells (empty commands[]): the preset key is
187
+ // the verification authority (frontend-test finalize, backend-test
188
+ // pipeline, evidence/artifact gates).
189
+ const shellRecord = task.shell;
190
+ const hasVerificationPreset = Object.keys(shellRecord).some((key) => STRUCTURED_VERIFICATION_SHELL_PRESETS.has(key));
191
+ if (hasVerificationPreset)
192
+ return true;
160
193
  const commands = stringList(task.shell.commands);
161
194
  return commands.some((command) => STRUCTURED_VERIFICATION_COMMAND_RE.test(command));
162
195
  });
@@ -0,0 +1,19 @@
1
+ import { parseFrontendHumanDecisionProjection } from "../../workflows/dag/frontend-human-decision.js";
2
+ /** Worker/Console persistence seam. Canonical validation and freshness remain in the workflow service. */
3
+ export function createFrontendHumanDecisionAdapter(store) {
4
+ return {
5
+ async load(operationId) {
6
+ const record = await store.get(operationId);
7
+ if (!record || record.revision === undefined || !record.frontendHumanDecision)
8
+ return undefined;
9
+ return { revision: record.revision, projection: parseFrontendHumanDecisionProjection(record.frontendHumanDecision) };
10
+ },
11
+ materialize(input) {
12
+ return store.materializeFrontendHumanDecision(input);
13
+ },
14
+ consume(input) {
15
+ return store.consumeFrontendHumanDecision(input);
16
+ },
17
+ initializeSplit: store.initializeFrontendHumanApprovedSplit.bind(store),
18
+ };
19
+ }
@@ -0,0 +1,20 @@
1
+ import { FrontendSplitOrchestrator, frontendSplitProjectionSchema, } from "../../workflows/dag/frontend-split-application-service.js";
2
+ /** Worker/Console boundary adapter. All orchestration policy remains canonical. */
3
+ export function createFrontendSplitOperationAdapter(dependencies) {
4
+ const ports = {
5
+ load: async (operationId) => {
6
+ const record = await dependencies.store.get(operationId);
7
+ return record?.frontendSplit === undefined ? undefined : frontendSplitProjectionSchema.parse(record.frontendSplit);
8
+ },
9
+ compareAndSwap: (input) => dependencies.store.compareAndSwapFrontendSplit(input),
10
+ materialize: async (request) => {
11
+ const result = await dependencies.materializer.materialize(request);
12
+ return { taskId: result.taskId, operationId: result.operationId };
13
+ },
14
+ schedule: dependencies.schedule,
15
+ auditStale: dependencies.auditStale,
16
+ loadChildCloseoutFacts: (input) => dependencies.store.loadChildCloseoutFacts(input),
17
+ settleAggregate: (input) => dependencies.store.settleFrontendSplitAggregate({ ...input, prepareAggregate: dependencies.prepareAggregate }),
18
+ };
19
+ return new FrontendSplitOrchestrator(ports);
20
+ }
@@ -11,6 +11,9 @@ export { createClosedResourceLoader, createInterviewResourceLoader, tryActivateT
11
11
  export { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
12
12
  export { openConsoleAppData, defaultConsoleAppDataRoot } from "./app-data.js";
13
13
  export { OperationStore } from "./operation-store.js";
14
+ export { createFrontendSplitOperationAdapter } from "./frontend-split-operation-adapter.js";
15
+ export { createFrontendHumanDecisionAdapter } from "./frontend-human-decision-adapter.js";
16
+ export { FrontendSplitTaskMaterializer } from "../materialize/frontend-split-task-materializer.js";
14
17
  export { createOperationEventStore } from "./operation-sse.js";
15
18
  export { DagConfirmationStore } from "./dag-confirmation.js";
16
19
  export { DraftStore } from "./draft-store.js";