issue-flow 0.7.1 → 0.7.3

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 (80) hide show
  1. package/README.md +11 -7
  2. package/dist/{analyze-R25HDGBJ.js → analyze-PJBY346H.js} +19 -14
  3. package/dist/analyze-PJBY346H.js.map +1 -0
  4. package/dist/{chunk-EM43T462.js → chunk-3ISZ7T5M.js} +17 -12
  5. package/dist/chunk-3ISZ7T5M.js.map +1 -0
  6. package/dist/{chunk-EHVKXFFN.js → chunk-4M4EWFZZ.js} +2 -2
  7. package/dist/{chunk-JNEHVQH5.js → chunk-6HGZYTAT.js} +223 -11
  8. package/dist/chunk-6HGZYTAT.js.map +1 -0
  9. package/dist/chunk-7HX2RPFY.js +123 -0
  10. package/dist/chunk-7HX2RPFY.js.map +1 -0
  11. package/dist/{chunk-RN2FAMUU.js → chunk-7YS3J4A2.js} +71 -22
  12. package/dist/chunk-7YS3J4A2.js.map +1 -0
  13. package/dist/{chunk-YGEWIEAI.js → chunk-EBJM6G2D.js} +90 -23
  14. package/dist/chunk-EBJM6G2D.js.map +1 -0
  15. package/dist/{chunk-TCOS2KFB.js → chunk-IWBHBDRU.js} +22 -23
  16. package/dist/chunk-IWBHBDRU.js.map +1 -0
  17. package/dist/{chunk-23WOEBGN.js → chunk-NVSJYJ72.js} +31 -22
  18. package/dist/chunk-NVSJYJ72.js.map +1 -0
  19. package/dist/{chunk-2AVZZAC6.js → chunk-OCXHO7YP.js} +332 -27
  20. package/dist/chunk-OCXHO7YP.js.map +1 -0
  21. package/dist/{chunk-HJ7JT777.js → chunk-R7X26MWK.js} +18 -13
  22. package/dist/chunk-R7X26MWK.js.map +1 -0
  23. package/dist/{chunk-YLXPLHEL.js → chunk-RKASM5FF.js} +2 -2
  24. package/dist/{chunk-ASMCLXQ2.js → chunk-SHG2PPN7.js} +20 -15
  25. package/dist/chunk-SHG2PPN7.js.map +1 -0
  26. package/dist/{chunk-ZAEBDO3Q.js → chunk-WEXU4R3H.js} +23 -15
  27. package/dist/chunk-WEXU4R3H.js.map +1 -0
  28. package/dist/{chunk-WHIBTNYI.js → chunk-YPIIIDTT.js} +3 -3
  29. package/dist/cli.js +13 -13
  30. package/dist/{config-SJAQMFWB.js → config-7N7UJP32.js} +3 -3
  31. package/dist/execute-EZKQKKLO.js +13 -0
  32. package/dist/{generate-LUK3XTIP.js → generate-KUQNDMMR.js} +19 -10
  33. package/dist/generate-KUQNDMMR.js.map +1 -0
  34. package/dist/init-GL6US653.js +9 -0
  35. package/dist/plan-XNXGWNGY.js +15 -0
  36. package/dist/pr-XILDNJLM.js +15 -0
  37. package/dist/pr-review-4WG2RPRP.js +15 -0
  38. package/dist/prd-BLJ2XBPL.js +15 -0
  39. package/dist/review-MRA7ZQQG.js +13 -0
  40. package/dist/{run-SDRQDMSZ.js → run-X62QZCXN.js} +35 -26
  41. package/dist/run-X62QZCXN.js.map +1 -0
  42. package/package.json +2 -2
  43. package/prompts/generate.md +1 -1
  44. package/prompts/prd.md +1 -1
  45. package/web/CLAUDE.md +35 -0
  46. package/web/public/app.css +5 -0
  47. package/web/public/app.js +80 -7
  48. package/dist/analyze-R25HDGBJ.js.map +0 -1
  49. package/dist/chunk-23WOEBGN.js.map +0 -1
  50. package/dist/chunk-2AVZZAC6.js.map +0 -1
  51. package/dist/chunk-ASMCLXQ2.js.map +0 -1
  52. package/dist/chunk-EM43T462.js.map +0 -1
  53. package/dist/chunk-HJ7JT777.js.map +0 -1
  54. package/dist/chunk-JNEHVQH5.js.map +0 -1
  55. package/dist/chunk-RN2FAMUU.js.map +0 -1
  56. package/dist/chunk-SLHZPB3C.js +0 -50
  57. package/dist/chunk-SLHZPB3C.js.map +0 -1
  58. package/dist/chunk-TCOS2KFB.js.map +0 -1
  59. package/dist/chunk-YGEWIEAI.js.map +0 -1
  60. package/dist/chunk-ZAEBDO3Q.js.map +0 -1
  61. package/dist/execute-VZ7SHLP4.js +0 -13
  62. package/dist/generate-LUK3XTIP.js.map +0 -1
  63. package/dist/init-5KXCG36P.js +0 -9
  64. package/dist/plan-R34SAWRK.js +0 -15
  65. package/dist/pr-P4BDQXUQ.js +0 -15
  66. package/dist/pr-review-TOSBE6ZS.js +0 -15
  67. package/dist/prd-RYLULM75.js +0 -15
  68. package/dist/review-ZSC5ELQA.js +0 -13
  69. package/dist/run-SDRQDMSZ.js.map +0 -1
  70. /package/dist/{chunk-EHVKXFFN.js.map → chunk-4M4EWFZZ.js.map} +0 -0
  71. /package/dist/{chunk-YLXPLHEL.js.map → chunk-RKASM5FF.js.map} +0 -0
  72. /package/dist/{chunk-WHIBTNYI.js.map → chunk-YPIIIDTT.js.map} +0 -0
  73. /package/dist/{config-SJAQMFWB.js.map → config-7N7UJP32.js.map} +0 -0
  74. /package/dist/{execute-VZ7SHLP4.js.map → execute-EZKQKKLO.js.map} +0 -0
  75. /package/dist/{init-5KXCG36P.js.map → init-GL6US653.js.map} +0 -0
  76. /package/dist/{plan-R34SAWRK.js.map → plan-XNXGWNGY.js.map} +0 -0
  77. /package/dist/{pr-P4BDQXUQ.js.map → pr-XILDNJLM.js.map} +0 -0
  78. /package/dist/{pr-review-TOSBE6ZS.js.map → pr-review-4WG2RPRP.js.map} +0 -0
  79. /package/dist/{prd-RYLULM75.js.map → prd-BLJ2XBPL.js.map} +0 -0
  80. /package/dist/{review-ZSC5ELQA.js.map → review-MRA7ZQQG.js.map} +0 -0
