@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.
- package/dist/.build-info.json +2 -2
- package/dist/.build-stamp.json +1 -1
- package/dist/commands/init.js +0 -2
- package/dist/commands/tasks.js +10 -6
- package/dist/commands/ui-harness.d.ts +26 -0
- package/dist/commands/ui-smoke.js +173 -7
- package/dist/core/close-out-failure.d.ts +7 -0
- package/dist/core/close-out-failure.js +7 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +93 -5
- package/dist/core/copy-inspector.d.ts +13 -0
- package/dist/core/copy-inspector.js +36 -1
- package/dist/core/doctor.js +1 -2
- package/dist/core/input.d.ts +11 -0
- package/dist/core/input.js +22 -2
- package/dist/core/providers/spend.d.ts +1 -64
- package/dist/core/providers/spend.js +7 -192
- package/dist/core/remote-hosts.d.ts +2 -1
- package/dist/core/remote-hosts.js +10 -0
- package/dist/core/shot-page.js +12 -2
- package/dist/core/shot-plan.d.ts +74 -0
- package/dist/core/shot-plan.js +99 -6
- package/dist/core/story-definition-files.d.ts +9 -1
- package/dist/core/story-definition-files.js +19 -7
- package/dist/core/support-bundle.js +0 -1
- package/dist/core/task.d.ts +7 -0
- package/dist/core/task.js +20 -0
- package/dist/core/types.d.ts +10 -3
- package/dist/server/agents.d.ts +52 -2
- package/dist/server/agents.js +142 -15
- package/dist/server/close-out-outcome.d.ts +20 -0
- package/dist/server/close-out-outcome.js +111 -0
- package/dist/server/handoff.js +3 -0
- package/dist/server/integration-orchestrator.d.ts +30 -5
- package/dist/server/integration-orchestrator.js +114 -47
- package/dist/server/live-index.d.ts +23 -0
- package/dist/server/preview.d.ts +33 -0
- package/dist/server/preview.js +83 -5
- package/dist/server/remote-validation.d.ts +27 -2
- package/dist/server/remote-validation.js +249 -53
- package/dist/server/review.d.ts +8 -0
- package/dist/server/review.js +16 -3
- package/dist/server/routes/config.js +8 -3
- package/dist/server/routes/copy-inspector.d.ts +7 -0
- package/dist/server/routes/copy-inspector.js +65 -17
- package/dist/server/routes/inputs.d.ts +23 -0
- package/dist/server/routes/inputs.js +147 -29
- package/dist/server/routes/model-providers.d.ts +0 -6
- package/dist/server/routes/model-providers.js +2 -40
- package/dist/server/routes/release.d.ts +17 -0
- package/dist/server/routes/release.js +35 -2
- package/dist/server/routes/stories.d.ts +9 -0
- package/dist/server/routes/stories.js +42 -1
- package/dist/server/routes/tasks.d.ts +15 -0
- package/dist/server/routes/tasks.js +263 -0
- package/dist/server/routes/types.d.ts +8 -0
- package/dist/server/server.js +40 -7
- package/dist/server/shot-capture.d.ts +58 -1
- package/dist/server/shot-capture.js +206 -31
- package/dist/server/shots.d.ts +16 -0
- package/dist/server/shots.js +12 -0
- package/dist/server/task-check.d.ts +1 -0
- package/dist/server/task-check.js +1 -1
- package/dist/server/watcher.js +3 -3
- package/dist/server/write.d.ts +13 -0
- package/dist/server/write.js +46 -0
- package/dist/ui/assets/{AgentModelModal-C-X9UXUK.js → AgentModelModal-BsjlDrGV.js} +1 -1
- package/dist/ui/assets/{AgentsView-DPFAB5vF.css → AgentsView-B-VbBCBQ.css} +1 -1
- package/dist/ui/assets/AgentsView-Dw5drin8.js +1 -0
- package/dist/ui/assets/ChecksView-BbMgmWzY.js +0 -0
- package/dist/ui/assets/ChecksView-DvW47w95.css +1 -0
- package/dist/ui/assets/ContextView-gAorV_po.js +4 -0
- package/dist/ui/assets/{DashboardView-D6GeYs6o.js → DashboardView-CxDsQ4b3.js} +1 -1
- package/dist/ui/assets/DeleteConfirmDialog-D_MMkeQr.js +1 -0
- package/dist/ui/assets/{DeploymentsView-BJZ6z_Gd.js → DeploymentsView-DjuSlTT3.js} +1 -1
- package/dist/ui/assets/{DiffView-D8pgjtSQ.js → DiffView-CMbwhk0u.js} +2 -2
- package/dist/ui/assets/InputsView-CMTBgj3R.css +1 -0
- package/dist/ui/assets/InputsView-DL0d3oUS.js +1 -0
- package/dist/ui/assets/{LoginView-DYvBzW5u.js → LoginView-B_dRypOC.js} +1 -1
- package/dist/ui/assets/ReleasesView-BprjtOwA.css +1 -0
- package/dist/ui/assets/ReleasesView-XScv3B-Z.js +3 -0
- package/dist/ui/assets/SettingsView-Z8oMEFep.js +4 -0
- package/dist/ui/assets/StoriesView-b2F5lCkA.js +1 -0
- package/dist/ui/assets/{VoiceDictate-DkW8pCow.js → VoiceDictate-BWfk7ny2.js} +2 -2
- package/dist/ui/assets/WorkView-C-bNqLWB.js +2 -0
- package/dist/ui/assets/{abnfDiagram-VCTEODGH-MSv6Drat.js → abnfDiagram-VCTEODGH-CqAauPub.js} +1 -1
- package/dist/ui/assets/architecture-7GRP2DOG-CJvLrOdr.js +1 -0
- package/dist/ui/assets/{architectureDiagram-5GKGNRK7-BZ_-qdv7.js → architectureDiagram-5GKGNRK7-Mn9BhY0r.js} +1 -1
- package/dist/ui/assets/{blockDiagram-I7D4REHJ-B5rJFlWv.js → blockDiagram-I7D4REHJ-Dr2d7jMu.js} +1 -1
- package/dist/ui/assets/{button-Y6WlSBKj.js → button-D_iuS-TU.js} +1 -1
- package/dist/ui/assets/{c4Diagram-7LVT6UL2-27feyEIL.js → c4Diagram-7LVT6UL2-Bc547NWd.js} +1 -1
- package/dist/ui/assets/channel-C9iszNFe.js +1 -0
- package/dist/ui/assets/{checkbox-D_jqCYo8.js → checkbox-C3INeJkY.js} +1 -1
- package/dist/ui/assets/{chunk-4HAMMTFA-eEnsDUUm.js → chunk-4HAMMTFA-LMvnFiev.js} +1 -1
- package/dist/ui/assets/{chunk-75Z2AOVW-CZJr44uW.js → chunk-75Z2AOVW-CkGddQ8f.js} +1 -1
- package/dist/ui/assets/{chunk-DU6HZSFF-CjmVMWOj.js → chunk-DU6HZSFF-CkgbhfOP.js} +1 -1
- package/dist/ui/assets/{chunk-F27PBJKO-4bUPHWpG.js → chunk-F27PBJKO-BOZLGrZh.js} +1 -1
- package/dist/ui/assets/{chunk-GMAD6QVW-C0bqKg0k.js → chunk-GMAD6QVW-8rzhes6G.js} +1 -1
- package/dist/ui/assets/{chunk-GVQU2GXP-CNduQ5rz.js → chunk-GVQU2GXP-Lmxg5hyM.js} +1 -1
- package/dist/ui/assets/{chunk-IMKFNOWR-Myux_1tI.js → chunk-IMKFNOWR-gTCpgc0s.js} +1 -1
- package/dist/ui/assets/{chunk-L3NEJ4N5-NuKwzg2U.js → chunk-L3NEJ4N5-UEiKAjtn.js} +1 -1
- package/dist/ui/assets/{chunk-OSK3NFVY-CEqMHLr2.js → chunk-OSK3NFVY-CikUp0z8.js} +1 -1
- package/dist/ui/assets/{chunk-P2QGCYS3-CR-84Lk5.js → chunk-P2QGCYS3-CiJdPSb7.js} +1 -1
- package/dist/ui/assets/{chunk-POPQ4Y6H-Br-XXvax.js → chunk-POPQ4Y6H-ChykmvUg.js} +1 -1
- package/dist/ui/assets/{chunk-PWAF6VOD-BG70h8rU.js → chunk-PWAF6VOD-BPze6f-e.js} +1 -1
- package/dist/ui/assets/{chunk-SHT3W25Y-BuA1JMD2.js → chunk-SHT3W25Y-BM8zH1eo.js} +1 -1
- package/dist/ui/assets/{chunk-SVP7TREG-pngt2L1r.js → chunk-SVP7TREG-B1P-I-70.js} +1 -1
- package/dist/ui/assets/{chunk-TICWLB2K-Cjy3Fajf.js → chunk-TICWLB2K-BLFSxuIL.js} +1 -1
- package/dist/ui/assets/classDiagram-ZZMXUADV-BZI0Zmv4.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BZI0Zmv4.js +1 -0
- package/dist/ui/assets/{cynefin-OW5HDTMX-BPprbkdm.js → cynefin-OW5HDTMX-CeZWlyrU.js} +1 -1
- package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-DDhXMGfJ.js → cynefinDiagram-5FMLGOSQ-DVEUYftt.js} +1 -1
- package/dist/ui/assets/{dagre-GXQ25YYZ-Dif9VtAE.js → dagre-GXQ25YYZ-BWaWi3Ih.js} +1 -1
- package/dist/ui/assets/{diagram-S7CK7UJ4-RboRw0Qp.js → diagram-S7CK7UJ4-D1Yg4vdl.js} +1 -1
- package/dist/ui/assets/{diagram-UQ7AKVKN-BALbR1VK.js → diagram-UQ7AKVKN-C-4RC5-M.js} +1 -1
- package/dist/ui/assets/{diagram-VSXAHHWV-DikETeUn.js → diagram-VSXAHHWV-9FoVzEJO.js} +1 -1
- package/dist/ui/assets/{diagram-VX7I27RA-BhzF4vum.js → diagram-VX7I27RA-D8tFlKbW.js} +1 -1
- package/dist/ui/assets/{diagram-Z3DM3KII-DfElw-8p.js → diagram-Z3DM3KII-CNPw5GH8.js} +1 -1
- package/dist/ui/assets/{ebnfDiagram-PWID7BFC-DWBcjdxW.js → ebnfDiagram-PWID7BFC-qr5wsHox.js} +1 -1
- package/dist/ui/assets/{erDiagram-RLTQ6QDP-9t0j2-5C.js → erDiagram-RLTQ6QDP-B5Ntd34K.js} +1 -1
- package/dist/ui/assets/eventmodeling-NTZA5JFV-Cri4g80G.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-X8oZd-rh.js +1 -0
- package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-BV4wbDOf.js → ganttDiagram-EL5Y4UJY-ltWKqUKF.js} +1 -1
- package/dist/ui/assets/{gitGraph-4MIJSDKK-CHLeBDuC.js → gitGraph-4MIJSDKK-B_MMUzrw.js} +1 -1
- package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-D1coFQQr.js → gitGraphDiagram-WWUBYQGX-CntguBHS.js} +1 -1
- package/dist/ui/assets/index-Cbq9dyjP.js +54 -0
- package/dist/ui/assets/index-Z2Tb6lCZ.css +1 -0
- package/dist/ui/assets/{info-A6RAGUB7-BA6DRUi0.js → info-A6RAGUB7-Cu3MBfy4.js} +1 -1
- package/dist/ui/assets/{infoDiagram-27XIBGKW-DelTcISi.js → infoDiagram-27XIBGKW-DnXjJ1it.js} +1 -1
- package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-D6SYXrWV.js → ishikawaDiagram-5VMMS53U-DvzRpbIH.js} +1 -1
- package/dist/ui/assets/{journeyDiagram-3NMN7TZE-iul5gKFN.js → journeyDiagram-3NMN7TZE-DLQHYyFc.js} +1 -1
- package/dist/ui/assets/{kanban-definition-UXKFOSKX-DmsdCDnD.js → kanban-definition-UXKFOSKX-Cs9Tnl-z.js} +1 -1
- package/dist/ui/assets/{line-6qxsfuaD.js → line-DQLJC3Zu.js} +1 -1
- package/dist/ui/assets/{mermaid-parser.core-DwVo-Y2S.js → mermaid-parser.core-C8gt4DSf.js} +3 -3
- package/dist/ui/assets/{mermaid.core-DMfVb5bs.js → mermaid.core-KxIl4lpw.js} +4 -4
- package/dist/ui/assets/{mindmap-definition-YA3MSWOX-CD5cXQfB.js → mindmap-definition-YA3MSWOX-DCeBgBgT.js} +1 -1
- package/dist/ui/assets/{packet-AYTQ26CC-DVdCUknr.js → packet-AYTQ26CC-DNlpEXFf.js} +1 -1
- package/dist/ui/assets/{pegDiagram-XKGWAZYB-DGmIIA1Q.js → pegDiagram-XKGWAZYB-BO3Gb0g_.js} +1 -1
- package/dist/ui/assets/{pie-WAS4IAKB-UGTX1jKx.js → pie-WAS4IAKB-BudehkF3.js} +1 -1
- package/dist/ui/assets/{pieDiagram-E7YTZNPT-DLbLermT.js → pieDiagram-E7YTZNPT-DHmWd9hB.js} +1 -1
- package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-DrKEGcJP.js → quadrantDiagram-AXDQQJYC-OoFEDhWd.js} +1 -1
- package/dist/ui/assets/{radar-RG4KPBEZ-Bts8VyZT.js → radar-RG4KPBEZ-D21BtL1U.js} +1 -1
- package/dist/ui/assets/{railroad-74A4TZTK-DEWurv57.js → railroad-74A4TZTK-BkbQQ-xO.js} +1 -1
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-Pqt7vgeQ.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-Dx9OloZg.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-DrivZvC4.js +1 -0
- package/dist/ui/assets/{railroadDiagram-O6MQD6OU-Bc2KS9ny.js → railroadDiagram-O6MQD6OU-SOKojSix.js} +1 -1
- package/dist/ui/assets/repo-C5kAzRzY.js +2 -0
- package/dist/ui/assets/{requirementDiagram-BXWQKSXE-DJYx_Rdm.js → requirementDiagram-BXWQKSXE-BtLk0WUA.js} +1 -1
- package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-BcEvC1QT.js → sankeyDiagram-P5KCCOFB-C1xbu6hH.js} +1 -1
- package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-ERKmG0r-.js → sequenceDiagram-WJ2MYXX4-HedfOFTZ.js} +1 -1
- package/dist/ui/assets/{stateDiagram-D77RDMKH-ChqxZBTP.js → stateDiagram-D77RDMKH-CXypbZi0.js} +1 -1
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-DailpsQY.js +1 -0
- package/dist/ui/assets/{swimlanes-42K2YHIH-DVgFWSH5.js → swimlanes-42K2YHIH-EGq8ZuPi.js} +1 -1
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-DzgEGbun.js +8 -0
- package/dist/ui/assets/{timeline-definition-24CTP7MA-Bs376mCS.js → timeline-definition-24CTP7MA-Dv3_eOK9.js} +1 -1
- package/dist/ui/assets/{title-CIBxYjzz.js → title-ByLphNfa.js} +1 -1
- package/dist/ui/assets/{treeView-Q6P3EWNA-B8wx19JO.js → treeView-Q6P3EWNA-DUs-qm8c.js} +1 -1
- package/dist/ui/assets/{treemap-WGGIJYW6-By-sUWWT.js → treemap-WGGIJYW6-CgFlf2C6.js} +1 -1
- package/dist/ui/assets/vendor-ui-Bsdh9CC0.js +3 -0
- package/dist/ui/assets/{vennDiagram-4TSXK5OY-DT2AgAWU.js → vennDiagram-4TSXK5OY-9CXcq-af.js} +1 -1
- package/dist/ui/assets/{wardley-WFR3VGLG-CRXTnxDX.js → wardley-WFR3VGLG-4CvLgPjx.js} +1 -1
- package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-CuRK6dGX.js → wardleyDiagram-VM6X3IG4-DotFAxqv.js} +1 -1
- package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-BPgu4rsi.js → xychartDiagram-S5SC5T6Z-CfChnrRi.js} +1 -1
- package/dist/ui/index.html +8 -8
- package/dist/ui/sw.js +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/AgentsView-FHNRZWLG.js +0 -1
- package/dist/ui/assets/ChecksView-DdI629ss.css +0 -1
- package/dist/ui/assets/ChecksView-DueoaSQy.js +0 -0
- package/dist/ui/assets/ContextView-CE6PlM7S.js +0 -4
- package/dist/ui/assets/InputsView-BDuzGLkC.css +0 -1
- package/dist/ui/assets/InputsView-C1MEfngw.js +0 -1
- package/dist/ui/assets/ReleasesView-B94OPqL-.css +0 -1
- package/dist/ui/assets/ReleasesView-CGzIvtxF.js +0 -3
- package/dist/ui/assets/SettingsView-DsoMIVuc.js +0 -4
- package/dist/ui/assets/StoriesView-BVQNDIfP.js +0 -1
- package/dist/ui/assets/WorkView-iBEEgB6t.js +0 -2
- package/dist/ui/assets/architecture-7GRP2DOG-DsFV151p.js +0 -1
- package/dist/ui/assets/channel-Cz6IiS37.js +0 -1
- package/dist/ui/assets/classDiagram-ZZMXUADV-BSdAza91.js +0 -1
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BSdAza91.js +0 -1
- package/dist/ui/assets/eventmodeling-NTZA5JFV-CkM-IpK3.js +0 -1
- package/dist/ui/assets/flowDiagram-HODETNUW-Cjc88Y5V.js +0 -1
- package/dist/ui/assets/index-CCwW1Tv4.css +0 -1
- package/dist/ui/assets/index-CvnEg0cY.js +0 -55
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-DKtrJ5pg.js +0 -1
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-8eArMXZA.js +0 -1
- package/dist/ui/assets/railroad-peg-WCYAUIDC-SfWjNEkF.js +0 -1
- package/dist/ui/assets/repo-BWyge_2n.js +0 -1
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-Crkx2xne.js +0 -1
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BVnIaNys.js +0 -8
- package/dist/ui/assets/vendor-ui-BInmYDPf.js +0 -3
package/dist/.build-info.json
CHANGED
package/dist/.build-stamp.json
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -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 ---
|
package/dist/commands/tasks.js
CHANGED
|
@@ -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
|
|
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(`
|
|
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
|
-
|
|
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()
|
|
64
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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
|
package/dist/core/config.js
CHANGED
|
@@ -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 = [];
|