@repo-os/repoos 0.5.51 → 0.5.52

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 (144) hide show
  1. package/README.md +3 -4
  2. package/dist/.build-info.json +2 -2
  3. package/dist/.build-stamp.json +1 -1
  4. package/dist/core/git.d.ts +7 -0
  5. package/dist/core/git.js +18 -8
  6. package/dist/core/review-verdict.d.ts +5 -4
  7. package/dist/core/review-verdict.js +21 -8
  8. package/dist/core/review-verdict.test.js +6 -0
  9. package/dist/core/story-definition-files.d.ts +49 -0
  10. package/dist/core/story-definition-files.js +252 -0
  11. package/dist/core/story-definitions.d.ts +3 -0
  12. package/dist/core/story-definitions.js +3 -0
  13. package/dist/core/story-display.d.ts +20 -0
  14. package/dist/core/story-display.js +55 -0
  15. package/dist/core/types.d.ts +13 -0
  16. package/dist/server/live-index.d.ts +16 -1
  17. package/dist/server/live-index.js +18 -1
  18. package/dist/server/review.d.ts +2 -2
  19. package/dist/server/review.js +23 -10
  20. package/dist/server/routes/index.d.ts +1 -0
  21. package/dist/server/routes/index.js +1 -0
  22. package/dist/server/routes/stories.d.ts +12 -0
  23. package/dist/server/routes/stories.js +66 -0
  24. package/dist/server/server.js +64 -18
  25. package/dist/server/story-pm.d.ts +20 -0
  26. package/dist/server/story-pm.js +114 -0
  27. package/dist/server/test-run.d.ts +9 -0
  28. package/dist/server/test-run.js +33 -23
  29. package/dist/server/test-run.test.d.ts +1 -0
  30. package/dist/server/test-run.test.js +28 -0
  31. package/dist/server/watcher.d.ts +1 -0
  32. package/dist/server/watcher.js +21 -4
  33. package/dist/ui/assets/{AgentsView-VpkTvsSg.js → AgentsView-UghV_I0u.js} +1 -1
  34. package/dist/ui/assets/ChecksView-C0TtYC-4.css +1 -0
  35. package/dist/ui/assets/ChecksView-CE9_UWEH.js +0 -0
  36. package/dist/ui/assets/{ContextView-B61GEm1Z.js → ContextView-C3cJribS.js} +2 -2
  37. package/dist/ui/assets/DashboardView-DydNm09c.js +1 -0
  38. package/dist/ui/assets/{DashboardView-CRdYYcWz.css → DashboardView-ZvEsCIs4.css} +1 -1
  39. package/dist/ui/assets/{DeploymentsView-BfRAdizb.js → DeploymentsView-DVMkUs04.js} +1 -1
  40. package/dist/ui/assets/{DiffView-DixEbWbX.js → DiffView-CCOUztfU.js} +1 -1
  41. package/dist/ui/assets/{InputsView-DZJ_-QCu.js → InputsView-uzRKFx_8.js} +1 -1
  42. package/dist/ui/assets/{LoginView-BhkLJwz2.js → LoginView-CV_1xAvv.js} +1 -1
  43. package/dist/ui/assets/{ReleasesView-Bc4mWRi1.js → ReleasesView-CMTPaJGR.js} +1 -1
  44. package/dist/ui/assets/{SettingsView-CaJPb-O7.js → SettingsView-3Y2d6VYS.js} +1 -1
  45. package/dist/ui/assets/StoriesView-DGnU5Cy9.js +1 -0
  46. package/dist/ui/assets/StoriesView-DTqtvuk_.css +1 -0
  47. package/dist/ui/assets/TaskCard-hX9bZtBs.js +1 -0
  48. package/dist/ui/assets/{WorkView-VjIRIAu3.js → WorkView-DpL4RTSC.js} +1 -1
  49. package/dist/ui/assets/{abnfDiagram-VCTEODGH-CsyiRZfO.js → abnfDiagram-VCTEODGH-CTfjwe9G.js} +1 -1
  50. package/dist/ui/assets/architecture-7GRP2DOG-iLDzG2hD.js +1 -0
  51. package/dist/ui/assets/{architectureDiagram-5GKGNRK7-B08h5dWI.js → architectureDiagram-5GKGNRK7-CVfIBdXM.js} +1 -1
  52. package/dist/ui/assets/{blockDiagram-I7D4REHJ-BPPeB-eQ.js → blockDiagram-I7D4REHJ-DHrovPE1.js} +1 -1
  53. package/dist/ui/assets/{c4Diagram-7LVT6UL2-BfADPuem.js → c4Diagram-7LVT6UL2-D1_IKyVU.js} +1 -1
  54. package/dist/ui/assets/channel-C2SsuvKM.js +1 -0
  55. package/dist/ui/assets/{chunk-4HAMMTFA-DOmKNumZ.js → chunk-4HAMMTFA-BQK2Qs9d.js} +1 -1
  56. package/dist/ui/assets/{chunk-75Z2AOVW-fvtO31Ky.js → chunk-75Z2AOVW-C560CV9I.js} +1 -1
  57. package/dist/ui/assets/{chunk-DU6HZSFF-egapwqdZ.js → chunk-DU6HZSFF-BX9aRA1h.js} +1 -1
  58. package/dist/ui/assets/{chunk-F27PBJKO-B4F_scL_.js → chunk-F27PBJKO-MId0AvWB.js} +1 -1
  59. package/dist/ui/assets/{chunk-GMAD6QVW-B0g7hXXo.js → chunk-GMAD6QVW-B8uV52pK.js} +1 -1
  60. package/dist/ui/assets/{chunk-GVQU2GXP-ByHTqLhx.js → chunk-GVQU2GXP-BHRWhJBs.js} +1 -1
  61. package/dist/ui/assets/{chunk-IMKFNOWR-BiX2WjGm.js → chunk-IMKFNOWR-C2p26tmJ.js} +1 -1
  62. package/dist/ui/assets/{chunk-L3NEJ4N5-Dtn3Nz1E.js → chunk-L3NEJ4N5-CiNIjzhG.js} +1 -1
  63. package/dist/ui/assets/{chunk-OSK3NFVY-D2FVEKbm.js → chunk-OSK3NFVY-DOZ8UOk-.js} +1 -1
  64. package/dist/ui/assets/{chunk-P2QGCYS3-DbpxXwpl.js → chunk-P2QGCYS3-CWkpd0sS.js} +1 -1
  65. package/dist/ui/assets/{chunk-POPQ4Y6H-wc9BY3N-.js → chunk-POPQ4Y6H-C_jAEIPq.js} +1 -1
  66. package/dist/ui/assets/{chunk-PWAF6VOD-CKBLutxN.js → chunk-PWAF6VOD-DlV-Z1d6.js} +1 -1
  67. package/dist/ui/assets/{chunk-SHT3W25Y-CtTrYLC0.js → chunk-SHT3W25Y-CDcQrQRH.js} +1 -1
  68. package/dist/ui/assets/{chunk-SVP7TREG-QwFEiE2U.js → chunk-SVP7TREG-CXMcWUhi.js} +1 -1
  69. package/dist/ui/assets/{chunk-TICWLB2K-DM5hYl74.js → chunk-TICWLB2K-C9NkrBRa.js} +1 -1
  70. package/dist/ui/assets/classDiagram-ZZMXUADV-DOc-KWik.js +1 -0
  71. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-DOc-KWik.js +1 -0
  72. package/dist/ui/assets/{cynefin-OW5HDTMX-DKbiErci.js → cynefin-OW5HDTMX-BOREGtlU.js} +1 -1
  73. package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-C_RVv0hh.js → cynefinDiagram-5FMLGOSQ-B4ry75uw.js} +1 -1
  74. package/dist/ui/assets/{dagre-GXQ25YYZ-BYlBs_VY.js → dagre-GXQ25YYZ-DaFuaEbQ.js} +1 -1
  75. package/dist/ui/assets/{diagram-S7CK7UJ4-BvYISuK2.js → diagram-S7CK7UJ4-CWKov5PL.js} +1 -1
  76. package/dist/ui/assets/{diagram-UQ7AKVKN-Bf4NWZw-.js → diagram-UQ7AKVKN-BIaDBoBe.js} +1 -1
  77. package/dist/ui/assets/{diagram-VSXAHHWV-DeBDsA1S.js → diagram-VSXAHHWV-BjTKaekf.js} +1 -1
  78. package/dist/ui/assets/{diagram-VX7I27RA-COq97Bh2.js → diagram-VX7I27RA-Yx3scpQA.js} +1 -1
  79. package/dist/ui/assets/{diagram-Z3DM3KII-DipozH1R.js → diagram-Z3DM3KII-Zj02SBoH.js} +1 -1
  80. package/dist/ui/assets/{ebnfDiagram-PWID7BFC-C7bjWaVV.js → ebnfDiagram-PWID7BFC-0YAqIwpj.js} +1 -1
  81. package/dist/ui/assets/{erDiagram-RLTQ6QDP-CkdV1U7x.js → erDiagram-RLTQ6QDP-D1qzM_C2.js} +1 -1
  82. package/dist/ui/assets/eventmodeling-NTZA5JFV-DoMudewO.js +1 -0
  83. package/dist/ui/assets/flowDiagram-HODETNUW-COa-0qtC.js +1 -0
  84. package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-CpLssEGJ.js → ganttDiagram-EL5Y4UJY-BfFGqNAR.js} +1 -1
  85. package/dist/ui/assets/{gitGraph-4MIJSDKK-ZNvMSPlV.js → gitGraph-4MIJSDKK-C8Hio8mK.js} +1 -1
  86. package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-BLzUTHIQ.js → gitGraphDiagram-WWUBYQGX-zDzdEhVR.js} +1 -1
  87. package/dist/ui/assets/index-CxZVHdeX.js +35 -0
  88. package/dist/ui/assets/index-DCiwKRGz.css +1 -0
  89. package/dist/ui/assets/{info-A6RAGUB7-CIyWcXp1.js → info-A6RAGUB7-Co-frmaH.js} +1 -1
  90. package/dist/ui/assets/{infoDiagram-27XIBGKW-8e2Woehk.js → infoDiagram-27XIBGKW-C8vJrt_c.js} +1 -1
  91. package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-C7lXCzD0.js → ishikawaDiagram-5VMMS53U-BGO19EHJ.js} +1 -1
  92. package/dist/ui/assets/{journeyDiagram-3NMN7TZE-Cw7e6vEa.js → journeyDiagram-3NMN7TZE-DduoU3Je.js} +1 -1
  93. package/dist/ui/assets/{kanban-definition-UXKFOSKX-40gNKOtZ.js → kanban-definition-UXKFOSKX-D7pp8Jfx.js} +1 -1
  94. package/dist/ui/assets/{line-Dx_6SLjf.js → line-un3qDwwl.js} +1 -1
  95. package/dist/ui/assets/{mermaid-parser.core-Cx9IgWvp.js → mermaid-parser.core-BbfKCEpr.js} +3 -3
  96. package/dist/ui/assets/{mermaid.core-CE4Rcwvp.js → mermaid.core-BN7qhUmq.js} +4 -4
  97. package/dist/ui/assets/{mindmap-definition-YA3MSWOX-Bpjh59WU.js → mindmap-definition-YA3MSWOX-BYD5kmA5.js} +1 -1
  98. package/dist/ui/assets/{packet-AYTQ26CC-TzUU1fUR.js → packet-AYTQ26CC-C4FJPnFC.js} +1 -1
  99. package/dist/ui/assets/{pegDiagram-XKGWAZYB-xfGkTiyU.js → pegDiagram-XKGWAZYB-DsBi7Fms.js} +1 -1
  100. package/dist/ui/assets/{pie-WAS4IAKB-Cy0RJX4e.js → pie-WAS4IAKB-C79YZGPK.js} +1 -1
  101. package/dist/ui/assets/{pieDiagram-E7YTZNPT-C_wFzDa5.js → pieDiagram-E7YTZNPT-BkOWAtjO.js} +1 -1
  102. package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-D8nlig_S.js → quadrantDiagram-AXDQQJYC-4cKZyntK.js} +1 -1
  103. package/dist/ui/assets/{radar-RG4KPBEZ-DpEbf4Sa.js → radar-RG4KPBEZ-BRRdMOI4.js} +1 -1
  104. package/dist/ui/assets/{railroad-74A4TZTK-CSOahYzS.js → railroad-74A4TZTK-Bbhl18lT.js} +1 -1
  105. package/dist/ui/assets/railroad-abnf-HS5TGJTU-DP10zylu.js +1 -0
  106. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-CD0qyNXa.js +1 -0
  107. package/dist/ui/assets/railroad-peg-WCYAUIDC-B4QvCxtr.js +1 -0
  108. package/dist/ui/assets/{railroadDiagram-O6MQD6OU-CgaqmbL8.js → railroadDiagram-O6MQD6OU-Bbg60dv2.js} +1 -1
  109. package/dist/ui/assets/{requirementDiagram-BXWQKSXE-b5SPb0kK.js → requirementDiagram-BXWQKSXE-DoLwPzhb.js} +1 -1
  110. package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-DORlq3KF.js → sankeyDiagram-P5KCCOFB-BVBS_VZc.js} +1 -1
  111. package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-BwS_-jk2.js → sequenceDiagram-WJ2MYXX4-BACpdW4n.js} +1 -1
  112. package/dist/ui/assets/{stateDiagram-D77RDMKH-DfgJqXRY.js → stateDiagram-D77RDMKH-DlGZE9N1.js} +1 -1
  113. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BAXOtgDm.js +1 -0
  114. package/dist/ui/assets/{swimlanes-42K2YHIH-aT_bNbqM.js → swimlanes-42K2YHIH-DLQ7Vteo.js} +1 -1
  115. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BBsmj1pm.js +8 -0
  116. package/dist/ui/assets/{timeline-definition-24CTP7MA-V7APg65Z.js → timeline-definition-24CTP7MA-b99HY5j-.js} +1 -1
  117. package/dist/ui/assets/{treeView-Q6P3EWNA-CkX4_NCQ.js → treeView-Q6P3EWNA-BKXQAE6F.js} +1 -1
  118. package/dist/ui/assets/{treemap-WGGIJYW6-CNj80LcR.js → treemap-WGGIJYW6-CZj1ym9X.js} +1 -1
  119. package/dist/ui/assets/{vennDiagram-4TSXK5OY-Cp2p3H8R.js → vennDiagram-4TSXK5OY-Syj4tlui.js} +1 -1
  120. package/dist/ui/assets/{wardley-WFR3VGLG-DZlFTJK6.js → wardley-WFR3VGLG-W_snBoVU.js} +1 -1
  121. package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-Lnl-KAaI.js → wardleyDiagram-VM6X3IG4-BnpA19Oz.js} +1 -1
  122. package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-BJxTLbnv.js → xychartDiagram-S5SC5T6Z-g474RiA-.js} +1 -1
  123. package/dist/ui/index.html +2 -2
  124. package/dist/ui/sw.js +2 -2
  125. package/package.json +1 -1
  126. package/dist/ui/assets/ChecksView-CDE2cfWk.js +0 -1
  127. package/dist/ui/assets/ChecksView-D79umAfM.css +0 -1
  128. package/dist/ui/assets/DashboardView-C5anCBko.js +0 -1
  129. package/dist/ui/assets/StoriesView-BdwyVyBi.css +0 -1
  130. package/dist/ui/assets/StoriesView-s-GXglwO.js +0 -1
  131. package/dist/ui/assets/TaskCard-CiDqR0-o.js +0 -1
  132. package/dist/ui/assets/architecture-7GRP2DOG-xBNBIq5T.js +0 -1
  133. package/dist/ui/assets/channel-BoRSmxIz.js +0 -1
  134. package/dist/ui/assets/classDiagram-ZZMXUADV-C0YZV-id.js +0 -1
  135. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-C0YZV-id.js +0 -1
  136. package/dist/ui/assets/eventmodeling-NTZA5JFV-C7NKZ2E3.js +0 -1
  137. package/dist/ui/assets/flowDiagram-HODETNUW-BsiwhEl2.js +0 -1
  138. package/dist/ui/assets/index-B0otXprn.css +0 -1
  139. package/dist/ui/assets/index-nZg6iOq8.js +0 -35
  140. package/dist/ui/assets/railroad-abnf-HS5TGJTU-8vJ8IZcu.js +0 -1
  141. package/dist/ui/assets/railroad-ebnf-LZEXJU2U-p9du8TJr.js +0 -1
  142. package/dist/ui/assets/railroad-peg-WCYAUIDC-BweyuB5K.js +0 -1
  143. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-D7zefEr8.js +0 -1
  144. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BqH3ZHk1.js +0 -8
