@repo-os/repoos 0.5.62 → 0.5.63

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 (193) hide show
  1. package/dist/.build-info.json +2 -2
  2. package/dist/.build-stamp.json +1 -1
  3. package/dist/commands/init.js +0 -2
  4. package/dist/commands/tasks.js +10 -6
  5. package/dist/commands/ui-harness.d.ts +26 -0
  6. package/dist/commands/ui-smoke.js +173 -7
  7. package/dist/core/close-out-failure.d.ts +7 -0
  8. package/dist/core/close-out-failure.js +7 -0
  9. package/dist/core/config.d.ts +16 -0
  10. package/dist/core/config.js +93 -5
  11. package/dist/core/copy-inspector.d.ts +13 -0
  12. package/dist/core/copy-inspector.js +36 -1
  13. package/dist/core/doctor.js +1 -2
  14. package/dist/core/input.d.ts +11 -0
  15. package/dist/core/input.js +22 -2
  16. package/dist/core/providers/spend.d.ts +1 -64
  17. package/dist/core/providers/spend.js +7 -192
  18. package/dist/core/remote-hosts.d.ts +2 -1
  19. package/dist/core/remote-hosts.js +10 -0
  20. package/dist/core/shot-page.js +12 -2
  21. package/dist/core/shot-plan.d.ts +74 -0
  22. package/dist/core/shot-plan.js +99 -6
  23. package/dist/core/story-definition-files.d.ts +9 -1
  24. package/dist/core/story-definition-files.js +19 -7
  25. package/dist/core/support-bundle.js +0 -1
  26. package/dist/core/task.d.ts +7 -0
  27. package/dist/core/task.js +20 -0
  28. package/dist/core/types.d.ts +10 -3
  29. package/dist/server/agents.d.ts +52 -2
  30. package/dist/server/agents.js +142 -15
  31. package/dist/server/close-out-outcome.d.ts +20 -0
  32. package/dist/server/close-out-outcome.js +111 -0
  33. package/dist/server/handoff.js +3 -0
  34. package/dist/server/integration-orchestrator.d.ts +30 -5
  35. package/dist/server/integration-orchestrator.js +114 -47
  36. package/dist/server/live-index.d.ts +23 -0
  37. package/dist/server/preview.d.ts +33 -0
  38. package/dist/server/preview.js +83 -5
  39. package/dist/server/remote-validation.d.ts +27 -2
  40. package/dist/server/remote-validation.js +249 -53
  41. package/dist/server/review.d.ts +8 -0
  42. package/dist/server/review.js +16 -3
  43. package/dist/server/routes/config.js +8 -3
  44. package/dist/server/routes/copy-inspector.d.ts +7 -0
  45. package/dist/server/routes/copy-inspector.js +65 -17
  46. package/dist/server/routes/inputs.d.ts +23 -0
  47. package/dist/server/routes/inputs.js +147 -29
  48. package/dist/server/routes/model-providers.d.ts +0 -6
  49. package/dist/server/routes/model-providers.js +2 -40
  50. package/dist/server/routes/release.d.ts +17 -0
  51. package/dist/server/routes/release.js +35 -2
  52. package/dist/server/routes/stories.d.ts +9 -0
  53. package/dist/server/routes/stories.js +42 -1
  54. package/dist/server/routes/tasks.d.ts +15 -0
  55. package/dist/server/routes/tasks.js +263 -0
  56. package/dist/server/routes/types.d.ts +8 -0
  57. package/dist/server/server.js +40 -7
  58. package/dist/server/shot-capture.d.ts +58 -1
  59. package/dist/server/shot-capture.js +206 -31
  60. package/dist/server/shots.d.ts +16 -0
  61. package/dist/server/shots.js +12 -0
  62. package/dist/server/task-check.d.ts +1 -0
  63. package/dist/server/task-check.js +1 -1
  64. package/dist/server/watcher.js +3 -3
  65. package/dist/server/write.d.ts +13 -0
  66. package/dist/server/write.js +46 -0
  67. package/dist/ui/assets/{AgentModelModal-C-X9UXUK.js → AgentModelModal-BsjlDrGV.js} +1 -1
  68. package/dist/ui/assets/{AgentsView-DPFAB5vF.css → AgentsView-B-VbBCBQ.css} +1 -1
  69. package/dist/ui/assets/AgentsView-Dw5drin8.js +1 -0
  70. package/dist/ui/assets/ChecksView-BbMgmWzY.js +0 -0
  71. package/dist/ui/assets/ChecksView-DvW47w95.css +1 -0
  72. package/dist/ui/assets/ContextView-gAorV_po.js +4 -0
  73. package/dist/ui/assets/{DashboardView-D6GeYs6o.js → DashboardView-CxDsQ4b3.js} +1 -1
  74. package/dist/ui/assets/DeleteConfirmDialog-D_MMkeQr.js +1 -0
  75. package/dist/ui/assets/{DeploymentsView-BJZ6z_Gd.js → DeploymentsView-DjuSlTT3.js} +1 -1
  76. package/dist/ui/assets/{DiffView-D8pgjtSQ.js → DiffView-CMbwhk0u.js} +2 -2
  77. package/dist/ui/assets/InputsView-CMTBgj3R.css +1 -0
  78. package/dist/ui/assets/InputsView-DL0d3oUS.js +1 -0
  79. package/dist/ui/assets/{LoginView-DYvBzW5u.js → LoginView-B_dRypOC.js} +1 -1
  80. package/dist/ui/assets/ReleasesView-BprjtOwA.css +1 -0
  81. package/dist/ui/assets/ReleasesView-XScv3B-Z.js +3 -0
  82. package/dist/ui/assets/SettingsView-Z8oMEFep.js +4 -0
  83. package/dist/ui/assets/StoriesView-b2F5lCkA.js +1 -0
  84. package/dist/ui/assets/{VoiceDictate-DkW8pCow.js → VoiceDictate-BWfk7ny2.js} +2 -2
  85. package/dist/ui/assets/WorkView-C-bNqLWB.js +2 -0
  86. package/dist/ui/assets/{abnfDiagram-VCTEODGH-MSv6Drat.js → abnfDiagram-VCTEODGH-CqAauPub.js} +1 -1
  87. package/dist/ui/assets/architecture-7GRP2DOG-CJvLrOdr.js +1 -0
  88. package/dist/ui/assets/{architectureDiagram-5GKGNRK7-BZ_-qdv7.js → architectureDiagram-5GKGNRK7-Mn9BhY0r.js} +1 -1
  89. package/dist/ui/assets/{blockDiagram-I7D4REHJ-B5rJFlWv.js → blockDiagram-I7D4REHJ-Dr2d7jMu.js} +1 -1
  90. package/dist/ui/assets/{button-Y6WlSBKj.js → button-D_iuS-TU.js} +1 -1
  91. package/dist/ui/assets/{c4Diagram-7LVT6UL2-27feyEIL.js → c4Diagram-7LVT6UL2-Bc547NWd.js} +1 -1
  92. package/dist/ui/assets/channel-C9iszNFe.js +1 -0
  93. package/dist/ui/assets/{checkbox-D_jqCYo8.js → checkbox-C3INeJkY.js} +1 -1
  94. package/dist/ui/assets/{chunk-4HAMMTFA-eEnsDUUm.js → chunk-4HAMMTFA-LMvnFiev.js} +1 -1
  95. package/dist/ui/assets/{chunk-75Z2AOVW-CZJr44uW.js → chunk-75Z2AOVW-CkGddQ8f.js} +1 -1
  96. package/dist/ui/assets/{chunk-DU6HZSFF-CjmVMWOj.js → chunk-DU6HZSFF-CkgbhfOP.js} +1 -1
  97. package/dist/ui/assets/{chunk-F27PBJKO-4bUPHWpG.js → chunk-F27PBJKO-BOZLGrZh.js} +1 -1
  98. package/dist/ui/assets/{chunk-GMAD6QVW-C0bqKg0k.js → chunk-GMAD6QVW-8rzhes6G.js} +1 -1
  99. package/dist/ui/assets/{chunk-GVQU2GXP-CNduQ5rz.js → chunk-GVQU2GXP-Lmxg5hyM.js} +1 -1
  100. package/dist/ui/assets/{chunk-IMKFNOWR-Myux_1tI.js → chunk-IMKFNOWR-gTCpgc0s.js} +1 -1
  101. package/dist/ui/assets/{chunk-L3NEJ4N5-NuKwzg2U.js → chunk-L3NEJ4N5-UEiKAjtn.js} +1 -1
  102. package/dist/ui/assets/{chunk-OSK3NFVY-CEqMHLr2.js → chunk-OSK3NFVY-CikUp0z8.js} +1 -1
  103. package/dist/ui/assets/{chunk-P2QGCYS3-CR-84Lk5.js → chunk-P2QGCYS3-CiJdPSb7.js} +1 -1
  104. package/dist/ui/assets/{chunk-POPQ4Y6H-Br-XXvax.js → chunk-POPQ4Y6H-ChykmvUg.js} +1 -1
  105. package/dist/ui/assets/{chunk-PWAF6VOD-BG70h8rU.js → chunk-PWAF6VOD-BPze6f-e.js} +1 -1
  106. package/dist/ui/assets/{chunk-SHT3W25Y-BuA1JMD2.js → chunk-SHT3W25Y-BM8zH1eo.js} +1 -1
  107. package/dist/ui/assets/{chunk-SVP7TREG-pngt2L1r.js → chunk-SVP7TREG-B1P-I-70.js} +1 -1
  108. package/dist/ui/assets/{chunk-TICWLB2K-Cjy3Fajf.js → chunk-TICWLB2K-BLFSxuIL.js} +1 -1
  109. package/dist/ui/assets/classDiagram-ZZMXUADV-BZI0Zmv4.js +1 -0
  110. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BZI0Zmv4.js +1 -0
  111. package/dist/ui/assets/{cynefin-OW5HDTMX-BPprbkdm.js → cynefin-OW5HDTMX-CeZWlyrU.js} +1 -1
  112. package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-DDhXMGfJ.js → cynefinDiagram-5FMLGOSQ-DVEUYftt.js} +1 -1
  113. package/dist/ui/assets/{dagre-GXQ25YYZ-Dif9VtAE.js → dagre-GXQ25YYZ-BWaWi3Ih.js} +1 -1
  114. package/dist/ui/assets/{diagram-S7CK7UJ4-RboRw0Qp.js → diagram-S7CK7UJ4-D1Yg4vdl.js} +1 -1
  115. package/dist/ui/assets/{diagram-UQ7AKVKN-BALbR1VK.js → diagram-UQ7AKVKN-C-4RC5-M.js} +1 -1
  116. package/dist/ui/assets/{diagram-VSXAHHWV-DikETeUn.js → diagram-VSXAHHWV-9FoVzEJO.js} +1 -1
  117. package/dist/ui/assets/{diagram-VX7I27RA-BhzF4vum.js → diagram-VX7I27RA-D8tFlKbW.js} +1 -1
  118. package/dist/ui/assets/{diagram-Z3DM3KII-DfElw-8p.js → diagram-Z3DM3KII-CNPw5GH8.js} +1 -1
  119. package/dist/ui/assets/{ebnfDiagram-PWID7BFC-DWBcjdxW.js → ebnfDiagram-PWID7BFC-qr5wsHox.js} +1 -1
  120. package/dist/ui/assets/{erDiagram-RLTQ6QDP-9t0j2-5C.js → erDiagram-RLTQ6QDP-B5Ntd34K.js} +1 -1
  121. package/dist/ui/assets/eventmodeling-NTZA5JFV-Cri4g80G.js +1 -0
  122. package/dist/ui/assets/flowDiagram-HODETNUW-X8oZd-rh.js +1 -0
  123. package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-BV4wbDOf.js → ganttDiagram-EL5Y4UJY-ltWKqUKF.js} +1 -1
  124. package/dist/ui/assets/{gitGraph-4MIJSDKK-CHLeBDuC.js → gitGraph-4MIJSDKK-B_MMUzrw.js} +1 -1
  125. package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-D1coFQQr.js → gitGraphDiagram-WWUBYQGX-CntguBHS.js} +1 -1
  126. package/dist/ui/assets/index-Cbq9dyjP.js +54 -0
  127. package/dist/ui/assets/index-Z2Tb6lCZ.css +1 -0
  128. package/dist/ui/assets/{info-A6RAGUB7-BA6DRUi0.js → info-A6RAGUB7-Cu3MBfy4.js} +1 -1
  129. package/dist/ui/assets/{infoDiagram-27XIBGKW-DelTcISi.js → infoDiagram-27XIBGKW-DnXjJ1it.js} +1 -1
  130. package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-D6SYXrWV.js → ishikawaDiagram-5VMMS53U-DvzRpbIH.js} +1 -1
  131. package/dist/ui/assets/{journeyDiagram-3NMN7TZE-iul5gKFN.js → journeyDiagram-3NMN7TZE-DLQHYyFc.js} +1 -1
  132. package/dist/ui/assets/{kanban-definition-UXKFOSKX-DmsdCDnD.js → kanban-definition-UXKFOSKX-Cs9Tnl-z.js} +1 -1
  133. package/dist/ui/assets/{line-6qxsfuaD.js → line-DQLJC3Zu.js} +1 -1
  134. package/dist/ui/assets/{mermaid-parser.core-DwVo-Y2S.js → mermaid-parser.core-C8gt4DSf.js} +3 -3
  135. package/dist/ui/assets/{mermaid.core-DMfVb5bs.js → mermaid.core-KxIl4lpw.js} +4 -4
  136. package/dist/ui/assets/{mindmap-definition-YA3MSWOX-CD5cXQfB.js → mindmap-definition-YA3MSWOX-DCeBgBgT.js} +1 -1
  137. package/dist/ui/assets/{packet-AYTQ26CC-DVdCUknr.js → packet-AYTQ26CC-DNlpEXFf.js} +1 -1
  138. package/dist/ui/assets/{pegDiagram-XKGWAZYB-DGmIIA1Q.js → pegDiagram-XKGWAZYB-BO3Gb0g_.js} +1 -1
  139. package/dist/ui/assets/{pie-WAS4IAKB-UGTX1jKx.js → pie-WAS4IAKB-BudehkF3.js} +1 -1
  140. package/dist/ui/assets/{pieDiagram-E7YTZNPT-DLbLermT.js → pieDiagram-E7YTZNPT-DHmWd9hB.js} +1 -1
  141. package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-DrKEGcJP.js → quadrantDiagram-AXDQQJYC-OoFEDhWd.js} +1 -1
  142. package/dist/ui/assets/{radar-RG4KPBEZ-Bts8VyZT.js → radar-RG4KPBEZ-D21BtL1U.js} +1 -1
  143. package/dist/ui/assets/{railroad-74A4TZTK-DEWurv57.js → railroad-74A4TZTK-BkbQQ-xO.js} +1 -1
  144. package/dist/ui/assets/railroad-abnf-HS5TGJTU-Pqt7vgeQ.js +1 -0
  145. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-Dx9OloZg.js +1 -0
  146. package/dist/ui/assets/railroad-peg-WCYAUIDC-DrivZvC4.js +1 -0
  147. package/dist/ui/assets/{railroadDiagram-O6MQD6OU-Bc2KS9ny.js → railroadDiagram-O6MQD6OU-SOKojSix.js} +1 -1
  148. package/dist/ui/assets/repo-C5kAzRzY.js +2 -0
  149. package/dist/ui/assets/{requirementDiagram-BXWQKSXE-DJYx_Rdm.js → requirementDiagram-BXWQKSXE-BtLk0WUA.js} +1 -1
  150. package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-BcEvC1QT.js → sankeyDiagram-P5KCCOFB-C1xbu6hH.js} +1 -1
  151. package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-ERKmG0r-.js → sequenceDiagram-WJ2MYXX4-HedfOFTZ.js} +1 -1
  152. package/dist/ui/assets/{stateDiagram-D77RDMKH-ChqxZBTP.js → stateDiagram-D77RDMKH-CXypbZi0.js} +1 -1
  153. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-DailpsQY.js +1 -0
  154. package/dist/ui/assets/{swimlanes-42K2YHIH-DVgFWSH5.js → swimlanes-42K2YHIH-EGq8ZuPi.js} +1 -1
  155. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-DzgEGbun.js +8 -0
  156. package/dist/ui/assets/{timeline-definition-24CTP7MA-Bs376mCS.js → timeline-definition-24CTP7MA-Dv3_eOK9.js} +1 -1
  157. package/dist/ui/assets/{title-CIBxYjzz.js → title-ByLphNfa.js} +1 -1
  158. package/dist/ui/assets/{treeView-Q6P3EWNA-B8wx19JO.js → treeView-Q6P3EWNA-DUs-qm8c.js} +1 -1
  159. package/dist/ui/assets/{treemap-WGGIJYW6-By-sUWWT.js → treemap-WGGIJYW6-CgFlf2C6.js} +1 -1
  160. package/dist/ui/assets/vendor-ui-Bsdh9CC0.js +3 -0
  161. package/dist/ui/assets/{vennDiagram-4TSXK5OY-DT2AgAWU.js → vennDiagram-4TSXK5OY-9CXcq-af.js} +1 -1
  162. package/dist/ui/assets/{wardley-WFR3VGLG-CRXTnxDX.js → wardley-WFR3VGLG-4CvLgPjx.js} +1 -1
  163. package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-CuRK6dGX.js → wardleyDiagram-VM6X3IG4-DotFAxqv.js} +1 -1
  164. package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-BPgu4rsi.js → xychartDiagram-S5SC5T6Z-CfChnrRi.js} +1 -1
  165. package/dist/ui/index.html +8 -8
  166. package/dist/ui/sw.js +2 -2
  167. package/package.json +1 -1
  168. package/dist/ui/assets/AgentsView-FHNRZWLG.js +0 -1
  169. package/dist/ui/assets/ChecksView-DdI629ss.css +0 -1
  170. package/dist/ui/assets/ChecksView-DueoaSQy.js +0 -0
  171. package/dist/ui/assets/ContextView-CE6PlM7S.js +0 -4
  172. package/dist/ui/assets/InputsView-BDuzGLkC.css +0 -1
  173. package/dist/ui/assets/InputsView-C1MEfngw.js +0 -1
  174. package/dist/ui/assets/ReleasesView-B94OPqL-.css +0 -1
  175. package/dist/ui/assets/ReleasesView-CGzIvtxF.js +0 -3
  176. package/dist/ui/assets/SettingsView-DsoMIVuc.js +0 -4
  177. package/dist/ui/assets/StoriesView-BVQNDIfP.js +0 -1
  178. package/dist/ui/assets/WorkView-iBEEgB6t.js +0 -2
  179. package/dist/ui/assets/architecture-7GRP2DOG-DsFV151p.js +0 -1
  180. package/dist/ui/assets/channel-Cz6IiS37.js +0 -1
  181. package/dist/ui/assets/classDiagram-ZZMXUADV-BSdAza91.js +0 -1
  182. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BSdAza91.js +0 -1
  183. package/dist/ui/assets/eventmodeling-NTZA5JFV-CkM-IpK3.js +0 -1
  184. package/dist/ui/assets/flowDiagram-HODETNUW-Cjc88Y5V.js +0 -1
  185. package/dist/ui/assets/index-CCwW1Tv4.css +0 -1
  186. package/dist/ui/assets/index-CvnEg0cY.js +0 -55
  187. package/dist/ui/assets/railroad-abnf-HS5TGJTU-DKtrJ5pg.js +0 -1
  188. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-8eArMXZA.js +0 -1
  189. package/dist/ui/assets/railroad-peg-WCYAUIDC-SfWjNEkF.js +0 -1
  190. package/dist/ui/assets/repo-BWyge_2n.js +0 -1
  191. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-Crkx2xne.js +0 -1
  192. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BVnIaNys.js +0 -8
  193. package/dist/ui/assets/vendor-ui-BInmYDPf.js +0 -3