package/README.md CHANGED
@@ -103,7 +103,7 @@ Verifies that `claude`, `gh` (authenticated), and `git` (inside a repo) are avai
103
103
  npx issue-flow analyze 42
104
104
  ```
105
105
 
106
- Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce a structured analysis saved to `issues/42/analysis.md`.
106
+ Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce a structured analysis saved to `~/.issue-flow/…/issues/42/analysis.md` (see [Pipeline State](#pipeline-state)).
107
107
 
108
108
  ### `prd` -- Generate a PRD
109
109
 
@@ -111,7 +111,7 @@ Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce
111
111
  npx issue-flow prd 42
112
112
  ```
113
113
 
114
- Generates a Product Requirements Document from the issue analysis. Reads `issues/N/analysis.md` as context if available. Saves to `issues/42/prd.md`.
114
+ Generates a Product Requirements Document from the issue analysis. Reads `analysis.md` from the same directory as context if available. Saves to `~/.issue-flow/…/issues/42/prd.md`.
115
115
 
116
116
  ### `plan` -- Convert PRD to task plan
117
117
 
@@ -119,7 +119,7 @@ Generates a Product Requirements Document from the issue analysis. Reads `issues
119
119
  npx issue-flow plan 42
120
120
  ```
121
121
 
122
- Converts the PRD into a structured `issues/42/tasks.json` with ordered user stories, acceptance criteria, and pipeline state. Validates the output with zod schemas.
122
+ Converts the PRD into a structured `~/.issue-flow/…/issues/42/tasks.json` with ordered user stories, acceptance criteria, and pipeline state. Validates the output with zod schemas.
123
123
 
124
124
  ### `execute` -- Run the story execution loop
125
125
 
@@ -133,7 +133,7 @@ Runs the iterative agent loop. Each iteration is a fresh Claude instance that pi
133
133
 
134
134
  | Flag | Description |
135
135
  |------|-------------|
136
- | `--issue N` | Issue number -- reads artifacts from `issues/N/` |
136
+ | `--issue N` | Issue number -- reads artifacts from `~/.issue-flow/…/issues/N/` |
137
137
  | `--max-iterations N` | Stop after N iterations (default: unlimited) |
138
138
  | `--retry-limit N` | Retry transient Claude failures up to N consecutive times (default: 10) |
139
139
  | `--retry-forever` | Retry transient Claude failures indefinitely |
