@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.10

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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -613
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -58
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -7,12 +7,26 @@ import { fileURLToPath } from "node:url";
7
7
  import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
8
8
  import { loadHarnessManifest } from "../governance/harness.js";
9
9
  import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, applyPiRetryMerge, applyProjectPiSettingsMerge, buildOpenCodeContextOverflowCompactPluginSource, buildProjectPiSettingsSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectProjectPiSettings, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
10
+ import { assessInitPiModelReadiness, discoverInitModelCatalog, validateInitModelMatrixAgainstCatalog, } from "./init-model-catalog.js";
10
11
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
11
12
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
12
13
  const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
13
14
  const GITIGNORE_BLOCK_END = "# LOOP_AGENT_INIT_END";
14
15
  const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
15
16
  const DEFAULT_GOVERNANCE_ROOT = "ai_workspace/loop-agent";
17
+ function buildSpecRegistry() {
18
+ return `${JSON.stringify({
19
+ version: 1,
20
+ roots: [],
21
+ discovery: {
22
+ enabled: true,
23
+ maxDepth: 3,
24
+ maxFiles: 100,
25
+ maxBytes: 1048576,
26
+ timeoutMs: 2000,
27
+ },
28
+ }, null, 2)}\n`;
29
+ }
16
30
  const CORE_DOC_FILES = [
17
31
  { source: "README.md", target: "README.md" },
18
32
  {
@@ -609,7 +623,7 @@ if (
609
623
  (assessment.status === "passed" && assessment.currentExitCode !== 0) ||
610
624
  (assessment.status === "baseline-debt" && assessment.currentExitCode === 0)
611
625
  ) fail();
612
- if (!assessment.baselineRef || assessment.baselineRef.nodeId !== "frontend-lint-baseline-shell") fail();
626
+ if (!assessment.baselineRef || assessment.baselineRef.nodeId !== "frontend-writer-admission-shell") fail();
613
627
  const baselineRaw = readContained(assessment.baselineRef.path);
614
628
  if (sha256(baselineRaw) !== assessment.baselineRef.sha256) fail();
615
629
  let baseline;
@@ -889,13 +903,13 @@ function buildHarness(input) {
889
903
  }
890
904
  // Init always writes a Pi tier matrix. `defaultModel` remains runtime
891
905
  // compatibility for old projects only and is never fresh-init output.
892
- if (input.model) {
906
+ if (input.model || input.modelMatrix) {
893
907
  const piExisting = isRecord(mergedExecutors.pi) ? mergedExecutors.pi : {};
894
908
  mergedExecutors.pi = {
895
909
  ...piExisting,
896
- LOW: input.model,
897
- MED: input.model,
898
- HIGH: input.model,
910
+ LOW: input.modelMatrix?.LOW ?? input.model,
911
+ MED: input.modelMatrix?.MED ?? input.model,
912
+ HIGH: input.modelMatrix?.HIGH ?? input.model,
899
913
  };
900
914
  }
901
915
  if (isRecord(mergedExecutors.pi)) {
@@ -1128,6 +1142,28 @@ export function normalizeInitModelReference(input) {
1128
1142
  }
1129
1143
  return model;
1130
1144
  }
1145
+ export function normalizeInitModelMatrix(input) {
1146
+ const tierInputs = [input.lowModel, input.medModel, input.highModel];
1147
+ if (tierInputs.every((value) => value === undefined))
1148
+ return undefined;
1149
+ if (input.provider !== undefined || input.model !== undefined) {
1150
+ throw new Error("init --low-model/--med-model/--high-model cannot be combined with --provider or --model");
1151
+ }
1152
+ if (tierInputs.some((value) => value === undefined)) {
1153
+ throw new Error("init model matrix requires --low-model, --med-model, and --high-model together");
1154
+ }
1155
+ const lowModel = normalizeInitModelReference({ model: input.lowModel });
1156
+ const medModel = normalizeInitModelReference({ model: input.medModel });
1157
+ const highModel = normalizeInitModelReference({ model: input.highModel });
1158
+ if (!lowModel || !medModel || !highModel) {
1159
+ throw new Error("init model matrix contains an empty model reference");
1160
+ }
1161
+ return {
1162
+ LOW: lowModel,
1163
+ MED: medModel,
1164
+ HIGH: highModel,
1165
+ };
1166
+ }
1131
1167
  function normalizeRelativePath(relativePath) {
1132
1168
  return relativePath.split(path.sep).join("/");
1133
1169
  }
@@ -1289,6 +1325,9 @@ async function buildDesiredSurfaceContent(input) {
1289
1325
  }), null, 2)}\n`,
1290
1326
  };
1291
1327
  }
1328
+ if (manifestPath === "docs/spec-registry.json") {
1329
+ return { content: buildSpecRegistry() };
1330
+ }
1292
1331
  if (manifestPath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH) {
1293
1332
  return { content: buildOpenCodeTransientRetryPluginSource() };
1294
1333
  }
@@ -1363,6 +1402,7 @@ async function readExistingSurfaceState(repoRoot) {
1363
1402
  /** The IDE schema reference remains a structural update signal. */
1364
1403
  const HARNESS_STRUCTURAL_VALUE_KEYS = new Set(["$schema"]);
1365
1404
  const PI_MODEL_ROUTING_KEYS = new Set(["LOW", "MED", "HIGH", "defaultModel"]);
1405
+ const HARNESS_OPEN_MAP_PATHS = new Set(["entrypoints", "artifacts", "scripts"]);
1366
1406
  function isPiModelRoutingPath(path) {
1367
1407
  return path.length === 2 && path[0] === "executors" && path[1] === "pi";
1368
1408
  }
@@ -1400,6 +1440,48 @@ function harnessJsonShapeMatches(current, desired, path = []) {
1400
1440
  }
1401
1441
  return true;
1402
1442
  }
1443
+ /**
1444
+ * Align harness.json object properties with the current generated contract while
1445
+ * preserving project-owned scalar/array values. Pi tier routing is deliberately
1446
+ * opaque: existing LOW/MED/HIGH values survive byte-for-value, and missing tiers
1447
+ * stay missing until model readiness selects them from the Pi catalog.
1448
+ */
1449
+ function reconcileHarnessStructure(current, desired, path = []) {
1450
+ if (!isRecord(desired)) {
1451
+ if (HARNESS_STRUCTURAL_VALUE_KEYS.has(path.at(-1) ?? ""))
1452
+ return desired;
1453
+ if (Array.isArray(desired) &&
1454
+ Array.isArray(current)) {
1455
+ return current;
1456
+ }
1457
+ return current !== undefined && typeof current === typeof desired
1458
+ ? current
1459
+ : desired;
1460
+ }
1461
+ const currentRecord = isRecord(current) ? current : {};
1462
+ const next = {};
1463
+ for (const [key, desiredValue] of Object.entries(desired)) {
1464
+ if (isPiModelRoutingPath(path) && PI_MODEL_ROUTING_KEYS.has(key)) {
1465
+ if (key in currentRecord)
1466
+ next[key] = currentRecord[key];
1467
+ continue;
1468
+ }
1469
+ next[key] = reconcileHarnessStructure(currentRecord[key], desiredValue, [...path, key]);
1470
+ }
1471
+ if (isPiModelRoutingPath(path)) {
1472
+ for (const key of PI_MODEL_ROUTING_KEYS) {
1473
+ if (key in currentRecord && !(key in next))
1474
+ next[key] = currentRecord[key];
1475
+ }
1476
+ }
1477
+ if (path.length === 1 && HARNESS_OPEN_MAP_PATHS.has(path[0])) {
1478
+ for (const [key, value] of Object.entries(currentRecord)) {
1479
+ if (!(key in next))
1480
+ next[key] = value;
1481
+ }
1482
+ }
1483
+ return next;
1484
+ }
1403
1485
  /**
1404
1486
  * Stable package/source anchor for generated desired content: sha256 of the
1405
1487
  * desired content re-rendered through buildDesiredSurfaceContent with fixed
@@ -1574,6 +1656,13 @@ async function writeInitSurfaceState(input) {
1574
1656
  file.acceptedMerge = { ...priorFile.acceptedMerge };
1575
1657
  }
1576
1658
  }
1659
+ for (const [pathName, priorFile] of Object.entries(input.preserveOwnershipFrom.files)) {
1660
+ if (state.files[pathName] || priorFile.mode === "state")
1661
+ continue;
1662
+ if (await exists(path.join(input.repoRoot, pathName))) {
1663
+ state.files[pathName] = { ...priorFile };
1664
+ }
1665
+ }
1577
1666
  }
1578
1667
  if (input.acceptCurrentHarnessAsMerge) {
1579
1668
  const harness = state.files["harness.json"];
@@ -1636,6 +1725,8 @@ async function listRelativeFiles(rootPath) {
1636
1725
  async function expectedLegacyInitFileSha(input) {
1637
1726
  if (input.legacyPath.startsWith("docs/")) {
1638
1727
  const doc = input.legacyPath.slice("docs/".length);
1728
+ if (doc === "spec-registry.json")
1729
+ return sha256Text(buildSpecRegistry());
1639
1730
  const generated = buildGeneratedCoreDoc({
1640
1731
  doc,
1641
1732
  projectName: input.projectName,
@@ -1725,7 +1816,8 @@ function assessHarnessModelMigration(harness) {
1725
1816
  }
1726
1817
  const pi = isRecord(executorRecord.pi) ? executorRecord.pi : {};
1727
1818
  const hasDefaultModel = "defaultModel" in pi;
1728
- const hasTier = PI_MODEL_TIERS.some((tier) => tier in pi);
1819
+ const tierCount = PI_MODEL_TIERS.filter((tier) => tier in pi).length;
1820
+ const hasTier = tierCount > 0;
1729
1821
  if (["requiresApiKey", "provider", "model"].some((field) => field in pi) ||
1730
1822
  Object.keys(pi).some((field) => !["description", "enabled", "defaultModel", ...PI_MODEL_TIERS].includes(field))) {
1731
1823
  return {
@@ -1739,8 +1831,16 @@ function assessHarnessModelMigration(harness) {
1739
1831
  reason: "legacy top-level model or workflow fields cannot be proven equivalent",
1740
1832
  };
1741
1833
  }
1834
+ if (tierCount > 0 && tierCount < PI_MODEL_TIERS.length) {
1835
+ return {
1836
+ kind: "ambiguous",
1837
+ reason: "executors.pi contains an incomplete LOW/MED/HIGH model matrix",
1838
+ };
1839
+ }
1742
1840
  if (!hasDefaultModel)
1743
1841
  return { kind: "none" };
1842
+ if (tierCount === PI_MODEL_TIERS.length)
1843
+ return { kind: "none" };
1744
1844
  if (hasTier) {
1745
1845
  return {
1746
1846
  kind: "ambiguous",
@@ -2428,7 +2528,9 @@ export function buildInitInstructions(input) {
2428
2528
  "",
2429
2529
  `- Project name: default \`${projectName}\`.`,