@@ -1,5 +1,5 @@
1
1
  {
2
- "hash": "0264ffb217828c3c1e53be5b58abbe310ff9b749bb8bd2c916d4d22e6dd164ac",
3
- "version": "0.5.62",
2
+ "hash": "78aa3a42b046cff40b116d148fb0658d06e650ee59dc63efa098b4a2ef4a44f4",
3
+ "version": "0.5.63",
4
4
  "devUi": false
5
5
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "generatedAt": "2026-10-02T12:12:44.891Z"
2
+ "generatedAt": "2026-10-04T01:58:52.804Z"
3
3
  }
@@ -432,8 +432,6 @@ const ENV_EXAMPLE = `# Copy to .env and fill in what you need — .env is gitign
432
432
  # REPOOS_OPENROUTER_API_KEY=...
433
433
  # REPOOS_OPENCODE_GO_API_KEY=...
434
434
  # REPOOS_DEEPINFRA_API_KEY=...
435
- # REPOOS_GITHUB_COPILOT_TOKEN=... # classic PAT — billing endpoints reject fine-grained tokens
436
- # REPOOS_GITHUB_COPILOT_SCOPE= # empty = personal plan; org:<name> / enterprise:<name> for centrally billed
437
435
  # REPOOS_WHISPER_KEY=... # or GROQ_API_KEY / OPENAI_API_KEY directly
438
436
 
439
437
  # --- Infrastructure ---
@@ -15,7 +15,7 @@ import { c, statusColor, priorityColor } from "../cli/colors.js";
15
15
  import { patchTaskFile } from "../server/write.js";
16
16
  import { writeHandoffRequest } from "../server/handoff-request.js";
17
17
  import { isAncestor } from "../core/git.js";
18
- import { parseShotPlan } from "../core/shot-plan.js";
18
+ import { declaredShotsSectionContent, parseShotPlan } from "../core/shot-plan.js";
19
19
  import { normalizeSectionHeading, replaceSection } from "../core/task.js";
20
20
  /**
21
21
  * RepoOS facade rooted at the LIVE BOARD's checkout (the main checkout), even
@@ -318,6 +318,7 @@ const UPDATE_FLAGS = {
318
318
  branch: "branch",
319
319
  "assigned-to": "assignedTo",
320
320
  "needs-input": "needsInput",
321
+ "needs-merge": "needsMerge",
321
322
  questions: "questions",
322
323
  section: "section",
323
324
  };
@@ -342,7 +343,7 @@ function shotsSectionContent(raw) {
342
343
  if (parsed.errors.length || parsed.shots.length === 0) {
343
344
  throw new Error(`Invalid --shots JSON: ${parsed.errors.join("; ") || "expected at least one shot"}`);
344
345
  }
345
- return `\`\`\`json\n${JSON.stringify(parsed.shots, null, 2)}\n\`\`\``;
346
+ return declaredShotsSectionContent(parsed.shots);
346
347
  }
