issue-flow 0.14.0 → 0.16.0

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 (155) hide show
  1. package/dist/{agent-OX3KPHE4.js → agent-PMWCDWOW.js} +12 -12
  2. package/dist/{analyze-WIND2SEB.js → analyze-556KCJKZ.js} +20 -20
  3. package/dist/{apply-JNOI2LR4.js → apply-WVV7G7WN.js} +12 -12
  4. package/dist/{bench-B4VZON7E.js → bench-UBFGKUHR.js} +18 -18
  5. package/dist/{chunk-2IQ6M4ZE.js → chunk-2H43OVTU.js} +11 -11
  6. package/dist/{chunk-PHW2YV6N.js → chunk-4FUWWBTP.js} +6 -6
  7. package/dist/{chunk-CGKLE6WR.js → chunk-73TS2H42.js} +14 -14
  8. package/dist/{chunk-LEW4QM74.js → chunk-AOAQU7PD.js} +2 -2
  9. package/dist/{chunk-WB2X37MO.js → chunk-BSBJXPLC.js} +8 -8
  10. package/dist/chunk-C5TL7T4V.js +27 -0
  11. package/dist/chunk-C5TL7T4V.js.map +1 -0
  12. package/dist/{chunk-76MMPJ53.js → chunk-DBEMD34J.js} +6 -6
  13. package/dist/{chunk-SZMHCEYV.js → chunk-EPFC4LHT.js} +9 -9
  14. package/dist/{chunk-3WFZETIM.js → chunk-FKWYD44M.js} +2 -2
  15. package/dist/{chunk-JN6D3MY6.js → chunk-G5LACAXO.js} +2 -2
  16. package/dist/{chunk-67YIU2VB.js → chunk-GPLP7EPL.js} +6 -6
  17. package/dist/{chunk-6BENHR2I.js → chunk-GV2DABNZ.js} +12 -12
  18. package/dist/{chunk-J3SOTUVI.js → chunk-GVOZ6NDO.js} +2 -2
  19. package/dist/{chunk-PZTWU277.js → chunk-H3EYBJZU.js} +7 -7
  20. package/dist/{chunk-3BWZO4YI.js → chunk-IAMIBZRF.js} +2 -2
  21. package/dist/{chunk-QJ5UZAH5.js → chunk-J2LJB2OH.js} +10 -10
  22. package/dist/{chunk-F6TOYSYY.js → chunk-KV2K7MK2.js} +9 -9
  23. package/dist/{chunk-REO3PXXO.js → chunk-MCLPSL6B.js} +3 -3
  24. package/dist/{chunk-OOVG2KBY.js → chunk-MXCOSPLJ.js} +2 -2
  25. package/dist/{chunk-KT7JKOXB.js → chunk-NQCR3HSU.js} +3 -3
  26. package/dist/{chunk-QD4VOILZ.js → chunk-OGN2NX57.js} +9 -9
  27. package/dist/{chunk-KMRFRXIT.js → chunk-OGVWJO4S.js} +3 -3
  28. package/dist/{chunk-X44FKNKP.js → chunk-OLUOZLU5.js} +42 -34
  29. package/dist/chunk-OLUOZLU5.js.map +1 -0
  30. package/dist/{chunk-RVO7L4FX.js → chunk-OOX3RMD3.js} +4 -4
  31. package/dist/{chunk-UJ3L2MS7.js → chunk-PGN2VX4Y.js} +6 -6
  32. package/dist/{chunk-QZVKRDJY.js → chunk-Q2TZA6AC.js} +11 -11
  33. package/dist/{chunk-LBGHI4S2.js → chunk-QVLIIGBU.js} +8 -3
  34. package/dist/chunk-QVLIIGBU.js.map +1 -0
  35. package/dist/{chunk-BRCNQXYQ.js → chunk-QZOBR6YF.js} +4 -4
  36. package/dist/{chunk-G5EK2XRN.js → chunk-R42YHLDV.js} +2 -2
  37. package/dist/{chunk-6QYMSNVA.js → chunk-ROIIEVZH.js} +3 -2
  38. package/dist/chunk-ROIIEVZH.js.map +1 -0
  39. package/dist/{chunk-TYRNTSPJ.js → chunk-SVDEWOUD.js} +4 -4
  40. package/dist/{chunk-B72RQZBV.js → chunk-SW2MF47D.js} +35 -20
  41. package/dist/chunk-SW2MF47D.js.map +1 -0
  42. package/dist/{chunk-V7MO5EDS.js → chunk-VK65RNFA.js} +3 -3
  43. package/dist/{chunk-QK7WUO7A.js → chunk-VTF4RWS5.js} +6 -6
  44. package/dist/{chunk-PBTA3QQX.js → chunk-WLJVL25O.js} +4 -4
  45. package/dist/{chunk-BEZ5BA5U.js → chunk-WNROH2YE.js} +2 -2
  46. package/dist/{chunk-OR43364P.js → chunk-X6GY3CM2.js} +3 -3
  47. package/dist/chunk-YA6FWFYW.js +891 -0
  48. package/dist/chunk-YA6FWFYW.js.map +1 -0
  49. package/dist/cli.js +61 -69
  50. package/dist/cli.js.map +1 -1
  51. package/dist/{config-HMFMCY2M.js → config-UE7IP36J.js} +11 -11
  52. package/dist/{conventions-NJF2OPVQ.js → conventions-Z3QC2NSG.js} +13 -13
  53. package/dist/{diagnostics-THH42ZMP.js → diagnostics-2Z5HFQOO.js} +6 -6
  54. package/dist/execute-QYLJ5L4T.js +36 -0
  55. package/dist/{generate-FW5C6NP7.js → generate-PB4L7V5N.js} +20 -20
  56. package/dist/{git-W72QQ6GT.js → git-7MUOGVRG.js} +2 -2
  57. package/dist/init-XBERAZ4O.js +24 -0
  58. package/dist/{operations-BCFOBH42.js → operations-QDJAFZUN.js} +13 -13
  59. package/dist/{permissions-4AKNQZHK.js → permissions-XVXXENVP.js} +3 -3
  60. package/dist/plan-RXOW32KY.js +34 -0
  61. package/dist/{policy-DT6Q6ZVM.js → policy-6WQZB6B2.js} +12 -12
  62. package/dist/pr-3WFYAVA7.js +39 -0
  63. package/dist/pr-review-RK63ZEMM.js +32 -0
  64. package/dist/prd-LRV6IW3E.js +33 -0
  65. package/dist/{ps-RFORTCUD.js → ps-CX7756QG.js} +10 -10
  66. package/dist/{recorder-KCCPMKQG.js → recorder-RK6XZYF3.js} +5 -5
  67. package/dist/{registry-GLPXOMFG.js → registry-POSB35Q4.js} +5 -5
  68. package/dist/{resume-M5RLKP4I.js → resume-OQJZV3JH.js} +37 -36
  69. package/dist/{resume-M5RLKP4I.js.map → resume-OQJZV3JH.js.map} +1 -1
  70. package/dist/review-AEFISJZD.js +35 -0
  71. package/dist/{routing-NNT5WKGX.js → routing-EA5NJGNU.js} +12 -12
  72. package/dist/run-HSC7R33Y.js +55 -0
  73. package/dist/{session-publisher-T7EI3CIP.js → session-publisher-VVXOCJ2S.js} +2 -2
  74. package/dist/{state-manager-GQH32C44.js → state-manager-TIC73GQY.js} +3 -3
  75. package/dist/{usage-V2CHE5PY.js → usage-WDDNSIHP.js} +10 -10
  76. package/dist/{web-EA3KGNFN.js → web-IPGY52AK.js} +21 -59
  77. package/dist/{web-EA3KGNFN.js.map → web-IPGY52AK.js.map} +1 -1
  78. package/package.json +1 -1
  79. package/web/AGENTS.md +9 -0
  80. package/web/public/app.css +13 -0
  81. package/web/public/app.js +77 -10
  82. package/web/public/index.html +6 -2
  83. package/dist/chunk-6QYMSNVA.js.map +0 -1
  84. package/dist/chunk-B72RQZBV.js.map +0 -1
  85. package/dist/chunk-LBGHI4S2.js.map +0 -1
  86. package/dist/chunk-LFEQIZLB.js +0 -558
  87. package/dist/chunk-LFEQIZLB.js.map +0 -1
  88. package/dist/chunk-X44FKNKP.js.map +0 -1
  89. package/dist/execute-KFNNZZHI.js +0 -35
  90. package/dist/init-HTN4V67Q.js +0 -24
  91. package/dist/plan-SIZLGA4V.js +0 -34
  92. package/dist/pr-OMWCGUOW.js +0 -39
  93. package/dist/pr-review-N24CZPKR.js +0 -32
  94. package/dist/prd-5OFTZAHJ.js +0 -33
  95. package/dist/review-YM4JAABY.js +0 -35
  96. package/dist/run-PZUKHPQK.js +0 -54
  97. /package/dist/{agent-OX3KPHE4.js.map → agent-PMWCDWOW.js.map} +0 -0
  98. /package/dist/{analyze-WIND2SEB.js.map → analyze-556KCJKZ.js.map} +0 -0
  99. /package/dist/{apply-JNOI2LR4.js.map → apply-WVV7G7WN.js.map} +0 -0
  100. /package/dist/{bench-B4VZON7E.js.map → bench-UBFGKUHR.js.map} +0 -0
  101. /package/dist/{chunk-2IQ6M4ZE.js.map → chunk-2H43OVTU.js.map} +0 -0
  102. /package/dist/{chunk-PHW2YV6N.js.map → chunk-4FUWWBTP.js.map} +0 -0
  103. /package/dist/{chunk-CGKLE6WR.js.map → chunk-73TS2H42.js.map} +0 -0
  104. /package/dist/{chunk-LEW4QM74.js.map → chunk-AOAQU7PD.js.map} +0 -0
  105. /package/dist/{chunk-WB2X37MO.js.map → chunk-BSBJXPLC.js.map} +0 -0
  106. /package/dist/{chunk-76MMPJ53.js.map → chunk-DBEMD34J.js.map} +0 -0
  107. /package/dist/{chunk-SZMHCEYV.js.map → chunk-EPFC4LHT.js.map} +0 -0
  108. /package/dist/{chunk-3WFZETIM.js.map → chunk-FKWYD44M.js.map} +0 -0
  109. /package/dist/{chunk-JN6D3MY6.js.map → chunk-G5LACAXO.js.map} +0 -0
  110. /package/dist/{chunk-67YIU2VB.js.map → chunk-GPLP7EPL.js.map} +0 -0
  111. /package/dist/{chunk-6BENHR2I.js.map → chunk-GV2DABNZ.js.map} +0 -0
  112. /package/dist/{chunk-J3SOTUVI.js.map → chunk-GVOZ6NDO.js.map} +0 -0
  113. /package/dist/{chunk-PZTWU277.js.map → chunk-H3EYBJZU.js.map} +0 -0
  114. /package/dist/{chunk-3BWZO4YI.js.map → chunk-IAMIBZRF.js.map} +0 -0
  115. /package/dist/{chunk-QJ5UZAH5.js.map → chunk-J2LJB2OH.js.map} +0 -0
  116. /package/dist/{chunk-F6TOYSYY.js.map → chunk-KV2K7MK2.js.map} +0 -0
  117. /package/dist/{chunk-REO3PXXO.js.map → chunk-MCLPSL6B.js.map} +0 -0
  118. /package/dist/{chunk-OOVG2KBY.js.map → chunk-MXCOSPLJ.js.map} +0 -0
  119. /package/dist/{chunk-KT7JKOXB.js.map → chunk-NQCR3HSU.js.map} +0 -0
  120. /package/dist/{chunk-QD4VOILZ.js.map → chunk-OGN2NX57.js.map} +0 -0
  121. /package/dist/{chunk-KMRFRXIT.js.map → chunk-OGVWJO4S.js.map} +0 -0
  122. /package/dist/{chunk-RVO7L4FX.js.map → chunk-OOX3RMD3.js.map} +0 -0
  123. /package/dist/{chunk-UJ3L2MS7.js.map → chunk-PGN2VX4Y.js.map} +0 -0
  124. /package/dist/{chunk-QZVKRDJY.js.map → chunk-Q2TZA6AC.js.map} +0 -0
  125. /package/dist/{chunk-BRCNQXYQ.js.map → chunk-QZOBR6YF.js.map} +0 -0
  126. /package/dist/{chunk-G5EK2XRN.js.map → chunk-R42YHLDV.js.map} +0 -0
  127. /package/dist/{chunk-TYRNTSPJ.js.map → chunk-SVDEWOUD.js.map} +0 -0
  128. /package/dist/{chunk-V7MO5EDS.js.map → chunk-VK65RNFA.js.map} +0 -0
  129. /package/dist/{chunk-QK7WUO7A.js.map → chunk-VTF4RWS5.js.map} +0 -0
  130. /package/dist/{chunk-PBTA3QQX.js.map → chunk-WLJVL25O.js.map} +0 -0
  131. /package/dist/{chunk-BEZ5BA5U.js.map → chunk-WNROH2YE.js.map} +0 -0
  132. /package/dist/{chunk-OR43364P.js.map → chunk-X6GY3CM2.js.map} +0 -0
  133. /package/dist/{config-HMFMCY2M.js.map → config-UE7IP36J.js.map} +0 -0
  134. /package/dist/{conventions-NJF2OPVQ.js.map → conventions-Z3QC2NSG.js.map} +0 -0
  135. /package/dist/{diagnostics-THH42ZMP.js.map → diagnostics-2Z5HFQOO.js.map} +0 -0
  136. /package/dist/{execute-KFNNZZHI.js.map → execute-QYLJ5L4T.js.map} +0 -0
  137. /package/dist/{generate-FW5C6NP7.js.map → generate-PB4L7V5N.js.map} +0 -0
  138. /package/dist/{git-W72QQ6GT.js.map → git-7MUOGVRG.js.map} +0 -0
  139. /package/dist/{init-HTN4V67Q.js.map → init-XBERAZ4O.js.map} +0 -0
  140. /package/dist/{operations-BCFOBH42.js.map → operations-QDJAFZUN.js.map} +0 -0
  141. /package/dist/{permissions-4AKNQZHK.js.map → permissions-XVXXENVP.js.map} +0 -0
  142. /package/dist/{plan-SIZLGA4V.js.map → plan-RXOW32KY.js.map} +0 -0
  143. /package/dist/{policy-DT6Q6ZVM.js.map → policy-6WQZB6B2.js.map} +0 -0
  144. /package/dist/{pr-OMWCGUOW.js.map → pr-3WFYAVA7.js.map} +0 -0
  145. /package/dist/{pr-review-N24CZPKR.js.map → pr-review-RK63ZEMM.js.map} +0 -0
  146. /package/dist/{prd-5OFTZAHJ.js.map → prd-LRV6IW3E.js.map} +0 -0
  147. /package/dist/{ps-RFORTCUD.js.map → ps-CX7756QG.js.map} +0 -0
  148. /package/dist/{recorder-KCCPMKQG.js.map → recorder-RK6XZYF3.js.map} +0 -0
  149. /package/dist/{registry-GLPXOMFG.js.map → registry-POSB35Q4.js.map} +0 -0
  150. /package/dist/{review-YM4JAABY.js.map → review-AEFISJZD.js.map} +0 -0
  151. /package/dist/{routing-NNT5WKGX.js.map → routing-EA5NJGNU.js.map} +0 -0
  152. /package/dist/{run-PZUKHPQK.js.map → run-HSC7R33Y.js.map} +0 -0
  153. /package/dist/{session-publisher-T7EI3CIP.js.map → session-publisher-VVXOCJ2S.js.map} +0 -0
  154. /package/dist/{state-manager-GQH32C44.js.map → state-manager-TIC73GQY.js.map} +0 -0
  155. /package/dist/{usage-V2CHE5PY.js.map → usage-WDDNSIHP.js.map} +0 -0
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ publishIssueDetails,
4
+ publishStorySeed,
5
+ runPipeline
6
+ } from "./chunk-OLUOZLU5.js";
7
+ import "./chunk-VK65RNFA.js";
8
+ import "./chunk-Q2TZA6AC.js";
9
+ import "./chunk-OGN2NX57.js";
10
+ import "./chunk-EPFC4LHT.js";
11
+ import "./chunk-X6GY3CM2.js";
12
+ import "./chunk-KV2K7MK2.js";
13
+ import "./chunk-SW2MF47D.js";
14
+ import "./chunk-YA6FWFYW.js";
15
+ import "./chunk-QZOBR6YF.js";
16
+ import "./chunk-BSBJXPLC.js";
17
+ import "./chunk-CRGIQFUQ.js";
18
+ import "./chunk-L6HSKTQE.js";
19
+ import "./chunk-PGN2VX4Y.js";
20
+ import "./chunk-GV2DABNZ.js";
21
+ import "./chunk-2H43OVTU.js";
22
+ import "./chunk-WLJVL25O.js";
23
+ import "./chunk-DBEMD34J.js";
24
+ import "./chunk-MCLPSL6B.js";
25
+ import "./chunk-H3EYBJZU.js";
26
+ import "./chunk-KMMASOA6.js";
27
+ import "./chunk-4FUWWBTP.js";
28
+ import "./chunk-OGVWJO4S.js";
29
+ import "./chunk-73TS2H42.js";
30
+ import "./chunk-AOAQU7PD.js";
31
+ import "./chunk-IAMIBZRF.js";
32
+ import "./chunk-MXCOSPLJ.js";
33
+ import "./chunk-C5TL7T4V.js";
34
+ import "./chunk-3D4A4B46.js";
35
+ import "./chunk-OOX3RMD3.js";
36
+ import "./chunk-J2LJB2OH.js";
37
+ import "./chunk-VTF4RWS5.js";
38
+ import "./chunk-SVDEWOUD.js";
39
+ import "./chunk-ROIIEVZH.js";
40
+ import "./chunk-FKWYD44M.js";
41
+ import "./chunk-HAKDLR6N.js";
42
+ import "./chunk-WNROH2YE.js";
43
+ import "./chunk-GPLP7EPL.js";
44
+ import "./chunk-P3LRUNC6.js";
45
+ import "./chunk-R42YHLDV.js";
46
+ import "./chunk-GVOZ6NDO.js";
47
+ import "./chunk-QVLIIGBU.js";
48
+ import "./chunk-7Z4Y3VKM.js";
49
+ import "./chunk-XEYH6ZZI.js";
50
+ export {
51
+ publishIssueDetails,
52
+ publishStorySeed,
53
+ runPipeline
54
+ };
55
+ //# sourceMappingURL=run-HSC7R33Y.js.map
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  getSessionPublisher,
4
4
  setSessionPublisher