@@ -185,22 +185,26 @@ Each setting resolves with the precedence **CLI flag > environment variable > `.
185
185
  | `--web-log-limit <n>` | `ISSUE_FLOW_WEB_LOG_LIMIT` | `web.logLimit` | `200` |
186
186
  | `--web-no-logs` | -- | `web.includeLogs` | logs included |
187
187
 
188
- Monitoring never affects the pipeline: with `--web` off the behavior is byte-for-byte identical, a busy port just skips the server with a warning, and killing the server mid-run has no effect on the execution. While enabled, the snapshot served at `/api/status` is also persisted to `issues/N/session.json` -- if your project commits the `issues/` directory, add `issues/*/session.json` to your `.gitignore`.
188
+ Monitoring never affects the pipeline: with `--web` off the behavior is byte-for-byte identical, a busy port just skips the server with a warning, and killing the server mid-run has no effect on the execution. While enabled, the snapshot served at `/api/status` is also persisted to `~/.issue-flow/…/issues/N/session.json` -- outside your working tree, so there is nothing to add to `.gitignore`.
189
189
 
190
190
  For the full documentation (endpoints, `session.json` format, Tailscale setup), see the [root README](../../README.md#web-monitoring).
191
191
 
192
192
  ## Pipeline State
193
193
 
194
- Each issue's state is tracked in `issues/N/tasks.json`:
194
+ Each issue's state is tracked in a directory of its own under `~/.issue-flow`, keyed by a deterministic project id and the issue identifier:
195
195
 
196
196
  ```
197
- issues/42/
197
+ ~/.issue-flow/projects/<project-id>/issues/42/
198
198
  analysis.md # Issue analysis
199
199
  prd.md # Product requirements
200
200
  tasks.json # Task plan with pipeline state and user stories
201
201
  progress.txt # Execution log
202
+ session.json # Live session snapshot (web monitoring only)
203
+ pr-review/ # PR review reports and index
202
204
  ```
203
205
 
206
+ Nothing is written to your repository. A `<projectRoot>/issues/` tree left by an earlier release is copied here automatically on first use and then treated as read-only -- see the [root README](../../README.md#global-storage) for the full layout, the project id derivation and the migration.
207
+
204
208
  The `pipeline` field tracks which phases have completed, enabling resume from any point:
205
209
 
206
210
  ```json
@@ -2,17 +2,18 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-RN2FAMUU.js";
5
+ } from "./chunk-7YS3J4A2.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-TCOS2KFB.js";
8
+ } from "./chunk-IWBHBDRU.js";
9
9
  import {
10
10
  applyPlaceholders,
11
- loadPrompt
12
- } from "./chunk-SLHZPB3C.js";
11
+ loadPrompt,
12
+ publishPhaseMetrics
13
+ } from "./chunk-7HX2RPFY.js";
13
14
  import {
14
- getIssueDir
15
- } from "./chunk-2AVZZAC6.js";
15
+ resolveIssuePaths
16
+ } from "./chunk-OCXHO7YP.js";
16
17
  import {
17
18
  getGlobalTimeout,
18
19
  loadTaskPlan,
@@ -20,20 +21,19 @@ import {
20
21
  printInfo,
21
22
  printSuccess,
22
23
  saveTaskPlan
23
- } from "./chunk-JNEHVQH5.js";
24
+ } from "./chunk-6HGZYTAT.js";
24
25
 
25
26
  // src/commands/analyze.ts
26
27
  import { mkdir, readFile, writeFile } from "fs/promises";
27
- import { join } from "path";
28
28
  async function runAnalyze(issue, resolvedIssue) {
29
29
  const issueNumber = issue.replace(/^#/, "");
30
- const issueDir = await getIssueDir(issueNumber);
31
- const analysisPath = join(issueDir, "analysis.md");
30
+ const paths = await resolveIssuePaths(issueNumber);
31
+ const analysisPath = paths.analysisFile;
32
32
  const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);
33
33
  if (!resolution.ok) {
34
34
  return resolution.code;
35
35
  }
36
- await mkdir(issueDir, { recursive: true });
36
+ await mkdir(paths.issueDir, { recursive: true });
37
37
  const template = await loadPrompt("analyze");
38
38
  const prompt = applyPlaceholders(template, {
39
39
  __ISSUE_NUMBER__: issueNumber,
@@ -42,14 +42,19 @@ async function runAnalyze(issue, resolvedIssue) {
42
42
  // body that happens to contain a placeholder is left untouched.
43
43
  ...issuePlaceholders(resolution.resolved)
44
44
  });
45
+ const startedAtMs = Date.now();
45
46
  const result = await runHeadless({
46
47
  prompt,
47
48
  maxTurns: 30,
48
49
  timeout: getGlobalTimeout() ?? 3e5,
49
- outputFormat: "text",
50
+ // json (not text) so the CLI reports usage: the envelope's `result` field
51
+ // carries the same assistant text this phase already consumed.
52
+ outputFormat: "json",
50
53
  allowedTools: ["Bash", "Read", "Glob", "Grep", "Write"],
54
+ addDirs: [paths.issueDir],
51
55
  statusMessage: `Analyzing issue #${issueNumber}...`
52
56
  });
57
+ publishPhaseMetrics("analyze", result.cost, startedAtMs);
53
58
  if (!result.success) {
54
59
  printError(`Analysis failed: ${result.error}`);
55
60
  return 1;
@@ -64,7 +69,7 @@ async function runAnalyze(issue, resolvedIssue) {
64
69
  printInfo("Headless did not create analysis file; saving output directly");
65
70
  await writeFile(analysisPath, result.result, "utf-8");
66
71
  }
67
- const tasksPath = join(issueDir, "tasks.json");
72
+ const tasksPath = paths.tasksFile;
68
73
  try {
69
74
  const plan = await loadTaskPlan(tasksPath);
70
75
  plan.pipeline.analyzeCompleted = true;
@@ -77,4 +82,4 @@ async function runAnalyze(issue, resolvedIssue) {
77
82
  export {
78
83
  runAnalyze
79
84
  };
80
- //# sourceMappingURL=analyze-R25HDGBJ.js.map
85
+ //# sourceMappingURL=analyze-PJBY346H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/analyze.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printInfo, printSuccess } from '../ui/logger.js';\n\nexport async function runAnalyze(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const analysisPath = paths.analysisFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const template = await loadPrompt('analyze');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __ANALYSIS_PATH__: analysisPath,\n // Last: the Issue content is substituted in but never scanned again, so a\n // body that happens to contain a placeholder is left untouched.\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 30,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Analyzing issue #${issueNumber}...`,\n });\n // Before the success check: the tokens were spent either way.\n publishPhaseMetrics('analyze', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Analysis failed: ${result.error}`);\n return 1;\n }\n\n // Verify the file was created\n try {\n const content = await readFile(analysisPath, 'utf-8');\n if (content.length < 10) {\n printError('Analysis file was created but appears empty');\n return 1;\n }\n } catch {\n // File wasn't created by headless — save the result as analysis\n printInfo('Headless did not create analysis file; saving output directly');\n await writeFile(analysisPath, result.result, 'utf-8');\n }\n\n // Update pipeline state\n const tasksPath = paths.tasksFile;\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.analyzeCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist yet — that's OK for standalone analyze\n }\n\n printSuccess(`Analysis saved to ${analysisPath}`);\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,iBAAiB;AAW3C,eAAsB,WAAW,OAAe,eAAgD;AAC9F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,eAAe,MAAM;AAE3B,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,MAAM,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE/C,QAAM,WAAW,MAAM,WAAW,SAAS;AAC3C,QAAM,SAAS,kBAAkB,UAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,mBAAmB;AAAA;AAAA;AAAA,IAGnB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;AAAA,IACtD,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,sBAAoB,WAAW,OAAO,MAAM,WAAW;AAEvD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,oBAAoB,OAAO,KAAK,EAAE;AAC7C,WAAO;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,cAAc,OAAO;AACpD,QAAI,QAAQ,SAAS,IAAI;AACvB,iBAAW,6CAA6C;AACxD,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAEN,cAAU,+DAA+D;AACzE,UAAM,UAAU,cAAc,OAAO,QAAQ,OAAO;AAAA,EACtD;AAGA,QAAM,YAAY,MAAM;AACxB,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,mBAAmB;AACjC,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,eAAa,qBAAqB,YAAY,EAAE;AAChD,SAAO;AACT;","names":[]}
@@ -2,27 +2,27 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-RN2FAMUU.js";
5
+ } from "./chunk-7YS3J4A2.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-TCOS2KFB.js";
8
+ } from "./chunk-IWBHBDRU.js";
9
9
  import {
10
10
  applyPlaceholders,
11
- loadPrompt
12
- } from "./chunk-SLHZPB3C.js";
11
+ loadPrompt,
12
+ publishPhaseMetrics
13
+ } from "./chunk-7HX2RPFY.js";
13
14
  import {
14
- getIssueDir
15
- } from "./chunk-2AVZZAC6.js";
15
+ resolveIssuePaths
16
+ } from "./chunk-OCXHO7YP.js";
16
17
  import {
17
18
  getGlobalTimeout,
18
19
  loadTaskPlan,
19
20
  printError,
20
21
  printSuccess,
21
22
  saveTaskPlan
22
- } from "./chunk-JNEHVQH5.js";
23
+ } from "./chunk-6HGZYTAT.js";
23
24
 
24
25
  // src/commands/review.ts
25
- import { join } from "path";
26
26
  function parseReviewResult(output) {
27
27
  const match = output.match(/<review-result>([\s\S]*?)<\/review-result>/);
28
28
  if (!match) {
@@ -47,8 +47,8 @@ function parseReviewResult(output) {
47
47
  }
48
48
  async function runReview(issue, resolvedIssue) {
49
49
  const issueNumber = issue.replace(/^#/, "");
50
- const issueDir = await getIssueDir(issueNumber);
51
- const tasksPath = join(issueDir, "tasks.json");
50
+ const paths = await resolveIssuePaths(issueNumber);
51
+ const tasksPath = paths.tasksFile;
52
52
  const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);
53
53
  if (!resolution.ok) {
54
54
  return resolution.code;
@@ -59,14 +59,19 @@ async function runReview(issue, resolvedIssue) {
59
59
  __TASKS_PATH__: tasksPath,
60
60
  ...issuePlaceholders(resolution.resolved)
61
61
  });
62
+ const startedAtMs = Date.now();
62
63
  const result = await runHeadless({
63
64
  prompt,
64
65
  maxTurns: 25,
65
66
  timeout: getGlobalTimeout() ?? 3e5,
66
- outputFormat: "text",
67
+ // json (not text) so the CLI reports usage: the envelope's `result` field
68
+ // carries the same assistant text parseReviewResult() already consumed.
69
+ outputFormat: "json",
67
70
  allowedTools: ["Bash", "Read", "Glob", "Grep"],
71
+ addDirs: [paths.issueDir],
68
72
  statusMessage: `Reviewing issue #${issueNumber} resolution...`
69
73
  });
74
+ publishPhaseMetrics("review", result.cost, startedAtMs);
70
75
  if (!result.success) {
71
76
  printError(`Review failed: ${result.error}`);
72
77
  return 1;
@@ -100,4 +105,4 @@ async function runReview(issue, resolvedIssue) {
100
105
  export {
101
106
  runReview
102
107
  };
103
- //# sourceMappingURL=chunk-EM43T462.js.map
108
+ //# sourceMappingURL=chunk-3ISZ7T5M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkBA,SAAS,kBAAkB,QAA8B;AACvD,QAAM,QAAQ,OAAO,MAAM,4CAA4C;AACvE,MAAI,CAAC,OAAO;AAEV,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC1B;AACA,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,YAAMA,iBAAgB,OAAO,MAAM,8BAA8B;AACjE,aAAO,EAAE,QAAQ,QAAQ,UAAUA,iBAAgB,CAAC,GAAG,KAAK,KAAK,mBAAmB;AAAA,IACtF;AAEA,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,kBAAkB,KAAK,KAAK,GAAG;AACjC,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,gBAAgB,MAAM,MAAM,oBAAoB;AACtD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,UAAU,gBAAgB,CAAC,GAAG,KAAK,KAAK;AAAA,EAC1C;AACF;AAEA,eAAsB,UAAU,OAAe,eAAgD;AAC7F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,YAAY,MAAM;AAExB,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,WAAW,MAAM,WAAW,QAAQ;AAC1C,QAAM,SAAS,kBAAkB,UAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAGD,sBAAoB,UAAU,OAAO,MAAM,WAAW;AAEtD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,kBAAkB,OAAO,KAAK,EAAE;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,kBAAkB,OAAO,MAAM;AAE9C,MAAI,OAAO,WAAW,QAAQ;AAE5B,QAAI;AACF,YAAM,OAAO,MAAM,aAAa,SAAS;AACzC,WAAK,SAAS,kBAAkB;AAChC,WAAK,qBAAqB;AAC1B,YAAM,aAAa,WAAW,IAAI;AAAA,IACpC,QAAQ;AAAA,IAER;AAEA,iBAAa,+DAA0D;AACvE,WAAO;AAAA,EACT;AAEA,aAAW,oCAA+B;AAC1C,MAAI,OAAO,UAAU;AACnB,YAAQ,IAAI,OAAO,QAAQ;AAAA,EAC7B;AAMA,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,kBAAkB;AAChC,SAAK,qBAAqB,OAAO,YAAY;AAC7C,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;","names":["findingsMatch"]}
@@ -7,7 +7,7 @@ import {
7
7
  getSessionPublisher,
8
8
  isoNow,
9
9
  printRetry
10
- } from "./chunk-JNEHVQH5.js";
10
+ } from "./chunk-6HGZYTAT.js";
11
11
 
12
12
  // src/core/phase-runner.ts
13
13
  import { readFile } from "fs/promises";
@@ -55,4 +55,4 @@ export {
55
55
  runPhaseWithRetry,
56
56
  readFileWithGrace
57
57
  };
58
- //# sourceMappingURL=chunk-EHVKXFFN.js.map
58
+ //# sourceMappingURL=chunk-4M4EWFZZ.js.map
@@ -2,6 +2,13 @@
2
2
 
3
3
  // src/schemas.ts
4
4
  import { z } from "zod";
5
+ var claudeUsageSchema = z.object({
6
+ inputTokens: z.number().optional(),
7
+ outputTokens: z.number().optional(),
8
+ cacheReadTokens: z.number().optional(),
9
+ cacheCreationTokens: z.number().optional(),
10
+ costUsd: z.number().optional()
11
+ });
5
12
  var userStorySchema = z.object({
6
13
  id: z.string(),
7
14
  title: z.string(),
@@ -9,7 +16,9 @@ var userStorySchema = z.object({
9
16
  acceptanceCriteria: z.array(z.string()),
10
17
  priority: z.number().int().positive(),
11
18
  passes: z.boolean(),
12
- notes: z.string()
19
+ notes: z.string(),
20
+ ...claudeUsageSchema.shape,
21
+ durationSeconds: z.number().optional()
13
22
  });
14
23
  var pipelineStateSchema = z.object({
15
24
  analyzeCompleted: z.boolean().optional(),
@@ -89,10 +98,7 @@ var taskPlanSchema = z.object({
89
98
  var headlessResultSchema = z.object({
90
99
  success: z.boolean(),
91
100
  result: z.string(),
92
- cost: z.object({
93
- inputTokens: z.number().optional(),
94
- outputTokens: z.number().optional()
95
- }).nullable(),
101
+ cost: claudeUsageSchema.nullable(),
96
102
  error: z.string().nullable()
97
103
  });
98
104
  var sessionLogEntrySchema = z.object({
@@ -100,20 +106,31 @@ var sessionLogEntrySchema = z.object({
100
106
  level: z.enum(["info", "warn", "error"]),
101
107
  message: z.string()
102
108
  });
109
+ var sessionUsageShape = {
110
+ inputTokens: z.number().nullable().default(null),
111
+ outputTokens: z.number().nullable().default(null),
112
+ cacheReadTokens: z.number().nullable().default(null),
113
+ cacheCreationTokens: z.number().nullable().default(null),
114
+ costUsd: z.number().nullable().default(null)
115
+ };
103
116
  var sessionPhaseSchema = z.object({
104
117
  name: z.string(),
105
118
  status: z.enum(["pending", "running", "completed", "failed"]),
106
119
  startedAt: z.string().nullable(),
107
120
  endedAt: z.string().nullable(),
108
121
  durationSeconds: z.number().nullable(),
109
- error: z.string().nullable()
122
+ error: z.string().nullable(),
123
+ ...sessionUsageShape
110
124
  });
111
125
  var sessionStorySchema = z.object({
112
126
  id: z.string(),
113
127
  title: z.string(),
114
128
  priority: z.number(),
115
129
  passes: z.boolean(),
116
- completedAt: z.string().nullable()
130
+ completedAt: z.string().nullable(),
131
+ // Also introduced with the metrics, hence the same tolerant default.
132
+ durationSeconds: z.number().nullable().default(null),
133
+ ...sessionUsageShape
117
134
  });
118
135
  var sessionSnapshotSchema = z.object({
119
136
  schemaVersion: z.literal(1),
@@ -143,6 +160,21 @@ var sessionSnapshotSchema = z.object({
143
160
  }).nullable(),
144
161
  phases: z.array(sessionPhaseSchema),
145
162
  stories: z.array(sessionStorySchema),
163
+ // The whole aggregate is additive: a snapshot from before it existed parses
164
+ // into the same "nothing reported" object the reducer starts from.
165
+ metrics: z.object({
166
+ totalInputTokens: z.number().nullable().default(null),
167
+ totalOutputTokens: z.number().nullable().default(null),
168
+ totalCacheReadTokens: z.number().nullable().default(null),
169
+ totalCacheCreationTokens: z.number().nullable().default(null),
170
+ totalCostUsd: z.number().nullable().default(null)
171
+ }).default({
172
+ totalInputTokens: null,
173
+ totalOutputTokens: null,
174
+ totalCacheReadTokens: null,
175
+ totalCacheCreationTokens: null,
176
+ totalCostUsd: null
177
+ }),
146
178
  execution: z.object({
147
179
  iteration: z.number(),
148
180
  retries: z.number(),
@@ -186,6 +218,24 @@ import { dirname } from "path";
186
218
  import { stripVTControlCharacters } from "util";
187
219
  var DEFAULT_LOG_LIMIT = 200;
188
220
  var DEFAULT_THROTTLE_MS = 1e3;
221
+ function emptyUsage() {
222
+ return {
223
+ inputTokens: null,
224
+ outputTokens: null,
225
+ cacheReadTokens: null,
226
+ cacheCreationTokens: null,
227
+ costUsd: null
228
+ };
229
+ }
230
+ function emptyMetrics() {
231
+ return {
232
+ totalInputTokens: null,
233
+ totalOutputTokens: null,
234
+ totalCacheReadTokens: null,
235
+ totalCacheCreationTokens: null,
236
+ totalCostUsd: null
237
+ };
238
+ }
189
239
  function createInitialSnapshot() {
190
240
  return {
191
241
  schemaVersion: 1,
@@ -210,6 +260,7 @@ function createInitialSnapshot() {
210
260
  currentActivity: null,
211
261
  phases: [],
212
262
  stories: [],
263
+ metrics: emptyMetrics(),
213
264
  execution: { iteration: 0, retries: 0, correctionCycle: 0, maxCorrectionCycles: null },
214
265
  git: { branch: null, baseBranch: null, commits: [] },
215
266
  pullRequests: [],
@@ -221,6 +272,19 @@ function createInitialSnapshot() {
221
272
  environment: null
222
273
  };
223
274
  }
275
+ function accumulate(current, delta) {
276
+ return delta === void 0 ? current : (current ?? 0) + delta;
277
+ }
278
+ function accumulateUsage(target, event) {
279
+ return {
280
+ ...target,
281
+ inputTokens: accumulate(target.inputTokens, event.inputTokens),
282
+ outputTokens: accumulate(target.outputTokens, event.outputTokens),
283
+ cacheReadTokens: accumulate(target.cacheReadTokens, event.cacheReadTokens),
284
+ cacheCreationTokens: accumulate(target.cacheCreationTokens, event.cacheCreationTokens),
285
+ costUsd: accumulate(target.costUsd, event.costUsd)
286
+ };
287
+ }
224
288
  function secondsBetween(from, to) {
225
289
  if (from === null) return null;
226
290
  const fromMs = Date.parse(from);
@@ -284,7 +348,8 @@ function applyEvent(snapshot, event, options) {
284
348
  startedAt: null,
285
349
  endedAt: null,
286
350
  durationSeconds: null,
287
- error: null
351
+ error: null,
352
+ ...emptyUsage()
288
353
  })),
289
354
  git: { branch: event.branch ?? null, baseBranch: event.baseBranch ?? null, commits: [] },
290
355
  environment: event.environment ?? null
@@ -308,7 +373,8 @@ function applyEvent(snapshot, event, options) {
308
373
  startedAt: event.at,
309
374
  endedAt: null,
310
375
  durationSeconds: null,
311
- error: null
376
+ error: null,
377
+ ...emptyUsage()
312
378
  }
313
379
  ];
314
380
  return {
@@ -363,7 +429,13 @@ function applyEvent(snapshot, event, options) {
363
429
  title: story.title,
364
430
  priority: story.priority,
365
431
  passes: story.passes,
366
- completedAt
432
+ completedAt,
433
+ durationSeconds: before?.durationSeconds ?? null,
434
+ inputTokens: before?.inputTokens ?? null,
435
+ outputTokens: before?.outputTokens ?? null,
436
+ cacheReadTokens: before?.cacheReadTokens ?? null,
437
+ cacheCreationTokens: before?.cacheCreationTokens ?? null,
438
+ costUsd: before?.costUsd ?? null
367
439
  };
368
440
  });
369
441
  return {
@@ -413,6 +485,44 @@ function applyEvent(snapshot, event, options) {
413
485
  maxCorrectionCycles: event.maxCycles
414
486
  }
415
487
  };
488
+ case "metrics:update": {
489
+ if (event.scope === "story") {
490
+ if (event.storyId === void 0) return snapshot;
491
+ if (!snapshot.stories.some((s) => s.id === event.storyId)) return snapshot;
492
+ return {
493
+ ...snapshot,
494
+ stories: snapshot.stories.map(
495
+ (story) => story.id === event.storyId ? {
496
+ ...accumulateUsage(story, event),
497
+ durationSeconds: accumulate(story.durationSeconds, event.durationSeconds)
498
+ } : story
499
+ )
500
+ };
501
+ }
502
+ if (event.phase === void 0) return snapshot;
503
+ if (!snapshot.phases.some((p) => p.name === event.phase)) return snapshot;
504
+ return {
505
+ ...snapshot,
506
+ // durationSeconds is deliberately untouched here: phase:start and
507
+ // phase:end remain the single source of a phase's wall-clock time.
508
+ phases: snapshot.phases.map(
509
+ (phase) => phase.name === event.phase ? accumulateUsage(phase, event) : phase
510
+ ),
511
+ metrics: {
512
+ totalInputTokens: accumulate(snapshot.metrics.totalInputTokens, event.inputTokens),
513
+ totalOutputTokens: accumulate(snapshot.metrics.totalOutputTokens, event.outputTokens),
514
+ totalCacheReadTokens: accumulate(
515
+ snapshot.metrics.totalCacheReadTokens,
516
+ event.cacheReadTokens
517
+ ),
518
+ totalCacheCreationTokens: accumulate(
519
+ snapshot.metrics.totalCacheCreationTokens,
520
+ event.cacheCreationTokens
521
+ ),
522
+ totalCostUsd: accumulate(snapshot.metrics.totalCostUsd, event.costUsd)
523
+ }
524
+ };
525
+ }
416
526
  case "session:end":
417
527
  return {
418
528
  ...snapshot,
@@ -599,6 +709,86 @@ async function writeFileAtomic(path, content) {
599
709
  }
600
710
  }
601
711
 
712
+ // src/core/metrics.ts
713
+ var USAGE_FIELDS = [
714
+ "inputTokens",
715
+ "outputTokens",
716
+ "cacheReadTokens",
717
+ "cacheCreationTokens",
718
+ "costUsd"
719
+ ];
720
+ function num(value) {
721
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
722
+ }
723
+ function isRecord(value) {
724
+ return typeof value === "object" && value !== null;
725
+ }
726
+ function parseUsage(payload) {
727
+ if (!isRecord(payload)) return null;
728
+ const usageRaw = isRecord(payload.usage) ? payload.usage : void 0;
729
+ const usage = {};
730
+ const inputTokens = usageRaw ? num(usageRaw.input_tokens) : void 0;
731
+ const outputTokens = usageRaw ? num(usageRaw.output_tokens) : void 0;
732
+ const cacheReadTokens = usageRaw ? num(usageRaw.cache_read_input_tokens) : void 0;
733
+ const cacheCreationTokens = usageRaw ? num(usageRaw.cache_creation_input_tokens) : void 0;
734
+ const costUsd = num(payload.total_cost_usd);
735
+ const legacyInput = num(payload.num_input_tokens);
736
+ const legacyOutput = num(payload.num_output_tokens);
737
+ const legacyCost = num(payload.cost_usd);
738
+ const resolvedInput = inputTokens ?? legacyInput;
739
+ const resolvedOutput = outputTokens ?? legacyOutput;
740
+ const resolvedCost = costUsd ?? legacyCost;
741
+ if (resolvedInput !== void 0) usage.inputTokens = resolvedInput;
742
+ if (resolvedOutput !== void 0) usage.outputTokens = resolvedOutput;
743
+ if (cacheReadTokens !== void 0) usage.cacheReadTokens = cacheReadTokens;
744
+ if (cacheCreationTokens !== void 0) usage.cacheCreationTokens = cacheCreationTokens;
745
+ if (resolvedCost !== void 0) usage.costUsd = resolvedCost;
746
+ return Object.keys(usage).length > 0 ? usage : null;
747
+ }
748
+ function sumUsage(a, b) {
749
+ const result = {};
750
+ for (const field of USAGE_FIELDS) {
751
+ const left = a?.[field];
752
+ const right = b?.[field];
753
+ if (left === void 0 && right === void 0) continue;
754
+ result[field] = (left ?? 0) + (right ?? 0);
755
+ }
756
+ return result;
757
+ }
758
+ function divideUsage(usage, parts) {
759
+ const result = {};
760
+ if (!usage) return result;
761
+ const divisor = Number.isFinite(parts) && parts > 1 ? parts : 1;
762
+ for (const field of USAGE_FIELDS) {
763
+ const value = usage[field];
764
+ if (value === void 0) continue;
765
+ result[field] = field === "costUsd" ? value / divisor : Math.round(value / divisor);
766
+ }
767
+ return result;
768
+ }
769
+ function compactTokens(value) {
770
+ const abs = Math.abs(value);
771
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
772
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
773
+ return String(Math.round(value));
774
+ }
775
+ function formatTokens(usage) {
776
+ if (!usage) return "";
777
+ const segments = [];
778
+ const io = [];
779
+ if (usage.inputTokens !== void 0) io.push(`${compactTokens(usage.inputTokens)} in`);
780
+ if (usage.outputTokens !== void 0) io.push(`${compactTokens(usage.outputTokens)} out`);
781
+ if (io.length > 0) segments.push(io.join(" / "));
782
+ const cache = usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0 ? (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0) : void 0;
783
+ if (cache !== void 0) segments.push(`${compactTokens(cache)} cache`);
784
+ if (usage.costUsd !== void 0) segments.push(`~$${usage.costUsd.toFixed(4)}`);
785
+ return segments.join(" \xB7 ");
786
+ }
787
+ function hasUsageData(usage) {
788
+ if (!usage) return false;
789
+ return USAGE_FIELDS.some((field) => usage[field] !== void 0);
790
+ }
791
+
602
792
  // src/core/state-manager.ts
603
793
  function isoNow() {
604
794
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -650,6 +840,21 @@ function allStoriesPass(plan) {
650
840
  }
651
841
  return plan.userStories.every((story) => story.passes === true);
652
842
  }
843
+ function applyStoryMetrics(plan, storyIds, share, durationSeconds) {
844
+ if (storyIds.length === 0) return plan;
845
+ const targets = new Set(storyIds);
846
+ return {
847
+ ...plan,
848
+ userStories: plan.userStories.map((story) => {
849
+ if (!targets.has(story.id)) return story;
850
+ const updated = { ...story, ...sumUsage(story, share) };
851
+ if (story.durationSeconds !== void 0 || durationSeconds !== void 0) {
852
+ updated.durationSeconds = (story.durationSeconds ?? 0) + (durationSeconds ?? 0);
853
+ }
854
+ return updated;
855
+ })
856
+ };
857
+ }
653
858
  function markIssueInProgress(plan, timestamp) {
654
859
  const ts = timestamp ?? isoNow();
655
860
  return {
@@ -848,6 +1053,7 @@ var ElapsedTimer = class {
848
1053
  this.onTick = onTick;
849
1054
  this.startTime = Date.now();
850
1055
  }
1056
+ onTick;
851
1057
  startTime;
852
1058
  intervalId = null;
853
1059
  cleanup = () => this.stop();
@@ -887,12 +1093,18 @@ export {
887
1093
  setSessionPublisher,
888
1094
  getSessionPublisher,
889
1095
  writeFileAtomic,
1096
+ parseUsage,
1097
+ sumUsage,
1098
+ divideUsage,
1099
+ formatTokens,
1100
+ hasUsageData,
890
1101
  isoNow,
891
1102
  loadTaskPlan,
892
1103
  saveTaskPlan,
893
1104
  initializeState,
894
1105
  hasPendingCorrection,
895
1106
  allStoriesPass,
1107
+ applyStoryMetrics,
896
1108
  markIssueInProgress,
897
1109
  markIssueCompleted,
898
1110
  setLastError,
@@ -919,4 +1131,4 @@ export {
919
1131
  formatDuration,
920
1132
  ElapsedTimer
921
1133
  };
922
- //# sourceMappingURL=chunk-JNEHVQH5.js.map
1134
+ //# sourceMappingURL=chunk-6HGZYTAT.js.map