2430
2530
  `- Governance root: default \`${governanceRoot}\`.`,
2431
- "- Confirm the Pi LOW/MED/HIGH matrix. Explicit input must be `--model provider/model` or a paired `--provider provider --model model`; do not guess a provider for a bare model.",
2531
+ "- Discover a credential-safe Pi/OpenCode model catalog with `loop-agent init model-catalog --repo-root <target> --json`. The command exposes only allowlisted model metadata, Pi setup status, and credential-readiness booleans, never raw config or credentials.",
2532
+ "- Preserve every existing LOW/MED/HIGH reference that still resolves in Pi. Select only missing or removed tiers from `eligibleForHarness=true`, then apply the complete matrix with `--low-model`, `--med-model`, and `--high-model`; update code keeps valid tiers unchanged.",
2533
+ "- If `piSetup.status` is `model-definition-required`, ask only for the missing provider/model id, API/base URL, context window, maximum output, inputs/reasoning, cost constraints, and credential method. Help merge Pi models.json without exposing credential values, rerun the catalog, and only then initialize. If definitions exist but credentials are missing, configure auth instead of rewriting harness tiers.",
2432
2534
  "- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
2433
2535
  "",
2434
2536
  "## Initialization Upgrade Routing",
@@ -2463,12 +2565,14 @@ export function buildInitInstructions(input) {
2463
2565
  "",
2464
2566
  "When a model/agent is asked to initialize a target project, treat initialization as one automated workflow. Do not run deterministic init and then wait for another user turn to fill project-specific content.",
2465
2567
  "",
2466
- "1. Resolve only the inputs that are genuinely unsafe to assume: provider/model, non-default governance root, overwrite policy, credentials, cost, or deployment side effects.",
2467
- "2. Run deterministic init with merge defaults.",
2468
- "3. Continue immediately into project inspection and adaptation.",
2469
- "4. Finish by running the init health checks and quick verification.",
2568
+ "1. Run `init model-catalog` and inspect `piSetup`. When Pi has no usable model setup, collect only the missing provider/model metadata or credential method, help configure Pi, and rerun the catalog before writing harness.json.",
2569
+ "2. Inspect the target repository. Preserve existing tiers that still resolve in Pi and select only missing/removed tiers from credential-ready Pi-compatible entries. OpenCode-only entries are context, not proof that the Pi executor can use them.",
2570
+ "3. Resolve with the user only inputs that remain genuinely unsafe to assume: material credential/cost/compliance choices, non-default governance root, overwrite policy, or deployment side effects.",
2571
+ "4. Run deterministic init with all three tier flags and merge defaults.",
2572
+ "5. Continue immediately into project inspection and adaptation.",
2573
+ "6. Finish by running the init health checks and quick verification; `init doctor` must confirm the Pi model matrix before any DAG is started.",
2470
2574
  "",
2471
- "If provider/model is not specified and the existing tier matrix is usable, proceed with it and record the assumption in the handoff. Ask the user only when the choice affects credentials, cost, availability, or an explicitly requested model backend. An ambiguous old model configuration must stay in a bounded harness.json model merge/human decision; never silently reinterpret it.",
2575
+ "A model reference that remains in Pi is project-owned and must not be reset merely because init templates changed. If its credentials are missing, preserve the tier and repair authentication. If a reference has left Pi, replace only that tier. Ask the user only when required model metadata, credentials, cost, compliance, or availability cannot be inferred. An ambiguous old model configuration stays in a bounded harness.json model merge/human decision; never silently reinterpret it.",
2472
2576
  "",
2473
2577
  "## Required Model Adaptation",
2474
2578
  "",
@@ -2502,6 +2606,7 @@ export async function initializeLoopAgentProject(options) {
2502
2606
  const profile = options.profile ?? "full";
2503
2607
  const merge = options.merge ?? true;
2504
2608
  const model = normalizeInitModelReference(options);
2609
+ const modelMatrix = options.modelMatrix;
2505
2610
  const written = [];
2506
2611
  const skipped = [];
2507
2612
  const assetRoot = await findPackageRoot();
@@ -2538,6 +2643,7 @@ export async function initializeLoopAgentProject(options) {
2538
2643
  projectName,
2539
2644
  governanceRoot,
2540
2645
  model,
2646
+ modelMatrix,
2541
2647
  });
2542
2648
  await writeText({
2543
2649
  repoRoot,
@@ -2619,6 +2725,14 @@ export async function initializeLoopAgentProject(options) {
2619
2725
  targetRelativePath: path.join(governanceRoot, "templates"),
2620
2726
  written,
2621
2727
  });
2728
+ await writeTextIfMissing({
2729
+ repoRoot,
2730
+ relativePath: path.join(governanceRoot, "spec-registry.json"),
2731
+ content: buildSpecRegistry(),
2732
+ merge,
2733
+ written,
2734
+ skipped,
2735
+ });
2622
2736
  if (profile === "full") {
2623
2737
  // Project repo-local skills live only under `.agents/skills/`.
2624
2738
  // The package still ships `skills/` as the built-in fallback source.
@@ -2680,7 +2794,7 @@ export async function initializeLoopAgentProject(options) {
2680
2794
  projectName,
2681
2795
  governanceRoot,
2682
2796
  stateKind: "recorded",
2683
- acceptCurrentHarnessAsMerge: model !== undefined,
2797
+ acceptCurrentHarnessAsMerge: model !== undefined || modelMatrix !== undefined,
2684
2798
  });
2685
2799
  written.push(INIT_SURFACE_STATE_PATH);
2686
2800
  return {
@@ -2743,11 +2857,7 @@ function modelMergeTaskFor(pathName, state, allPaths, recordedState) {
2743
2857
  "Keep writes inside allowedPaths only.",
2744
2858
  ],
2745
2859
  verification: [
2746
- "loop-agent init check-update --repo-root . --json",
2747
- "loop-agent init doctor --repo-root .",
2748
- "loop-agent inspect --repo-root .",
2749
- "loop-agent docs audit --repo-root .",
2750
- "bash scripts/check-repo.sh",
2860
+ `git diff --check -- ${pathName}`,
2751
2861
  ],
2752
2862
  evidence: {
2753
2863
  baseSha256: recordedState?.files[pathName]?.currentSha256,
@@ -2900,15 +3010,52 @@ export async function checkInitUpdate(input) {
2900
3010
  (state.mode === "copied" || state.mode === "generated") &&
2901
3011
  state.currentSha256 !== undefined &&
2902
3012
  recordedState.files[pathName]?.currentSha256 === state.currentSha256) {
3013
+ if (pathName === "harness.json") {
3014
+ // harness.json always contains project-owned routing and configuration.
3015
+ // Even when its bytes still match the recorded baseline, package evolution
3016
+ // must use a semantic merge instead of replacing the whole file.
3017
+ modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
3018
+ }
3019
+ else {
3020
+ deterministicActions.push({
3021
+ type: "refresh-owned-file",
3022
+ path: pathName,
3023
+ mode: state.mode,
3024
+ reason: "refresh an unchanged recorded init file to the current generated or bundled content",
3025
+ });
3026
+ }
3027
+ }
3028
+ else {
3029
+ modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
3030
+ }
3031
+ }
3032
+ }
3033
+ const retiredActionPaths = new Set(retiredLayoutActions.map((action) => action.path));
3034
+ if (recordedState) {
3035
+ for (const [pathName, recordedFile] of Object.entries(recordedState.files)) {
3036
+ if (currentState.files[pathName] ||
3037
+ retiredActionPaths.has(pathName) ||
3038
+ recordedFile.mode === "state" ||
3039
+ recordedFile.mode === "directory") {
3040
+ continue;
3041
+ }
3042
+ const currentSha256 = await fileSha256IfExists(path.join(repoRoot, pathName));
3043
+ if (!currentSha256)
3044
+ continue;
3045
+ if (recordedFile.currentSha256 &&
3046
+ currentSha256 === recordedFile.currentSha256) {
2903
3047
  deterministicActions.push({
2904
- type: "refresh-owned-file",
3048
+ type: "remove-owned-file",
2905
3049
  path: pathName,
2906
- mode: state.mode,
2907
- reason: "refresh an unchanged recorded init file to the current generated or bundled content",
3050
+ expectedSha256: currentSha256,
3051
+ reason: "remove an unchanged recorded init file that is no longer part of the current initialization surface",
2908
3052
  });
2909
3053
  }
2910
- else {
2911
- modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
3054
+ else if (!humanDecisions.some((decision) => decision.path === pathName)) {
3055
+ humanDecisions.push({
3056
+ path: pathName,
3057
+ reason: "this recorded init file was removed from the current surface but has local changes; review whether to preserve or delete it",
3058
+ });
2912
3059
  }
2913
3060
  }
2914
3061
  }
@@ -2916,9 +3063,12 @@ export async function checkInitUpdate(input) {
2916
3063
  // Safe harness hygiene: strip obsolete pi.requiresApiKey without full harness rewrite.
2917
3064
  // Cursor requiresApiKey is intentionally kept when present.
2918
3065
  const harnessPath = path.join(repoRoot, "harness.json");
3066
+ let currentHarness;
2919
3067
  if (await exists(harnessPath)) {
2920
3068
  try {
2921
3069
  const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
3070
+ if (isRecord(harness))
3071
+ currentHarness = harness;
2922
3072
  if (recordedState &&
2923
3073
  isRecord(harness) &&
2924
3074
  harness.$schema !== expectedHarnessSchemaRef(governanceRoot)) {
@@ -2942,10 +3092,11 @@ export async function checkInitUpdate(input) {
2942
3092
  // A harness that already matches the current generated surface except for
2943
3093
  // opaque Pi routing fields is modern, not a legacy-routing candidate.
2944
3094
  // Keep the explicit migration assessment for every other harness shape.
3095
+ let modelMigration = { kind: "none" };
2945
3096
  if (isRecord(harness) &&
2946
3097
  currentState.files["harness.json"]?.relationship !==
2947
3098
  "matches-current-generated") {
2948
- const modelMigration = assessHarnessModelMigration(harness);
3099
+ modelMigration = assessHarnessModelMigration(harness);
2949
3100
  if (modelMigration.kind === "safe") {
2950
3101
  deterministicActions.push({
2951
3102
  type: "migrate-harness-model-fields",
@@ -2975,11 +3126,83 @@ export async function checkInitUpdate(input) {
2975
3126
  modelMergeTasks.push(mergeTask);
2976
3127
  }
2977
3128
  }
3129
+ if (recordedState &&
3130
+ isRecord(harness) &&
3131
+ modelMigration.kind !== "ambiguous") {
3132
+ const desired = await buildDesiredSurfaceContent({
3133
+ assetRoot,
3134
+ repoRoot,
3135
+ projectName,
3136
+ governanceRoot,
3137
+ entry: { path: "harness.json", mode: "generated" },
3138
+ });
3139
+ if (desired.content !== undefined) {
3140
+ const desiredHarness = JSON.parse(desired.content);
3141
+ const reconciled = reconcileHarnessStructure(harness, desiredHarness);
3142
+ if (!isDeepStrictEqual(reconciled, harness)) {
3143
+ deterministicActions.push({
3144
+ type: "reconcile-harness-structure",
3145
+ path: "harness.json",
3146
+ reason: "add current harness properties, remove retired properties, and preserve project-owned values",
3147
+ });
3148
+ const existingTask = modelMergeTasks.findIndex((task) => task.path === "harness.json");
3149
+ if (existingTask >= 0)
3150
+ modelMergeTasks.splice(existingTask, 1);
3151
+ }
3152
+ }
3153
+ }
2978
3154
  }
2979
3155
  catch {
2980
3156
  // leave malformed harness for model merge / doctor
2981
3157
  }
2982
3158
  }
3159
+ const modelReadiness = input.modelCatalog
3160
+ ? assessInitPiModelReadiness(isRecord(currentHarness?.executors) &&
3161
+ isRecord(currentHarness.executors.pi)
3162
+ ? currentHarness.executors.pi
3163
+ : undefined, input.modelCatalog)
3164
+ : undefined;
3165
+ if (modelReadiness?.status === "selection-required" ||
3166
+ modelReadiness?.status === "reselection-required") {
3167
+ const state = currentState.files["harness.json"];
3168
+ const task = modelMergeTaskFor("harness.json", state, allPaths, recordedState);
3169
+ task.reason =
3170
+ modelReadiness.status === "selection-required"
3171
+ ? "harness.json has no complete Pi model matrix that resolves against the current Pi model catalog"
3172
+ : `only unavailable Pi tiers need reselection: ${modelReadiness.reselectTiers.join(", ")}`;
3173
+ task.mergeRules = [
3174
+ `Preserve these currently resolvable tiers byte-for-byte: ${modelReadiness.preserveTiers.join(", ") || "none"}.`,
3175
+ `Select replacements only for: ${modelReadiness.reselectTiers.join(", ")}.`,
3176
+ "Choose replacements from eligibleReferences using target-project needs, capability, context window, latency, and cost; tiers may intentionally share a model.",
3177
+ "Apply the complete matrix through init update --apply-safe with all three tier flags so code can revalidate it; valid existing tiers must keep their current effective reference.",
3178
+ "Do not add credentials or raw Pi configuration to harness.json.",
3179
+ ];
3180
+ task.verification = [
3181
+ "loop-agent init model-catalog --repo-root . --json",
3182
+ "loop-agent init check-update --repo-root . --json",
3183
+ "loop-agent init doctor --repo-root .",
3184
+ "bash scripts/check-repo.sh",
3185
+ ];
3186
+ const existingTask = modelMergeTasks.findIndex((candidate) => candidate.path === "harness.json");
3187
+ if (existingTask >= 0)
3188
+ modelMergeTasks[existingTask] = task;
3189
+ else
3190
+ modelMergeTasks.push(task);
3191
+ }
3192
+ else if (modelReadiness &&
3193
+ modelReadiness.status !== "ready") {
3194
+ const setup = input.modelCatalog?.piSetup;
3195
+ const reason = setup?.status === "model-definition-required"
3196
+ ? `${setup.reason} Ask for: ${setup.requiredUserInput.join("; ")}.`
3197
+ : setup?.status === "credentials-required" ||
3198
+ setup?.status === "discovery-failed"
3199
+ ? setup.reason
3200
+ : "One or more configured Pi tiers are known but not credential-ready; enable their provider without changing the tier selection.";
3201
+ humanDecisions.push({
3202
+ path: "~/.pi/agent/models.json",
3203
+ reason,
3204
+ });
3205
+ }
2983
3206
  const partial = {
2984
3207
  repoRoot,
2985
3208
  controllerVersion: currentState.controllerVersion,
@@ -2987,6 +3210,7 @@ export async function checkInitUpdate(input) {
2987
3210
  deterministicActions,
2988
3211
  modelMergeTasks,
2989
3212
  humanDecisions,
3213
+ ...(modelReadiness ? { modelReadiness } : {}),
2990
3214
  summary: {
2991
3215
  missing: deterministicActions.filter((action) => action.type !== "bootstrap-surface").length,
2992
3216
  modelMerge: modelMergeTasks.length,
@@ -2994,6 +3218,9 @@ export async function checkInitUpdate(input) {
2994
3218
  },
2995
3219
  };
2996
3220
  const recommendedNext = recommendedNextFor(partial);
3221
+ if (modelReadiness && modelReadiness.status !== "ready") {
3222
+ recommendedNext.unshift("loop-agent init model-catalog --repo-root <target> --json");
3223
+ }
2997
3224
  // Project settings are part of the surface. Do not inspect user home by default.
2998
3225
  const piInspection = await inspectProjectPiSettings({ repoRoot });
2999
3226
  return {
@@ -3105,9 +3332,36 @@ async function applySafeAction(input) {
3105
3332
  const target = path.join(input.repoRoot, input.action.path);
3106
3333
  if (!(await exists(target)))
3107
3334
  return false;
3335
+ if (input.action.expectedSha256 &&
3336
+ (await fileSha256IfExists(target)) !== input.action.expectedSha256) {
3337
+ return false;
3338
+ }
3108
3339
  await rm(target, { force: true });
3109
3340
  return true;
3110
3341
  }
3342
+ if (input.action.type === "reconcile-harness-structure") {
3343
+ const target = path.join(input.repoRoot, "harness.json");
3344
+ if (!(await exists(target)))
3345
+ return false;
3346
+ const current = JSON.parse(await readFile(target, "utf-8"));
3347
+ if (!isRecord(current))
3348
+ return false;
3349
+ const assetRoot = await findPackageRoot();
3350
+ const desired = await buildDesiredSurfaceContent({
3351
+ assetRoot,
3352
+ repoRoot: input.repoRoot,
3353
+ projectName: input.projectName,
3354
+ governanceRoot: input.governanceRoot,
3355
+ entry: { path: "harness.json", mode: "generated" },
3356
+ });
3357
+ if (desired.content === undefined)
3358
+ return false;
3359
+ const next = reconcileHarnessStructure(current, JSON.parse(desired.content));
3360
+ if (isDeepStrictEqual(next, current))
3361
+ return false;
3362
+ await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
3363
+ return true;
3364
+ }
3111
3365
  if (input.action.type === "remove-empty-directory") {
3112
3366
  const target = path.join(input.repoRoot, input.action.path);
3113
3367
  if (!(await isDirectoryEmpty(target)))
@@ -3162,6 +3416,8 @@ async function applySafeAction(input) {
3162
3416
  return true;
3163
3417
  }
3164
3418
  if (input.action.type === "refresh-owned-file") {
3419
+ if (input.action.path === "harness.json")
3420
+ return false;
3165
3421
  await writeFile(target, desired.content, "utf-8");
3166
3422
  return true;
3167
3423
  }
@@ -3180,6 +3436,52 @@ export async function applyInitUpdate(input) {
3180
3436
  const applied = [];
3181
3437
  const skipped = [];
3182
3438
  const clientRecoveryMode = input.clientRecovery ?? "auto";
3439
+ const requestedModelMatrix = input.modelMatrix ??
3440
+ (input.model
3441
+ ? { LOW: input.model, MED: input.model, HIGH: input.model }
3442
+ : undefined);
3443
+ if (requestedModelMatrix) {
3444
+ if (!input.modelCatalog) {
3445
+ throw new Error("init update model selection requires a fresh Pi model catalog");
3446
+ }
3447
+ validateInitModelMatrixAgainstCatalog(requestedModelMatrix, input.modelCatalog);
3448
+ const harnessPath = path.join(repoRoot, "harness.json");
3449
+ const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
3450
+ if (!isRecord(harness) ||
3451
+ !isRecord(harness.executors) ||
3452
+ !isRecord(harness.executors.pi)) {
3453
+ throw new Error("init update cannot select models until harness.json executors.pi exists");
3454
+ }
3455
+ const readiness = assessInitPiModelReadiness(harness.executors.pi, input.modelCatalog);
3456
+ if (readiness.status === "credentials-required" ||
3457
+ readiness.status === "model-definition-required" ||
3458
+ readiness.status === "discovery-failed") {
3459
+ throw new Error(`init update cannot change harness model routing while Pi model readiness is ${readiness.status}`);
3460
+ }
3461
+ for (const tier of readiness.preserveTiers) {
3462
+ const currentReference = readiness.tiers[tier].resolvedReference;
3463
+ if (currentReference &&
3464
+ requestedModelMatrix[tier] !== currentReference) {
3465
+ throw new Error(`init update preserves the current Pi ${tier} model because it is still in the Pi catalog: ${currentReference}`);
3466
+ }
3467
+ }
3468
+ if (readiness.reselectTiers.length > 0) {
3469
+ const nextPi = { ...harness.executors.pi };
3470
+ for (const tier of readiness.reselectTiers) {
3471
+ nextPi[tier] = requestedModelMatrix[tier];
3472
+ }
3473
+ const next = {
3474
+ ...harness,
3475
+ executors: { ...harness.executors, pi: nextPi },
3476
+ };
3477
+ await writeFile(harnessPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
3478
+ applied.push({
3479
+ type: "write-harness-model-matrix",
3480
+ path: "harness.json",
3481
+ reason: `replace only unavailable Pi tiers: ${readiness.reselectTiers.join(", ")}`,
3482
+ });
3483
+ }
3484
+ }
3183
3485
  if (input.bootstrapSurface) {
3184
3486
  await writeInitSurfaceState({
3185
3487
  repoRoot,
@@ -3206,6 +3508,7 @@ export async function applyInitUpdate(input) {
3206
3508
  governanceRoot,
3207
3509
  clientRecovery: clientRecoveryMode,
3208
3510
  homeDir: input.homeDir,
3511
+ modelCatalog: input.modelCatalog,
3209
3512
  });
3210
3513
  for (const action of report.deterministicActions) {
3211
3514
  if (action.type === "bootstrap-surface") {
@@ -3245,6 +3548,7 @@ export async function applyInitUpdate(input) {
3245
3548
  governanceRoot,
3246
3549
  clientRecovery: clientRecoveryMode,
3247
3550
  homeDir: input.homeDir,
3551
+ modelCatalog: input.modelCatalog,
3248
3552
  }),
3249
3553
  };
3250
3554
  }
@@ -3258,6 +3562,16 @@ function formatClientRecoverySummary(report) {
3258
3562
  `clientRecovery.pi.path: ${pi.path}`,
3259
3563
  ];
3260
3564
  }
3565
+ function formatModelReadinessSummary(report) {
3566
+ const readiness = report.modelReadiness;
3567
+ if (!readiness)
3568
+ return [];
3569
+ return [
3570
+ `modelReadiness.status: ${readiness.status}`,
3571
+ `modelReadiness.preserveTiers: ${readiness.preserveTiers.join(",") || "none"}`,
3572
+ `modelReadiness.reselectTiers: ${readiness.reselectTiers.join(",") || "none"}`,
3573
+ ];
3574
+ }
3261
3575
  function formatCheckUpdateText(report) {
3262
3576
  return [
3263
3577
  `loop-agent init update check for ${report.repoRoot}`,
@@ -3266,6 +3580,7 @@ function formatCheckUpdateText(report) {
3266
3580
  `deterministicActions: ${report.deterministicActions.length}`,
3267
3581
  `modelMergeTasks: ${report.modelMergeTasks.length}`,
3268
3582
  `humanDecisions: ${report.humanDecisions.length}`,
3583
+ ...formatModelReadinessSummary(report),
3269
3584
  ...formatClientRecoverySummary(report),
3270
3585
  "recommendedNext:",
3271
3586
  ...report.recommendedNext.map((item) => `- ${item}`),
@@ -3279,6 +3594,14 @@ function formatCheckUpdateMarkdown(report) {
3279
3594
  `- repoRoot: \`${report.repoRoot}\``,
3280
3595
  `- controllerVersion: \`${report.controllerVersion}\``,
3281
3596
  `- surfaceState: \`${report.surfaceState}\``,
3597
+ ...(report.modelReadiness
3598
+ ? [
3599
+ "",
3600
+ "## Pi Model Readiness",
3601
+ "",
3602
+ ...formatModelReadinessSummary(report).map((line) => `- ${line}`),
3603
+ ]
3604
+ : []),
3282
3605
  ...(piLines.length > 0
3283
3606
  ? ["", "## Client Recovery (Pi user config, read-only)", "", ...piLines]
3284
3607
  : []),
@@ -3313,6 +3636,14 @@ export async function runInitDoctor(input) {
3313
3636
  const manifest = await loadHarnessManifest(repoRoot);
3314
3637
  add("harness.json", manifest.adapter === "loop-agent" || manifest.project === "loop-agent", `project=${manifest.project}`);
3315
3638
  add("governance root", await exists(path.join(repoRoot, manifest.governanceRoot, "README.md")), manifest.governanceRoot);
3639
+ if (input.modelCatalog) {
3640
+ const readiness = assessInitPiModelReadiness(isRecord(manifest.executors?.pi)
3641
+ ? manifest.executors.pi
3642
+ : undefined, input.modelCatalog);
3643
+ add("Pi model matrix", readiness.status === "ready", readiness.status === "ready"
3644
+ ? "LOW/MED/HIGH resolve against the current Pi model catalog"
3645
+ : `${readiness.status}; preserve=${readiness.preserveTiers.join(",") || "none"}; reselect=${readiness.reselectTiers.join(",") || "none"}`);
3646
+ }
3316
3647
  }
3317
3648
  catch (error) {
3318
3649
  add("harness.json", false, error instanceof Error ? error.message : String(error));
@@ -3354,6 +3685,7 @@ export async function runInitReconcile(input) {
3354
3685
  repoRoot,
3355
3686
  projectName: input.projectName,
3356
3687
  governanceRoot: input.governanceRoot,
3688
+ modelCatalog: input.modelCatalog,
3357
3689
  });
3358
3690
  if (preReport.surfaceState === "missing") {
3359
3691
  return { status: "needs-baseline", report: preReport };
@@ -3374,6 +3706,7 @@ export async function runInitReconcile(input) {
3374
3706
  projectName: input.projectName,
3375
3707
  governanceRoot: input.governanceRoot,
3376
3708
  applySafe: true,
3709
+ modelCatalog: input.modelCatalog,
3377
3710
  });
3378
3711
  const status = update.report.ok
3379
3712
  ? "clean"
@@ -3395,6 +3728,9 @@ function parseInitArgs(repoRoot, args) {
3395
3728
  let merge = true;
3396
3729
  let provider;
3397
3730
  let model;
3731
+ let lowModel;
3732
+ let medModel;
3733
+ let highModel;
3398
3734
  let clientRecovery = "auto";
3399
3735
  let json = false;
3400
3736
  let markdown = false;
@@ -3409,6 +3745,7 @@ function parseInitArgs(repoRoot, args) {
3409
3745
  const arg = args[i];
3410
3746
  if ((arg === "instructions" ||
3411
3747
  arg === "doctor" ||
3748
+ arg === "model-catalog" ||
3412
3749
  arg === "check-update" ||
3413
3750
  arg === "update" ||
3414
3751
  arg === "reconcile" ||
@@ -3441,6 +3778,18 @@ function parseInitArgs(repoRoot, args) {
3441
3778
  model = args[++i];
3442
3779
  else if (arg.startsWith("--model="))
3443
3780
  model = arg.slice("--model=".length);
3781
+ else if (arg === "--low-model")
3782
+ lowModel = args[++i];
3783
+ else if (arg.startsWith("--low-model="))
3784
+ lowModel = arg.slice("--low-model=".length);
3785
+ else if (arg === "--med-model")
3786
+ medModel = args[++i];
3787
+ else if (arg.startsWith("--med-model="))
3788
+ medModel = arg.slice("--med-model=".length);
3789
+ else if (arg === "--high-model")
3790
+ highModel = args[++i];
3791
+ else if (arg.startsWith("--high-model="))
3792
+ highModel = arg.slice("--high-model=".length);
3444
3793
  else if (arg === "--client-recovery")
3445
3794
  clientRecovery = parseClientRecoveryMode(args[++i]);
3446
3795
  else if (arg.startsWith("--client-recovery="))
@@ -3478,6 +3827,13 @@ function parseInitArgs(repoRoot, args) {
3478
3827
  throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
3479
3828
  }
3480
3829
  const normalizedModel = normalizeInitModelReference({ provider, model });
3830
+ const modelMatrix = normalizeInitModelMatrix({
3831
+ provider,
3832
+ model,
3833
+ lowModel,
3834
+ medModel,
3835
+ highModel,
3836
+ });
3481
3837
  return {
3482
3838
  repoRoot,
3483
3839
  projectName,
@@ -3486,6 +3842,7 @@ function parseInitArgs(repoRoot, args) {
3486
3842
  merge,
3487
3843
  provider,
3488
3844
  model: normalizedModel,
3845
+ modelMatrix,
3489
3846
  clientRecovery,
3490
3847
  subcommand,
3491
3848
  json,
@@ -3499,21 +3856,49 @@ function parseInitArgs(repoRoot, args) {
3499
3856
  versionChoice,
3500
3857
  };
3501
3858
  }
3859
+ async function discoverInitCatalogForRepo(repoRoot) {
3860
+ const harness = await readJsonIfExists(path.join(repoRoot, "harness.json"));
3861
+ const pi = isRecord(harness.executors) && isRecord(harness.executors.pi)
3862
+ ? harness.executors.pi
3863
+ : undefined;
3864
+ const includePiReferences = pi
3865
+ ? PI_MODEL_TIERS.flatMap((tier) => {
3866
+ const value = pi[tier];
3867
+ const model = typeof value === "string"
3868
+ ? value
3869
+ : isRecord(value) && typeof value.model === "string"
3870
+ ? value.model
3871
+ : undefined;
3872
+ return model && isQualifiedModelReference(model) ? [model] : [];
3873
+ })
3874
+ : [];
3875
+ return discoverInitModelCatalog({ repoRoot, includePiReferences });
3876
+ }
3502
3877
  export async function runInit(repoRoot, rawArgs, dependencies) {
3503
3878
  const parsed = parseInitArgs(repoRoot, rawArgs);
3504
3879
  if (parsed.subcommand === "instructions") {
3505
3880
  console.log(buildInitInstructions(parsed));
3506
3881
  return;
3507
3882
  }
3883
+ if (parsed.subcommand === "model-catalog") {
3884
+ const catalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3885
+ console.log(JSON.stringify(catalog, null, 2));
3886
+ return;
3887
+ }
3508
3888
  if (parsed.subcommand === "doctor") {
3509
- const report = await runInitDoctor({ repoRoot: parsed.repoRoot });
3889
+ const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3890
+ const report = await runInitDoctor({
3891
+ repoRoot: parsed.repoRoot,
3892
+ modelCatalog,
3893
+ });
3510
3894
  console.log(JSON.stringify(report, null, 2));
3511
3895
  if (!report.ok)
3512
3896
  process.exitCode = 1;
3513
3897
  return;
3514
3898
  }
3515
3899
  if (parsed.subcommand === "check-update") {
3516
- const report = await checkInitUpdate(parsed);
3900
+ const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3901
+ const report = await checkInitUpdate({ ...parsed, modelCatalog });
3517
3902
  if (parsed.json)
3518
3903
  console.log(JSON.stringify(report, null, 2));
3519
3904
  else if (parsed.markdown)
@@ -3526,7 +3911,11 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3526
3911
  if (!parsed.bootstrapSurface && !parsed.applySafe) {
3527
3912
  throw new Error("usage: init update [--bootstrap-surface] [--apply-safe]");
3528
3913
  }
3529
- const result = await applyInitUpdate(parsed);
3914
+ if ((parsed.model || parsed.modelMatrix) && !parsed.applySafe) {
3915
+ throw new Error("init update model selection requires --apply-safe");
3916
+ }
3917
+ const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3918
+ const result = await applyInitUpdate({ ...parsed, modelCatalog });
3530
3919
  console.log(parsed.json
3531
3920
  ? JSON.stringify(result, null, 2)
3532
3921
  : formatInitUpdateResult(result));
@@ -3537,12 +3926,23 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3537
3926
  const selected = [parsed.upgradeStatus, parsed.upgradeContinue, parsed.upgradeReport].filter(Boolean).length;
3538
3927
  if (selected > 1)
3539
3928
  throw new Error("init upgrade accepts only one of --status, --continue, or --report");
3929
+ const mode = parsed.upgradeStatus
3930
+ ? "status"
3931
+ : parsed.upgradeContinue
3932
+ ? "continue"
3933
+ : parsed.upgradeReport
3934
+ ? "report"
3935
+ : "start";
3936
+ const modelCatalog = mode === "start"
3937
+ ? await discoverInitCatalogForRepo(parsed.repoRoot)
3938
+ : undefined;
3540
3939
  const result = await runInitUpgrade({
3541
3940
  repoRoot: parsed.repoRoot,
3542
3941
  runId: parsed.runId,
3543
- mode: parsed.upgradeStatus ? "status" : parsed.upgradeContinue ? "continue" : parsed.upgradeReport ? "report" : "start",
3942
+ mode,
3544
3943
  versionChoice: parsed.versionChoice,
3545
3944
  readRuntimeActivity: dependencies.readRuntimeActivity,
3945
+ modelCatalog,
3546
3946
  });
3547
3947
  if ("markdown" in result)
3548
3948
  console.log(result.markdown);
@@ -3551,8 +3951,10 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3551
3951
  return;
3552
3952
  }
3553
3953
  if (parsed.subcommand === "reconcile") {
3954
+ const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3554
3955
  const result = await runInitReconcile({
3555
3956
  ...parsed,
3957
+ modelCatalog,
3556
3958
  readRuntimeActivity: dependencies.readRuntimeActivity,
3557
3959
  });
3558
3960
  console.log(parsed.json
@@ -3560,6 +3962,31 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3560
3962
  : formatReconcileResult(result));
3561
3963
  return;
3562
3964
  }
3965
+ const catalog = await discoverInitCatalogForRepo(parsed.repoRoot);
3966
+ const requestedMatrix = parsed.modelMatrix ??
3967
+ (parsed.model
3968
+ ? { LOW: parsed.model, MED: parsed.model, HIGH: parsed.model }
3969
+ : undefined);
3970
+ if (requestedMatrix) {
3971
+ validateInitModelMatrixAgainstCatalog(requestedMatrix, catalog);
3972
+ }
3973
+ else {
3974
+ const harness = await readJsonIfExists(path.join(parsed.repoRoot, "harness.json"));
3975
+ const readiness = assessInitPiModelReadiness(isRecord(harness.executors) && isRecord(harness.executors.pi)
3976
+ ? harness.executors.pi
3977
+ : undefined, catalog);
3978
+ if (readiness.status !== "ready") {
3979
+ const setup = catalog.piSetup;
3980
+ if (setup.status === "model-definition-required") {
3981
+ throw new Error(`Pi model setup is required before init. Ask the user for: ${setup.requiredUserInput.join("; ")}. Help merge the definition into ${setup.configPath} without storing credential values, rerun init model-catalog, then initialize with all three tier flags.`);
3982
+ }
3983
+ if (setup.status === "credentials-required" ||
3984
+ setup.status === "discovery-failed") {
3985
+ throw new Error(`${setup.reason} ${setup.nextSteps.join(" ")}`);
3986
+ }
3987
+ throw new Error(`Pi model selection is required before init (${readiness.status}). Run init model-catalog, preserve tiers still present in Pi, select only missing tiers, and pass --low-model/--med-model/--high-model.`);
3988
+ }
3989
+ }
3563
3990
  const result = await initializeLoopAgentProject(parsed);
3564
3991
  console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitResult(result));
3565
3992
  }