5
- } from "./chunk-6QYMSNVA.js";
5
+ } from "./chunk-ROIIEVZH.js";
6
6
  import "./chunk-P3LRUNC6.js";
7
7
  export {
8
8
  getSessionPublisher,
9
9
  setSessionPublisher
10
10
  };
11
- //# sourceMappingURL=session-publisher-T7EI3CIP.js.map
11
+ //# sourceMappingURL=session-publisher-VVXOCJ2S.js.map
@@ -14,8 +14,8 @@ import {
14
14
  selectActiveStory,
15
15
  setLastError,
16
16
  trimErrorMessage
17
- } from "./chunk-J3SOTUVI.js";
18
- import "./chunk-LBGHI4S2.js";
17
+ } from "./chunk-GVOZ6NDO.js";
18
+ import "./chunk-QVLIIGBU.js";
19
19
  import "./chunk-7Z4Y3VKM.js";
20
20
  export {
21
21
  allStoriesPass,
@@ -33,4 +33,4 @@ export {
33
33
  setLastError,
34
34
  trimErrorMessage
35
35
  };
36
- //# sourceMappingURL=state-manager-GQH32C44.js.map
36
+ //# sourceMappingURL=state-manager-TIC73GQY.js.map
@@ -5,26 +5,26 @@ import {
5
5
  } from "./chunk-CRGIQFUQ.js";
6
6
  import {
7
7
  resolveProjectPaths
8
- } from "./chunk-QK7WUO7A.js";
8
+ } from "./chunk-VTF4RWS5.js";
9
9
  import {
10
10
  printError,
11
11
  printInfo
12
- } from "./chunk-TYRNTSPJ.js";
13
- import "./chunk-6QYMSNVA.js";
14
- import "./chunk-3WFZETIM.js";
12
+ } from "./chunk-SVDEWOUD.js";
13
+ import "./chunk-ROIIEVZH.js";
14
+ import "./chunk-FKWYD44M.js";
15
15
  import {
16
16
  getIssuePaths
17
17
  } from "./chunk-HAKDLR6N.js";