347
348
  /**
348
349
  * `repoos update <id> [--title ...] [--area ...] [--story ...] [--priority ...]
@@ -361,7 +362,7 @@ function shotsSectionContent(raw) {
361
362
  */
362
363
  export function cmdUpdate(args) {
363
364
  const [id, ...rest] = args;
364
- const usage = ' Usage: repoos update <id> [--title "..."] [--area a,b] [--story "Delivery slice"] [--depends-on 0542,0538] [--priority p] [--type t] [--body "..."|-] [--branch b] [--assigned-to ai|human] [--needs-input true|false] [--questions "Question one\\nQuestion two"] [--clear-questions] [--shots "<JSON list>"|- | --section "<heading>" --section-body ...] [--force]';
365
+ const usage = ' Usage: repoos update <id> [--title "..."] [--area a,b] [--story "Delivery slice"] [--depends-on 0542,0538] [--priority p] [--type t] [--body "..."|-] [--branch b] [--assigned-to ai|human] [--needs-input true|false] [--needs-merge true|false] [--questions "Question one\\nQuestion two"] [--clear-questions] [--shots "<JSON list>"|- | --section "<heading>" --section-body ...] [--force]';
365
366
  if (!id) {
366
367
  console.error(c.red(usage));
367
368
  process.exitCode = 1;
@@ -435,13 +436,16 @@ export function cmdUpdate(args) {
435
436
  process.exitCode = 1;
436
437
  return;
437
438
  }
438
- if (field === "needsInput") {
439
+ if (field === "needsInput" || field === "needsMerge") {
439
440
  if (raw !== "true" && raw !== "false") {
440
- console.error(c.red(` --needs-input must be true or false`));
441
+ console.error(c.red(` --${field === "needsInput" ? "needs-input" : "needs-merge"} must be true or false`));
441
442
  process.exitCode = 1;
442
443
  return;
443
444
  }
444
- patch.needsInput = raw === "true";
445
+ if (field === "needsInput")
446
+ patch.needsInput = raw === "true";
447
+ else
448
+ patch.needsMerge = raw === "true";
445
449
  }
446
450
  else if (field === "questions") {
447
451
  patch.questions = parseQuestions(raw);
@@ -6,10 +6,20 @@ export interface PreviewServer {
6
6
  export interface SmokeConsoleMessage {
7
7
  type(): string;
8
8
  text(): string;
9
+ /** Where the console message originated — used to key resource errors to
10
+ * the URL that failed, so known-benign 404s can be tolerated per-URL. */
11
+ location(): {
12
+ url: string;
13
+ };
14
+ }
15
+ export interface SmokeResponse {
16
+ status(): number;
17
+ url(): string;
9
18
  }
10
19
  export interface SmokePage {
11
20
  on(event: "console", handler: (msg: SmokeConsoleMessage) => void): void;
12
21
  on(event: "pageerror", handler: (err: Error) => void): void;
22
+ on(event: "response", handler: (res: SmokeResponse) => void): void;
13
23
  route(url: string, handler: (route: {
14
24
  fetch(): Promise<{
15
25
  json(): Promise<unknown>;
@@ -41,6 +51,22 @@ export interface SmokePage {
41
51
  timeout?: number;
42
52
  }): Promise<unknown>;
43
53
  waitForTimeout(ms: number): Promise<void>;
54
+ /**
55
+ * Real pointer click: unlike `el.click()` in evaluate, the browser runs the
56
+ * focus-on-click default action, which is the whole point of the #0638
57
+ * smoke flow (a teleported input must hold focus after a real click).
58
+ */
59
+ click(selector: string, options?: {
60
+ timeout?: number;
61
+ }): Promise<void>;
62
+ /** Programmatic focus — still dispatches focusin/focusout normally. */
63
+ focus(selector: string, options?: {
64
+ timeout?: number;
65
+ }): Promise<void>;
66
+ keyboard: {
67
+ type(text: string): Promise<void>;
68
+ press(key: string): Promise<void>;
69
+ };
44
70
  route(url: string, handler: (route: {
45
71
  fetch(): Promise<{
46
72
  json(): Promise<unknown>;
@@ -27,6 +27,43 @@ function makeSmokeFixture() {
27
27
  const root = mkdtempSync(join(tmpdir(), "repoos-smoke-"));
28
28
  mkdirSync(join(root, "work"), { recursive: true });
29
29
  writeFileSync(join(root, "repoos.toml"), 'theme = "dark"\nuiTheme = "classic"\n\n');
30
+ // A review task with a stored report drives the send-to-engineer dialog
31
+ // flow below (#0638). No `branch` in the frontmatter — review recovery
32
+ // skips branchless tasks, so the fixture server never spawns a reviewer
33
+ // for it, and the seeded report is never treated as interrupted work.
34
+ const now = new Date().toISOString();
35
+ writeFileSync(join(root, "work", "9001-smoke-review-fixture.md"), [
36
+ "---",
37
+ 'id: "9001"',
38
+ "title: Smoke review fixture",
39
+ "type: feature",
40
+ "status: review",
41
+ "priority: p2",
42
+ "area: web",
43
+ `created_at: "${now}"`,
44
+ `updated_at: "${now}"`,
45
+ "---",
46
+ "",
47
+ "Fixture task for the send-to-engineer note dialog in the UI smoke test.",
48
+ "",
49
+ ].join("\n"));
50
+ mkdirSync(join(root, ".repoos", "reviews"), { recursive: true });
51
+ writeFileSync(join(root, ".repoos", "reviews", "9001.md"), [
52
+ "---",
53
+ 'task: "9001"',
54
+ `at: "${now}"`,
55
+ "agent: reviewer",
56
+ "cli: smoke",
57
+ "model: smoke",
58
+ "branch: feat/smoke-fixture",
59
+ "state: ok",
60
+ "---",
61
+ "",
62
+ "## Verdict",
63
+ "",
64
+ "good to go",
65
+ "",
66
+ ].join("\n"));
30
67
  return root;
31
68
  }
32
69
  /**
@@ -59,9 +96,31 @@ async function runUISmokeTest() {
59
96
  const page = await context.newPage();
60
97
  const consoleErrs = [];
61
98
  const pageErrors = [];
99
+ // Known-benign API 404s, tracked by URL: the drawer's best-effort usage
100
+ // fetch (`loadTaskUsage`) 404s for a task with no recorded sessions, and
101
+ // the browser logs every failed resource as a console error. The app
102
+ // handles the response (same precedent as the /api/ pageerror filter
103
+ // below), so one tolerated 404 per such URL keeps the gate honest without
104
+ // masking real failures.
105
+ const benign404Urls = new Set();
106
+ // Resource-failure console errors, keyed to the URL that failed so each
107
+ // can be matched against `benign404Urls` rather than tolerated by count.
108
+ const resourceErrs = [];
62
109
  page.on("console", (msg) => {
63
- if (msg.type() === "error")
64
- consoleErrs.push(msg.text());
110
+ if (msg.type() !== "error")
111
+ return;
112
+ const text = msg.text();
113
+ if (text.startsWith("Failed to load resource")) {
114
+ resourceErrs.push({ text, url: msg.location().url });
115
+ }
116
+ else {
117
+ consoleErrs.push(text);
118
+ }
119
+ });
120
+ page.on("response", (res) => {
121
+ if (res.status() === 404 && /\/api\/tasks\/[^/]+\/stats$/.test(res.url())) {
122
+ benign404Urls.add(res.url());
123
+ }
65
124
  });
66
125
  page.on("pageerror", (err) => {
67
126
  // WebKit reports failed optional API requests as page errors when the
@@ -192,6 +251,104 @@ async function runUISmokeTest() {
192
251
  });
193
252
  await page.waitForTimeout(500);
194
253
  await assertUtilitySpacing("settings");
254
+ // ── Teleported text fields take real clicks and focus (#0638) ───────
255
+ // The area picker's free-text input and the send-to-engineer note both
256
+ // live in body-teleported layers above a modal drawer. The drawer's Radix
257
+ // focus trap used to yank focus straight back (no caret, no typing), and
258
+ // an intermediate fix suppressed document-level focus events, which
259
+ // starved the global tooltip handler. The layers now pause the trap via
260
+ // their own radix FocusScope, so drive both flows with REAL pointer
261
+ // clicks (page.click, not el.click() — only real pointer events trigger
262
+ // the browser's focus-on-click default action): focus must stick, typing
263
+ // must land, and a focused control inside the layer must still raise the
264
+ // global tooltip.
265
+ const goWork = async () => {
266
+ await page.evaluate(() => {
267
+ const navItems = document.querySelectorAll(".nav-item");
268
+ for (const item of Array.from(navItems)) {
269
+ if (item.textContent?.includes("Work"))
270
+ item.click();
271
+ }
272
+ });
273
+ await page.waitForTimeout(500);
274
+ };
275
+ const assertActive = async (sel, what) => {
276
+ const ok = await page.evaluate((s) => document.activeElement === document.querySelector(s), sel);
277
+ if (!ok)
278
+ throw new Error(`${what} did not take focus on a real click (#0638)`);
279
+ };
280
+ // (a) Area picker over the New-task drawer's modal dialog.
281
+ await goWork();
282
+ await page.click(".new-btn");
283
+ await page.waitForTimeout(400);
284
+ await page.click(".drawer-tabs .tab-btn:nth-child(2)"); // Manual mode form
285
+ await page.waitForTimeout(200);
286
+ await page.click("#nt-area"); // opens the teleported picker panel
287
+ await page.waitForTimeout(300);
288
+ const areaInput = 'input[aria-label="Add a custom area"]';
289
+ await page.click(areaInput);
290
+ await assertActive(areaInput, "area picker free-text input");
291
+ await page.keyboard.type("smoke-area");
292
+ const typedArea = await page.evaluate((s) => document.querySelector(s)?.value ?? "", areaInput);
293
+ if (typedArea !== "smoke-area") {
294
+ throw new Error(`area picker input did not accept typing: "${typedArea}"`);
295
+ }
296
+ await page.keyboard.press("Enter");
297
+ await page.waitForTimeout(200);
298
+ // Enter committed the custom area, so the picker now offers to register
299
+ // it — a title-carrying control INSIDE the layer. Focusing it must raise
300
+ // the global tooltip (round-2 regression: suppressed focus events starved
301
+ // exactly this handler).
302
+ const offerBtn = 'div[data-overlay-layer="floating"] button[title]';
303
+ const hasOffer = await page.evaluate((s) => document.querySelector(s) !== null, offerBtn);
304
+ if (!hasOffer) {
305
+ throw new Error("custom area was not committed to the picker selection");
306
+ }
307
+ await page.focus(offerBtn);
308
+ await page.waitForTimeout(120);
309
+ const tooltipShown = await page.evaluate(() => {
310
+ const tip = document.querySelector(".app-tooltip");
311
+ return (tip instanceof HTMLElement &&
312
+ tip.classList.contains("show") &&
313
+ (tip.textContent ?? "").includes("repoos.toml"));
314
+ });
315
+ if (!tooltipShown) {
316
+ throw new Error("global tooltip did not appear for a focused control inside the area picker");
317
+ }
318
+ await page.keyboard.press("Escape"); // close the picker panel
319
+ await page.keyboard.press("Escape"); // close the new-task drawer
320
+ await page.waitForTimeout(300);
321
+ // (b) Send-to-engineer note over a review task's drawer. The fixture
322
+ // seeds task 9001 (status review) with a stored report so the button is
323
+ // enabled; the flow stops before confirming, so nothing is spawned.
324
+ await goWork();
325
+ await page.click('[data-task-id="9001"]');
326
+ await page.waitForTimeout(600); // drawer opens on the Review tab
327
+ await page.waitForFunction(() => {
328
+ const btn = Array.from(document.querySelectorAll("button")).find((b) => (b.textContent ?? "").includes("Send engineer"));
329
+ return Boolean(btn && !btn.hasAttribute("disabled"));
330
+ }, { timeout: 5_000 });
331
+ await page.evaluate(() => {
332
+ const btn = Array.from(document.querySelectorAll("button")).find((b) => (b.textContent ?? "").includes("Send engineer"));
333
+ btn.click();
334
+ });
335
+ await page.waitForTimeout(300);
336
+ const noteArea = "textarea.ste-note";
337
+ await page.click(noteArea);
338
+ await assertActive(noteArea, "send-to-engineer note textarea");
339
+ await page.keyboard.type("smoke note text");
340
+ const noteValue = await page.evaluate(() => document.querySelector("textarea.ste-note")?.value ?? "");
341
+ if (noteValue !== "smoke note text") {
342
+ throw new Error(`send-to-engineer note did not accept typing: "${noteValue}"`);
343
+ }
344
+ const withNote = await page.evaluate(() => (document.querySelector(".ste-actions button:nth-child(2)")?.textContent ?? "").includes("with note"));
345
+ if (!withNote) {
346
+ throw new Error("confirm button did not reflect the typed note");
347
+ }
348
+ await page.keyboard.press("Escape"); // close the note dialog (never confirmed)
349
+ await page.waitForTimeout(200);
350
+ await page.keyboard.press("Escape"); // close the drawer
351
+ await page.waitForTimeout(300);
195
352
  // ── UI recovery banner regression (#0420) ───────────────────────────
196
353
  // Intercept /api/health to return a build hash that differs from the
197
354
  // client's, which triggers checkUiBuild → showStaleUi on the next
@@ -229,11 +386,20 @@ async function runUISmokeTest() {
229
386
  }
230
387
  }
231
388
  // Check for zero console errors
232
- if (consoleErrs.length > 0) {
233
- let msg = "Console errors (" + consoleErrs.length + "): " + consoleErrs.join("; ");
234
- if (pageErrors.length > 0)
235
- msg += " | Page errors: " + pageErrors.join("; ");
236
- throw new Error(msg);
389
+ {
390
+ // Tolerate failed-resource errors only for the known-benign stats 404
391
+ // URLs (the drawer's best-effort usage fetch on a task with no
392
+ // sessions); every other failed resource is a real failure.
393
+ const unexpected = resourceErrs
394
+ .filter((e) => !benign404Urls.has(e.url))
395
+ .map((e) => e.text + (e.url ? " (" + e.url + ")" : ""));
396
+ const fatal = [...consoleErrs, ...unexpected];
397
+ if (fatal.length > 0) {
398
+ let msg = "Console errors (" + fatal.length + "): " + fatal.join("; ");
399
+ if (pageErrors.length > 0)
400
+ msg += " | Page errors: " + pageErrors.join("; ");
401
+ throw new Error(msg);
402
+ }
237
403
  }
238
404
  if (pageErrors.length > 0) {
239
405
  throw new Error("Page errors (" + pageErrors.length + "): " + pageErrors.join("; "));
@@ -32,6 +32,13 @@ export interface CloseOutFailure {
32
32
  detail?: string;
33
33
  /** Guidance paragraph; replaces the default conflict hint when set. */
34
34
  hint?: string;
35
+ /**
36
+ * Fixed one-sentence tl;dr (cause + next action) for failure kinds the
37
+ * Debugger doesn't diagnose — the cause is already known, so no model run is
38
+ * needed. Unset for `conflict`/`validating`, whose tl;dr comes from the
39
+ * Debugger. The card shows the Debugger's sentence in preference when present.
40
+ */
41
+ summary?: string;
35
42
  }
36
43
  /**
37
44
  * Turn a failed close-out's recorded `phase` + `reason` into what the error
@@ -122,6 +122,7 @@ export function describeCloseOutFailure(phase, reason) {
122
122
  "the branch and worktree are untouched. Retry Move to done when the machine is less " +
123
123
  "loaded, or raise the budget in Settings → Close-out timeout (`closeOut.timeoutMs` in " +
124
124
  "repoos.toml).",
125
+ summary: "Move to done timed out before anything merged and your branch is untouched — retry when the machine is quieter, or raise the Close-out timeout in Settings.",
125
126
  };
126
127
  }
127
128
  case "validating": {
@@ -155,6 +156,9 @@ export function describeCloseOutFailure(phase, reason) {
155
156
  hint: inWorktree
156
157
  ? "The task's worktree has uncommitted changes the merge would not carry, and close-out deletes the worktree with them. Commit them on the task branch, then retry."
157
158
  : "Main's working tree has uncommitted changes that the merge would overwrite. Commit or stash them on main, then retry.",
159
+ summary: inWorktree
160
+ ? "The task worktree has uncommitted changes — commit them on the task branch, then retry Move to done."
161
+ : "Main has uncommitted changes the merge would overwrite — commit or stash them on main, then retry Move to done.",
158
162
  };
159
163
  }
160
164
  case "syncing":
@@ -166,6 +170,7 @@ export function describeCloseOutFailure(phase, reason) {
166
170
  step: "sync",
167
171
  detail: clean || undefined,
168
172
  hint: "The branch could not be brought up to date with main. Fix the issue in the feature branch's worktree, then retry.",
173
+ summary: "RepoOS couldn't bring this branch up to date with main — fix the issue in the task worktree, then retry Move to done.",
169
174
  };
170
175
  case "publishing":
171
176
  return {
@@ -174,6 +179,7 @@ export function describeCloseOutFailure(phase, reason) {
174
179
  step: "publish",
175
180
  detail: clean || undefined,
176
181
  hint: "The merge to main failed at publish time. Retry the close-out.",
182
+ summary: "The merge to main failed at the final publish step — retry Move to done.",
177
183
  };
178
184
  default:
179
185
  return {
@@ -182,6 +188,7 @@ export function describeCloseOutFailure(phase, reason) {
182
188
  step: "merge",
183
189
  detail: clean || undefined,
184
190
  hint: "Retry the close-out. If it keeps failing, check the feature branch's worktree for issues.",
191
+ summary: "Move to done failed unexpectedly — retry it, and if it keeps failing, check the task worktree for issues.",
185
192
  };
186
193
  }
187
194
  }
@@ -186,6 +186,22 @@ export interface PreviewOverlay {
186
186
  * `auth.enabled`, leaving every other `[auth]` key at its base value.
187
187
  */
188
188
  export declare function parsePreviewOverlays(parsed: Record<string, unknown>): PreviewOverlay;
189
+ /**
190
+ * Validate a repo-relative directory setting (`storiesDir` today; the shape any
191
+ * future sibling must satisfy). Returns the cleaned value, or `null` when
192
+ * unusable — the caller falls back to the default with a warning instead of
193
+ * pointing RepoOS outside the repo:
194
+ *
195
+ * - empty, absolute (`/x`, `C:\x`), home-relative (`~/x`), or a backslash
196
+ * path (a literal filename on POSIX, never a separator)
197
+ * - any `..` segment (escaping the repo root)
198
+ * - empty or `.` segments beyond the redundant `./` prefix and trailing `/`,
199
+ * which are stripped so `./stories/` parses as `stories`
200
+ *
201
+ * Warning, not a load failure: a typo in `repoos.toml` must never stop the
202
+ * server from booting; it just gets the default directory back (#0637).
203
+ */
204
+ export declare function normalizeRelativeDir(raw: unknown, key: string): string | null;
189
205
  /**
190
206
  * The preview-only override keys a repo's `repoos.toml` declares that the
191
207
  * runtime will actually apply (supported base keys only), sorted. Fail-soft: a
@@ -115,6 +115,7 @@ export const DEFAULT_CONFIG = {
115
115
  docsDir: "docs",
116
116
  skillsDir: "skills",
117
117
  inputsDir: "inputs",
118
+ storiesDir: "stories",
118
119
  taskExtensions: [".md"],
119
120
  defaultStatus: "inbox",
120
121
  defaultAssignee: "unassigned",
@@ -161,6 +162,7 @@ export const DEFAULT_CONFIG = {
161
162
  idleShutdownMinutes: 8,
162
163
  maxServerLifetimeMinutes: 120,
163
164
  fallbackToLocal: false,
165
+ retryOtherHosts: false,
164
166
  maxConcurrent: 1,
165
167
  },
166
168
  // Close-out (Move to done) pipeline budget (#0573): a 6-minute wall clock
@@ -787,6 +789,48 @@ export function parsePreviewOverlays(parsed) {
787
789
  function isOverridableConfigPath(path) {
788
790
  return SUPPORTED_TOML_KEYS.includes(path);
789
791
  }
792
+ /**
793
+ * Validate a repo-relative directory setting (`storiesDir` today; the shape any
794
+ * future sibling must satisfy). Returns the cleaned value, or `null` when
795
+ * unusable — the caller falls back to the default with a warning instead of
796
+ * pointing RepoOS outside the repo:
797
+ *
798
+ * - empty, absolute (`/x`, `C:\x`), home-relative (`~/x`), or a backslash
799
+ * path (a literal filename on POSIX, never a separator)
800
+ * - any `..` segment (escaping the repo root)
801
+ * - empty or `.` segments beyond the redundant `./` prefix and trailing `/`,
802
+ * which are stripped so `./stories/` parses as `stories`
803
+ *
804
+ * Warning, not a load failure: a typo in `repoos.toml` must never stop the
805
+ * server from booting; it just gets the default directory back (#0637).
806
+ */
807
+ export function normalizeRelativeDir(raw, key) {
808
+ const invalid = (why) => {
809
+ console.warn(`[config] ${key} must be a repo-relative directory (${why}) — ignoring "${raw}"`);
810
+ return null;
811
+ };
812
+ if (typeof raw !== "string")
813
+ return invalid("not a string");
814
+ let value = raw.trim();
815
+ if (!value)
816
+ return invalid("empty");
817
+ if (/^[/\\]/.test(value) || /^[A-Za-z]:/.test(value) || value.startsWith("~")) {
818
+ return invalid("must not be absolute or home-relative");
819
+ }
820
+ value = value.replace(/\/+$/, "");
821
+ while (value.startsWith("./"))
822
+ value = value.slice(2);
823
+ if (!value)
824
+ return invalid("empty");
825
+ for (const segment of value.split("/")) {
826
+ if (segment === "..")
827
+ return invalid('must not contain ".."');
828
+ if (!segment || segment === "." || segment.includes("\\")) {
829
+ return invalid("must not contain empty or dot segments");
830
+ }
831
+ }
832
+ return value;
833
+ }
790
834
  /**
791
835
  * The preview-only override keys a repo's `repoos.toml` declares that the
792
836
  * runtime will actually apply (supported base keys only), sorted. Fail-soft: a
@@ -846,6 +890,15 @@ export function loadConfig(rootArg, options = {}) {
846
890
  cfg.skillsDir = get("skillsDir");
847
891
  if (typeof get("inputsDir") === "string")
848
892
  cfg.inputsDir = get("inputsDir");
893
+ // storiesDir (#0637): validated, not trusted — an absolute or escaping
894
+ // value falls back to the default ("stories") with a warning, so a bad
895
+ // entry can never point the story registry or the close-out drift check
896
+ // outside the repo. Present-but-wrong-typed values warn the same way.
897
+ const storiesDirRaw = get("storiesDir");
898
+ if (storiesDirRaw !== undefined) {
899
+ cfg.storiesDir =
900
+ normalizeRelativeDir(storiesDirRaw, "storiesDir") ?? DEFAULT_CONFIG.storiesDir;
901
+ }
849
902
  if (Array.isArray(get("taskExtensions")))
850
903
  cfg.taskExtensions = get("taskExtensions");
851
904
  if (typeof get("defaultStatus") === "string")
@@ -1315,6 +1368,25 @@ export function loadConfig(rootArg, options = {}) {
1315
1368
  if (typeof rvReleases === "boolean") {
1316
1369
  cfg.remoteValidation = { ...cfg.remoteValidation, useForReleases: rvReleases };
1317
1370
  }
1371
+ const rvRetry = parsed["remoteValidation.retryOtherHosts"];
1372
+ if (typeof rvRetry === "boolean") {
1373
+ cfg.remoteValidation = { ...cfg.remoteValidation, retryOtherHosts: rvRetry };
1374
+ }
1375
+ // Default: true when 2+ tailscale hosts are configured, else false. The
1376
+ // guard tests the TOML key, NOT the merged object: DEFAULT_CONFIG already
1377
+ // fills `retryOtherHosts: false`, so a merged-object `=== undefined` check
1378
+ // could never fire and the dynamic default was dead code (#0632 review
1379
+ // round 1). An explicit `false` in repoos.toml must stay false; an absent
1380
+ // key reads as "unset" and gets the conditional default. The host list is
1381
+ // already folded (shorthand `tailscaleHost` + list + [[rows]]) above, so
1382
+ // its length is the resolved pool size.
1383
+ const hostCount = cfg.remoteValidation?.tailscaleHosts?.length ?? 0;
1384
+ if (rvRetry === undefined &&
1385
+ cfg.remoteValidation?.enabled === true &&
1386
+ cfg.remoteValidation?.provider === "tailscale" &&
1387
+ hostCount >= 2) {
1388
+ cfg.remoteValidation = { ...cfg.remoteValidation, retryOtherHosts: true };
1389
+ }
1318
1390
  // [closeOut] section — wall-clock budget for one Move-to-done attempt
1319
1391
  // (#0573). Invalid/negative values are clamped back to the default with a
1320
1392
  // clear warning rather than failing config load: a typo in repoos.toml must
@@ -1348,13 +1420,9 @@ export function loadConfig(rootArg, options = {}) {
1348
1420
  const deepinfraApiKey = process.env.REPOOS_DEEPINFRA_API_KEY;
1349
1421
  if (deepinfraApiKey)
1350
1422
  modelProviders.deepinfraApiKey = deepinfraApiKey;
1351
- const githubCopilotToken = process.env.REPOOS_GITHUB_COPILOT_TOKEN;
1352
- if (githubCopilotToken)
1353
- modelProviders.githubCopilotToken = githubCopilotToken;
1354
1423
  if (modelProviders.openrouterApiKey ||
1355
1424
  modelProviders.opencodeGoApiKey ||
1356
- modelProviders.deepinfraApiKey ||
1357
- modelProviders.githubCopilotToken) {
1425
+ modelProviders.deepinfraApiKey) {
1358
1426
  cfg.modelProviders = modelProviders;
1359
1427
  }
1360
1428
  cfg.builtInAgents = loadBuiltInAgentsConfig(root, cfg.cacheDir);
@@ -1502,6 +1570,15 @@ export function getConfigSchema() {
1502
1570
  default: DEFAULT_CONFIG.skillsDir,
1503
1571
  description: "Directory holding skills (relative to repo root)",
1504
1572
  },
1573
+ {
1574
+ key: "storiesDir",
1575
+ label: "Stories directory",
1576
+ type: "string",
1577
+ tier: "guarded",
1578
+ restartRequired: true,
1579
+ default: DEFAULT_CONFIG.storiesDir,
1580
+ description: "Directory holding story definitions (relative to repo root)",
1581
+ },
1505
1582
  {
1506
1583
  key: "taskExtensions",
1507
1584
  label: "Task extensions",
@@ -1744,6 +1821,15 @@ export function getConfigSchema() {
1744
1821
  default: false,
1745
1822
  description: "Cut releases on the same Hetzner runner as close-outs (off by default — a release is watched live, so the provision delay reads as a regression; opt in per repo). Only applies when the runner is enabled.",
1746
1823
  },
1824
+ {
1825
+ key: "remoteValidation.retryOtherHosts",
1826
+ label: "Remote validation: retry on other hosts",
1827
+ type: "boolean",
1828
+ tier: "restart",
1829
+ restartRequired: true,
1830
+ default: false,
1831
+ description: "When a transient failure occurs on one tailscale host, retry the run on another healthy, free host before falling back to local. Default true when 2+ hosts are configured.",
1832
+ },
1747
1833
  {
1748
1834
  key: "dev.inspector.enabled",
1749
1835
  label: "Copy inspector",
@@ -1792,6 +1878,7 @@ export const SUPPORTED_TOML_KEYS = [
1792
1878
  "docsDir",
1793
1879
  "skillsDir",
1794
1880
  "inputsDir",
1881
+ "storiesDir",
1795
1882
  "cacheDir",
1796
1883
  "taskExtensions",
1797
1884
  // Board behavior
@@ -1933,6 +2020,7 @@ export const SUPPORTED_TOML_KEYS = [
1933
2020
  "remoteValidation.maxServerLifetimeMinutes",
1934
2021
  "remoteValidation.maxConcurrent",
1935
2022
  "remoteValidation.fallbackToLocal",
2023
+ "remoteValidation.retryOtherHosts",
1936
2024
  "remoteValidation.useForReleases",
1937
2025
  ];
1938
2026
  function serializeTomlVal(val) {
@@ -15,6 +15,19 @@ export declare function resolveCopyInspectorTarget(root: string, file: string, l
15
15
  repoRel: string;
16
16
  line: number | null;
17
17
  } | null;
18
+ /**
19
+ * Resolve a repo-relative file without the copy-inspector's `src/` restriction,
20
+ * so callers can open other tracked files (a task markdown under `work/`) in
21
+ * the configured editor. Same escape guard as {@link resolveCopyInspectorTarget},
22
+ * but hardened against symlinks: both the repo root and the target are
23
+ * `realpath`-resolved, and the REAL target must stay inside the REAL root, so a
24
+ * symlink pointing outside the repo cannot smuggle an external file through.
25
+ * The returned `repoRel` is the real location (what the editor should open).
26
+ */
27
+ export declare function resolveRepoFileTarget(root: string, file: string): {
28
+ absPath: string;
29
+ repoRel: string;
30
+ } | null;
18
31
  /** Split a configured editor command into argv (no shell). Supports " and ' quotes. */
19
32
  export declare function tokenizeEditorCommand(command: string): string[];
20
33
  /**
@@ -2,7 +2,7 @@
2
2
  * Dev-only click-to-locate UI copy (#0509): path guards and editor-command parsing.
3
3
  * Zero runtime dependencies — spawn uses argv arrays built here.
4
4
  */
5
- import { existsSync, readFileSync, statSync } from "node:fs";
5
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
6
6
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  /** True when this repo checkout ships editable RepoOS UI sources. */
8
8
  export function hasRepoOsUiSource(root) {
@@ -52,6 +52,41 @@ export function resolveCopyInspectorTarget(root, file, line) {
52
52
  const lineNum = typeof line === "number" && Number.isFinite(line) && line >= 1 ? Math.floor(line) : null;
53
53
  return { absPath: abs, repoRel: back.replace(/\\/g, "/"), line: lineNum };
54
54
  }
55
+ /**
56
+ * Resolve a repo-relative file without the copy-inspector's `src/` restriction,
57
+ * so callers can open other tracked files (a task markdown under `work/`) in
58
+ * the configured editor. Same escape guard as {@link resolveCopyInspectorTarget},
59
+ * but hardened against symlinks: both the repo root and the target are
60
+ * `realpath`-resolved, and the REAL target must stay inside the REAL root, so a
61
+ * symlink pointing outside the repo cannot smuggle an external file through.
62
+ * The returned `repoRel` is the real location (what the editor should open).
63
+ */
64
+ export function resolveRepoFileTarget(root, file) {
65
+ const rel = file.trim().replace(/\\/g, "/");
66
+ if (!rel || rel.includes("..") || rel.startsWith("/"))
67
+ return null;
68
+ const abs = resolve(root, rel);
69
+ let realRoot;
70
+ let realAbs;
71
+ try {
72
+ realRoot = realpathSync(root);
73
+ realAbs = realpathSync(abs);
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ const back = relative(realRoot, realAbs);
79
+ if (!back || back.startsWith("..") || isAbsolute(back))
80
+ return null;
81
+ try {
82
+ if (!statSync(realAbs).isFile())
83
+ return null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ return { absPath: realAbs, repoRel: back.replace(/\\/g, "/") };
89
+ }
55
90
  /** Split a configured editor command into argv (no shell). Supports " and ' quotes. */
56
91
  export function tokenizeEditorCommand(command) {
57
92
  const out = [];