package/README.md CHANGED
@@ -308,10 +308,9 @@ loopback servers need zero setup; remote servers use short-lived, revocable
308
308
  capability tokens (created over HTTPS on the server, stored in the Keychain)
309
309
  for their attention and search endpoints.
310
310
 
311
- The Hub's DMG is published on
312
- [GitHub Releases](https://github.com/repo-os/repoos/releases) with each tag
313
- release (the first Hub DMG is pending). Setup, pairing, and limitation details
314
- are in the [Hub guide](https://github.com/repo-os/repoos/blob/main/user-docs/macos-hub.md).
311
+ Download the latest [RepoOS Hub DMG](https://github.com/repo-os/repoos/releases/latest/download/RepoOSHub.dmg),
312
+ or browse [GitHub Releases](https://github.com/repo-os/repoos/releases). Setup,
313
+ pairing, and limitation details are in the [Hub guide](https://docs.repoos.org/macos-hub).
315
314
 
316
315
  ## Roadmap
317
316
 
@@ -1,4 +1,4 @@
1
1
  {
2
- "hash": "9978dc66ddd3de60d301a374cf6cf80f0e7f6fa67545d1dcab764e930f29b35a",
3
- "version": "0.5.51"
2
+ "hash": "c59ba0faf030004be937d5bd9fb28afc6fdd9156857ae68454cba1c959f0f979",
3
+ "version": "0.5.52"
4
4
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "generatedAt": "2026-09-23T02:28:26.540Z"
2
+ "generatedAt": "2026-09-23T08:36:16.981Z"
3
3
  }
@@ -396,6 +396,13 @@ export declare function preflightMerge(root: string, branch: string, opts?: {
396
396
  * is not clean; callers should treat that as a merge blocker.
397
397
  */
398
398
  export declare function commitTaskFile(root: string, absPath: string, message: string): boolean;
399
+ /**
400
+ * `commitTaskFile` for several paths in one commit — e.g. a story definition
401
+ * renamed on disk, where both the removed old path and the new one must land
402
+ * together. A path may be deleted from the working tree; `git add` stages the
403
+ * removal. Same fail-soft contract and `--only` scoping as `commitTaskFile`.
404
+ */
405
+ export declare function commitFiles(root: string, absPaths: string[], message: string): boolean;
399
406
  /**
400
407
  * The dirty/uncommitted file paths in a checkout (`git status --porcelain`),
401
408
  * including tracked modifications and untracked files, in repo-relative form.
package/dist/core/git.js CHANGED
@@ -1047,20 +1047,30 @@ export async function preflightMerge(root, branch, opts = {}) {
1047
1047
  * is not clean; callers should treat that as a merge blocker.
1048
1048
  */
1049
1049
  export function commitTaskFile(root, absPath, message) {
1050
- const rel = relative(root, absPath);
1051
- if (rel.startsWith("..") || isAbsolute(rel))
1050
+ return commitFiles(root, [absPath], message);
1051
+ }
1052
+ /**
1053
+ * `commitTaskFile` for several paths in one commit — e.g. a story definition
1054
+ * renamed on disk, where both the removed old path and the new one must land
1055
+ * together. A path may be deleted from the working tree; `git add` stages the
1056
+ * removal. Same fail-soft contract and `--only` scoping as `commitTaskFile`.
1057
+ */
1058
+ export function commitFiles(root, absPaths, message) {
1059
+ const rels = absPaths.map((p) => relative(root, p));
1060
+ if (rels.length === 0 || rels.some((rel) => rel.startsWith("..") || isAbsolute(rel))) {
1052
1061
  return false;
1053
- const status = git(root, ["status", "--porcelain", "--", rel]);
1062
+ }
1063
+ const status = git(root, ["status", "--porcelain", "--", ...rels]);
1054
1064
  if (status === null)
1055
1065
  return false;
1056
1066
  if (status.trim() === "")
1057
1067
  return true;
1058
- if (git(root, ["add", "--", rel]) === null)
1068
+ if (git(root, ["add", "-A", "--", ...rels]) === null)
1059
1069
  return false;
1060
- // `--only` with a pathspec commits just this file and leaves everything else
1061
- // already in the index staged (#0353): a plain `git commit` would sweep in
1062
- // whatever a human or agent had staged in the checkout under our message.
1063
- return git(root, ["commit", "-o", "-m", message, "--", rel]) !== null;
1070
+ // `--only` with a pathspec commits just these files and leaves everything
1071
+ // else already in the index staged (#0353): a plain `git commit` would sweep
1072
+ // in whatever a human or agent had staged in the checkout under our message.
1073
+ return git(root, ["commit", "-o", "-m", message, "--", ...rels]) !== null;
1064
1074
  }
1065
1075
  /**
1066
1076
  * The dirty/uncommitted file paths in a checkout (`git status --porcelain`),
@@ -14,10 +14,11 @@
14
14
  * The UI was right and the server's gate was wrong — this is now the single
15
15
  * shared implementation both sides use, so they can never disagree again.
16
16
  *
17
- * Order matters: each list is checked most-specific-first so a report that
18
- * mentions a milder verdict in passing while explaining a worse one (e.g.
19
- * "back to the drawing board" text discussing what would merely "need some
20
- * work") doesn't get misread as the mild outcome.
17
+ * When the report has a `## Verdict` section, only that section is read (the
18
+ * first non-empty line there). Review prose often names the other outcomes
19
+ * while discussing tests or alternatives; scanning the entire report can turn
20
+ * a declared "good to go" into a false rejection. Reports without that heading
21
+ * (legacy) still use a whole-document scan so older stored reports keep working.
21
22
  */
22
23
  export type ReviewVerdict = "good to go" | "needs some work" | "back to the drawing board";
23
24
  export type ReviewRelevance = "still relevant" | "no longer needed" | "needs rescoping";
@@ -14,10 +14,11 @@
14
14
  * The UI was right and the server's gate was wrong — this is now the single
15
15
  * shared implementation both sides use, so they can never disagree again.
16
16
  *
17
- * Order matters: each list is checked most-specific-first so a report that
18
- * mentions a milder verdict in passing while explaining a worse one (e.g.
19
- * "back to the drawing board" text discussing what would merely "need some
20
- * work") doesn't get misread as the mild outcome.
17
+ * When the report has a `## Verdict` section, only that section is read (the
18
+ * first non-empty line there). Review prose often names the other outcomes
19
+ * while discussing tests or alternatives; scanning the entire report can turn
20
+ * a declared "good to go" into a false rejection. Reports without that heading
21
+ * (legacy) still use a whole-document scan so older stored reports keep working.
21
22
  */
22
23
  const VERDICT_ORDER = [
23
24
  "back to the drawing board",
@@ -29,16 +30,28 @@ const RELEVANCE_ORDER = [
29
30
  "needs rescoping",
30
31
  "still relevant",
31
32
  ];
32
- export function parseReviewVerdict(markdown) {
33
- if (!markdown)
34
- return null;
35
- const lower = markdown.toLowerCase();
33
+ function verdictFromText(text) {
34
+ const lower = text.toLowerCase();
36
35
  for (const label of VERDICT_ORDER) {
37
36
  if (lower.includes(label))
38
37
  return label;
39
38
  }
40
39
  return null;
41
40
  }
41
+ export function parseReviewVerdict(markdown) {
42
+ if (!markdown)
43
+ return null;
44
+ const heading = /^#{1,6}\s+verdict\s*$/im.exec(markdown);
45
+ if (heading) {
46
+ const section = markdown
47
+ .slice((heading.index ?? 0) + heading[0].length)
48
+ .split(/^#{1,6}\s+/m, 1)[0];
49
+ const line = section.split(/\r?\n/).find((value) => value.trim());
50
+ return line ? verdictFromText(line) : null;
51
+ }
52
+ // Legacy reports without a Verdict heading: scan the full body.
53
+ return verdictFromText(markdown);
54
+ }
42
55
  export function parseReviewRelevance(markdown) {
43
56
  if (!markdown)
44
57
  return null;
@@ -23,12 +23,18 @@ describe("parseReviewVerdict", () => {
23
23
  it("prefers the worse verdict when a milder one is only mentioned in passing", () => {
24
24
  expect(parseReviewVerdict("## Verdict\nback to the drawing board — closer work would only need some work, but this misses the mark entirely.")).toBe("back to the drawing board");
25
25
  });
26
+ it("uses the declared verdict instead of later mentions in suggestions", () => {
27
+ expect(parseReviewVerdict("## Verdict\n`good to go` — nothing blocks sign-off.\n\n## Suggestions\nTest needs some work and back to the drawing board paths too.")).toBe("good to go");
28
+ });
26
29
  it("is case-insensitive", () => {
27
30
  expect(parseReviewVerdict("## Verdict\nGood To Go.")).toBe("good to go");
28
31
  });
29
32
  it("returns null when no known verdict phrase is present", () => {
30
33
  expect(parseReviewVerdict("## Bugs\nNone found.")).toBeNull();
31
34
  });
35
+ it("scans the full document for legacy reports without a Verdict heading", () => {
36
+ expect(parseReviewVerdict("## Summary\nStill reviewing.\n\n## Outcome\ngood to go — ship it when ready.")).toBe("good to go");
37
+ });
32
38
  });
33
39
  describe("parseReviewRelevance", () => {
34
40
  it("parses a bold-wrapped relevance label", () => {
@@ -0,0 +1,49 @@
1
+ import type { RepoOSConfig } from "./types.js";
2
+ import type { StoryDefinition } from "./story-display.js";
3
+ export declare const STORIES_DIR = "stories";
4
+ export declare function fallbackStoryName(text: string): string;
5
+ export declare function setStoryPmWorking(path: string, on: boolean): void;
6
+ export declare function isStoryPmWorking(path: string): boolean;
7
+ export declare function storySlug(name: string): string;
8
+ export declare function collisionFreeStoryPath(config: RepoOSConfig, name: string,
9
+ /** A path the caller is replacing, so it never counts as a collision. */
10
+ ownPath?: string): string;
11
+ export declare function parseStoryDefinitionFile(content: string, path: string): StoryDefinition | null;
12
+ export declare function listStoryDefinitions(config: RepoOSConfig): StoryDefinition[];
13
+ export declare function findStoryDefinitionByKey(config: RepoOSConfig, key: string): StoryDefinition | null;
14
+ export interface WriteStoryDefinitionInput {
15
+ name: string;
16
+ body: string;
17
+ createdBy?: string;
18
+ path?: string;
19
+ }
20
+ export declare function writeStoryDefinition(config: RepoOSConfig, input: WriteStoryDefinitionInput): StoryDefinition;
21
+ /**
22
+ * Replace an existing definition's name and body in place, keeping its
23
+ * `created_at` / `created_by`. The PM flesh-out uses this to swap the
24
+ * placeholder written at submit time for the generated story. When the new
25
+ * name slugs differently the file is renamed to match; `previousPath` is set
26
+ * then so the caller can commit the removal too.
27
+ */
28
+ export declare function rewriteStoryDefinition(config: RepoOSConfig, path: string, input: {
29
+ name: string;
30
+ body: string;
31
+ }): {
32
+ definition: StoryDefinition;
33
+ previousPath?: string;
34
+ };
35
+ export interface ParsedGeneratedStory {
36
+ name: string;
37
+ body: string;
38
+ /** Ids of existing tasks the PM judged part of this story. */
39
+ taskIds: string[];
40
+ hadFrontmatter: boolean;
41
+ }
42
+ /** An existing task the PM may pull into a new story. */
43
+ export interface StoryTaskCandidate {
44
+ id: string;
45
+ title: string;
46
+ status: string;
47
+ }
48
+ export declare function storyFreeformPrompt(humanName: string, description: string, candidates?: StoryTaskCandidate[]): string;
49
+ export declare function parseGeneratedStoryDefinition(rawOutput: string, humanName: string, rawDescription: string): ParsedGeneratedStory;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Git-tracked story definition files under `stories/` (#0486). Node-only I/O.
3
+ */
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { FM_DELIM, parseDocument, serializeDocument } from "./frontmatter.js";
7
+ import { normalizeStoryName, storyKey } from "./stories.js";
8
+ export const STORIES_DIR = "stories";
9
+ export function fallbackStoryName(text) {
10
+ const line = text
11
+ .split("\n")
12
+ .map((l) => l.trim())
13
+ .find((l) => Boolean(l) && l !== FM_DELIM) ?? "";
14
+ if (line.length > 0) {
15
+ return line.length <= 60 ? line : `${line.slice(0, 57).trimEnd()}…`;
16
+ }
17
+ const flat = text
18
+ .replace(/\s+/g, " ")
19
+ .replace(/\s*---\s*/g, " ")
20
+ .trim();
21
+ return flat.length <= 60 ? flat || "Untitled story" : `${flat.slice(0, 57).trimEnd()}…`;
22
+ }
23
+ function storiesRoot(config) {
24
+ return join(config.root, STORIES_DIR);
25
+ }
26
+ function isStoryFile(name) {
27
+ return name.endsWith(".md") && !name.startsWith(".");
28
+ }
29
+ /**
30
+ * Story files the PM agent is fleshing out right now (by repo-relative path).
31
+ * In-memory on purpose: a server reload drops the run, and the indicator must
32
+ * clear with it rather than stick. Written by `src/server/story-pm.ts`.
33
+ */
34
+ const pmWorking = new Set();
35
+ export function setStoryPmWorking(path, on) {
36
+ if (on)
37
+ pmWorking.add(path);
38
+ else
39
+ pmWorking.delete(path);
40
+ }
41
+ export function isStoryPmWorking(path) {
42
+ return pmWorking.has(path);
43
+ }
44
+ export function storySlug(name) {
45
+ return (normalizeStoryName(name)
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, "-")
48
+ .replace(/^-|-$/g, "")
49
+ .slice(0, 55) || "story");
50
+ }
51
+ export function collisionFreeStoryPath(config, name,
52
+ /** A path the caller is replacing, so it never counts as a collision. */
53
+ ownPath) {
54
+ const base = storySlug(name);
55
+ let path = `${STORIES_DIR}/${base}.md`;
56
+ let n = 2;
57
+ while (path !== ownPath && existsSync(join(config.root, path))) {
58
+ path = `${STORIES_DIR}/${base}-${n}.md`;
59
+ n += 1;
60
+ }
61
+ return path;
62
+ }
63
+ export function parseStoryDefinitionFile(content, path) {
64
+ const { data, body, hadFrontmatter } = parseDocument(content);
65
+ if (!hadFrontmatter)
66
+ return null;
67
+ const name = normalizeStoryName(data.name);
68
+ if (!name)
69
+ return null;
70
+ const createdAt = typeof data.created_at === "string" && data.created_at.trim() ? data.created_at.trim() : "";
71
+ const createdBy = typeof data.created_by === "string" && data.created_by.trim() ? data.created_by.trim() : "";
72
+ return {
73
+ key: storyKey(name),
74
+ name,
75
+ path,
76
+ body: body.trim(),
77
+ createdAt,
78
+ createdBy,
79
+ };
80
+ }
81
+ export function listStoryDefinitions(config) {
82
+ const dir = storiesRoot(config);
83
+ if (!existsSync(dir))
84
+ return [];
85
+ return readdirSync(dir)
86
+ .filter(isStoryFile)
87
+ .map((file) => {
88
+ const path = `${STORIES_DIR}/${file}`;
89
+ try {
90
+ return parseStoryDefinitionFile(readFileSync(join(dir, file), "utf8"), path);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ })
96
+ .filter((d) => d !== null)
97
+ .sort((a, b) => a.name.localeCompare(b.name));
98
+ }
99
+ export function findStoryDefinitionByKey(config, key) {
100
+ const k = key.toLowerCase();
101
+ return listStoryDefinitions(config).find((d) => d.key === k) ?? null;
102
+ }
103
+ export function writeStoryDefinition(config, input) {
104
+ const name = normalizeStoryName(input.name);
105
+ if (!name)
106
+ throw new Error("story name is required");
107
+ const body = input.body.trim();
108
+ if (!body)
109
+ throw new Error("story description is required");
110
+ const existing = findStoryDefinitionByKey(config, storyKey(name));
111
+ if (existing) {
112
+ throw new Error(`A story named "${existing.name}" already exists`);
113
+ }
114
+ const path = input.path ?? collisionFreeStoryPath(config, name);
115
+ if (!path.startsWith(`${STORIES_DIR}/`) || !path.endsWith(".md")) {
116
+ throw new Error("invalid story definition path");
117
+ }
118
+ const createdAt = new Date().toISOString();
119
+ const createdBy = input.createdBy?.trim() || "unknown";
120
+ const content = serializeDocument({ name, created_at: createdAt, created_by: createdBy }, body.endsWith("\n") ? body : `${body}\n`);
121
+ const absPath = join(config.root, path);
122
+ mkdirSync(storiesRoot(config), { recursive: true });
123
+ writeFileSync(absPath, content, "utf8");
124
+ return {
125
+ key: storyKey(name),
126
+ name,
127
+ path,
128
+ body,
129
+ createdAt,
130
+ createdBy,
131
+ };
132
+ }
133
+ /**
134
+ * Replace an existing definition's name and body in place, keeping its
135
+ * `created_at` / `created_by`. The PM flesh-out uses this to swap the
136
+ * placeholder written at submit time for the generated story. When the new
137
+ * name slugs differently the file is renamed to match; `previousPath` is set
138
+ * then so the caller can commit the removal too.
139
+ */
140
+ export function rewriteStoryDefinition(config, path, input) {
141
+ const absOld = join(config.root, path);
142
+ if (!existsSync(absOld))
143
+ throw new Error(`story definition not found: ${path}`);
144
+ const current = parseStoryDefinitionFile(readFileSync(absOld, "utf8"), path);
145
+ if (!current)
146
+ throw new Error(`story definition is unreadable: ${path}`);
147
+ const name = normalizeStoryName(input.name);
148
+ if (!name)
149
+ throw new Error("story name is required");
150
+ const body = input.body.trim();
151
+ if (!body)
152
+ throw new Error("story description is required");
153
+ const clash = findStoryDefinitionByKey(config, storyKey(name));
154
+ if (clash && clash.path !== path) {
155
+ throw new Error(`A story named "${clash.name}" already exists`);
156
+ }
157
+ const nextPath = storySlug(name) === storySlug(current.name) ? path : collisionFreeStoryPath(config, name, path);
158
+ const content = serializeDocument({ name, created_at: current.createdAt, created_by: current.createdBy }, `${body}\n`);
159
+ writeFileSync(join(config.root, nextPath), content, "utf8");
160
+ if (nextPath !== path)
161
+ unlinkSync(absOld);
162
+ return {
163
+ definition: {
164
+ key: storyKey(name),
165
+ name,
166
+ path: nextPath,
167
+ body,
168
+ createdAt: current.createdAt,
169
+ createdBy: current.createdBy,
170
+ },
171
+ previousPath: nextPath !== path ? path : undefined,
172
+ };
173
+ }
174
+ export function storyFreeformPrompt(humanName, description, candidates = []) {
175
+ const nameHint = humanName.trim().length > 0
176
+ ? `The human already chose the story name: "${normalizeStoryName(humanName)}". Use that exact name in frontmatter.`
177
+ : "The human did not provide a name — propose a concise story name in frontmatter.";
178
+ const taskList = candidates.length
179
+ ? [
180
+ "",
181
+ "Existing tasks that are not yet tagged with any story (id · status · title).",
182
+ "If any of them clearly belong to this story, list their ids in a `tasks`",
183
+ "frontmatter list. Leave it out (or empty) when none fit — only include a",
184
+ "task you are confident belongs here.",
185
+ "",
186
+ ...candidates.map((t) => `${t.id} · ${t.status} · ${t.title.replace(/\s+/g, " ")}`),
187
+ ]
188
+ : [];
189
+ return [
190
+ "You are the PM agent for RepoOS. Turn the user's freeform description into a",
191
+ "story definition: a delivery slice that may later be broken into tasks tagged",
192
+ "with the same story name. This is NOT a task file — no id, status, branch, or",
193
+ "assignee.",
194
+ "",
195
+ nameHint,
196
+ "",
197
+ "The user's description:",
198
+ "",
199
+ "```",
200
+ description.trim(),
201
+ "```",
202
+ ...taskList,
203
+ "",
204
+ "Respond with a frontmatter block and markdown body only, like:",
205
+ "---",
206
+ "name: Project updates email",
207
+ 'tasks: ["0123", "0456"]',
208
+ "---",
209
+ "",
210
+ "# Project updates email",
211
+ "",
212
+ "Scope, outcomes, non-goals, and open questions in helpful PM prose.",
213
+ "",
214
+ "Do NOT include created_at or created_by — the system sets those.",
215
+ "Respond with ONLY the frontmatter block and body, starting with '---', with no",
216
+ "preamble, commentary, or code fences.",
217
+ ].join("\n");
218
+ }
219
+ function parseTaskIds(raw) {
220
+ const items = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(/[\s,]+/) : [];
221
+ const ids = items
222
+ .map((v) => String(v ?? "")
223
+ .replace(/^#/, "")
224
+ .trim())
225
+ .filter((v) => /^\d+$/.test(v))
226
+ .map((v) => v.padStart(4, "0"));
227
+ return [...new Set(ids)];
228
+ }
229
+ export function parseGeneratedStoryDefinition(rawOutput, humanName, rawDescription) {
230
+ // Agents sometimes wrap the whole answer in a ```markdown fence despite the
231
+ // prompt; unwrap it so the frontmatter is still found.
232
+ const trimmed = rawOutput.trim();
233
+ const fenced = trimmed.match(/^```[a-z]*\n([\s\S]*?)\n```$/i);
234
+ const { data, body, hadFrontmatter } = parseDocument(fenced ? fenced[1].trim() : trimmed);
235
+ const fromFm = typeof data.name === "string" && data.name.trim() ? normalizeStoryName(data.name) : "";
236
+ const human = normalizeStoryName(humanName);
237
+ if (hadFrontmatter && (fromFm || body.trim())) {
238
+ return {
239
+ // The human's explicit name always wins over the PM's proposal.
240
+ name: human || fromFm || fallbackStoryName(rawDescription),
241
+ body: body.trim() || rawDescription.trim(),
242
+ taskIds: parseTaskIds(data.tasks),
243
+ hadFrontmatter: true,
244
+ };
245
+ }
246
+ return {
247
+ name: human || fallbackStoryName(rawDescription),
248
+ body: rawDescription.trim(),
249
+ taskIds: [],
250
+ hadFrontmatter: false,
251
+ };
252
+ }
@@ -0,0 +1,3 @@
1
+ /** Re-exports for server/tests — UI must import `./story-display.js` only. */
2
+ export * from "./story-display.js";
3
+ export * from "./story-definition-files.js";
@@ -0,0 +1,3 @@
1
+ /** Re-exports for server/tests — UI must import `./story-display.js` only. */
2
+ export * from "./story-display.js";
3
+ export * from "./story-definition-files.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Browser-safe story display helpers (#0486): merge registered definitions with
3
+ * task-derived roll-ups. No Node imports — safe for the UI bundle.
4
+ */
5
+ import { type StoryGroup, type StoryTaskLike } from "./stories.js";
6
+ export interface StoryDefinition {
7
+ key: string;
8
+ name: string;
9
+ path: string;
10
+ body: string;
11
+ createdAt: string;
12
+ createdBy: string;
13
+ }
14
+ export interface MergedStoryGroup extends StoryGroup {
15
+ excerpt: string;
16
+ body: string;
17
+ registered: boolean;
18
+ }
19
+ export declare function storyExcerpt(body: string, max?: number): string;
20
+ export declare function mergeStoriesForDisplay<T extends StoryTaskLike>(tasks: T[], definitions: StoryDefinition[]): MergedStoryGroup[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Browser-safe story display helpers (#0486): merge registered definitions with
3
+ * task-derived roll-ups. No Node imports — safe for the UI bundle.
4
+ */
5
+ import { deriveStories, sortStories } from "./stories.js";
6
+ import { STATUSES } from "./types.js";
7
+ export function storyExcerpt(body, max = 200) {
8
+ const flat = body
9
+ .replace(/^#+\s+/gm, "")
10
+ .replace(/[*_`>#-]/g, " ")
11
+ .replace(/\s+/g, " ")
12
+ .trim();
13
+ if (flat.length <= max)
14
+ return flat;
15
+ return `${flat.slice(0, max - 1).trimEnd()}…`;
16
+ }
17
+ export function mergeStoriesForDisplay(tasks, definitions) {
18
+ const defByKey = new Map(definitions.map((d) => [d.key, d]));
19
+ const derived = deriveStories(tasks);
20
+ const merged = [];
21
+ const seen = new Set();
22
+ for (const group of derived) {
23
+ seen.add(group.key);
24
+ const def = defByKey.get(group.key);
25
+ merged.push({
26
+ ...group,
27
+ name: def?.name ?? group.name,
28
+ excerpt: def ? storyExcerpt(def.body) : "",
29
+ body: def?.body ?? "",
30
+ registered: !!def,
31
+ });
32
+ }
33
+ const emptyCounts = () => Object.fromEntries(STATUSES.map((s) => [s, 0]));
34
+ for (const def of definitions) {
35
+ if (seen.has(def.key))
36
+ continue;
37
+ merged.push({
38
+ key: def.key,
39
+ name: def.name,
40
+ tasks: [],
41
+ total: 0,
42
+ done: 0,
43
+ counts: emptyCounts(),
44
+ active: 0,
45
+ review: 0,
46
+ attention: 0,
47
+ complete: false,
48
+ lastActivity: null,
49
+ excerpt: storyExcerpt(def.body),
50
+ body: def.body,
51
+ registered: true,
52
+ });
53
+ }
54
+ return sortStories(merged);
55
+ }
@@ -1095,4 +1095,17 @@ export interface BoardIndex {
1095
1095
  taskCount: number;
1096
1096
  tasks: BoardTask[];
1097
1097
  counts: Record<Status, number>;
1098
+ /** Registered story definition files (`stories/*.md`) when `[stories] enabled`. */
1099
+ storyDefinitions?: StoryDefinitionRecord[];
1100
+ }
1101
+ /** One git-tracked story definition surfaced on the board payload (#0486). */
1102
+ export interface StoryDefinitionRecord {
1103
+ key: string;
1104
+ name: string;
1105
+ path: string;
1106
+ body: string;
1107
+ createdAt: string;
1108
+ createdBy: string;
1109
+ /** True while the PM agent is fleshing this story out in the background. */
1110
+ pmWorking?: boolean;
1098
1111
  }
@@ -104,7 +104,7 @@ export type RepoEvent = {
104
104
  | {
105
105
  type: "review";
106
106
  id: string;
107
- state: "running" | "ready" | "failed" | "cancelled";
107
+ state: "running" | "ready" | "failed" | "incomplete" | "cancelled";
108
108
  at: string;
109
109
  error?: string;
110
110
  }
@@ -154,6 +154,19 @@ export type RepoEvent = {
154
154
  type: "hello";
155
155
  taskCount: number;
156
156
  at: string;
157
+ } | {
158
+ type: "story.definitionsChanged";
159
+ at: string;
160
+ }
161
+ /** The PM finished fleshing out a New story (ok or not); `path` is its final file. */
162
+ | {
163
+ type: "story.pmFinished";
164
+ path: string;
165
+ name: string;
166
+ ok: boolean;
167
+ reason?: string;
168
+ taggedTaskIds: string[];
169
+ at: string;
157
170
  } | {
158
171
  type: "system.stats";
159
172
  stats: SystemStats;
@@ -308,5 +321,7 @@ export declare class LiveIndex {
308
321
  boardSnapshot(): BoardIndex;
309
322
  on(fn: Listener): () => void;
310
323
  private emit;
324
+ /** Story definition files under `stories/` changed on disk (#0486). */
325
+ notifyStoryDefinitionsChanged(): void;
311
326
  }
312
327
  export {};
@@ -14,6 +14,7 @@ import { STATUSES, PRIORITIES } from "../core/types.js";
14
14
  import { parseTask } from "../core/task.js";
15
15
  import { isGitRepo, localBranches, lastCommitForFile, worktreeStatus, worktreePaths, worktreeList, currentBranch, emptyGitInfo, resolveWorktreeStatuses, } from "../core/git.js";
16
16
  import { buildIndex, buildIndexAsync, readWorktreeDirtyCache, writeWorktreeDirtyCache, } from "../core/indexer.js";
17
+ import { isStoryPmWorking, listStoryDefinitions } from "../core/story-definition-files.js";
17
18
  import { patchTaskFile } from "./write.js";
18
19
  function priorityRank(p) {
19
20
  const i = PRIORITIES.indexOf(p);
@@ -352,7 +353,7 @@ export class LiveIndex {
352
353
  /** Lightweight board snapshot — skips body, extra, agent overrides. */
353
354
  boardSnapshot() {
354
355
  const tasks = this.getTasks();
355
- return {
356
+ const base = {
356
357
  version: 1,
357
358
  generatedAt: now(),
358
359
  root: this.config.root,
@@ -360,6 +361,18 @@ export class LiveIndex {
360
361
  tasks: tasks.map(toBoardTask),
361
362
  counts: this.counts(),
362
363
  };
364
+ if (this.config.stories?.enabled === true) {
365
+ base.storyDefinitions = listStoryDefinitions(this.config).map((d) => ({
366
+ key: d.key,
367
+ name: d.name,
368
+ path: d.path,
369
+ body: d.body,
370
+ createdAt: d.createdAt,
371
+ createdBy: d.createdBy,
372
+ pmWorking: isStoryPmWorking(d.path),
373
+ }));
374
+ }
375
+ return base;
363
376
  }
364
377
  // ---- events ----
365
378
  on(fn) {
@@ -376,6 +389,10 @@ export class LiveIndex {
376
389
  }
377
390
  }
378
391
  }
392
+ /** Story definition files under `stories/` changed on disk (#0486). */
393
+ notifyStoryDefinitionsChanged() {
394
+ this.emit({ type: "story.definitionsChanged", at: now() });
395
+ }
379
396
  }
380
397
  function now() {
381
398
  return new Date().toISOString();