issue-flow 0.12.0 → 0.14.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 (197) hide show
  1. package/README.md +79 -274
  2. package/dist/agent-OX3KPHE4.js +30 -0
  3. package/dist/{analyze-OEET7KGE.js → analyze-WIND2SEB.js} +31 -16
  4. package/dist/analyze-WIND2SEB.js.map +1 -0
  5. package/dist/{apply-ZIA3C52S.js → apply-JNOI2LR4.js} +18 -7
  6. package/dist/{apply-ZIA3C52S.js.map → apply-JNOI2LR4.js.map} +1 -1
  7. package/dist/bench-B4VZON7E.js +910 -0
  8. package/dist/bench-B4VZON7E.js.map +1 -0
  9. package/dist/{chunk-FADTCQGM.js → chunk-2IQ6M4ZE.js} +94 -32
  10. package/dist/chunk-2IQ6M4ZE.js.map +1 -0
  11. package/dist/chunk-3BWZO4YI.js +141 -0
  12. package/dist/chunk-3BWZO4YI.js.map +1 -0
  13. package/dist/chunk-3WFZETIM.js +163 -0
  14. package/dist/chunk-3WFZETIM.js.map +1 -0
  15. package/dist/chunk-67YIU2VB.js +362 -0
  16. package/dist/chunk-67YIU2VB.js.map +1 -0
  17. package/dist/{chunk-QCPGHBSZ.js → chunk-6BENHR2I.js} +63 -21
  18. package/dist/chunk-6BENHR2I.js.map +1 -0
  19. package/dist/{chunk-BYZDPR7N.js → chunk-6QYMSNVA.js} +194 -235
  20. package/dist/chunk-6QYMSNVA.js.map +1 -0
  21. package/dist/{chunk-Y5ZDJ4PJ.js → chunk-76MMPJ53.js} +20 -10
  22. package/dist/chunk-76MMPJ53.js.map +1 -0
  23. package/dist/chunk-7Z4Y3VKM.js +26 -0
  24. package/dist/chunk-7Z4Y3VKM.js.map +1 -0
  25. package/dist/{chunk-3QHK4YQA.js → chunk-B72RQZBV.js} +346 -119
  26. package/dist/chunk-B72RQZBV.js.map +1 -0
  27. package/dist/chunk-BEZ5BA5U.js +625 -0
  28. package/dist/chunk-BEZ5BA5U.js.map +1 -0
  29. package/dist/chunk-BRCNQXYQ.js +74 -0
  30. package/dist/chunk-BRCNQXYQ.js.map +1 -0
  31. package/dist/chunk-CGKLE6WR.js +1203 -0
  32. package/dist/chunk-CGKLE6WR.js.map +1 -0
  33. package/dist/chunk-CRGIQFUQ.js +83 -0
  34. package/dist/chunk-CRGIQFUQ.js.map +1 -0
  35. package/dist/{chunk-NS6R37II.js → chunk-F6TOYSYY.js} +141 -12
  36. package/dist/chunk-F6TOYSYY.js.map +1 -0
  37. package/dist/chunk-G5EK2XRN.js +25 -0
  38. package/dist/chunk-G5EK2XRN.js.map +1 -0
  39. package/dist/chunk-HAKDLR6N.js +134 -0
  40. package/dist/chunk-HAKDLR6N.js.map +1 -0
  41. package/dist/chunk-J3SOTUVI.js +298 -0
  42. package/dist/chunk-J3SOTUVI.js.map +1 -0
  43. package/dist/chunk-JN6D3MY6.js +30 -0
  44. package/dist/chunk-JN6D3MY6.js.map +1 -0
  45. package/dist/{chunk-LAFNLLQP.js → chunk-KMRFRXIT.js} +5 -5
  46. package/dist/chunk-KT7JKOXB.js +72 -0
  47. package/dist/chunk-KT7JKOXB.js.map +1 -0
  48. package/dist/chunk-L6HSKTQE.js +77 -0
  49. package/dist/chunk-L6HSKTQE.js.map +1 -0
  50. package/dist/chunk-LBGHI4S2.js +1264 -0
  51. package/dist/chunk-LBGHI4S2.js.map +1 -0
  52. package/dist/chunk-LEW4QM74.js +180 -0
  53. package/dist/chunk-LEW4QM74.js.map +1 -0
  54. package/dist/{chunk-FJP3UQGD.js → chunk-LFEQIZLB.js} +115 -20
  55. package/dist/chunk-LFEQIZLB.js.map +1 -0
  56. package/dist/chunk-OOVG2KBY.js +77 -0
  57. package/dist/chunk-OOVG2KBY.js.map +1 -0
  58. package/dist/{chunk-56KS3TFQ.js → chunk-OR43364P.js} +115 -160
  59. package/dist/chunk-OR43364P.js.map +1 -0
  60. package/dist/chunk-P3LRUNC6.js +16 -0
  61. package/dist/chunk-P3LRUNC6.js.map +1 -0
  62. package/dist/{chunk-TSOLWK7Y.js → chunk-PBTA3QQX.js} +8 -7
  63. package/dist/{chunk-TSOLWK7Y.js.map → chunk-PBTA3QQX.js.map} +1 -1
  64. package/dist/{chunk-MJULIKQU.js → chunk-PHW2YV6N.js} +23 -47
  65. package/dist/chunk-PHW2YV6N.js.map +1 -0
  66. package/dist/{chunk-73ATPIYX.js → chunk-PZTWU277.js} +36 -71
  67. package/dist/chunk-PZTWU277.js.map +1 -0
  68. package/dist/{chunk-M5UCXTIT.js → chunk-QD4VOILZ.js} +22 -18
  69. package/dist/chunk-QD4VOILZ.js.map +1 -0
  70. package/dist/chunk-QJ5UZAH5.js +2718 -0
  71. package/dist/chunk-QJ5UZAH5.js.map +1 -0
  72. package/dist/chunk-QK7WUO7A.js +283 -0
  73. package/dist/chunk-QK7WUO7A.js.map +1 -0
  74. package/dist/{chunk-A4SLHOW7.js → chunk-QZVKRDJY.js} +27 -19
  75. package/dist/chunk-QZVKRDJY.js.map +1 -0
  76. package/dist/chunk-REO3PXXO.js +71 -0
  77. package/dist/chunk-REO3PXXO.js.map +1 -0
  78. package/dist/{chunk-EZATO3DW.js → chunk-RVO7L4FX.js} +594 -17
  79. package/dist/chunk-RVO7L4FX.js.map +1 -0
  80. package/dist/{chunk-XQUSIEKA.js → chunk-SZMHCEYV.js} +63 -14
  81. package/dist/chunk-SZMHCEYV.js.map +1 -0
  82. package/dist/chunk-TYRNTSPJ.js +243 -0
  83. package/dist/chunk-TYRNTSPJ.js.map +1 -0
  84. package/dist/chunk-UJ3L2MS7.js +238 -0
  85. package/dist/chunk-UJ3L2MS7.js.map +1 -0
  86. package/dist/chunk-V7MO5EDS.js +120 -0
  87. package/dist/chunk-V7MO5EDS.js.map +1 -0
  88. package/dist/chunk-WB2X37MO.js +415 -0
  89. package/dist/chunk-WB2X37MO.js.map +1 -0
  90. package/dist/{chunk-NTMMF5UD.js → chunk-X44FKNKP.js} +525 -252
  91. package/dist/chunk-X44FKNKP.js.map +1 -0
  92. package/dist/chunk-XEYH6ZZI.js +289 -0
  93. package/dist/chunk-XEYH6ZZI.js.map +1 -0
  94. package/dist/cli.js +267 -44
  95. package/dist/cli.js.map +1 -1
  96. package/dist/{config-PRJGWW7J.js → config-HMFMCY2M.js} +31 -5
  97. package/dist/contract-JH2WBIO6.js +8 -0
  98. package/dist/conventions-NJF2OPVQ.js +146 -0
  99. package/dist/conventions-NJF2OPVQ.js.map +1 -0
  100. package/dist/diagnostics-THH42ZMP.js +23 -0
  101. package/dist/execute-KFNNZZHI.js +35 -0
  102. package/dist/{generate-EGB4KV2B.js → generate-FW5C6NP7.js} +37 -20
  103. package/dist/{generate-EGB4KV2B.js.map → generate-FW5C6NP7.js.map} +1 -1
  104. package/dist/git-W72QQ6GT.js +33 -0
  105. package/dist/init-HTN4V67Q.js +24 -0
  106. package/dist/{operations-BYT626KQ.js → operations-BCFOBH42.js} +27 -15
  107. package/dist/{operations-BYT626KQ.js.map → operations-BCFOBH42.js.map} +1 -1
  108. package/dist/permissions-4AKNQZHK.js +18 -0
  109. package/dist/plan-SIZLGA4V.js +34 -0
  110. package/dist/{policy-6S4GFC76.js → policy-DT6Q6ZVM.js} +20 -6
  111. package/dist/policy-DT6Q6ZVM.js.map +1 -0
  112. package/dist/pr-OMWCGUOW.js +39 -0
  113. package/dist/pr-review-N24CZPKR.js +32 -0
  114. package/dist/pr-review-N24CZPKR.js.map +1 -0
  115. package/dist/prd-5OFTZAHJ.js +33 -0
  116. package/dist/prd-5OFTZAHJ.js.map +1 -0
  117. package/dist/ps-RFORTCUD.js +93 -0
  118. package/dist/ps-RFORTCUD.js.map +1 -0
  119. package/dist/recorder-KCCPMKQG.js +32 -0
  120. package/dist/recorder-KCCPMKQG.js.map +1 -0
  121. package/dist/registry-GLPXOMFG.js +15 -0
  122. package/dist/registry-GLPXOMFG.js.map +1 -0
  123. package/dist/{resume-PPCNQVVV.js → resume-M5RLKP4I.js} +55 -34
  124. package/dist/{resume-PPCNQVVV.js.map → resume-M5RLKP4I.js.map} +1 -1
  125. package/dist/review-YM4JAABY.js +35 -0
  126. package/dist/review-YM4JAABY.js.map +1 -0
  127. package/dist/routing-NNT5WKGX.js +94 -0
  128. package/dist/routing-NNT5WKGX.js.map +1 -0
  129. package/dist/run-PZUKHPQK.js +54 -0
  130. package/dist/run-PZUKHPQK.js.map +1 -0
  131. package/dist/session-publisher-T7EI3CIP.js +11 -0
  132. package/dist/session-publisher-T7EI3CIP.js.map +1 -0
  133. package/dist/{state-manager-2EDVDBAR.js → state-manager-GQH32C44.js} +4 -2
  134. package/dist/state-manager-GQH32C44.js.map +1 -0
  135. package/dist/usage-V2CHE5PY.js +177 -0
  136. package/dist/usage-V2CHE5PY.js.map +1 -0
  137. package/dist/{web-EF5WIIGG.js → web-EA3KGNFN.js} +45 -30
  138. package/dist/web-EA3KGNFN.js.map +1 -0
  139. package/package.json +6 -2
  140. package/prompts/execute.md +7 -8
  141. package/prompts/plan.md +4 -5
  142. package/prompts/pr.md +2 -2
  143. package/web/AGENTS.md +152 -13
  144. package/web/public/app.css +380 -109
  145. package/web/public/app.js +525 -25
  146. package/web/public/index.html +68 -1
  147. package/dist/analyze-OEET7KGE.js.map +0 -1
  148. package/dist/chunk-3QHK4YQA.js.map +0 -1
  149. package/dist/chunk-42PJFMAR.js +0 -605
  150. package/dist/chunk-42PJFMAR.js.map +0 -1
  151. package/dist/chunk-4JSOYE4G.js +0 -368
  152. package/dist/chunk-4JSOYE4G.js.map +0 -1
  153. package/dist/chunk-56KS3TFQ.js.map +0 -1
  154. package/dist/chunk-73ATPIYX.js.map +0 -1
  155. package/dist/chunk-A4SLHOW7.js.map +0 -1
  156. package/dist/chunk-BAUPQXPE.js +0 -173
  157. package/dist/chunk-BAUPQXPE.js.map +0 -1
  158. package/dist/chunk-BYZDPR7N.js.map +0 -1
  159. package/dist/chunk-CD5ULRVC.js +0 -615
  160. package/dist/chunk-CD5ULRVC.js.map +0 -1
  161. package/dist/chunk-DIDUVL6O.js +0 -1365
  162. package/dist/chunk-DIDUVL6O.js.map +0 -1
  163. package/dist/chunk-EZATO3DW.js.map +0 -1
  164. package/dist/chunk-FADTCQGM.js.map +0 -1
  165. package/dist/chunk-FJP3UQGD.js.map +0 -1
  166. package/dist/chunk-LWRSMTTZ.js +0 -18
  167. package/dist/chunk-LWRSMTTZ.js.map +0 -1
  168. package/dist/chunk-M5UCXTIT.js.map +0 -1
  169. package/dist/chunk-M66SJHQQ.js +0 -206
  170. package/dist/chunk-M66SJHQQ.js.map +0 -1
  171. package/dist/chunk-MJULIKQU.js.map +0 -1
  172. package/dist/chunk-NS6R37II.js.map +0 -1
  173. package/dist/chunk-NTMMF5UD.js.map +0 -1
  174. package/dist/chunk-QCPGHBSZ.js.map +0 -1
  175. package/dist/chunk-XQUSIEKA.js.map +0 -1
  176. package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
  177. package/dist/execute-XBNVL753.js +0 -18
  178. package/dist/init-AJHCXJBP.js +0 -10
  179. package/dist/plan-QOZ3RIO6.js +0 -21
  180. package/dist/policy-6S4GFC76.js.map +0 -1
  181. package/dist/pr-EBBHD2ZX.js +0 -26
  182. package/dist/pr-review-VVMW5PO4.js +0 -20
  183. package/dist/prd-JQSZF7PR.js +0 -21
  184. package/dist/review-3F6MARWH.js +0 -19
  185. package/dist/run-RXE5GDQO.js +0 -36
  186. package/dist/web-EF5WIIGG.js.map +0 -1
  187. /package/dist/{config-PRJGWW7J.js.map → agent-OX3KPHE4.js.map} +0 -0
  188. /package/dist/{chunk-LAFNLLQP.js.map → chunk-KMRFRXIT.js.map} +0 -0
  189. /package/dist/{execute-XBNVL753.js.map → config-HMFMCY2M.js.map} +0 -0
  190. /package/dist/{init-AJHCXJBP.js.map → contract-JH2WBIO6.js.map} +0 -0
  191. /package/dist/{plan-QOZ3RIO6.js.map → diagnostics-THH42ZMP.js.map} +0 -0
  192. /package/dist/{pr-EBBHD2ZX.js.map → execute-KFNNZZHI.js.map} +0 -0
  193. /package/dist/{pr-review-VVMW5PO4.js.map → git-W72QQ6GT.js.map} +0 -0
  194. /package/dist/{prd-JQSZF7PR.js.map → init-HTN4V67Q.js.map} +0 -0
  195. /package/dist/{review-3F6MARWH.js.map → permissions-4AKNQZHK.js.map} +0 -0
  196. /package/dist/{run-RXE5GDQO.js.map → plan-SIZLGA4V.js.map} +0 -0
  197. /package/dist/{state-manager-2EDVDBAR.js.map → pr-OMWCGUOW.js.map} +0 -0
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/storage/paths.ts
4
+ import { createHash } from "crypto";
5
+ import { homedir } from "os";
6
+ import { basename, join, resolve } from "path";
7
+ var GLOBAL_DIR_NAME = ".issue-flow";
8
+ var GLOBAL_ROOT_ENV = "ISSUE_FLOW_HOME";
9
+ var PROJECTS_DIR_NAME = "projects";
10
+ var LOGS_DIR_NAME = "logs";
11
+ var ISSUES_DIR_NAME = "issues";
12
+ var QUEUES_DIR_NAME = "queues";
13
+ var RUN_LOCK_FILENAME = "run.lock";
14
+ var PROVIDERS_HEALTH_FILENAME = "providers.json";
15
+ var SESSION_FILENAME = "session.json";
16
+ var EVENTS_FILENAME = "events.jsonl";
17
+ var ROTATED_EVENTS_FILENAME = "events.1.jsonl";
18
+ var VERIFY_FILENAME = "verify.json";
19
+ var RUN_LOG_FILENAME = "run.log";
20
+ var ROTATED_RUN_LOG_FILENAME = "run.log.1";
21
+ function getGlobalRoot(options = {}) {
22
+ const env = options.env ?? process.env;
23
+ const override = env[GLOBAL_ROOT_ENV]?.trim();
24
+ if (override) {
25
+ return resolve(override);
26
+ }
27
+ let home;
28
+ try {
29
+ home = homedir();
30
+ } catch {
31
+ home = "";
32
+ }
33
+ if (home.trim() === "") {
34
+ throw new Error(
35
+ `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`
36
+ );
37
+ }
38
+ return join(home, GLOBAL_DIR_NAME);
39
+ }
40
+ function getDiagnosticsDir(options = {}) {
41
+ return join(getGlobalRoot(options), LOGS_DIR_NAME);
42
+ }
43
+ var SLUG_MAX_LENGTH = 32;
44
+ var HASH_LENGTH = 12;
45
+ var FALLBACK_SLUG = "project";
46
+ function slugify(name) {
47
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
48
+ return slug === "" ? FALLBACK_SLUG : slug;
49
+ }
50
+ function projectIdFromRemote(remote, projectRoot) {
51
+ let seed;
52
+ let name;
53
+ if (remote) {
54
+ seed = `remote:${remote}`;
55
+ name = remote.split("/").pop() ?? "";
56
+ } else {
57
+ const absolute = resolve(projectRoot);
58
+ seed = `path:${absolute}`;
59
+ name = basename(absolute);
60
+ }
61
+ const hash = createHash("sha256").update(seed).digest("hex").slice(0, HASH_LENGTH);
62
+ return `${slugify(name)}-${hash}`;
63
+ }
64
+ function getProjectDir(projectId, options = {}) {
65
+ return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);
66
+ }
67
+ function getQueuePaths(projectId, queueId, options = {}) {
68
+ const queueDir = join(
69
+ getProjectDir(projectId, options),
70
+ QUEUES_DIR_NAME,
71
+ normalizeIssueNumber(queueId)
72
+ );
73
+ return { queueDir, planFile: join(queueDir, "execution-plan.json") };
74
+ }
75
+ function normalizeIssueNumber(issueNumber) {
76
+ const normalized = String(issueNumber).trim().replace(/^#/, "");
77
+ if (normalized.length === 0) {
78
+ throw new Error("Issue identifier cannot be empty");
79
+ }
80
+ if (/[/\\]/.test(normalized) || normalized === "." || normalized === "..") {
81
+ throw new Error(`Invalid issue identifier: '${issueNumber}'`);
82
+ }
83
+ return normalized;
84
+ }
85
+ function getIssuePaths(projectId, issueNumber, options = {}) {
86
+ const issueDir = join(
87
+ getProjectDir(projectId, options),
88
+ ISSUES_DIR_NAME,
89
+ normalizeIssueNumber(issueNumber)
90
+ );
91
+ return {
92
+ issueDir,
93
+ issueFile: join(issueDir, "issue.md"),
94
+ metadataFile: join(issueDir, "metadata.json"),
95
+ prdFile: join(issueDir, "prd.md"),
96
+ tasksFile: join(issueDir, "tasks.json"),
97
+ progressFile: join(issueDir, "progress.txt"),
98
+ analysisFile: join(issueDir, "analysis.md"),
99
+ sessionFile: join(issueDir, SESSION_FILENAME),
100
+ // The journal, and the single generation kept when it rotates. The
101
+ // snapshot above is the projection; this pair is the history.
102
+ eventsFile: join(issueDir, EVENTS_FILENAME),
103
+ rotatedEventsFile: join(issueDir, ROTATED_EVENTS_FILENAME),
104
+ runLogFile: join(issueDir, RUN_LOG_FILENAME),
105
+ rotatedRunLogFile: join(issueDir, ROTATED_RUN_LOG_FILENAME),
106
+ // "This issue looks larger than one run": written only when the signals
107
+ // agree, and read by a person rather than by the pipeline.
108
+ decompositionFile: join(issueDir, "decomposition.md"),
109
+ verifyFile: join(issueDir, VERIFY_FILENAME),
110
+ lastBranchFile: join(issueDir, ".last-branch"),
111
+ archiveDir: join(issueDir, "archive"),
112
+ prReviewDir: join(issueDir, "pr-review")
113
+ };
114
+ }
115
+
116
+ export {
117
+ GLOBAL_ROOT_ENV,
118
+ PROJECTS_DIR_NAME,
119
+ ISSUES_DIR_NAME,
120
+ QUEUES_DIR_NAME,
121
+ RUN_LOCK_FILENAME,
122
+ PROVIDERS_HEALTH_FILENAME,
123
+ SESSION_FILENAME,
124
+ EVENTS_FILENAME,
125
+ ROTATED_EVENTS_FILENAME,
126
+ VERIFY_FILENAME,
127
+ getGlobalRoot,
128
+ getDiagnosticsDir,
129
+ projectIdFromRemote,
130
+ getProjectDir,
131
+ getQueuePaths,
132
+ getIssuePaths
133
+ };
134
+ //# sourceMappingURL=chunk-HAKDLR6N.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage/paths.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\n\n/** Directory created under the user's home when no override is provided. */\nexport const GLOBAL_DIR_NAME = '.issue-flow';\n\n/** Environment variable that relocates the whole global storage tree. */\nexport const GLOBAL_ROOT_ENV = 'ISSUE_FLOW_HOME';\n\n/** Directory under the global root holding one folder per project. */\nexport const PROJECTS_DIR_NAME = 'projects';\n\n/** Machine-wide structured diagnostic logs, independent of any project. */\nexport const LOGS_DIR_NAME = 'logs';\n\n/** Directory under a project holding one folder per issue. */\nexport const ISSUES_DIR_NAME = 'issues';\n\n/** Directory under a project holding one folder per multi-issue execution queue. */\nexport const QUEUES_DIR_NAME = 'queues';\n\n/** Run-ownership lock, a sibling of `issues/` inside the project directory. */\nexport const RUN_LOCK_FILENAME = 'run.lock';\n\n/** Persisted agent-provider health, shared by every run of one project. */\nexport const PROVIDERS_HEALTH_FILENAME = 'providers.json';\n\n/** Projection and journal filenames shared by storage and the web reader. */\nexport const SESSION_FILENAME = 'session.json';\nexport const EVENTS_FILENAME = 'events.jsonl';\nexport const ROTATED_EVENTS_FILENAME = 'events.1.jsonl';\n/**\n * Acceptance-contract evidence. Named here — and not at the call site — because\n * `verify/run-issue.ts` also writes it in standalone mode, where there is no\n * `IssuePaths` to ask: one constant, two callers, no second spelling.\n */\nexport const VERIFY_FILENAME = 'verify.json';\n\nexport const RUN_LOG_FILENAME = 'run.log';\nexport const ROTATED_RUN_LOG_FILENAME = 'run.log.1';\n\nexport interface GetGlobalRootOptions {\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve the root directory of the global storage tree (`~/.issue-flow`).\n *\n * Every path under the global storage MUST be derived from this function: it is\n * the single seam where `ISSUE_FLOW_HOME` takes effect, which is what lets\n * tests, CI and sandboxes point the whole tree at a temporary directory instead\n * of touching the real `$HOME`. A call site that joins `homedir()` by hand\n * silently opts out of that isolation.\n *\n * Pure and synchronous: it never creates directories nor touches the\n * filesystem — callers decide when (and whether) the directory should exist.\n */\nexport function getGlobalRoot(options: GetGlobalRootOptions = {}): string {\n const env = options.env ?? process.env;\n\n const override = env[GLOBAL_ROOT_ENV]?.trim();\n if (override) {\n // A relative override is resolved against the CWD so that every consumer\n // gets an absolute path, exactly as in the homedir() branch below.\n return resolve(override);\n }\n\n // The only failure mode: an environment where the home directory cannot be\n // determined (some containers and CI images). The override is the escape\n // hatch, so the error names it.\n let home: string;\n try {\n home = homedir();\n } catch {\n home = '';\n }\n if (home.trim() === '') {\n throw new Error(\n `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`,\n );\n }\n\n return join(home, GLOBAL_DIR_NAME);\n}\n\nexport function getDiagnosticsDir(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), LOGS_DIR_NAME);\n}\n\n/** Maximum length of the human-readable half of a project id. */\nconst SLUG_MAX_LENGTH = 32;\n\n/** Number of hex characters kept from the sha256 of the seed. */\nconst HASH_LENGTH = 12;\n\n/** Used when the repository name has no character that survives sanitization. */\nconst FALLBACK_SLUG = 'project';\n\n/**\n * Reduce an arbitrary repository name to a path-safe slug: lowercase, only\n * `[a-z0-9-]`, runs of separators collapsed, truncated to {@link SLUG_MAX_LENGTH}.\n */\nfunction slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, SLUG_MAX_LENGTH)\n .replace(/-+$/g, '');\n\n return slug === '' ? FALLBACK_SLUG : slug;\n}\n\n/**\n * Derive a stable identifier for a project, in the form `<slug>-<hash12>`.\n *\n * The seed is canonical rather than local, so the same repository yields the\n * same id on any machine: `remote:<normalizedRemote>` whenever an `origin`\n * remote exists, falling back to `path:<absoluteProjectRoot>` when it does not.\n * That is what makes moving or renaming the local folder harmless for a cloned\n * repository — the history stays attached to the remote identity.\n *\n * The `remote:` / `path:` prefix is part of the hashed seed, so a project\n * identified by path can never collide with one identified by remote even if\n * the two strings were to coincide. The slug is only cosmetic (it makes the\n * directory recognizable); the hash carries the identity.\n *\n * Known limitation of the path fallback: for a repository with no `origin`\n * remote, the absolute path *is* the identity. Moving or renaming that folder\n * produces a different id, and the previous history is left behind under the\n * old one. Configuring a remote before adopting the global storage avoids it.\n */\nexport async function getProjectId(projectRoot: string): Promise<string> {\n const remote = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n return projectIdFromRemote(remote, projectRoot);\n}\n\n/**\n * Pure half of {@link getProjectId}: derive the id once the normalized remote\n * (or `null`) is already known.\n *\n * Exported so a caller that already resolved the remote for another reason\n * (e.g. {@link resolveStorageMode} in `compat.ts`, which also persists it into\n * `metadata.json`) can compute the id without shelling out to git a second\n * time.\n */\nexport function projectIdFromRemote(remote: string | null, projectRoot: string): string {\n let seed: string;\n let name: string;\n if (remote) {\n // `host/org/repo` -> the repository name is the last path segment.\n seed = `remote:${remote}`;\n name = remote.split('/').pop() ?? '';\n } else {\n const absolute = resolve(projectRoot);\n seed = `path:${absolute}`;\n name = basename(absolute);\n }\n\n const hash = createHash('sha256').update(seed).digest('hex').slice(0, HASH_LENGTH);\n\n return `${slugify(name)}-${hash}`;\n}\n\n/**\n * Absolute directory of a project inside the global storage tree.\n *\n * `projectId` is expected to come from {@link getProjectId}, which already\n * guarantees a single safe path segment.\n */\nexport function getProjectDir(projectId: string, options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);\n}\n\n/**\n * Every artifact of one issue, resolved from a single place.\n *\n * Note that `prdFile` is the requirements document (`prd.md`) and `tasksFile`\n * is the task plan (`tasks.json`) — unlike `ResolvedPaths.prdFile` in\n * `types.ts`, which historically points at `tasks.json`.\n */\nexport interface IssuePaths {\n issueDir: string;\n issueFile: string;\n metadataFile: string;\n prdFile: string;\n tasksFile: string;\n progressFile: string;\n analysisFile: string;\n sessionFile: string;\n eventsFile: string;\n rotatedEventsFile: string;\n runLogFile: string;\n rotatedRunLogFile: string;\n decompositionFile: string;\n verifyFile: string;\n lastBranchFile: string;\n archiveDir: string;\n prReviewDir: string;\n}\n\n/**\n * Artifacts of one multi-issue execution queue.\n *\n * A queue coordinates several issues that share a branch; each issue keeps its\n * own directory under {@link ISSUES_DIR_NAME} exactly as before, and only the\n * coordination state (order, per-issue status, shared branch, consolidated Pull\n * Request) lives here. That is what keeps a single-issue run byte-identical: it\n * never creates a queue at all.\n */\nexport interface QueuePaths {\n queueDir: string;\n planFile: string;\n}\n\n/**\n * Resolve the paths of one execution queue under the global storage.\n *\n * The queue id is the identifier of the primary issue — the first one the user\n * asked for — so re-running the same command finds the same queue and resumes\n * it instead of starting a parallel one.\n *\n * Pure and synchronous like every other helper here: nothing is created.\n */\nexport function getQueuePaths(\n projectId: string,\n queueId: string | number,\n options: GetGlobalRootOptions = {},\n): QueuePaths {\n const queueDir = join(\n getProjectDir(projectId, options),\n QUEUES_DIR_NAME,\n normalizeIssueNumber(queueId),\n );\n\n return { queueDir, planFile: join(queueDir, 'execution-plan.json') };\n}\n\n/**\n * Issue identifiers become path segments, so anything that could escape the\n * issues directory is rejected before it reaches `join` — the same discipline\n * as `normalizeId()` in `issues/providers/local.ts`.\n *\n * Non-numeric identifiers (`auth-refactor`, `pr-184`) are first-class here:\n * the local provider already supports them, so the global storage must too.\n */\nfunction normalizeIssueNumber(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n\n if (normalized.length === 0) {\n throw new Error('Issue identifier cannot be empty');\n }\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n\n return normalized;\n}\n\n/**\n * Resolve every path of a single issue under the global storage.\n *\n * This is the only place allowed to know the layout of an issue directory:\n * call sites ask for the artifact they need instead of joining names by hand,\n * so renaming or adding an artifact stays a one-file change.\n *\n * Pure and synchronous: nothing here creates directories or touches the\n * filesystem.\n */\nexport function getIssuePaths(\n projectId: string,\n issueNumber: string | number,\n options: GetGlobalRootOptions = {},\n): IssuePaths {\n const issueDir = join(\n getProjectDir(projectId, options),\n ISSUES_DIR_NAME,\n normalizeIssueNumber(issueNumber),\n );\n\n return {\n issueDir,\n issueFile: join(issueDir, 'issue.md'),\n metadataFile: join(issueDir, 'metadata.json'),\n prdFile: join(issueDir, 'prd.md'),\n tasksFile: join(issueDir, 'tasks.json'),\n progressFile: join(issueDir, 'progress.txt'),\n analysisFile: join(issueDir, 'analysis.md'),\n sessionFile: join(issueDir, SESSION_FILENAME),\n // The journal, and the single generation kept when it rotates. The\n // snapshot above is the projection; this pair is the history.\n eventsFile: join(issueDir, EVENTS_FILENAME),\n rotatedEventsFile: join(issueDir, ROTATED_EVENTS_FILENAME),\n runLogFile: join(issueDir, RUN_LOG_FILENAME),\n rotatedRunLogFile: join(issueDir, ROTATED_RUN_LOG_FILENAME),\n // \"This issue looks larger than one run\": written only when the signals\n // agree, and read by a person rather than by the pipeline.\n decompositionFile: join(issueDir, 'decomposition.md'),\n verifyFile: join(issueDir, VERIFY_FILENAME),\n lastBranchFile: join(issueDir, '.last-branch'),\n archiveDir: join(issueDir, 'archive'),\n prReviewDir: join(issueDir, 'pr-review'),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ isProcessAlive,
4
+ taskPlanSchema
5
+ } from "./chunk-LBGHI4S2.js";
6
+ import {
7
+ writeFileAtomic
8
+ } from "./chunk-7Z4Y3VKM.js";
9
+
10
+ // src/core/state-manager.ts
11
+ import { readFile } from "fs/promises";
12
+ import { ZodError } from "zod";
13
+
14
+ // src/telemetry/reconcile.ts
15
+ import { hostname } from "os";
16
+ function nowIso() {
17
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
18
+ }
19
+ function reconcileInterruptedExecutions(plan) {
20
+ if (plan.executions === void 0 || plan.executions.length === 0) return plan;
21
+ let changed = false;
22
+ const executions = plan.executions.map((record) => {
23
+ if (record.status !== "running" || record.finishedAt !== null) return record;
24
+ const owner = record.owner;
25
+ if (owner === void 0 || owner === null) {
26
+ changed = true;
27
+ return { ...record, status: "interrupted", finishedAt: nowIso(), owner: null };
28
+ }
29
+ if (owner.host !== hostname()) return record;
30
+ if (isProcessAlive(owner.pid)) return record;
31
+ changed = true;
32
+ return { ...record, status: "interrupted", finishedAt: nowIso(), owner: null };
33
+ });
34
+ return changed ? { ...plan, executions } : plan;
35
+ }
36
+
37
+ // src/core/metrics.ts
38
+ var USAGE_FIELDS = [
39
+ "inputTokens",
40
+ "outputTokens",
41
+ "cacheReadTokens",
42
+ "cacheCreationTokens",
43
+ "costUsd"
44
+ ];
45
+ function num(value) {
46
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
47
+ }
48
+ function isRecord(value) {
49
+ return typeof value === "object" && value !== null;
50
+ }
51
+ function parseCodexUsage(payload) {
52
+ if (!isRecord(payload)) return null;
53
+ const usage = {};
54
+ const inputTokens = num(payload.input_tokens);
55
+ const cachedInput = num(payload.cached_input_tokens);
56
+ const cacheWrite = num(payload.cache_write_input_tokens);
57
+ const outputTokens = num(payload.output_tokens);
58
+ if (inputTokens !== void 0) {
59
+ usage.inputTokens = cachedInput !== void 0 ? Math.max(0, inputTokens - cachedInput) : inputTokens;
60
+ }
61
+ if (outputTokens !== void 0) usage.outputTokens = outputTokens;
62
+ if (cachedInput !== void 0) usage.cacheReadTokens = cachedInput;
63
+ if (cacheWrite !== void 0) usage.cacheCreationTokens = cacheWrite;
64
+ return Object.keys(usage).length > 0 ? usage : null;
65
+ }
66
+ function parseUsage(payload) {
67
+ if (!isRecord(payload)) return null;
68
+ const usageRaw = isRecord(payload.usage) ? payload.usage : void 0;
69
+ const usage = {};
70
+ const inputTokens = usageRaw ? num(usageRaw.input_tokens) : void 0;
71
+ const outputTokens = usageRaw ? num(usageRaw.output_tokens) : void 0;
72
+ const cacheReadTokens = usageRaw ? num(usageRaw.cache_read_input_tokens) : void 0;
73
+ const cacheCreationTokens = usageRaw ? num(usageRaw.cache_creation_input_tokens) : void 0;
74
+ const costUsd = num(payload.total_cost_usd);
75
+ const legacyInput = num(payload.num_input_tokens);
76
+ const legacyOutput = num(payload.num_output_tokens);
77
+ const legacyCost = num(payload.cost_usd);
78
+ const resolvedInput = inputTokens ?? legacyInput;
79
+ const resolvedOutput = outputTokens ?? legacyOutput;
80
+ const resolvedCost = costUsd ?? legacyCost;
81
+ if (resolvedInput !== void 0) usage.inputTokens = resolvedInput;
82
+ if (resolvedOutput !== void 0) usage.outputTokens = resolvedOutput;
83
+ if (cacheReadTokens !== void 0) usage.cacheReadTokens = cacheReadTokens;
84
+ if (cacheCreationTokens !== void 0) usage.cacheCreationTokens = cacheCreationTokens;
85
+ if (resolvedCost !== void 0) usage.costUsd = resolvedCost;
86
+ const cliDurationMs = num(payload.duration_ms);
87
+ const apiDurationMs = num(payload.duration_api_ms);
88
+ const ttftMs = num(payload.ttft_ms) ?? num(payload.time_to_first_token_ms);
89
+ const numTurns = num(payload.num_turns);
90
+ if (cliDurationMs !== void 0) usage.cliDurationMs = cliDurationMs;
91
+ if (apiDurationMs !== void 0) usage.apiDurationMs = apiDurationMs;
92
+ if (ttftMs !== void 0) usage.ttftMs = ttftMs;
93
+ if (numTurns !== void 0) usage.numTurns = numTurns;
94
+ return Object.keys(usage).length > 0 ? usage : null;
95
+ }
96
+ function sumUsage(a, b) {
97
+ const result = {};
98
+ for (const field of USAGE_FIELDS) {
99
+ const left = a?.[field];
100
+ const right = b?.[field];
101
+ if (left === void 0 && right === void 0) continue;
102
+ result[field] = (left ?? 0) + (right ?? 0);
103
+ }
104
+ return result;
105
+ }
106
+ function divideUsage(usage, parts) {
107
+ const result = {};
108
+ if (!usage) return result;
109
+ const divisor = Number.isFinite(parts) && parts > 1 ? parts : 1;
110
+ for (const field of USAGE_FIELDS) {
111
+ const value = usage[field];
112
+ if (value === void 0) continue;
113
+ result[field] = field === "costUsd" ? value / divisor : Math.round(value / divisor);
114
+ }
115
+ return result;
116
+ }
117
+ function compactTokens(value) {
118
+ const abs = Math.abs(value);
119
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
120
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
121
+ return String(Math.round(value));
122
+ }
123
+ function formatTokens(usage) {
124
+ if (!usage) return "";
125
+ const segments = [];
126
+ const io = [];
127
+ if (usage.inputTokens !== void 0) io.push(`${compactTokens(usage.inputTokens)} in`);
128
+ if (usage.outputTokens !== void 0) io.push(`${compactTokens(usage.outputTokens)} out`);
129
+ if (io.length > 0) segments.push(io.join(" / "));
130
+ const cache = usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0 ? (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0) : void 0;
131
+ if (cache !== void 0) segments.push(`${compactTokens(cache)} cache`);
132
+ if (usage.costUsd !== void 0) segments.push(`~$${usage.costUsd.toFixed(4)}`);
133
+ return segments.join(" \xB7 ");
134
+ }
135
+ function hasUsageData(usage) {
136
+ if (!usage) return false;
137
+ return USAGE_FIELDS.some((field) => usage[field] !== void 0);
138
+ }
139
+
140
+ // src/core/state-manager.ts
141
+ function isoNow() {
142
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
143
+ }
144
+ async function loadTaskPlan(path) {
145
+ const content = await readFile(path, "utf-8");
146
+ const raw = JSON.parse(content);
147
+ try {
148
+ const parsed = taskPlanSchema.parse(raw);
149
+ const reconciled = reconcileInterruptedExecutions(parsed);
150
+ if (reconciled !== parsed) {
151
+ try {
152
+ await saveTaskPlan(path, reconciled);
153
+ } catch {
154
+ }
155
+ }
156
+ return reconciled;
157
+ } catch (err) {
158
+ if (err instanceof ZodError) {
159
+ const issues = err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
160
+ throw new Error(`Invalid tasks.json at ${path}:
161
+ ${issues}`);
162
+ }
163
+ throw err;
164
+ }
165
+ }
166
+ async function saveTaskPlan(path, plan) {
167
+ await writeFileAtomic(path, `${JSON.stringify(plan, null, 2)}
168
+ `);
169
+ }
170
+ function initializeState(plan) {
171
+ const defaultPipeline = {
172
+ prdCompleted: false,
173
+ jsonCompleted: false,
174
+ executionCompleted: false,
175
+ reviewCompleted: false,
176
+ prCreated: false
177
+ };
178
+ return {
179
+ ...plan,
180
+ issueStatus: plan.issueStatus ?? "pending",
181
+ completedAt: plan.completedAt ?? null,
182
+ lastAttemptAt: plan.lastAttemptAt ?? null,
183
+ lastError: plan.lastError ?? null,
184
+ correctionCycle: plan.correctionCycle ?? 0,
185
+ maxCorrectionCycles: plan.maxCorrectionCycles ?? 3,
186
+ lastReviewFindings: plan.lastReviewFindings ?? null,
187
+ pipeline: plan.pipeline ? { ...defaultPipeline, ...plan.pipeline } : defaultPipeline
188
+ };
189
+ }
190
+ function hasPendingCorrection(plan) {
191
+ return Boolean(plan.lastReviewFindings);
192
+ }
193
+ function allStoriesPass(plan) {
194
+ if (plan.userStories.length === 0) {
195
+ return false;
196
+ }
197
+ return plan.userStories.every((story) => story.passes === true);
198
+ }
199
+ function selectActiveStory(stories) {
200
+ return [...stories].filter((story) => !story.passes).sort((a, b) => a.priority - b.priority)[0];
201
+ }
202
+ function markStoryPassing(plan, storyId) {
203
+ return {
204
+ ...plan,
205
+ userStories: plan.userStories.map(
206
+ (story) => story.id === storyId ? { ...story, passes: true } : story
207
+ )
208
+ };
209
+ }
210
+ function applyStoryMetrics(plan, storyIds, share, durationSeconds) {
211
+ if (storyIds.length === 0) return plan;
212
+ const targets = new Set(storyIds);
213
+ return {
214
+ ...plan,
215
+ userStories: plan.userStories.map((story) => {
216
+ if (!targets.has(story.id)) return story;
217
+ const updated = { ...story, ...sumUsage(story, share) };
218
+ if (story.durationSeconds !== void 0 || durationSeconds !== void 0) {
219
+ updated.durationSeconds = (story.durationSeconds ?? 0) + (durationSeconds ?? 0);
220
+ }
221
+ return updated;
222
+ })
223
+ };
224
+ }
225
+ function markIssueInProgress(plan, timestamp) {
226
+ const ts = timestamp ?? isoNow();
227
+ return {
228
+ ...plan,
229
+ issueStatus: "in_progress",
230
+ completedAt: null,
231
+ lastAttemptAt: ts
232
+ };
233
+ }
234
+ function markIssueCompleted(plan) {
235
+ const ts = isoNow();
236
+ return {
237
+ ...plan,
238
+ issueStatus: "completed",
239
+ completedAt: ts,
240
+ lastAttemptAt: ts,
241
+ lastError: null,
242
+ pipeline: plan.pipeline ? { ...plan.pipeline, executionCompleted: true } : plan.pipeline
243
+ };
244
+ }
245
+ function setLastError(plan, category, message) {
246
+ const ts = isoNow();
247
+ const error = {
248
+ category,
249
+ message,
250
+ at: ts
251
+ };
252
+ return {
253
+ ...plan,
254
+ lastAttemptAt: ts,
255
+ lastError: error
256
+ };
257
+ }
258
+ function clearLastError(plan, attemptStartedAt) {
259
+ const ts = isoNow();
260
+ if (plan.lastError && plan.lastError.at > attemptStartedAt) {
261
+ return {
262
+ ...plan,
263
+ lastAttemptAt: ts
264
+ };
265
+ }
266
+ return {
267
+ ...plan,
268
+ lastAttemptAt: ts,
269
+ lastError: null
270
+ };
271
+ }
272
+ function trimErrorMessage(message) {
273
+ return message.split("\n").filter((line) => line.trim().length > 0).slice(0, 8).join("\n");
274
+ }
275
+
276
+ export {
277
+ parseCodexUsage,
278
+ parseUsage,
279
+ sumUsage,
280
+ divideUsage,
281
+ formatTokens,
282
+ hasUsageData,
283
+ isoNow,
284
+ loadTaskPlan,
285
+ saveTaskPlan,
286
+ initializeState,
287
+ hasPendingCorrection,
288
+ allStoriesPass,
289
+ selectActiveStory,
290
+ markStoryPassing,
291
+ applyStoryMetrics,
292
+ markIssueInProgress,
293
+ markIssueCompleted,
294
+ setLastError,
295
+ clearLastError,
296
+ trimErrorMessage
297
+ };
298
+ //# sourceMappingURL=chunk-J3SOTUVI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/state-manager.ts","../src/telemetry/reconcile.ts","../src/core/metrics.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { ZodError } from 'zod';\nimport { taskPlanSchema } from '../schemas.js';\nimport { reconcileInterruptedExecutions } from '../telemetry/reconcile.js';\nimport type { LastError, PipelineState, TaskPlan, UserStory } from '../types.js';\nimport { writeFileAtomic } from '../utils/fs.js';\nimport { type ClaudeUsage, sumUsage } from './metrics.js';\n\n/**\n * Get the current ISO timestamp.\n */\nexport function isoNow(): string {\n return new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\n/**\n * Load and parse a tasks.json file.\n * Validates that it has the expected structure.\n */\nexport async function loadTaskPlan(path: string): Promise<TaskPlan> {\n const content = await readFile(path, 'utf-8');\n const raw = JSON.parse(content);\n\n try {\n const parsed = taskPlanSchema.parse(raw) as TaskPlan;\n const reconciled = reconcileInterruptedExecutions(parsed);\n if (reconciled !== parsed) {\n try {\n await saveTaskPlan(path, reconciled);\n } catch {\n // Observational: a failed persist of interrupted status still returns\n // the reconciled plan to this reader.\n }\n }\n return reconciled;\n } catch (err) {\n if (err instanceof ZodError) {\n const issues = err.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`).join('\\n');\n throw new Error(`Invalid tasks.json at ${path}:\\n${issues}`);\n }\n throw err;\n }\n}\n\n/**\n * Save a TaskPlan to disk atomically (write-to-temp + rename).\n * This prevents corruption if the process is interrupted during write.\n */\nexport async function saveTaskPlan(path: string, plan: TaskPlan): Promise<void> {\n await writeFileAtomic(path, `${JSON.stringify(plan, null, 2)}\\n`);\n}\n\n/**\n * Initialize default state fields on a TaskPlan.\n * Fills in missing fields with defaults, matching the Bash script behavior.\n *\n * The opt-in fields (`pullRequest`, `prReview`, `pipeline.prReviewCompleted`)\n * are deliberately absent from the defaults: they ride along on the spread when\n * present and stay out of the file entirely when the phases never ran.\n */\nexport function initializeState(plan: TaskPlan): TaskPlan {\n const defaultPipeline: PipelineState = {\n prdCompleted: false,\n jsonCompleted: false,\n executionCompleted: false,\n reviewCompleted: false,\n prCreated: false,\n };\n\n return {\n ...plan,\n issueStatus: plan.issueStatus ?? 'pending',\n completedAt: plan.completedAt ?? null,\n lastAttemptAt: plan.lastAttemptAt ?? null,\n lastError: plan.lastError ?? null,\n correctionCycle: plan.correctionCycle ?? 0,\n maxCorrectionCycles: plan.maxCorrectionCycles ?? 3,\n lastReviewFindings: plan.lastReviewFindings ?? null,\n pipeline: plan.pipeline ? { ...defaultPipeline, ...plan.pipeline } : defaultPipeline,\n };\n}\n\n/**\n * Whether the issue has a review failure that execute must still address,\n * even if every userStories[].passes is already true.\n */\nexport function hasPendingCorrection(plan: TaskPlan): boolean {\n return Boolean(plan.lastReviewFindings);\n}\n\n/**\n * Check if all user stories have passes=true.\n * Returns false if there are no stories.\n */\nexport function allStoriesPass(plan: TaskPlan): boolean {\n if (plan.userStories.length === 0) {\n return false;\n }\n\n return plan.userStories.every((story) => story.passes === true);\n}\n\n/**\n * The story `execute` should work on next: the highest-priority (lowest\n * `priority` number) story with `passes: false`. This is the exact same rule\n * `prompts/execute.md` gives the agent (\"pick the highest priority user\n * story where `passes: false`\") — kept here as a single, pure, exported\n * helper so the engine's `iteration:start` publication and the terminal\n * renderer read the same computed identity instead of two independent\n * heuristics.\n *\n * Pure: never mutates `stories`. Returns `undefined` when every story\n * already passes (or the plan has none).\n */\nexport function selectActiveStory(stories: readonly UserStory[]): UserStory | undefined {\n return [...stories].filter((story) => !story.passes).sort((a, b) => a.priority - b.priority)[0];\n}\n\n/**\n * Mark a specific story as passing.\n */\nexport function markStoryPassing(plan: TaskPlan, storyId: string): TaskPlan {\n return {\n ...plan,\n userStories: plan.userStories.map((story) =>\n story.id === storyId ? { ...story, passes: true } : story,\n ),\n };\n}\n\n/**\n * Accumulate a usage share (and duration) onto the given stories.\n *\n * Pure, like the rest of this module: returns a new plan and never touches the\n * input. Accumulation is summing and treats `undefined` as \"not reported\": a\n * field neither the story nor the share carries stays absent, so a plan written\n * by a CLI that reports no usage never gains artificial zeros.\n *\n * The share is what `divideUsage()` already split across the stories that\n * completed in the same execute iteration — see core/engine.ts.\n */\nexport function applyStoryMetrics(\n plan: TaskPlan,\n storyIds: string[],\n share: ClaudeUsage,\n durationSeconds?: number,\n): TaskPlan {\n if (storyIds.length === 0) return plan;\n\n const targets = new Set(storyIds);\n\n return {\n ...plan,\n userStories: plan.userStories.map((story) => {\n if (!targets.has(story.id)) return story;\n\n const updated: UserStory = { ...story, ...sumUsage(story, share) };\n if (story.durationSeconds !== undefined || durationSeconds !== undefined) {\n updated.durationSeconds = (story.durationSeconds ?? 0) + (durationSeconds ?? 0);\n }\n return updated;\n }),\n };\n}\n\n/**\n * Mark the issue as in_progress.\n */\nexport function markIssueInProgress(plan: TaskPlan, timestamp?: string): TaskPlan {\n const ts = timestamp ?? isoNow();\n return {\n ...plan,\n issueStatus: 'in_progress',\n completedAt: null,\n lastAttemptAt: ts,\n };\n}\n\n/**\n * Mark the issue as completed.\n */\nexport function markIssueCompleted(plan: TaskPlan): TaskPlan {\n const ts = isoNow();\n return {\n ...plan,\n issueStatus: 'completed',\n completedAt: ts,\n lastAttemptAt: ts,\n lastError: null,\n pipeline: plan.pipeline ? { ...plan.pipeline, executionCompleted: true } : plan.pipeline,\n };\n}\n\n/**\n * Set the lastError field on the task plan.\n */\nexport function setLastError(plan: TaskPlan, category: string, message: string): TaskPlan {\n const ts = isoNow();\n const error: LastError = {\n category,\n message,\n at: ts,\n };\n\n return {\n ...plan,\n lastAttemptAt: ts,\n lastError: error,\n };\n}\n\n/**\n * Clear the lastError field, but only if it was set before the attempt started.\n * This prevents clearing errors that were set during the current attempt.\n */\nexport function clearLastError(plan: TaskPlan, attemptStartedAt: string): TaskPlan {\n const ts = isoNow();\n\n // If the error was set after the attempt started, keep it\n if (plan.lastError && plan.lastError.at > attemptStartedAt) {\n return {\n ...plan,\n lastAttemptAt: ts,\n };\n }\n\n return {\n ...plan,\n lastAttemptAt: ts,\n lastError: null,\n };\n}\n\n/**\n * Trim an error message to at most 8 non-empty lines.\n */\nexport function trimErrorMessage(message: string): string {\n return message\n .split('\\n')\n .filter((line) => line.trim().length > 0)\n .slice(0, 8)\n .join('\\n');\n}\n","import { hostname } from 'node:os';\nimport { isProcessAlive } from '../storage/lock.js';\nimport type { TaskPlan } from '../types.js';\n\nfunction nowIso(): string {\n return new Date().toISOString().replace(/\\.\\d{3}Z$/, 'Z');\n}\n\n/**\n * Convert orphan `running` records to `interrupted` using the two-signal\n * rule from `storage/lock.ts`: a pid on this host that is gone.\n *\n * A record owned by another host is left `running` — we cannot see that pid.\n */\nexport function reconcileInterruptedExecutions(plan: TaskPlan): TaskPlan {\n if (plan.executions === undefined || plan.executions.length === 0) return plan;\n let changed = false;\n const executions = plan.executions.map((record) => {\n if (record.status !== 'running' || record.finishedAt !== null) return record;\n const owner = record.owner;\n if (owner === undefined || owner === null) {\n changed = true;\n return { ...record, status: 'interrupted' as const, finishedAt: nowIso(), owner: null };\n }\n if (owner.host !== hostname()) return record;\n if (isProcessAlive(owner.pid)) return record;\n changed = true;\n return { ...record, status: 'interrupted' as const, finishedAt: nowIso(), owner: null };\n });\n return changed ? { ...plan, executions } : plan;\n}\n","/**\n * Single source of truth for translating the `claude` CLI JSON output into\n * token/cost metrics.\n *\n * Three call sites consume this (headless json, headless stream-json and the\n * execute-phase executor); keeping the parsing here is what stops them from\n * diverging again the next time the CLI payload changes shape.\n */\n\nexport interface ClaudeUsage {\n inputTokens?: number;\n outputTokens?: number;\n cacheReadTokens?: number;\n cacheCreationTokens?: number;\n costUsd?: number;\n /** Envelope `duration_ms` — request onward, not process startup. */\n cliDurationMs?: number;\n /** Envelope `duration_api_ms` when the harness reports it. */\n apiDurationMs?: number;\n /** Time to first output, when the harness reports it. */\n ttftMs?: number;\n numTurns?: number;\n}\n\n/** Field names of {@link ClaudeUsage}, used by the summing helper. */\nconst USAGE_FIELDS = [\n 'inputTokens',\n 'outputTokens',\n 'cacheReadTokens',\n 'cacheCreationTokens',\n 'costUsd',\n] as const;\n\n/**\n * Coerce a value to a finite number, or undefined when it is absent/unusable.\n * Never throws — malformed payloads simply yield undefined.\n */\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Parse a `claude` CLI result payload into a {@link ClaudeUsage}.\n *\n * Reads the current format (`total_cost_usd` plus a nested `usage` object) and\n * falls back to the legacy flat keys (`cost_usd`, `num_input_tokens`,\n * `num_output_tokens`) when the newer ones are absent.\n *\n * Returns null when the payload carries no recognizable field at all, and a\n * partial object when only some fields are present. Never throws.\n */\n/**\n * Parse a Codex `turn.completed.usage` object.\n *\n * Codex reports `input_tokens` including the cache (Decision 6 of #62), so\n * `inputTokens` is `input_tokens − cached_input_tokens`, clamped at 0. Codex\n * never reports USD; `costUsd` stays absent — \"not reported\", never zero.\n */\nexport function parseCodexUsage(payload: unknown): ClaudeUsage | null {\n if (!isRecord(payload)) return null;\n\n const usage: ClaudeUsage = {};\n const inputTokens = num(payload.input_tokens);\n const cachedInput = num(payload.cached_input_tokens);\n const cacheWrite = num(payload.cache_write_input_tokens);\n const outputTokens = num(payload.output_tokens);\n\n if (inputTokens !== undefined) {\n usage.inputTokens =\n cachedInput !== undefined ? Math.max(0, inputTokens - cachedInput) : inputTokens;\n }\n if (outputTokens !== undefined) usage.outputTokens = outputTokens;\n if (cachedInput !== undefined) usage.cacheReadTokens = cachedInput;\n if (cacheWrite !== undefined) usage.cacheCreationTokens = cacheWrite;\n\n return Object.keys(usage).length > 0 ? usage : null;\n}\n\nexport function parseUsage(payload: unknown): ClaudeUsage | null {\n if (!isRecord(payload)) return null;\n\n const usageRaw = isRecord(payload.usage) ? payload.usage : undefined;\n\n const usage: ClaudeUsage = {};\n\n const inputTokens = usageRaw ? num(usageRaw.input_tokens) : undefined;\n const outputTokens = usageRaw ? num(usageRaw.output_tokens) : undefined;\n const cacheReadTokens = usageRaw ? num(usageRaw.cache_read_input_tokens) : undefined;\n const cacheCreationTokens = usageRaw ? num(usageRaw.cache_creation_input_tokens) : undefined;\n const costUsd = num(payload.total_cost_usd);\n\n // Legacy fallbacks — only consulted when the modern key is missing.\n const legacyInput = num(payload.num_input_tokens);\n const legacyOutput = num(payload.num_output_tokens);\n const legacyCost = num(payload.cost_usd);\n\n const resolvedInput = inputTokens ?? legacyInput;\n const resolvedOutput = outputTokens ?? legacyOutput;\n const resolvedCost = costUsd ?? legacyCost;\n\n if (resolvedInput !== undefined) usage.inputTokens = resolvedInput;\n if (resolvedOutput !== undefined) usage.outputTokens = resolvedOutput;\n if (cacheReadTokens !== undefined) usage.cacheReadTokens = cacheReadTokens;\n if (cacheCreationTokens !== undefined) usage.cacheCreationTokens = cacheCreationTokens;\n if (resolvedCost !== undefined) usage.costUsd = resolvedCost;\n\n const cliDurationMs = num(payload.duration_ms);\n const apiDurationMs = num(payload.duration_api_ms);\n const ttftMs = num(payload.ttft_ms) ?? num(payload.time_to_first_token_ms);\n const numTurns = num(payload.num_turns);\n if (cliDurationMs !== undefined) usage.cliDurationMs = cliDurationMs;\n if (apiDurationMs !== undefined) usage.apiDurationMs = apiDurationMs;\n if (ttftMs !== undefined) usage.ttftMs = ttftMs;\n if (numTurns !== undefined) usage.numTurns = numTurns;\n\n return Object.keys(usage).length > 0 ? usage : null;\n}\n\n/**\n * Sum two usages field by field.\n *\n * `undefined` means \"not reported\", not zero: a field absent on both sides\n * stays absent in the result, while a field present on a single side is\n * carried over untouched.\n */\nexport function sumUsage(\n a: ClaudeUsage | null | undefined,\n b: ClaudeUsage | null | undefined,\n): ClaudeUsage {\n const result: ClaudeUsage = {};\n\n for (const field of USAGE_FIELDS) {\n const left = a?.[field];\n const right = b?.[field];\n if (left === undefined && right === undefined) continue;\n result[field] = (left ?? 0) + (right ?? 0);\n }\n\n return result;\n}\n\n/**\n * Split a usage evenly across `parts`.\n *\n * This is the rateio used when several stories complete in the same execute\n * iteration: the iteration's tokens cannot be attributed to a single story, so\n * each one receives an equal share. Token counts are rounded to integers;\n * the cost keeps full precision.\n *\n * `parts <= 1` (or a non-finite value) returns the usage unchanged. Fields the\n * CLI never reported stay absent — dividing \"not reported\" still yields\n * \"not reported\", never zero.\n */\nexport function divideUsage(usage: ClaudeUsage | null | undefined, parts: number): ClaudeUsage {\n const result: ClaudeUsage = {};\n if (!usage) return result;\n\n const divisor = Number.isFinite(parts) && parts > 1 ? parts : 1;\n\n for (const field of USAGE_FIELDS) {\n const value = usage[field];\n if (value === undefined) continue;\n result[field] = field === 'costUsd' ? value / divisor : Math.round(value / divisor);\n }\n\n return result;\n}\n\n/**\n * Compact token count: `1523` → `1.5k`, `2_400_000` → `2.4M`.\n */\nfunction compactTokens(value: number): string {\n const abs = Math.abs(value);\n if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;\n if (abs >= 1_000) return `${(value / 1_000).toFixed(1)}k`;\n return String(Math.round(value));\n}\n\n/**\n * Human-readable one-liner, e.g. `2 in / 4 out · 15.5k cache · ~$0.1607`.\n *\n * Segments whose data is missing are omitted entirely — callers can rely on an\n * empty string meaning \"no metrics at all\" and skip the line altogether rather\n * than printing zeros or NaN.\n */\nexport function formatTokens(usage: ClaudeUsage | null | undefined): string {\n if (!usage) return '';\n\n const segments: string[] = [];\n\n const io: string[] = [];\n if (usage.inputTokens !== undefined) io.push(`${compactTokens(usage.inputTokens)} in`);\n if (usage.outputTokens !== undefined) io.push(`${compactTokens(usage.outputTokens)} out`);\n if (io.length > 0) segments.push(io.join(' / '));\n\n const cache =\n usage.cacheReadTokens !== undefined || usage.cacheCreationTokens !== undefined\n ? (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0)\n : undefined;\n if (cache !== undefined) segments.push(`${compactTokens(cache)} cache`);\n\n if (usage.costUsd !== undefined) segments.push(`~$${usage.costUsd.toFixed(4)}`);\n\n return segments.join(' · ');\n}\n\n/**\n * True when the usage carries at least one reported field. Handy for deciding\n * whether a metrics line or event is worth emitting at all.\n */\nexport function hasUsageData(usage: ClaudeUsage | null | undefined): boolean {\n if (!usage) return false;\n return USAGE_FIELDS.some((field) => usage[field] !== undefined);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ sessionSnapshotSchema
4
+ } from "./chunk-LBGHI4S2.js";
5
+
6
+ // src/storage/session-file.ts
7
+ import { readFile, stat as statFile } from "fs/promises";
8
+ async function readSessionFile(filePath) {
9
+ let raw;
10
+ let mtimeMs;
11
+ try {
12
+ const [content, stat] = await Promise.all([readFile(filePath, "utf-8"), statFile(filePath)]);
13
+ raw = content;
14
+ mtimeMs = stat.mtimeMs;
15
+ } catch {
16
+ return null;
17
+ }
18
+ try {
19
+ const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));
20
+ if (!parsed.success || parsed.data.sessionId === null) return null;
21
+ return { snapshot: parsed.data, updatedAtMs: mtimeMs };
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ export {
28
+ readSessionFile
29
+ };
30
+ //# sourceMappingURL=chunk-JN6D3MY6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/storage/session-file.ts"],"sourcesContent":["import { readFile, stat as statFile } from 'node:fs/promises';\nimport { sessionSnapshotSchema, type ValidatedSessionSnapshot } from '../schemas.js';\n\nexport interface ReadSessionFileResult {\n snapshot: ValidatedSessionSnapshot;\n updatedAtMs: number;\n}\n\n/**\n * Read and validate one `session.json`. Anything short of a fully valid file\n * (missing, mid-write, corrupted, written by an incompatible future version)\n * reads as \"not currently a session\" — a directory scan must never fail\n * because one file is momentarily inconsistent.\n */\nexport async function readSessionFile(filePath: string): Promise<ReadSessionFileResult | null> {\n let raw: string;\n let mtimeMs: number;\n try {\n const [content, stat] = await Promise.all([readFile(filePath, 'utf-8'), statFile(filePath)]);\n raw = content;\n mtimeMs = stat.mtimeMs;\n } catch {\n return null;\n }\n\n try {\n const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));\n if (!parsed.success || parsed.data.sessionId === null) return null;\n return { snapshot: parsed.data, updatedAtMs: mtimeMs };\n } catch {\n return null;\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,UAAU,QAAQ,gBAAgB;AAc3C,eAAsB,gBAAgB,UAAyD;AAC7F,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,UAAM,CAAC,SAAS,IAAI,IAAI,MAAM,QAAQ,IAAI,CAAC,SAAS,UAAU,OAAO,GAAG,SAAS,QAAQ,CAAC,CAAC;AAC3F,UAAM;AACN,cAAU,KAAK;AAAA,EACjB,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,SAAS,sBAAsB,UAAU,KAAK,MAAM,GAAG,CAAC;AAC9D,QAAI,CAAC,OAAO,WAAW,OAAO,KAAK,cAAc,KAAM,QAAO;AAC9D,WAAO,EAAE,UAAU,OAAO,MAAM,aAAa,QAAQ;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- getProjectRoot
4
- } from "./chunk-DIDUVL6O.js";
5
2
  import {
6
3
  printWarning
7
- } from "./chunk-BYZDPR7N.js";
4
+ } from "./chunk-TYRNTSPJ.js";
5
+ import {
6
+ getProjectRoot
7
+ } from "./chunk-BEZ5BA5U.js";
8
8
 