18
- import "./chunk-BEZ5BA5U.js";
18
+ import "./chunk-WNROH2YE.js";
19
19
  import {
20
20
  discardedExecutionCount
21
- } from "./chunk-67YIU2VB.js";
21
+ } from "./chunk-GPLP7EPL.js";
22
22
  import "./chunk-P3LRUNC6.js";
23
- import "./chunk-G5EK2XRN.js";
23
+ import "./chunk-R42YHLDV.js";
24
24
  import {
25
25
  loadTaskPlan
26
- } from "./chunk-J3SOTUVI.js";
27
- import "./chunk-LBGHI4S2.js";
26
+ } from "./chunk-GVOZ6NDO.js";
27
+ import "./chunk-QVLIIGBU.js";
28
28
  import "./chunk-7Z4Y3VKM.js";
29
29
  import "./chunk-XEYH6ZZI.js";
30
30
 
@@ -174,4 +174,4 @@ export {
174
174
  formatUsageReport,
175
175
  runUsage
176
176
  };
177
- //# sourceMappingURL=usage-V2CHE5PY.js.map
177
+ //# sourceMappingURL=usage-WDDNSIHP.js.map
@@ -1,29 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readSessionFile
4
- } from "./chunk-JN6D3MY6.js";
4
+ } from "./chunk-G5LACAXO.js";
5
5
  import {
6
6
  ensureSingleWebServer,
7
- getWebLockFile,
8
- readWebLock,
9
- removeWebLock
10
- } from "./chunk-LFEQIZLB.js";
11
- import "./chunk-UJ3L2MS7.js";
12
- import "./chunk-KMRFRXIT.js";
7
+ stopWebMonitor
8
+ } from "./chunk-YA6FWFYW.js";
9
+ import "./chunk-PGN2VX4Y.js";
10
+ import "./chunk-OGVWJO4S.js";
13
11
  import {
14
12
  parseJournal
15
- } from "./chunk-3BWZO4YI.js";
13
+ } from "./chunk-IAMIBZRF.js";
14
+ import "./chunk-C5TL7T4V.js";
16
15
  import {
17
16
  loadWebConfig
18
- } from "./chunk-QJ5UZAH5.js";
19
- import "./chunk-QK7WUO7A.js";
20
- import {
21
- printError,
22
- printInfo,
23
- printWarning
24
- } from "./chunk-TYRNTSPJ.js";
25
- import "./chunk-6QYMSNVA.js";
26
- import "./chunk-3WFZETIM.js";
17
+ } from "./chunk-J2LJB2OH.js";
18
+ import "./chunk-VTF4RWS5.js";
19
+ import "./chunk-SVDEWOUD.js";
20
+ import "./chunk-ROIIEVZH.js";
21
+ import "./chunk-FKWYD44M.js";
27
22
  import {
28
23
  EVENTS_FILENAME,
29
24
  ISSUES_DIR_NAME,
@@ -32,12 +27,12 @@ import {
32
27
  SESSION_FILENAME,
33
28
  getGlobalRoot
34
29
  } from "./chunk-HAKDLR6N.js";
35
- import "./chunk-BEZ5BA5U.js";
36
- import "./chunk-67YIU2VB.js";
30
+ import "./chunk-WNROH2YE.js";
31
+ import "./chunk-GPLP7EPL.js";
37
32
  import "./chunk-P3LRUNC6.js";
38
- import "./chunk-G5EK2XRN.js";
39
- import "./chunk-J3SOTUVI.js";
40
- import "./chunk-LBGHI4S2.js";
33
+ import "./chunk-R42YHLDV.js";
34
+ import "./chunk-GVOZ6NDO.js";
35
+ import "./chunk-QVLIIGBU.js";
41
36
  import "./chunk-7Z4Y3VKM.js";
42
37
  import "./chunk-XEYH6ZZI.js";
43
38
 
@@ -130,11 +125,6 @@ function watchSessionDirectory(options = {}) {
130
125
  }
131
126
 
132
127
  // src/commands/web.ts
133
- var STOP_POLL_INTERVAL_MS = 100;
134
- var STOP_TIMEOUT_MS = 5e3;
135
- function sleep(ms) {
136
- return new Promise((resolve) => setTimeout(resolve, ms));
137
- }
138
128
  async function runWebServe(options) {
139
129
  const cli = {};
140
130
  if (options.port !== void 0) cli.port = options.port;
@@ -169,40 +159,12 @@ async function runWebServe(options) {
169
159
  return 0;
170
160
  }
171
161
  async function runWebStop() {
172
- const lockFile = getWebLockFile();
173
- const lock = await readWebLock(lockFile);
174
- if (lock === null) {
175
- printInfo("No web monitor is currently running.");
176
- return 0;
177
- }
178
- try {
179
- process.kill(lock.pid, "SIGTERM");
180
- } catch (err) {
181
- if (err.code === "ESRCH") {
182
- printWarning(
183
- "The web monitor lock referenced a process that is no longer running; removing it."
184
- );
185
- await removeWebLock(lockFile);
186
- return 0;
187
- }
188
- printError(
189
- `Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`
190
- );
191
- return 1;
192
- }
193
- const deadline = Date.now() + STOP_TIMEOUT_MS;
194
- while (Date.now() < deadline) {
195
- if (await readWebLock(lockFile) === null) {
196
- printInfo(`Web monitor (pid ${lock.pid}) stopped.`);
197
- return 0;
198
- }
199
- await sleep(STOP_POLL_INTERVAL_MS);
200
- }
201
- printWarning("Sent the stop signal, but the web monitor did not confirm shutdown in time.");
202
- return 0;
162
+ const webConfig = await loadWebConfig();
163
+ const result = await stopWebMonitor({ port: webConfig.port, host: webConfig.host });
164
+ return result === "failed" || result === "unowned" ? 1 : 0;
203
165
  }
204
166
  export {
205
167
  runWebServe,
206
168
  runWebStop
207
169
  };
208
- //# sourceMappingURL=web-EA3KGNFN.js.map
170
+ //# sourceMappingURL=web-IPGY52AK.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type JournalEntry, parseJournal } from '../core/journal.js';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport {\n EVENTS_FILENAME,\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n SESSION_FILENAME,\n} from '../storage/paths.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` touches a live session every 10s without rewriting\n * its content. Three missed heartbeats plus ample scheduling/filesystem slack\n * avoid flapping while still removing an abruptly killed run promptly.\n */\nexport const DEFAULT_STALE_AFTER_MS = 90_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Journal entries for one active session, oldest generation first. */\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));\n }),\n );\n\n return perProject.flat();\n}\n\nasync function readJournalFile(filePath: string): Promise<JournalEntry[]> {\n try {\n return parseJournal(await readFile(filePath, 'utf-8'));\n } catch {\n return [];\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n events: async (sessionId: string) => {\n const session = sessions.get(sessionId);\n if (session === undefined) return undefined;\n const [rotated, current] = await Promise.all([\n readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),\n readJournalFile(join(session.issueDir, EVENTS_FILENAME)),\n ]);\n return [...rotated, ...current];\n },\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { printError, printInfo, printWarning } from '../ui/logger.js';\nimport { ensureSingleWebServer, getWebLockFile, readWebLock, removeWebLock } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nconst STOP_POLL_INTERVAL_MS = 100;\nconst STOP_TIMEOUT_MS = 5000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const lockFile = getWebLockFile();\n const lock = await readWebLock(lockFile);\n if (lock === null) {\n printInfo('No web monitor is currently running.');\n return 0;\n }\n\n try {\n process.kill(lock.pid, 'SIGTERM');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ESRCH') {\n printWarning(\n 'The web monitor lock referenced a process that is no longer running; removing it.',\n );\n await removeWebLock(lockFile);\n return 0;\n }\n printError(\n `Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if ((await readWebLock(lockFile)) === null) {\n printInfo(`Web monitor (pid ${lock.pid}) stopped.`);\n return 0;\n }\n await sleep(STOP_POLL_INTERVAL_MS);\n }\n\n printWarning('Sent the stop signal, but the web monitor did not confirm shutdown in time.');\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,SAAS,gBAAgB;AAClC,SAAS,YAAY;AAkCd,IAAM,2BAA2B;AAQjC,IAAM,yBAAyB;AAiCtC,eAAe,mBAAmB,KAAgC;AAChE,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAI1D,WAAO,QAAQ,OAAO,CAAC,UAAU,MAAM,YAAY,CAAC,EAAE,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,EACjF,QAAQ;AAGN,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAe,qBAAqB,MAAiC;AACnE,QAAM,cAAc,KAAK,MAAM,iBAAiB;AAChD,QAAM,aAAa,MAAM,mBAAmB,WAAW;AAEvD,QAAM,aAAa,MAAM,QAAQ;AAAA,IAC/B,WAAW,IAAI,OAAO,cAAc;AAClC,YAAM,YAAY,KAAK,aAAa,WAAW,eAAe;AAC9D,YAAM,WAAW,MAAM,mBAAmB,SAAS;AACnD,aAAO,SAAS,IAAI,CAAC,YAAY,KAAK,WAAW,SAAS,gBAAgB,CAAC;AAAA,IAC7E,CAAC;AAAA,EACH;AAEA,SAAO,WAAW,KAAK;AACzB;AAEA,eAAe,gBAAgB,UAA2C;AACxE,MAAI;AACF,WAAO,aAAa,MAAM,SAAS,UAAU,OAAO,CAAC;AAAA,EACvD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAUO,SAAS,sBACd,UAAmC,CAAC,GACZ;AACxB,QAAM,OAAO,cAAc,OAAO;AAClC,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,OAAO,QAAQ;AAIrB,MAAI,WAAW,oBAAI,IAA2B;AAC9C,MAAI,SAAS;AAEb,iBAAe,OAAsB;AACnC,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,qBAAqB,IAAI;AAAA,IACzC,SAAS,KAAK;AACZ,UAAI,CAAC,QAAQ;AACX,iBAAS;AACT;AAAA,UACE,+FACE,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,QACF;AAAA,MACF;AACA;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,OAAO,oBAAI,IAA2B;AAC5C,eAAW,YAAY,OAAO;AAC5B,YAAM,SAAS,MAAM,gBAAgB,QAAQ;AAC7C,UAAI,WAAW,KAAM;AACrB,UAAI,MAAM,OAAO,cAAc,aAAc;AAC7C,YAAM,YAAY,OAAO,SAAS;AAClC,WAAK,IAAI,WAAW;AAAA,QAClB,UAAU,KAAK,UAAU,IAAI;AAAA,QAC7B;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,aAAa,OAAO;AAAA,MACtB,CAAC;AAAA,IACH;AACA,eAAW;AAAA,EACb;AAKA,OAAK,KAAK;AACV,QAAM,QAAQ,YAAY,MAAM,KAAK,KAAK,GAAG,cAAc;AAC3D,QAAM,MAAM;AAEZ,SAAO;AAAA,IACL,UAAU,MAAM,CAAC,GAAG,SAAS,OAAO,CAAC;AAAA,IACrC,YAAY,CAAC,cAAsB,SAAS,IAAI,SAAS;AAAA,IACzD,QAAQ,OAAO,cAAsB;AACnC,YAAM,UAAU,SAAS,IAAI,SAAS;AACtC,UAAI,YAAY,OAAW,QAAO;AAClC,YAAM,CAAC,SAAS,OAAO,IAAI,MAAM,QAAQ,IAAI;AAAA,QAC3C,gBAAgB,KAAK,QAAQ,UAAU,uBAAuB,CAAC;AAAA,QAC/D,gBAAgB,KAAK,QAAQ,UAAU,eAAe,CAAC;AAAA,MACzD,CAAC;AACD,aAAO,CAAC,GAAG,SAAS,GAAG,OAAO;AAAA,IAChC;AAAA,IACA,SAAS;AAAA,IACT,OAAO,MAAM,cAAc,KAAK;AAAA,EAClC;AACF;;;AC5KA,IAAM,wBAAwB;AAC9B,IAAM,kBAAkB;AAExB,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAeA,eAAsB,YAAY,SAA8C;AAK9E,QAAM,MAA0B,CAAC;AACjC,MAAI,QAAQ,SAAS,OAAW,KAAI,OAAO,QAAQ;AACnD,MAAI,QAAQ,SAAS,OAAW,KAAI,OAAO,QAAQ;AACnD,MAAI,QAAQ,YAAY,OAAW,KAAI,iBAAiB,QAAQ;AAChE,QAAM,YAAY,MAAM,cAAc,EAAE,IAAI,CAAC;AAE7C,QAAM,WAAW,sBAAsB;AAEvC,QAAM,OAAO,MAAY;AAAA,EAAC;AAC1B,QAAM,SAAS,MAAM,sBAAsB;AAAA,IACzC;AAAA,IACA,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,IAChB,gBAAgB,UAAU;AAAA,IAC1B,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAED,MAAI,WAAW,MAAM;AACnB,aAAS,MAAM;AACf,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,WAAW,QAAW;AAI/B,aAAS,MAAM;AACf,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,OAAO;AAC7B,SAAO,QAAQ,YAAY;AACzB,UAAM,cAAc;AACpB,aAAS,MAAM;AAAA,EACjB;AAEA,SAAO;AACT;AAGA,eAAsB,aAA8B;AAClD,QAAM,WAAW,eAAe;AAChC,QAAM,OAAO,MAAM,YAAY,QAAQ;AACvC,MAAI,SAAS,MAAM;AACjB,cAAU,sCAAsC;AAChD,WAAO;AAAA,EACT;AAEA,MAAI;AACF,YAAQ,KAAK,KAAK,KAAK,SAAS;AAAA,EAClC,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,SAAS;AACnD;AAAA,QACE;AAAA,MACF;AACA,YAAM,cAAc,QAAQ;AAC5B,aAAO;AAAA,IACT;AACA;AAAA,MACE,mCAAmC,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IACrF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,QAAK,MAAM,YAAY,QAAQ,MAAO,MAAM;AAC1C,gBAAU,oBAAoB,KAAK,GAAG,YAAY;AAClD,aAAO;AAAA,IACT;AACA,UAAM,MAAM,qBAAqB;AAAA,EACnC;AAEA,eAAa,6EAA6E;AAC1F,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type JournalEntry, parseJournal } from '../core/journal.js';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport {\n EVENTS_FILENAME,\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n SESSION_FILENAME,\n} from '../storage/paths.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` touches a live session every 10s without rewriting\n * its content. Three missed heartbeats plus ample scheduling/filesystem slack\n * avoid flapping while still removing an abruptly killed run promptly.\n */\nexport const DEFAULT_STALE_AFTER_MS = 90_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Journal entries for one active session, oldest generation first. */\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));\n }),\n );\n\n return perProject.flat();\n}\n\nasync function readJournalFile(filePath: string): Promise<JournalEntry[]> {\n try {\n return parseJournal(await readFile(filePath, 'utf-8'));\n } catch {\n return [];\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n events: async (sessionId: string) => {\n const session = sessions.get(sessionId);\n if (session === undefined) return undefined;\n const [rotated, current] = await Promise.all([\n readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),\n readJournalFile(join(session.issueDir, EVENTS_FILENAME)),\n ]);\n return [...rotated, ...current];\n },\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { ensureSingleWebServer, stopWebMonitor } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const webConfig = await loadWebConfig();\n const result = await stopWebMonitor({ port: webConfig.port, host: webConfig.host });\n return result === 'failed' || result === 'unowned' ? 1 : 0;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "issue-flow",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "Unified CLI for orchestrating the full issue-flow pipeline via Claude Code, Codex CLI, Cursor CLI or Antigravity CLI",
5
5
  "type": "module",
6
6
  "bin": {
package/web/AGENTS.md CHANGED
@@ -18,6 +18,15 @@ informado). Os dois significam "não informado" e nunca podem virar `0`,
18
18
  qualquer coisa que não seja número finito para `null`. Prefira
19
19
  `x !== null && x !== undefined` a `!x`: zero é um valor legítimo.
20
20
 
21
+ O chip de versão no header é a versão do **monitor** (`/api/health`), não a da
22
+ CLI que executa o pipeline: os assets desta página vêm da memória daquele
23
+ processo, então é ela que explica o que está na tela. As duas aparecem juntas no
24
+ card de configuração, e a divergência entre elas vira um aviso ali.
25
+
26
+ Toda resposta nova carrega `X-Issue-Flow-Instance`. O client guarda a primeira
27
+ identidade observada e chama `window.location.reload()` quando ela muda: isso é
28
+ o handoff de assets depois de `--restart-web`, não um estado de sessão.
29
+
21
30
  Com **uma** sessão ativa o painel abre direto no detalhe (comportamento
22
31
  histórico). Com **duas ou mais**, `renderDashboard()` lista um card por
23
32
  execução; o clique define `state.selectedSessionId` e o poll passa a usar o
@@ -266,6 +266,19 @@ a:hover {
266
266
  font-weight: 700;
267
267
  }
268
268
 
269
+ /* Versão do processo que serve o painel. Par medido: --text-muted sobre
270
+ --surface-sunken (6,10 claro / 5,33 escuro). */
271
+ .app-version {
272
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
273
+ font-size: 0.72rem;
274
+ font-weight: 500;
275
+ padding: 1px 7px;
276
+ border-radius: 999px;
277
+ background: var(--surface-sunken);
278
+ color: var(--text-muted);
279
+ vertical-align: middle;
280
+ }
281
+
269
282
  .issue-link {
270
283
  font-weight: 600;
271
284
  }
package/web/public/app.js CHANGED
@@ -89,6 +89,8 @@
89
89
  now: document.getElementById('now'),
90
90
  resilience: document.getElementById('resilience'),
91
91
  configuration: document.getElementById('configuration'),
92
+ appVersion: document.getElementById('app-version'),
93
+ appVersionDashboard: document.getElementById('app-version-dashboard'),
92
94
  phases: document.getElementById('phases'),
93
95
  nextSteps: document.getElementById('next-steps'),
94
96
  stories: document.getElementById('stories'),
@@ -127,6 +129,11 @@
127
129
  // guardar o nó levaria a uma referência morta.
128
130
  selectedDetail: null,
129
131
  configWritable: false,
132
+ // Identifica o processo que serviu os assets atuais. Uma troca significa
133
+ // que --restart-web colocou código novo no mesmo origin e exige reload.
134
+ serverInstanceId: null,
135
+ // Versão do monitor, vinda de /api/health. Null enquanto não respondeu.
136
+ monitorVersion: null,
130
137
  // Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
131
138
  // selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
132
139
  sessions: [],
@@ -528,6 +535,35 @@
528
535
  state.eventsUrl = null;
529
536
  }
530
537
 
538
+ // A versão exibida no header é a do processo que serviu esta página, nunca a
539
+ // da CLI que iniciou a execução: os assets vivem na memória desse processo,
540
+ // então é ela que explica o que está na tela. As duas aparecem juntas no card
541
+ // de configuração quando divergem.
542
+ function renderMonitorVersion(version) {
543
+ const label = typeof version === 'string' && version !== '' ? 'v' + version : null;
544
+ state.monitorVersion = label === null ? null : version;
545
+ for (const node of [els.appVersion, els.appVersionDashboard]) {
546
+ if (!node) continue;
547
+ node.textContent = label || '';
548
+ node.title = label === null ? '' : 'Versão do monitor que serve este painel';
549
+ node.hidden = label === null;
550
+ }
551
+ }
552
+
553
+ function serverInstanceChanged(response) {
554
+ const instanceId = response.headers.get('X-Issue-Flow-Instance');
555
+ if (!instanceId) return false; // servidor anterior à identidade de instância
556
+ if (state.serverInstanceId === null) {
557
+ state.serverInstanceId = instanceId;
558
+ return false;
559
+ }
560
+ if (state.serverInstanceId !== instanceId) {
561
+ window.location.reload();
562
+ return true;
563
+ }
564
+ return false;
565
+ }
566
+
531
567
  function requestPoll() {
532
568
  clearTimer();
533
569
  if (state.polling) {
@@ -560,6 +596,7 @@
560
596
  state.pollAgain = false;
561
597
  try {
562
598
  const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
599
+ if (serverInstanceChanged(sessionsRes)) return;
563
600
  if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
564
601
  const sessions = await sessionsRes.json();
565
602
  if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
@@ -1032,6 +1069,34 @@
1032
1069
 
1033
1070
  function renderConfiguration(snapshot) {
1034
1071
  clear(els.configuration);
1072
+
1073
+ // Quem executou e quem está mostrando. São processos diferentes e podem
1074
+ // estar em versões diferentes; este é o único lugar onde as duas aparecem
1075
+ // lado a lado, então a divergência é dita aqui.
1076
+ const env = snapshot.environment;
1077
+ const runVersion = env && env.cliVersion ? env.cliVersion : null;
1078
+ const runtime = el('dl', 'now-grid');
1079
+ const runtimeBits = [runVersion ? 'v' + runVersion : 'versão não registrada'];
1080
+ if (env && env.node) runtimeBits.push(env.node);
1081
+ if (env && env.platform) runtimeBits.push(env.platform);
1082
+ nowRow(runtime, 'Issue Flow (execução)', runtimeBits.join(' · '));
1083
+ nowRow(
1084
+ runtime,
1085
+ 'Monitor (este painel)',
1086
+ state.monitorVersion ? 'v' + state.monitorVersion : '—',
1087
+ );
1088
+ els.configuration.appendChild(runtime);
1089
+ if (runVersion !== null && state.monitorVersion && runVersion !== state.monitorVersion) {
1090
+ els.configuration.appendChild(
1091
+ el(
1092
+ 'p',
1093
+ 'alert-entry level-warn',
1094
+ 'Este painel é servido por uma versão diferente da que executa o pipeline. ' +
1095
+ 'Reinicie o monitor com --restart-web para ver a interface desta versão.',
1096
+ ),
1097
+ );
1098
+ }
1099
+
1035
1100
  const config = snapshot.configuration;
1036
1101
  if (!config) {
1037
1102
  els.configuration.appendChild(el('p', 'empty', 'Configuração não capturada nesta execução.'));
@@ -1721,18 +1786,20 @@
1721
1786
  // Default do seletor vem da configuração do servidor (/api/health);
1722
1787
  // a escolha do usuário em localStorage tem precedência.
1723
1788
  const stored = readStoredRefresh();
1724
- if (stored !== null) {
1725
- state.refreshSeconds = stored;
1726
- } else {
1727
- try {
1728
- const health = await fetch('api/health', { cache: 'no-store' }).then((r) => r.json());
1729
- state.configWritable = list(health.capabilities).includes('config:agent:write');
1730
- const suggested = Number(health.refreshSeconds);
1731
- if (Number.isFinite(suggested) && suggested > 0) state.refreshSeconds = suggested;
1732
- } catch (err) {
1733
- // Sem /api/health segue o default local (5s).
1789
+ try {
1790
+ const healthRes = await fetch('api/health', { cache: 'no-store' });
1791
+ serverInstanceChanged(healthRes);
1792
+ const health = await healthRes.json();
1793
+ renderMonitorVersion(health.version);
1794
+ state.configWritable = list(health.capabilities).includes('config:agent:write');
1795
+ const suggested = Number(health.refreshSeconds);
1796
+ if (stored === null && Number.isFinite(suggested) && suggested > 0) {
1797
+ state.refreshSeconds = suggested;
1734
1798
  }
1799
+ } catch (err) {
1800
+ // Sem /api/health segue o default local (5s).
1735
1801
  }
1802
+ if (stored !== null) state.refreshSeconds = stored;
1736
1803
  buildRefreshSelect();
1737
1804
 
1738
1805
  window.setInterval(renderTimers, 1000);
@@ -37,7 +37,10 @@
37
37
  <div id="view-dashboard" class="view" hidden>
38
38
  <header class="card header">
39
39
  <div class="header-main">
40
- <h1><span class="brand">issue-flow</span></h1>
40
+ <h1>
41
+ <span class="brand">issue-flow</span>
42
+ <span id="app-version-dashboard" class="app-version" hidden></span>
43
+ </h1>
41
44
  <p class="muted">Execuções ativas</p>
42
45
  </div>
43
46
  <div class="header-side">
@@ -69,6 +72,7 @@
69
72
  </button>
70
73
  <h1>
71
74
  <span class="brand">issue-flow</span>
75
+ <span id="app-version" class="app-version" hidden></span>
72
76
  <a id="issue-link" class="issue-link" target="_blank" rel="noopener" hidden>#—</a>
73
77
  </h1>
74
78
  <p id="branch-line" class="muted mono"></p>
@@ -142,7 +146,7 @@
142
146
  <section class="card">
143
147
  <div class="section-head">
144
148
  <h2>Harnesses e configuração efetiva</h2>
145
- <span class="muted">preferências futuras e overrides desta execução</span>
149
+ <span class="muted">versões, preferências futuras e overrides desta execução</span>
146
150
  </div>
147
151
  <div id="configuration"></div>
148
152
  </section>