@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.
- package/README.md +3 -4
- package/dist/.build-info.json +2 -2
- package/dist/.build-stamp.json +1 -1
- package/dist/core/git.d.ts +7 -0
- package/dist/core/git.js +18 -8
- package/dist/core/review-verdict.d.ts +5 -4
- package/dist/core/review-verdict.js +21 -8
- package/dist/core/review-verdict.test.js +6 -0
- package/dist/core/story-definition-files.d.ts +49 -0
- package/dist/core/story-definition-files.js +252 -0
- package/dist/core/story-definitions.d.ts +3 -0
- package/dist/core/story-definitions.js +3 -0
- package/dist/core/story-display.d.ts +20 -0
- package/dist/core/story-display.js +55 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/server/live-index.d.ts +16 -1
- package/dist/server/live-index.js +18 -1
- package/dist/server/review.d.ts +2 -2
- package/dist/server/review.js +23 -10
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/stories.d.ts +12 -0
- package/dist/server/routes/stories.js +66 -0
- package/dist/server/server.js +64 -18
- package/dist/server/story-pm.d.ts +20 -0
- package/dist/server/story-pm.js +114 -0
- package/dist/server/test-run.d.ts +9 -0
- package/dist/server/test-run.js +33 -23
- package/dist/server/test-run.test.d.ts +1 -0
- package/dist/server/test-run.test.js +28 -0
- package/dist/server/watcher.d.ts +1 -0
- package/dist/server/watcher.js +21 -4
- package/dist/ui/assets/{AgentsView-VpkTvsSg.js → AgentsView-UghV_I0u.js} +1 -1
- package/dist/ui/assets/ChecksView-C0TtYC-4.css +1 -0
- package/dist/ui/assets/ChecksView-CE9_UWEH.js +0 -0
- package/dist/ui/assets/{ContextView-B61GEm1Z.js → ContextView-C3cJribS.js} +2 -2
- package/dist/ui/assets/DashboardView-DydNm09c.js +1 -0
- package/dist/ui/assets/{DashboardView-CRdYYcWz.css → DashboardView-ZvEsCIs4.css} +1 -1
- package/dist/ui/assets/{DeploymentsView-BfRAdizb.js → DeploymentsView-DVMkUs04.js} +1 -1
- package/dist/ui/assets/{DiffView-DixEbWbX.js → DiffView-CCOUztfU.js} +1 -1
- package/dist/ui/assets/{InputsView-DZJ_-QCu.js → InputsView-uzRKFx_8.js} +1 -1
- package/dist/ui/assets/{LoginView-BhkLJwz2.js → LoginView-CV_1xAvv.js} +1 -1
- package/dist/ui/assets/{ReleasesView-Bc4mWRi1.js → ReleasesView-CMTPaJGR.js} +1 -1
- package/dist/ui/assets/{SettingsView-CaJPb-O7.js → SettingsView-3Y2d6VYS.js} +1 -1
- package/dist/ui/assets/StoriesView-DGnU5Cy9.js +1 -0
- package/dist/ui/assets/StoriesView-DTqtvuk_.css +1 -0
- package/dist/ui/assets/TaskCard-hX9bZtBs.js +1 -0
- package/dist/ui/assets/{WorkView-VjIRIAu3.js → WorkView-DpL4RTSC.js} +1 -1
- package/dist/ui/assets/{abnfDiagram-VCTEODGH-CsyiRZfO.js → abnfDiagram-VCTEODGH-CTfjwe9G.js} +1 -1
- package/dist/ui/assets/architecture-7GRP2DOG-iLDzG2hD.js +1 -0
- package/dist/ui/assets/{architectureDiagram-5GKGNRK7-B08h5dWI.js → architectureDiagram-5GKGNRK7-CVfIBdXM.js} +1 -1
- package/dist/ui/assets/{blockDiagram-I7D4REHJ-BPPeB-eQ.js → blockDiagram-I7D4REHJ-DHrovPE1.js} +1 -1
- package/dist/ui/assets/{c4Diagram-7LVT6UL2-BfADPuem.js → c4Diagram-7LVT6UL2-D1_IKyVU.js} +1 -1
- package/dist/ui/assets/channel-C2SsuvKM.js +1 -0
- package/dist/ui/assets/{chunk-4HAMMTFA-DOmKNumZ.js → chunk-4HAMMTFA-BQK2Qs9d.js} +1 -1
- package/dist/ui/assets/{chunk-75Z2AOVW-fvtO31Ky.js → chunk-75Z2AOVW-C560CV9I.js} +1 -1
- package/dist/ui/assets/{chunk-DU6HZSFF-egapwqdZ.js → chunk-DU6HZSFF-BX9aRA1h.js} +1 -1
- package/dist/ui/assets/{chunk-F27PBJKO-B4F_scL_.js → chunk-F27PBJKO-MId0AvWB.js} +1 -1
- package/dist/ui/assets/{chunk-GMAD6QVW-B0g7hXXo.js → chunk-GMAD6QVW-B8uV52pK.js} +1 -1
- package/dist/ui/assets/{chunk-GVQU2GXP-ByHTqLhx.js → chunk-GVQU2GXP-BHRWhJBs.js} +1 -1
- package/dist/ui/assets/{chunk-IMKFNOWR-BiX2WjGm.js → chunk-IMKFNOWR-C2p26tmJ.js} +1 -1
- package/dist/ui/assets/{chunk-L3NEJ4N5-Dtn3Nz1E.js → chunk-L3NEJ4N5-CiNIjzhG.js} +1 -1
- package/dist/ui/assets/{chunk-OSK3NFVY-D2FVEKbm.js → chunk-OSK3NFVY-DOZ8UOk-.js} +1 -1
- package/dist/ui/assets/{chunk-P2QGCYS3-DbpxXwpl.js → chunk-P2QGCYS3-CWkpd0sS.js} +1 -1
- package/dist/ui/assets/{chunk-POPQ4Y6H-wc9BY3N-.js → chunk-POPQ4Y6H-C_jAEIPq.js} +1 -1
- package/dist/ui/assets/{chunk-PWAF6VOD-CKBLutxN.js → chunk-PWAF6VOD-DlV-Z1d6.js} +1 -1
- package/dist/ui/assets/{chunk-SHT3W25Y-CtTrYLC0.js → chunk-SHT3W25Y-CDcQrQRH.js} +1 -1
- package/dist/ui/assets/{chunk-SVP7TREG-QwFEiE2U.js → chunk-SVP7TREG-CXMcWUhi.js} +1 -1
- package/dist/ui/assets/{chunk-TICWLB2K-DM5hYl74.js → chunk-TICWLB2K-C9NkrBRa.js} +1 -1
- package/dist/ui/assets/classDiagram-ZZMXUADV-DOc-KWik.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-DOc-KWik.js +1 -0
- package/dist/ui/assets/{cynefin-OW5HDTMX-DKbiErci.js → cynefin-OW5HDTMX-BOREGtlU.js} +1 -1
- package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-C_RVv0hh.js → cynefinDiagram-5FMLGOSQ-B4ry75uw.js} +1 -1
- package/dist/ui/assets/{dagre-GXQ25YYZ-BYlBs_VY.js → dagre-GXQ25YYZ-DaFuaEbQ.js} +1 -1
- package/dist/ui/assets/{diagram-S7CK7UJ4-BvYISuK2.js → diagram-S7CK7UJ4-CWKov5PL.js} +1 -1
- package/dist/ui/assets/{diagram-UQ7AKVKN-Bf4NWZw-.js → diagram-UQ7AKVKN-BIaDBoBe.js} +1 -1
- package/dist/ui/assets/{diagram-VSXAHHWV-DeBDsA1S.js → diagram-VSXAHHWV-BjTKaekf.js} +1 -1
- package/dist/ui/assets/{diagram-VX7I27RA-COq97Bh2.js → diagram-VX7I27RA-Yx3scpQA.js} +1 -1
- package/dist/ui/assets/{diagram-Z3DM3KII-DipozH1R.js → diagram-Z3DM3KII-Zj02SBoH.js} +1 -1
- package/dist/ui/assets/{ebnfDiagram-PWID7BFC-C7bjWaVV.js → ebnfDiagram-PWID7BFC-0YAqIwpj.js} +1 -1
- package/dist/ui/assets/{erDiagram-RLTQ6QDP-CkdV1U7x.js → erDiagram-RLTQ6QDP-D1qzM_C2.js} +1 -1
- package/dist/ui/assets/eventmodeling-NTZA5JFV-DoMudewO.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-COa-0qtC.js +1 -0
- package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-CpLssEGJ.js → ganttDiagram-EL5Y4UJY-BfFGqNAR.js} +1 -1
- package/dist/ui/assets/{gitGraph-4MIJSDKK-ZNvMSPlV.js → gitGraph-4MIJSDKK-C8Hio8mK.js} +1 -1
- package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-BLzUTHIQ.js → gitGraphDiagram-WWUBYQGX-zDzdEhVR.js} +1 -1
- package/dist/ui/assets/index-CxZVHdeX.js +35 -0
- package/dist/ui/assets/index-DCiwKRGz.css +1 -0
- package/dist/ui/assets/{info-A6RAGUB7-CIyWcXp1.js → info-A6RAGUB7-Co-frmaH.js} +1 -1
- package/dist/ui/assets/{infoDiagram-27XIBGKW-8e2Woehk.js → infoDiagram-27XIBGKW-C8vJrt_c.js} +1 -1
- package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-C7lXCzD0.js → ishikawaDiagram-5VMMS53U-BGO19EHJ.js} +1 -1
- package/dist/ui/assets/{journeyDiagram-3NMN7TZE-Cw7e6vEa.js → journeyDiagram-3NMN7TZE-DduoU3Je.js} +1 -1
- package/dist/ui/assets/{kanban-definition-UXKFOSKX-40gNKOtZ.js → kanban-definition-UXKFOSKX-D7pp8Jfx.js} +1 -1
- package/dist/ui/assets/{line-Dx_6SLjf.js → line-un3qDwwl.js} +1 -1
- package/dist/ui/assets/{mermaid-parser.core-Cx9IgWvp.js → mermaid-parser.core-BbfKCEpr.js} +3 -3
- package/dist/ui/assets/{mermaid.core-CE4Rcwvp.js → mermaid.core-BN7qhUmq.js} +4 -4
- package/dist/ui/assets/{mindmap-definition-YA3MSWOX-Bpjh59WU.js → mindmap-definition-YA3MSWOX-BYD5kmA5.js} +1 -1
- package/dist/ui/assets/{packet-AYTQ26CC-TzUU1fUR.js → packet-AYTQ26CC-C4FJPnFC.js} +1 -1
- package/dist/ui/assets/{pegDiagram-XKGWAZYB-xfGkTiyU.js → pegDiagram-XKGWAZYB-DsBi7Fms.js} +1 -1
- package/dist/ui/assets/{pie-WAS4IAKB-Cy0RJX4e.js → pie-WAS4IAKB-C79YZGPK.js} +1 -1
- package/dist/ui/assets/{pieDiagram-E7YTZNPT-C_wFzDa5.js → pieDiagram-E7YTZNPT-BkOWAtjO.js} +1 -1
- package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-D8nlig_S.js → quadrantDiagram-AXDQQJYC-4cKZyntK.js} +1 -1
- package/dist/ui/assets/{radar-RG4KPBEZ-DpEbf4Sa.js → radar-RG4KPBEZ-BRRdMOI4.js} +1 -1
- package/dist/ui/assets/{railroad-74A4TZTK-CSOahYzS.js → railroad-74A4TZTK-Bbhl18lT.js} +1 -1
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-DP10zylu.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-CD0qyNXa.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-B4QvCxtr.js +1 -0
- package/dist/ui/assets/{railroadDiagram-O6MQD6OU-CgaqmbL8.js → railroadDiagram-O6MQD6OU-Bbg60dv2.js} +1 -1
- package/dist/ui/assets/{requirementDiagram-BXWQKSXE-b5SPb0kK.js → requirementDiagram-BXWQKSXE-DoLwPzhb.js} +1 -1
- package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-DORlq3KF.js → sankeyDiagram-P5KCCOFB-BVBS_VZc.js} +1 -1
- package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-BwS_-jk2.js → sequenceDiagram-WJ2MYXX4-BACpdW4n.js} +1 -1
- package/dist/ui/assets/{stateDiagram-D77RDMKH-DfgJqXRY.js → stateDiagram-D77RDMKH-DlGZE9N1.js} +1 -1
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BAXOtgDm.js +1 -0
- package/dist/ui/assets/{swimlanes-42K2YHIH-aT_bNbqM.js → swimlanes-42K2YHIH-DLQ7Vteo.js} +1 -1
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BBsmj1pm.js +8 -0
- package/dist/ui/assets/{timeline-definition-24CTP7MA-V7APg65Z.js → timeline-definition-24CTP7MA-b99HY5j-.js} +1 -1
- package/dist/ui/assets/{treeView-Q6P3EWNA-CkX4_NCQ.js → treeView-Q6P3EWNA-BKXQAE6F.js} +1 -1
- package/dist/ui/assets/{treemap-WGGIJYW6-CNj80LcR.js → treemap-WGGIJYW6-CZj1ym9X.js} +1 -1
- package/dist/ui/assets/{vennDiagram-4TSXK5OY-Cp2p3H8R.js → vennDiagram-4TSXK5OY-Syj4tlui.js} +1 -1
- package/dist/ui/assets/{wardley-WFR3VGLG-DZlFTJK6.js → wardley-WFR3VGLG-W_snBoVU.js} +1 -1
- package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-Lnl-KAaI.js → wardleyDiagram-VM6X3IG4-BnpA19Oz.js} +1 -1
- package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-BJxTLbnv.js → xychartDiagram-S5SC5T6Z-g474RiA-.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/ChecksView-CDE2cfWk.js +0 -1
- package/dist/ui/assets/ChecksView-D79umAfM.css +0 -1
- package/dist/ui/assets/DashboardView-C5anCBko.js +0 -1
- package/dist/ui/assets/StoriesView-BdwyVyBi.css +0 -1
- package/dist/ui/assets/StoriesView-s-GXglwO.js +0 -1
- package/dist/ui/assets/TaskCard-CiDqR0-o.js +0 -1
- package/dist/ui/assets/architecture-7GRP2DOG-xBNBIq5T.js +0 -1
- package/dist/ui/assets/channel-BoRSmxIz.js +0 -1
- package/dist/ui/assets/classDiagram-ZZMXUADV-C0YZV-id.js +0 -1
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-C0YZV-id.js +0 -1
- package/dist/ui/assets/eventmodeling-NTZA5JFV-C7NKZ2E3.js +0 -1
- package/dist/ui/assets/flowDiagram-HODETNUW-BsiwhEl2.js +0 -1
- package/dist/ui/assets/index-B0otXprn.css +0 -1
- package/dist/ui/assets/index-nZg6iOq8.js +0 -35
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-8vJ8IZcu.js +0 -1
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-p9du8TJr.js +0 -1
- package/dist/ui/assets/railroad-peg-WCYAUIDC-BweyuB5K.js +0 -1
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-D7zefEr8.js +0 -1
- 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
|
-
|
|
312
|
-
[GitHub Releases](https://github.com/repo-os/repoos/releases)
|
|
313
|
-
|
|
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
|
|
package/dist/.build-info.json
CHANGED
package/dist/.build-stamp.json
CHANGED
package/dist/core/git.d.ts
CHANGED
|
@@ -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
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
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", "--",
|
|
1068
|
+
if (git(root, ["add", "-A", "--", ...rels]) === null)
|
|
1059
1069
|
return false;
|
|
1060
|
-
// `--only` with a pathspec commits just
|
|
1061
|
-
// already in the index staged (#0353): a plain `git commit` would sweep
|
|
1062
|
-
// whatever a human or agent had staged in the checkout under our message.
|
|
1063
|
-
return git(root, ["commit", "-o", "-m", message, "--",
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
33
|
-
|
|
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,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
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|