9
9
  // src/core/prompt-resolver.ts
10
10
  import { existsSync } from "fs";
@@ -107,4 +107,4 @@ export {
107
107
  loadPrompt,
108
108
  applyPlaceholders
109
109
  };
110
- //# sourceMappingURL=chunk-LAFNLLQP.js.map
110
+ //# sourceMappingURL=chunk-KMRFRXIT.js.map
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ readSessionFile
4
+ } from "./chunk-JN6D3MY6.js";
5
+ import {
6
+ PROJECTS_DIR_NAME,
7
+ RUN_LOCK_FILENAME,
8
+ getGlobalRoot,
9
+ getIssuePaths
10
+ } from "./chunk-HAKDLR6N.js";
11
+ import {
12
+ isProcessAlive,
13
+ isRunLockStale,
14
+ readRunLock
15
+ } from "./chunk-LBGHI4S2.js";
16
+
17
+ // src/execution/registry.ts
18
+ import { readdir } from "fs/promises";
19
+ import { join } from "path";
20
+ function classifyRunLock(lock) {
21
+ if (!isProcessAlive(lock.pid)) return "orphan";
22
+ if (isRunLockStale(lock)) return "unsignaled";
23
+ return "running";
24
+ }
25
+ async function listLiveRuns(options = {}) {
26
+ const root = getGlobalRoot(options);
27
+ const projectsDir = join(root, PROJECTS_DIR_NAME);
28
+ let projectIds;
29
+ try {
30
+ const entries = await readdir(projectsDir, { withFileTypes: true });
31
+ projectIds = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
32
+ } catch {
33
+ return [];
34
+ }
35
+ const runs = await Promise.all(
36
+ projectIds.map(async (projectId) => {
37
+ const lockFile = join(projectsDir, projectId, RUN_LOCK_FILENAME);
38
+ const lock = await readRunLock(lockFile);
39
+ if (lock === null) return null;
40
+ return enrichRun(projectId, lockFile, lock, options);
41
+ })
42
+ );
43
+ return runs.filter((run) => run !== null).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
44
+ }
45
+ async function enrichRun(projectId, lockFile, lock, options) {
46
+ const session = await readSessionFile(getIssuePaths(projectId, lock.target, options).sessionFile);
47
+ return {
48
+ projectId,
49
+ target: lock.target,
50
+ pid: lock.pid,
51
+ host: lock.host,
52
+ detached: lock.detached === true,
53
+ status: classifyRunLock(lock),
54
+ startedAt: lock.startedAt,
55
+ lastHeartbeatAt: lock.lastHeartbeatAt,
56
+ issue: session?.snapshot.issue.number ?? numericTarget(lock.target),
57
+ phase: session?.snapshot.currentPhase ?? null,
58
+ storiesCompleted: session?.snapshot.progress.storiesCompleted ?? null,
59
+ storiesTotal: session?.snapshot.progress.storiesTotal ?? null,
60
+ elapsedSeconds: session?.snapshot.elapsedSeconds ?? null,
61
+ lockFile
62
+ };
63
+ }
64
+ function numericTarget(target) {
65
+ return /^\d+$/.test(target) ? Number(target) : null;
66
+ }
67
+
68
+ export {
69
+ classifyRunLock,
70
+ listLiveRuns
71
+ };
72
+ //# sourceMappingURL=chunk-KT7JKOXB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/execution/registry.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { isProcessAlive, isRunLockStale, readRunLock } from '../storage/lock.js';\nimport {\n type GetGlobalRootOptions,\n getGlobalRoot,\n getIssuePaths,\n PROJECTS_DIR_NAME,\n RUN_LOCK_FILENAME,\n} from '../storage/paths.js';\nimport type { RunLock } from '../storage/schemas.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\nexport type LiveRunStatus = 'running' | 'unsignaled' | 'orphan';\n\nexport interface LiveRun {\n projectId: string;\n target: string;\n pid: number;\n host: string;\n detached: boolean;\n status: LiveRunStatus;\n startedAt: string;\n lastHeartbeatAt: string;\n issue: number | null;\n phase: string | null;\n storiesCompleted: number | null;\n storiesTotal: number | null;\n elapsedSeconds: number | null;\n lockFile: string;\n}\n\nexport function classifyRunLock(lock: RunLock): LiveRunStatus {\n if (!isProcessAlive(lock.pid)) return 'orphan';\n if (isRunLockStale(lock)) return 'unsignaled';\n return 'running';\n}\n\n/**\n * Every run.lock under the global projects tree, enriched with session.json\n * when one exists. The lock is the source of truth for existence; the\n * snapshot only fills phase and progress.\n */\nexport async function listLiveRuns(options: GetGlobalRootOptions = {}): Promise<LiveRun[]> {\n const root = getGlobalRoot(options);\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n let projectIds: string[];\n try {\n const entries = await readdir(projectsDir, { withFileTypes: true });\n projectIds = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n return [];\n }\n\n const runs = await Promise.all(\n projectIds.map(async (projectId) => {\n const lockFile = join(projectsDir, projectId, RUN_LOCK_FILENAME);\n const lock = await readRunLock(lockFile);\n if (lock === null) return null;\n return enrichRun(projectId, lockFile, lock, options);\n }),\n );\n\n return runs\n .filter((run): run is LiveRun => run !== null)\n .sort((a, b) => a.startedAt.localeCompare(b.startedAt));\n}\n\nasync function enrichRun(\n projectId: string,\n lockFile: string,\n lock: RunLock,\n options: GetGlobalRootOptions,\n): Promise<LiveRun> {\n const session = await readSessionFile(getIssuePaths(projectId, lock.target, options).sessionFile);\n\n return {\n projectId,\n target: lock.target,\n pid: lock.pid,\n host: lock.host,\n detached: lock.detached === true,\n status: classifyRunLock(lock),\n startedAt: lock.startedAt,\n lastHeartbeatAt: lock.lastHeartbeatAt,\n issue: session?.snapshot.issue.number ?? numericTarget(lock.target),\n phase: session?.snapshot.currentPhase ?? null,\n storiesCompleted: session?.snapshot.progress.storiesCompleted ?? null,\n storiesTotal: session?.snapshot.progress.storiesTotal ?? null,\n elapsedSeconds: session?.snapshot.elapsedSeconds ?? null,\n lockFile,\n };\n}\n\nfunction numericTarget(target: string): number | null {\n return /^\\d+$/.test(target) ? Number(target) : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AACxB,SAAS,YAAY;AA+Bd,SAAS,gBAAgB,MAA8B;AAC5D,MAAI,CAAC,eAAe,KAAK,GAAG,EAAG,QAAO;AACtC,MAAI,eAAe,IAAI,EAAG,QAAO;AACjC,SAAO;AACT;AAOA,eAAsB,aAAa,UAAgC,CAAC,GAAuB;AACzF,QAAM,OAAO,cAAc,OAAO;AAClC,QAAM,cAAc,KAAK,MAAM,iBAAiB;AAChD,MAAI;AACJ,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC;AAClE,iBAAa,QAAQ,OAAO,CAAC,UAAU,MAAM,YAAY,CAAC,EAAE,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,EACvF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,OAAO,MAAM,QAAQ;AAAA,IACzB,WAAW,IAAI,OAAO,cAAc;AAClC,YAAM,WAAW,KAAK,aAAa,WAAW,iBAAiB;AAC/D,YAAM,OAAO,MAAM,YAAY,QAAQ;AACvC,UAAI,SAAS,KAAM,QAAO;AAC1B,aAAO,UAAU,WAAW,UAAU,MAAM,OAAO;AAAA,IACrD,CAAC;AAAA,EACH;AAEA,SAAO,KACJ,OAAO,CAAC,QAAwB,QAAQ,IAAI,EAC5C,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;AAC1D;AAEA,eAAe,UACb,WACA,UACA,MACA,SACkB;AAClB,QAAM,UAAU,MAAM,gBAAgB,cAAc,WAAW,KAAK,QAAQ,OAAO,EAAE,WAAW;AAEhG,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,KAAK,KAAK;AAAA,IACV,MAAM,KAAK;AAAA,IACX,UAAU,KAAK,aAAa;AAAA,IAC5B,QAAQ,gBAAgB,IAAI;AAAA,IAC5B,WAAW,KAAK;AAAA,IAChB,iBAAiB,KAAK;AAAA,IACtB,OAAO,SAAS,SAAS,MAAM,UAAU,cAAc,KAAK,MAAM;AAAA,IAClE,OAAO,SAAS,SAAS,gBAAgB;AAAA,IACzC,kBAAkB,SAAS,SAAS,SAAS,oBAAoB;AAAA,IACjE,cAAc,SAAS,SAAS,SAAS,gBAAgB;AAAA,IACzD,gBAAgB,SAAS,SAAS,kBAAkB;AAAA,IACpD;AAAA,EACF;AACF;AAEA,SAAS,cAAc,QAA+B;AACpD,SAAO,QAAQ,KAAK,MAAM,IAAI,OAAO,MAAM,IAAI;AACjD;","names":[]}