issue-flow 0.8.0 → 0.10.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 (85) hide show
  1. package/README.md +16 -0
  2. package/dist/{analyze-RBNKXWL5.js → analyze-UZYJAWAP.js} +6 -6
  3. package/dist/{chunk-J4SICTAN.js → chunk-2LNQL4XH.js} +53 -9
  4. package/dist/chunk-2LNQL4XH.js.map +1 -0
  5. package/dist/{chunk-A4M6AKIK.js → chunk-2Z7OEZZT.js} +6 -6
  6. package/dist/{chunk-F52S6ZGF.js → chunk-652DJBCN.js} +17 -3
  7. package/dist/chunk-652DJBCN.js.map +1 -0
  8. package/dist/{chunk-TGP6WK65.js → chunk-67ZO5ROQ.js} +75 -11
  9. package/dist/chunk-67ZO5ROQ.js.map +1 -0
  10. package/dist/{chunk-VSF2CR3G.js → chunk-EWA3OGCM.js} +6 -6
  11. package/dist/{chunk-VPDWTZ3F.js → chunk-EZMOEBLU.js} +2 -2
  12. package/dist/{chunk-JWZKV3CL.js → chunk-MXTGCD5C.js} +2 -2
  13. package/dist/{chunk-QO2MWMAZ.js → chunk-PPLIXZI5.js} +33 -10
  14. package/dist/chunk-PPLIXZI5.js.map +1 -0
  15. package/dist/{chunk-HSGTZ3CT.js → chunk-S67Q4I2O.js} +219 -3
  16. package/dist/chunk-S67Q4I2O.js.map +1 -0
  17. package/dist/chunk-SP7DD4ID.js +265 -0
  18. package/dist/chunk-SP7DD4ID.js.map +1 -0
  19. package/dist/{chunk-36ECROTC.js → chunk-UH4KAJBJ.js} +7 -7
  20. package/dist/{chunk-RQW5W4XC.js → chunk-UUY4PCWW.js} +2 -2
  21. package/dist/{chunk-BH7DYADV.js → chunk-V5OHFSW4.js} +319 -181
  22. package/dist/chunk-V5OHFSW4.js.map +1 -0
  23. package/dist/{chunk-J3PJBZXJ.js → chunk-VOKNTBOO.js} +3 -3
  24. package/dist/{chunk-BASRRXAM.js → chunk-VYFKOUUR.js} +222 -6
  25. package/dist/chunk-VYFKOUUR.js.map +1 -0
  26. package/dist/cli.js +98 -29
  27. package/dist/cli.js.map +1 -1
  28. package/dist/{config-R7O5YC65.js → config-YCF3HIKR.js} +7 -3
  29. package/dist/execute-QYXA225W.js +14 -0
  30. package/dist/{generate-TGJQLGMV.js → generate-TW6QKSW5.js} +6 -6
  31. package/dist/init-G57UXKDG.js +9 -0
  32. package/dist/plan-YJLGBYBQ.js +16 -0
  33. package/dist/policy-NP4ZAVF6.js +1013 -0
  34. package/dist/policy-NP4ZAVF6.js.map +1 -0
  35. package/dist/pr-V5H65MRW.js +20 -0
  36. package/dist/pr-review-RCMXKXVP.js +16 -0
  37. package/dist/prd-3XR5KYJS.js +16 -0
  38. package/dist/review-ZZG3CSUO.js +14 -0
  39. package/dist/run-DIBC3YKX.js +1860 -0
  40. package/dist/run-DIBC3YKX.js.map +1 -0
  41. package/dist/{web-JVPK2PIF.js → web-O2FVOYZW.js} +4 -4
  42. package/package.json +1 -1
  43. package/prompts/execute.md +2 -2
  44. package/prompts/plan.md +10 -1
  45. package/prompts/pr.md +9 -4
  46. package/web/CLAUDE.md +22 -8
  47. package/web/public/app.css +178 -0
  48. package/web/public/app.js +336 -17
  49. package/web/public/index.html +24 -0
  50. package/dist/chunk-BASRRXAM.js.map +0 -1
  51. package/dist/chunk-BH7DYADV.js.map +0 -1
  52. package/dist/chunk-F52S6ZGF.js.map +0 -1
  53. package/dist/chunk-HSGTZ3CT.js.map +0 -1
  54. package/dist/chunk-J4SICTAN.js.map +0 -1
  55. package/dist/chunk-MK43N4X3.js +0 -123
  56. package/dist/chunk-MK43N4X3.js.map +0 -1
  57. package/dist/chunk-QO2MWMAZ.js.map +0 -1
  58. package/dist/chunk-TGP6WK65.js.map +0 -1
  59. package/dist/execute-LU3HWA54.js +0 -14
  60. package/dist/init-WP7Z2LFQ.js +0 -9
  61. package/dist/plan-2MWMCKRY.js +0 -16
  62. package/dist/pr-XVQDEQ7B.js +0 -16
  63. package/dist/pr-review-VO6ZXAFM.js +0 -16
  64. package/dist/prd-FPZAHQEO.js +0 -16
  65. package/dist/review-AHALLMXO.js +0 -14
  66. package/dist/run-2CXVR3VZ.js +0 -733
  67. package/dist/run-2CXVR3VZ.js.map +0 -1
  68. /package/dist/{analyze-RBNKXWL5.js.map → analyze-UZYJAWAP.js.map} +0 -0
  69. /package/dist/{chunk-A4M6AKIK.js.map → chunk-2Z7OEZZT.js.map} +0 -0
  70. /package/dist/{chunk-VSF2CR3G.js.map → chunk-EWA3OGCM.js.map} +0 -0
  71. /package/dist/{chunk-VPDWTZ3F.js.map → chunk-EZMOEBLU.js.map} +0 -0
  72. /package/dist/{chunk-JWZKV3CL.js.map → chunk-MXTGCD5C.js.map} +0 -0
  73. /package/dist/{chunk-36ECROTC.js.map → chunk-UH4KAJBJ.js.map} +0 -0
  74. /package/dist/{chunk-RQW5W4XC.js.map → chunk-UUY4PCWW.js.map} +0 -0
  75. /package/dist/{chunk-J3PJBZXJ.js.map → chunk-VOKNTBOO.js.map} +0 -0
  76. /package/dist/{config-R7O5YC65.js.map → config-YCF3HIKR.js.map} +0 -0
  77. /package/dist/{execute-LU3HWA54.js.map → execute-QYXA225W.js.map} +0 -0
  78. /package/dist/{generate-TGJQLGMV.js.map → generate-TW6QKSW5.js.map} +0 -0
  79. /package/dist/{init-WP7Z2LFQ.js.map → init-G57UXKDG.js.map} +0 -0
  80. /package/dist/{plan-2MWMCKRY.js.map → plan-YJLGBYBQ.js.map} +0 -0
  81. /package/dist/{pr-XVQDEQ7B.js.map → pr-V5H65MRW.js.map} +0 -0
  82. /package/dist/{pr-review-VO6ZXAFM.js.map → pr-review-RCMXKXVP.js.map} +0 -0
  83. /package/dist/{prd-FPZAHQEO.js.map → prd-3XR5KYJS.js.map} +0 -0
  84. /package/dist/{review-AHALLMXO.js.map → review-ZZG3CSUO.js.map} +0 -0
  85. /package/dist/{web-JVPK2PIF.js.map → web-O2FVOYZW.js.map} +0 -0
package/README.md CHANGED
@@ -85,10 +85,18 @@ npx issue-flow run 42 --mode manual
85
85
 
86
86
  # Watch the run live in the browser (see "Web Monitoring" below)
87
87
  npx issue-flow run 42 --web
88
+
89
+ # Continue User Story numbering from the last used in this project
90
+ npx issue-flow run 42 --continue
91
+
92
+ # Force User Story numbering to start at a specific number
93
+ npx issue-flow run 42 --start-us 27
88
94
  ```
89
95
 
90
96
  Executes all phases in order: **init** → **analyze** → **prd** → **plan** → **execute** → **review** → **pr**. Automatically resumes from the last incomplete phase if pipeline state exists. On review failure, its findings are saved verbatim to `lastReviewFindings` in `tasks.json`, and a correction cycle (re-execute + re-review) runs up to `maxCorrectionCycles`. The re-execute step reads `lastReviewFindings` and treats the issue as unresolved even if every user story already has `passes: true`, until the findings are addressed and the field is cleared back to `null`.
91
97
 
98
+ `--continue` and `--start-us <n>` control User Story (`US-NNN`) numbering continuity across `plan` runs of the same project — see [`plan`](#plan----convert-prd-to-task-plan) below.
99
+
92
100
  ### `init` -- Check prerequisites
93
101
 
94
102
  ```bash
@@ -117,10 +125,18 @@ Generates a Product Requirements Document from the issue analysis. Reads `analys
117
125
 
118
126
  ```bash
119
127
  npx issue-flow plan 42
128
+
129
+ # Continue User Story numbering from the last used in this project
130
+ npx issue-flow plan 42 --continue
131
+
132
+ # Force numbering to start at a specific number, ignoring history
133
+ npx issue-flow plan 42 --start-us 27
120
134
  ```
121
135
 
122
136
  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
137
 
138
+ `US-NNN` numbering continues automatically across `plan` runs of the same project: the highest number already used in any of the project's `tasks.json` files is recovered and the next story continues from there, falling back to `US-001` when the project has no history yet. `--continue` names that automatic recovery explicitly; `--start-us <n>` forces a specific starting number instead, ignoring history — the two flags are mutually exclusive. The decision is always printed to the terminal and recorded in the project's `metadata.json` (`userStoryNumbering`) for audit. See the root [README](https://github.com/fabioassuncao/issue-flow#user-story-numbering-continuity) for the full cascade.
139
+
124
140
  ### `execute` -- Run the story execution loop
125
141
 
126
142
  ```bash
@@ -2,20 +2,20 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-HSGTZ3CT.js";
5
+ } from "./chunk-S67Q4I2O.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-JWZKV3CL.js";
8
+ } from "./chunk-MXTGCD5C.js";
9
9
  import {
10
10
  publishPhaseMetrics
11
- } from "./chunk-QO2MWMAZ.js";
11
+ } from "./chunk-PPLIXZI5.js";
12
12
  import {
13
13
  applyPlaceholders,
14
14
  loadPrompt
15
15
  } from "./chunk-SLHZPB3C.js";
16
16
  import {
17
17
  resolveIssuePaths
18
- } from "./chunk-BASRRXAM.js";
18
+ } from "./chunk-VYFKOUUR.js";
19
19
  import {
20
20
  getGlobalTimeout,
21
21
  loadTaskPlan,
@@ -23,7 +23,7 @@ import {
23
23
  printInfo,
24
24
  printSuccess,
25
25
  saveTaskPlan
26
- } from "./chunk-BH7DYADV.js";
26
+ } from "./chunk-V5OHFSW4.js";
27
27
 
28
28
  // src/commands/analyze.ts
29
29
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -84,4 +84,4 @@ async function runAnalyze(issue, resolvedIssue) {
84
84
  export {
85
85
  runAnalyze
86
86
  };
87
- //# sourceMappingURL=analyze-RBNKXWL5.js.map
87
+ //# sourceMappingURL=analyze-UZYJAWAP.js.map
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runPhaseWithRetry
4
- } from "./chunk-RQW5W4XC.js";
4
+ } from "./chunk-UUY4PCWW.js";
5
5
  import {
6
6
  issuePlaceholders,
7
7
  resolveCommandIssue
8
- } from "./chunk-HSGTZ3CT.js";
8
+ } from "./chunk-S67Q4I2O.js";
9
9
  import {
10
10
  isTransientFailure
11
11
  } from "./chunk-HDV3YDTG.js";
12
12
  import {
13
13
  runHeadless
14
- } from "./chunk-JWZKV3CL.js";
14
+ } from "./chunk-MXTGCD5C.js";
15
15
  import {
16
16
  publishPhaseMetrics
17
- } from "./chunk-QO2MWMAZ.js";
17
+ } from "./chunk-PPLIXZI5.js";
18
18
  import {
19
19
  applyPlaceholders,
20
20
  loadPrompt
21
21
  } from "./chunk-SLHZPB3C.js";
22
22
  import {
23
23
  resolveIssuePaths
24
- } from "./chunk-BASRRXAM.js";
24
+ } from "./chunk-VYFKOUUR.js";
25
25
  import {
26
26
  getGlobalTimeout,
27
27
  isoNow,
@@ -29,7 +29,7 @@ import {
29
29
  printError,
30
30
  printSuccess,
31
31
  saveTaskPlan
32
- } from "./chunk-BH7DYADV.js";
32
+ } from "./chunk-V5OHFSW4.js";
33
33
 
34
34
  // src/commands/pr.ts
35
35
  import { execa } from "execa";
@@ -47,7 +47,46 @@ function issueClosesLine(issue, fallbackId) {
47
47
  }
48
48
  return `Closes #${issue.number ?? fallbackId}`;
49
49
  }
50
- async function runPr(issue, resolvedIssue) {
50
+ function issueRef(entry) {
51
+ return entry.number === null ? entry.id : `#${entry.number}`;
52
+ }
53
+ function issueClosesLines(issues) {
54
+ return issues.filter((entry) => entry.source === "github" && entry.number !== null).map((entry) => `Closes #${entry.number}`).join("\n");
55
+ }
56
+ function multiIssueContext(queue) {
57
+ if (queue === void 0 || queue.issues.length <= 1) {
58
+ return "";
59
+ }
60
+ const order = queue.issues.map(
61
+ (entry, index) => `${index + 1}. ${issueRef(entry)}${entry.title ? ` \u2014 ${entry.title}` : ""}`
62
+ ).join("\n");
63
+ const pending = [
64
+ ...queue.excluded.map(
65
+ (entry) => `- ${issueRef(entry)}${entry.title ? ` \u2014 ${entry.title}` : ""}: ${entry.reason}`
66
+ ),
67
+ ...queue.pending.map((note) => `- ${note}`)
68
+ ].join("\n");
69
+ return [
70
+ "",
71
+ "This Pull Request consolidates several issues implemented on this same branch,",
72
+ "in this execution order:",
73
+ "",
74
+ order,
75
+ "",
76
+ "The PR body MUST additionally contain:",
77
+ '- an "Issues implemented" section listing every issue above, in that order,',
78
+ " with one line per issue describing what it delivered;",
79
+ '- a "Pending" section with the items below, verbatim, plus anything you find',
80
+ ' unfinished while reviewing the diff. Write "None" when the list below is',
81
+ " empty and you find nothing else;",
82
+ "",
83
+ pending === "" ? "(no known pending items)" : pending,
84
+ "",
85
+ "The commits of this branch are scoped per issue (`feat(issue-N): \u2026`), which is",
86
+ "how you tell which change belongs to which issue in `git log`."
87
+ ].join("\n");
88
+ }
89
+ async function runPr(issue, resolvedIssue, options = {}) {
51
90
  const issueNumber = issue.replace(/^#/, "");
52
91
  const paths = await resolveIssuePaths(issueNumber);
53
92
  const tasksPath = paths.tasksFile;
@@ -67,12 +106,15 @@ async function runPr(issue, resolvedIssue) {
67
106
  printError("Failed to get current branch");
68
107
  return 1;
69
108
  }
109
+ const queue = options.queue;
110
+ const consolidating = queue !== void 0 && queue.issues.length > 1;
70
111
  const template = await loadPrompt("pr");
71
112
  const prompt = applyPlaceholders(template, {
72
113
  __ISSUE_NUMBER__: issueNumber,
73
114
  __BRANCH_NAME__: branchName,
74
115
  __TASKS_PATH__: tasksPath,
75
- __ISSUE_CLOSES__: issueClosesLine(resolution.resolved.issue, issueNumber),
116
+ __ISSUE_CLOSES__: consolidating ? issueClosesLines(queue.issues) : issueClosesLine(resolution.resolved.issue, issueNumber),
117
+ __MULTI_ISSUE_CONTEXT__: multiIssueContext(queue),
76
118
  ...issuePlaceholders(resolution.resolved)
77
119
  });
78
120
  let headlessOutput = "";
@@ -132,6 +174,8 @@ async function runPr(issue, resolvedIssue) {
132
174
  }
133
175
 
134
176
  export {
177
+ issueClosesLines,
178
+ multiIssueContext,
135
179
  runPr
136
180
  };
137
- //# sourceMappingURL=chunk-J4SICTAN.js.map
181
+ //# sourceMappingURL=chunk-2LNQL4XH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/pr.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { runHeadless } from '../core/headless.js';\nimport { runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { Issue, IssueSource, ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/**\n * Extract a PR URL from headless output.\n */\nfunction parsePrUrl(output: string): string | null {\n const match = output.match(/(https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+)/);\n return match?.[1] ?? null;\n}\n\n/**\n * The numeric id inside a PR URL, so later phases (`pr-review`) address the\n * Pull Request without querying GitHub again.\n */\nfunction parsePrNumber(url: string): number | null {\n const match = url.match(/\\/pull\\/(\\d+)/);\n return match?.[1] === undefined ? null : Number(match[1]);\n}\n\n/**\n * The `Closes #N` line for the PR body, empty when the Issue has no remote\n * counterpart: GitHub only understands the reference for Issues it hosts, and\n * an invented `#N` would silently point at an unrelated Issue.\n */\nfunction issueClosesLine(issue: Issue, fallbackId: string): string {\n if (issue.remoteRef === null) {\n return '';\n }\n return `Closes #${issue.number ?? fallbackId}`;\n}\n\n/** One Issue of a queue, as the consolidated Pull Request has to describe it. */\nexport interface PrQueueIssue {\n id: string;\n number: number | null;\n title: string;\n /** `null` when the Issue was never read — see {@link issueClosesLines}. */\n url: string | null;\n /** Origin the Issue came from; `github` is what GitHub can close. */\n source: IssueSource;\n}\n\n/**\n * What a multi-issue queue hands to the `pr` phase.\n *\n * Absent for every standalone run, and that absence is what guarantees the\n * single-issue Pull Request is byte-for-byte the one this command has always\n * produced: the extra placeholders resolve to empty strings.\n */\nexport interface PrQueueContext {\n /** Issues of the queue, in the order they were executed. */\n issues: PrQueueIssue[];\n /** Issues discovered but not executed, with the reason. */\n excluded: { id: string; number: number | null; title: string; reason: string }[];\n /** Anything else worth reporting as pending (unresolved review findings). */\n pending: string[];\n}\n\nexport interface RunPrOptions {\n /** Set only when the Pull Request consolidates a queue. */\n queue?: PrQueueContext;\n}\n\n/** `#51` when the Issue has a number, its raw identifier otherwise. */\nfunction issueRef(entry: { id: string; number: number | null }): string {\n return entry.number === null ? entry.id : `#${entry.number}`;\n}\n\n/**\n * Every `Closes #N` line of a consolidated Pull Request, one per line.\n *\n * Issues that GitHub does not host are skipped for the same reason a\n * single-issue Pull Request skips them — it cannot close what it does not\n * host — and a queue that mixes both origins still gets a valid body.\n *\n * The test is the origin and the number, not the URL: an Issue whose read\n * failed during discovery reaches the queue with `url: null`, and it is still\n * executed and still closed by the queue. Filtering on the URL would leave it\n * out of the very body that is supposed to reference it.\n */\nexport function issueClosesLines(issues: readonly PrQueueIssue[]): string {\n return issues\n .filter((entry) => entry.source === 'github' && entry.number !== null)\n .map((entry) => `Closes #${entry.number}`)\n .join('\\n');\n}\n\n/**\n * The extra instructions the prompt receives when the Pull Request covers a\n * whole queue.\n *\n * Returns `''` for a standalone run, which is what keeps `pr.md` rendering\n * exactly as before for the single-issue path — no empty \"Issues implemented\"\n * section, no redundant ordering of a list of one.\n */\nexport function multiIssueContext(queue: PrQueueContext | undefined): string {\n if (queue === undefined || queue.issues.length <= 1) {\n return '';\n }\n\n const order = queue.issues\n .map(\n (entry, index) => `${index + 1}. ${issueRef(entry)}${entry.title ? ` — ${entry.title}` : ''}`,\n )\n .join('\\n');\n\n const pending = [\n ...queue.excluded.map(\n (entry) => `- ${issueRef(entry)}${entry.title ? ` — ${entry.title}` : ''}: ${entry.reason}`,\n ),\n ...queue.pending.map((note) => `- ${note}`),\n ].join('\\n');\n\n return [\n '',\n 'This Pull Request consolidates several issues implemented on this same branch,',\n 'in this execution order:',\n '',\n order,\n '',\n 'The PR body MUST additionally contain:',\n '- an \"Issues implemented\" section listing every issue above, in that order,',\n ' with one line per issue describing what it delivered;',\n '- a \"Pending\" section with the items below, verbatim, plus anything you find',\n ' unfinished while reviewing the diff. Write \"None\" when the list below is',\n ' empty and you find nothing else;',\n '',\n pending === '' ? '(no known pending items)' : pending,\n '',\n 'The commits of this branch are scoped per issue (`feat(issue-N): …`), which is',\n 'how you tell which change belongs to which issue in `git log`.',\n ].join('\\n');\n}\n\nexport async function runPr(\n issue: string,\n resolvedIssue?: ResolvedIssue,\n options: RunPrOptions = {},\n): 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 // Get current branch\n let branchName: string;\n try {\n const proc = await execa('git', ['branch', '--show-current'], { reject: false });\n branchName = proc.stdout?.toString().trim() ?? '';\n if (!branchName) {\n printError('Could not determine current branch');\n return 1;\n }\n } catch {\n printError('Failed to get current branch');\n return 1;\n }\n\n const queue = options.queue;\n const consolidating = queue !== undefined && queue.issues.length > 1;\n\n const template = await loadPrompt('pr');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __BRANCH_NAME__: branchName,\n __TASKS_PATH__: tasksPath,\n __ISSUE_CLOSES__: consolidating\n ? issueClosesLines(queue.issues)\n : issueClosesLine(resolution.resolved.issue, issueNumber),\n __MULTI_ISSUE_CONTEXT__: multiIssueContext(queue),\n ...issuePlaceholders(resolution.resolved),\n });\n\n let headlessOutput = '';\n\n const outcome = await runPhaseWithRetry({\n phase: 'pr',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 15,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text parsePrUrl() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Creating PR for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('pr', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `PR creation failed: ${result.error}`,\n };\n }\n\n headlessOutput = result.result;\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `PR creation failed for issue #${issueNumber}`);\n return 1;\n }\n\n const prUrl = parsePrUrl(headlessOutput);\n\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prCreated = true;\n const prNumber = prUrl === null ? null : parsePrNumber(prUrl);\n // Without a URL the PR is still assumed created (the phase succeeded), but\n // there is nothing trustworthy to record: an invented number would send\n // `pr-review` at an unrelated Pull Request.\n if (prUrl !== null && prNumber !== null) {\n plan.pullRequest = {\n number: prNumber,\n url: prUrl,\n headBranch: branchName,\n createdAt: isoNow(),\n };\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n if (prUrl) {\n printSuccess(`PR created: ${prUrl}`);\n } else {\n printSuccess('PR creation completed');\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
@@ -2,27 +2,27 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-HSGTZ3CT.js";
5
+ } from "./chunk-S67Q4I2O.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-JWZKV3CL.js";
8
+ } from "./chunk-MXTGCD5C.js";
9
9
  import {
10
10
  publishPhaseMetrics
11
- } from "./chunk-QO2MWMAZ.js";
11
+ } from "./chunk-PPLIXZI5.js";
12
12
  import {
13
13
  applyPlaceholders,
14
14
  loadPrompt
15
15
  } from "./chunk-SLHZPB3C.js";
16
16
  import {
17
17
  resolveIssuePaths
18
- } from "./chunk-BASRRXAM.js";
18
+ } from "./chunk-VYFKOUUR.js";
19
19
  import {
20
20
  getGlobalTimeout,
21
21
  loadTaskPlan,
22
22
  printError,
23
23
  printSuccess,
24
24
  saveTaskPlan
25
- } from "./chunk-BH7DYADV.js";
25
+ } from "./chunk-V5OHFSW4.js";
26
26
 
27
27
  // src/commands/review.ts
28
28
  function parseReviewResult(output) {
@@ -107,4 +107,4 @@ async function runReview(issue, resolvedIssue) {
107
107
  export {
108
108
  runReview
109
109
  };
110
- //# sourceMappingURL=chunk-A4M6AKIK.js.map
110
+ //# sourceMappingURL=chunk-2Z7OEZZT.js.map
@@ -5,13 +5,13 @@ import {
5
5
  import {
6
6
  getGlobalRoot,
7
7
  webLockSchema
8
- } from "./chunk-BASRRXAM.js";
8
+ } from "./chunk-VYFKOUUR.js";
9
9
  import {
10
10
  NullPublisher,
11
11
  isoNow,
12
12
  printInfo,
13
13
  printWarning
14
- } from "./chunk-BH7DYADV.js";
14
+ } from "./chunk-V5OHFSW4.js";
15
15
 
16
16
  // src/web/lock.ts
17
17
  import { spawn as nodeSpawn } from "child_process";
@@ -27,6 +27,7 @@ import { createRequire } from "module";
27
27
  import { join } from "path";
28
28
  import chalk from "chalk";
29
29
  var require2 = createRequire(import.meta.url);
30
+ var SESSION_LIST_DESCRIPTION_MAX = 280;
30
31
  function readPackageVersion() {
31
32
  try {
32
33
  return require2("../../package.json").version;
@@ -34,6 +35,13 @@ function readPackageVersion() {
34
35
  return "0.0.0";
35
36
  }
36
37
  }
38
+ function truncateSessionDescription(text, max = SESSION_LIST_DESCRIPTION_MAX) {
39
+ if (text === null || text === void 0) return null;
40
+ const normalized = text.replace(/\s+/g, " ").trim();
41
+ if (normalized.length === 0) return normalized;
42
+ if (normalized.length <= max) return normalized;
43
+ return `${normalized.slice(0, max - 1).trimEnd()}\u2026`;
44
+ }
37
45
  var JSON_TYPE = "application/json; charset=utf-8";
38
46
  function publisherSessionSource(publisher) {
39
47
  return {
@@ -159,6 +167,12 @@ async function startWebServer(options) {
159
167
  source.list().map((snapshot) => ({
160
168
  sessionId: snapshot.sessionId,
161
169
  issueNumber: snapshot.issue.number,
170
+ issueTitle: snapshot.issue.title,
171
+ issueDescription: truncateSessionDescription(snapshot.issue.description),
172
+ repositoryName: snapshot.repository.name,
173
+ currentPhase: snapshot.currentPhase,
174
+ progressPercent: snapshot.progress.percent,
175
+ elapsedSeconds: snapshot.elapsedSeconds,
162
176
  status: snapshot.status,
163
177
  startedAt: snapshot.startedAt,
164
178
  updatedAt: snapshot.updatedAt,
@@ -444,4 +458,4 @@ export {
444
458
  ensureSingleWebServer,
445
459
  ensureWebMonitor
446
460
  };
447
- //# sourceMappingURL=chunk-F52S6ZGF.js.map
461
+ //# sourceMappingURL=chunk-652DJBCN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/lock.ts","../src/web/server.ts"],"sourcesContent":["import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { isoNow } from '../core/state-manager.js';\nimport { type GetGlobalRootOptions, getGlobalRoot } from '../storage/paths.js';\nimport { type WebLock, webLockSchema } from '../storage/schemas.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport { startWebServer, type WebServerHandle, type WebServerOptions } from './server.js';\n\n/**\n * Single-instance guard for the web monitoring server (`~/.issue-flow/web.lock`).\n *\n * A machine runs at most one monitoring server. Two entry points build on top\n * of the same lock:\n *\n * - `ensureSingleWebServer` binds (or reuses) a server **in the calling\n * process**. It is the low-level primitive, used by the `web serve` command\n * (the standalone process that ends up owning the lock) and by this\n * module's own tests. Nothing else should call it directly.\n * - `ensureWebMonitor` (US-002) is what `run`/`execute` call: it reuses an\n * active instance exactly like `ensureSingleWebServer`, but when none\n * exists it spawns `web serve` as a **detached** child instead of binding\n * locally, so the server outlives the pipeline process. Falls back to the\n * legacy in-process `startWebServer` (US-006) when the global storage tree\n * itself is unreachable.\n */\n\n/** Lock file name, sibling of `config.json` at the global storage root. */\nexport const WEB_LOCK_FILENAME = 'web.lock';\n\n/** How long a health probe against a candidate instance waits before giving up. */\nconst HEALTH_PROBE_TIMEOUT_MS = 1000;\n\n/**\n * A process that loses the atomic lock creation retries this many times,\n * waiting for the winner to finish starting up, before giving up.\n */\nconst LOCK_CLAIM_RETRIES = 5;\nconst LOCK_CLAIM_RETRY_DELAY_MS = 100;\n\nexport function getWebLockFile(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), WEB_LOCK_FILENAME);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `process.kill(pid, 0)` throws ESRCH when the pid is gone, EPERM when it is\n * alive but owned by another user — only ESRCH means \"not alive\". */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** 0.0.0.0 is not a connectable address; probe loopback instead, same mapping\n * `startWebServer` uses to build its own display URL. */\nfunction probeUrl(host: string, port: number): string {\n const probeHost = host === '0.0.0.0' ? '127.0.0.1' : host;\n return `http://${probeHost}:${port}/api/health`;\n}\n\nasync function probeHealth(host: string, port: number): Promise<boolean> {\n try {\n const res = await fetch(probeUrl(host, port), {\n signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/** Malformed content (partial write, older/newer incompatible shape) reads as\n * \"no lock\" — the same degrade-to-absent behavior as `loadGlobalConfig()`. */\nexport async function readWebLock(lockFile: string): Promise<WebLock | null> {\n let raw: string;\n try {\n raw = await readFile(lockFile, 'utf-8');\n } catch {\n return null;\n }\n try {\n const result = webLockSchema.safeParse(JSON.parse(raw));\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nexport async function removeWebLock(lockFile: string): Promise<void> {\n try {\n await rm(lockFile, { force: true });\n } catch {\n /* best-effort cleanup */\n }\n}\n\n/**\n * A lock is only trustworthy when both signals agree: the owning process is\n * alive AND it answers `/api/health`. A dead pid is the obvious stale case;\n * a live pid that does not answer covers a process that still exists but\n * crashed before binding, was killed -9 leaving a zombie-adjacent state, or\n * whose port was reclaimed by something else entirely. Either way the lock is\n * removed so the next attempt starts clean instead of looping on it forever.\n */\nexport async function detectActiveInstance(lockFile: string): Promise<WebLock | null> {\n const lock = await readWebLock(lockFile);\n if (lock === null) return null;\n\n if (isProcessAlive(lock.pid) && (await probeHealth(lock.host, lock.port))) {\n return lock;\n }\n\n await removeWebLock(lockFile);\n return null;\n}\n\n/**\n * Atomic exclusive create: `wx` fails with EEXIST when another process won\n * the race, which is what lets two concurrent invocations agree on a single\n * winner without a separate lock-of-the-lock.\n */\nasync function claimWebLock(lockFile: string, lock: WebLock): Promise<boolean> {\n try {\n await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: 'utf-8', flag: 'wx' });\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;\n throw err;\n }\n}\n\n/** Handle returned when reusing an already-running instance instead of\n * binding a new one — there is no local `Server` to hold, and closing it\n * must not tear down a server another process owns. */\nfunction reusedHandle(lock: WebLock): WebServerHandle {\n const displayHost = lock.host === '0.0.0.0' ? 'localhost' : lock.host;\n return {\n host: lock.host,\n port: lock.port,\n url: `http://${displayHost}:${lock.port}`,\n close: async () => {},\n };\n}\n\n/**\n * Start the web monitoring server, first checking whether a live instance\n * already owns `web.lock` and reusing it instead of binding a second one.\n *\n * The lock is claimed only *after* a successful bind, never before: claiming\n * it with the port the server is about to (but hasn't yet) listen on would\n * make that lock briefly answer no health probe at all, and a concurrent\n * invocation would misread that gap as \"stale\" and delete a perfectly good\n * lock out from under its owner. Binding first also means two invocations\n * racing for the same fixed port never both reach the claim step — the OS\n * itself lets only one `listen()` succeed. The `wx` claim then exists for the\n * remaining race: two invocations that *both* manage to bind (only possible\n * with an ephemeral port) still agree on exactly one winner. The loser closes\n * the server it just opened and defers to whichever lock exists.\n *\n * Returns null exactly when `startWebServer` would: nothing here may affect\n * the pipeline, so a lock we cannot resolve after every retry degrades to \"no\n * server\" rather than throwing.\n */\nexport async function ensureSingleWebServer(\n options: WebServerOptions,\n lockOptions: GetGlobalRootOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n const lockFile = getWebLockFile(lockOptions);\n\n for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const handle = await startWebServer(options);\n if (handle === null) {\n // Could not bind. Most likely another instance's listen() won a race\n // on the same fixed port between our detection above and this call —\n // its lock should be visible shortly. Anything else (port taken by an\n // unrelated process, invalid host, ...) fails the same way on the next\n // retry too, and the loop's own limit stops it from spinning forever.\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n let claimed: boolean;\n try {\n claimed = await claimWebLock(lockFile, {\n pid: process.pid,\n port: handle.port,\n host: handle.host,\n startedAt: isoNow(),\n });\n } catch (err) {\n await handle.close();\n throw err;\n }\n\n if (!claimed) {\n // We bound successfully (an ephemeral port let both of us through),\n // but another invocation's lock got written first. We are not the\n // canonical instance: close what we just opened and defer to it.\n await handle.close();\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n await removeWebLock(lockFile);\n };\n return handle;\n }\n\n warn('Could not determine the web monitor lock ownership. Continuing without the web server.');\n return null;\n}\n\n/** Bounded wait for a freshly spawned `web serve` child to claim the lock. */\nconst DETACHED_CLAIM_TIMEOUT_MS = 5000;\nconst DETACHED_CLAIM_POLL_MS = 150;\n\nexport interface EnsureWebMonitorOptions extends GetGlobalRootOptions {\n /** Injectable for tests. Defaults to `node:child_process`'s `spawn`. */\n spawn?: (\n command: string,\n args: string[],\n options: { detached: true; stdio: 'ignore' },\n ) => ChildProcess;\n /** Node executable to spawn. Defaults to `process.execPath`. */\n execPath?: string;\n /** CLI entry script re-invoked as `<entry> web serve ...`. Defaults to `process.argv[1]`. */\n entryScript?: string;\n /** How long to wait for the spawned instance's lock to become healthy. */\n claimTimeoutMs?: number;\n claimPollIntervalMs?: number;\n}\n\n/**\n * Entry point for `run`/`execute`: get a web monitor without binding one in\n * this process (US-002).\n *\n * 1. Reuse a live, healthy instance exactly like `ensureSingleWebServer`.\n * 2. Otherwise, spawn `<node> <cli> web serve --port … --host … [--refresh …]`\n * detached (`{ detached: true, stdio: 'ignore' }`) and `unref()`ed, so it\n * survives this process exiting and never pipes its own stdio back here.\n * 3. Poll the lock file (bounded) until the spawned instance claims it, then\n * return a *reused* handle for it — this process never owns that `Server`,\n * the same shape `ensureSingleWebServer`'s reuse path already returns.\n *\n * Two failure modes degrade gracefully rather than affecting the pipeline:\n * the global storage tree being unreachable (no resolvable home directory)\n * falls back to `startWebServer` bound right here, exactly the pre-US-001\n * behavior (US-006); a spawn or claim failure with the storage tree otherwise\n * healthy just returns null, same contract `startWebServer` itself has.\n */\nexport async function ensureWebMonitor(\n options: WebServerOptions,\n monitorOptions: EnsureWebMonitorOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n\n let lockFile: string;\n try {\n lockFile = getWebLockFile(monitorOptions);\n } catch (err) {\n warn(\n `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`,\n );\n return startWebServer(options);\n }\n\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const execPath = monitorOptions.execPath ?? process.execPath;\n const entryScript = monitorOptions.entryScript ?? process.argv[1];\n if (entryScript === undefined) {\n warn('Web monitor: could not determine the CLI entry point to spawn a detached server.');\n return null;\n }\n\n const args = [\n entryScript,\n 'web',\n 'serve',\n '--port',\n String(options.port),\n '--host',\n options.host,\n ];\n if (options.refreshSeconds !== undefined) {\n args.push('--refresh', String(options.refreshSeconds));\n }\n\n const spawnFn = monitorOptions.spawn ?? nodeSpawn;\n try {\n const child = spawnFn(execPath, args, { detached: true, stdio: 'ignore' });\n child.unref();\n } catch (err) {\n warn(\n `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`,\n );\n return null;\n }\n\n const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;\n const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const claimed = await detectActiveInstance(lockFile);\n if (claimed !== null) {\n info(`Web monitor running at ${chalk.bold.cyan(reusedHandle(claimed).url)}`);\n return reusedHandle(claimed);\n }\n await sleep(pollMs);\n }\n\n warn(\n 'Web monitor: the detached server did not start in time. Continuing without the web server.',\n );\n return null;\n}\n","import { createHash } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { resolvePackageDir } from '../core/prompt-resolver.js';\nimport {\n NullPublisher,\n type SessionPublisher,\n type SessionSnapshot,\n} from '../core/session-state.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport type { SessionDirectoryHandle } from './session-directory.js';\n\n/**\n * HTTP server for the web monitoring mode. Plain node:http — no new runtime\n * dependencies. Serves the publisher's in-memory snapshot (it never re-reads\n * issues/N/session.json) and the static UI assets.\n *\n * Resilience contract: nothing here may ever affect the pipeline. Listen\n * failures (EADDRINUSE included) log a warning and the execution continues\n * without a server; the server is unref()'d so it cannot keep the process\n * alive; request handling is wrapped so a handler error answers 500 instead\n * of crashing the process.\n */\n\nconst require = createRequire(import.meta.url);\n\n/** Max length of `issueDescription` on GET /api/sessions (dashboard preview). */\nexport const SESSION_LIST_DESCRIPTION_MAX = 280;\n\nfunction readPackageVersion(): string {\n try {\n return (require('../../package.json') as { version: string }).version;\n } catch {\n return '0.0.0';\n }\n}\n\n/** Collapse whitespace and truncate for the sessions list payload. */\nexport function truncateSessionDescription(\n text: string | null | undefined,\n max = SESSION_LIST_DESCRIPTION_MAX,\n): string | null {\n if (text === null || text === undefined) return null;\n const normalized = text.replace(/\\s+/g, ' ').trim();\n if (normalized.length === 0) return normalized;\n if (normalized.length <= max) return normalized;\n return `${normalized.slice(0, max - 1).trimEnd()}…`;\n}\n\nexport interface WebServerOptions {\n /**\n * Legacy single-session mode (pre-US-003, still used by the US-006\n * fallback): serves this publisher's in-memory snapshot directly, with no\n * directory scan and no lock file involved. Mutually exclusive with\n * `sessions` — when both are given, `sessions` wins.\n */\n publisher?: SessionPublisher;\n /**\n * Multi-session mode: sessions discovered by polling the global storage\n * tree (`web/session-directory.ts`). This is what every current entry\n * point (`web serve`) passes; `publisher` only exists for the legacy path.\n */\n sessions?: SessionDirectoryHandle;\n port: number;\n host: string;\n /** Suggested UI polling interval, exposed via /api/health. */\n refreshSeconds?: number;\n /** Package version reported by /api/health. Default: read from package.json. */\n version?: string;\n /** Directory holding index.html/app.css/app.js. Default: auto-resolved. */\n publicDir?: string;\n /** Info logger. Default: printInfo. */\n info?: (message: string) => void;\n /** Warning logger. Default: printWarning. */\n warn?: (message: string) => void;\n /**\n * Whether to `unref()` the underlying socket so it never keeps the process\n * alive on its own. Default `true`, matching the historical contract for a\n * server bound inline in the pipeline process. The standalone `web serve`\n * process (the detached server itself) passes `false`: it has nothing else\n * to do, so staying alive for as long as the server is bound *is* the job.\n */\n unref?: boolean;\n}\n\nexport interface WebServerHandle {\n /**\n * Absent when this handle represents an existing instance reused through\n * `ensureSingleWebServer` (`web/lock.ts`) instead of one bound locally.\n */\n server?: Server;\n /** Host the server is bound to. */\n host: string;\n /** Actual bound port (relevant when options.port is 0). */\n port: number;\n /** Human-facing access URL. */\n url: string;\n /** Close the server and release signal handlers. Idempotent, never rejects. */\n close(): Promise<void>;\n}\n\ninterface StaticAsset {\n body: string;\n contentType: string;\n}\n\nconst JSON_TYPE = 'application/json; charset=utf-8';\n\n/**\n * Uniform view over \"whatever sessions this server knows about\", so the route\n * handlers below never care whether they are backed by a single in-memory\n * publisher (legacy mode) or by the global directory scan (US-003/US-004).\n */\ninterface SessionSource {\n /** Every session currently considered active, in no particular order. */\n list(): SessionSnapshot[];\n /** A specific session by id, or undefined when it is not currently active. */\n get(sessionId: string): SessionSnapshot | undefined;\n}\n\nfunction publisherSessionSource(publisher: SessionPublisher): SessionSource {\n return {\n list: () => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === null ? [] : [snapshot];\n },\n get: (sessionId) => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === sessionId ? snapshot : undefined;\n },\n };\n}\n\nfunction directorySessionSource(handle: SessionDirectoryHandle): SessionSource {\n return {\n list: () => handle.sessions().map((entry) => entry.snapshot),\n get: (sessionId) => handle.getSession(sessionId)?.snapshot,\n };\n}\n\nfunction resolveSessionSource(options: WebServerOptions): SessionSource {\n if (options.sessions) return directorySessionSource(options.sessions);\n return publisherSessionSource(options.publisher ?? new NullPublisher());\n}\n\nconst STATIC_ROUTES: Record<string, { file: string; contentType: string }> = {\n '/': { file: 'index.html', contentType: 'text/html; charset=utf-8' },\n '/app.css': { file: 'app.css', contentType: 'text/css; charset=utf-8' },\n '/app.js': { file: 'app.js', contentType: 'text/javascript; charset=utf-8' },\n};\n\n/** Assets are read once at startup; missing files simply 404. */\nasync function loadStaticAssets(publicDir: string | null): Promise<Map<string, StaticAsset>> {\n const assets = new Map<string, StaticAsset>();\n if (publicDir === null) return assets;\n for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {\n try {\n const body = await readFile(join(publicDir, file), 'utf-8');\n assets.set(route, { body, contentType });\n } catch {\n // Asset not present (e.g. UI not built yet) — route answers 404.\n }\n }\n return assets;\n}\n\n/** Headers applied to every response, including 304s and errors. */\nfunction baseHeaders(res: ServerResponse): void {\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('Referrer-Policy', 'no-referrer');\n res.setHeader('X-Robots-Tag', 'noindex, nofollow');\n}\n\nfunction respond(res: ServerResponse, status: number, contentType: string, body: string): void {\n res.statusCode = status;\n res.setHeader('Content-Type', contentType);\n res.end(body);\n}\n\nfunction respondJson(res: ServerResponse, status: number, payload: unknown): void {\n respond(res, status, JSON_TYPE, JSON.stringify(payload));\n}\n\n/**\n * Start the monitoring HTTP server. Returns null when the server could not\n * listen (port in use, invalid host, ...) — the pipeline continues without\n * monitoring, it is never brought down by the server.\n */\nexport async function startWebServer(options: WebServerOptions): Promise<WebServerHandle | null> {\n const info = options.info ?? printInfo;\n const warn = options.warn ?? printWarning;\n const version = options.version ?? readPackageVersion();\n const startedAtMs = Date.now();\n\n // The UI ships at the package root as web/public/ (sibling of prompts/),\n // resolved the same way from src/ and from the published dist/ layout.\n const assets = await loadStaticAssets(\n options.publicDir ?? resolvePackageDir(join('web', 'public')),\n );\n\n const source = resolveSessionSource(options);\n\n // JSON serialization memoized per session id: an unchanged poll answers 304\n // with an empty body. Content-hashed rather than counter-based, unlike the\n // pre-multi-session version — a directory-backed session has no in-process\n // publisher to hand out a monotonic version(), and a hash works uniformly\n // for both sources.\n const statusCache = new Map<string, { body: string; etag: string }>();\n const statusPayload = (snapshot: SessionSnapshot): { body: string; etag: string } => {\n const key = snapshot.sessionId ?? '';\n const body = JSON.stringify(snapshot);\n const cached = statusCache.get(key);\n if (cached !== undefined && cached.body === body) return cached;\n const etag = `\"${createHash('sha1').update(body).digest('hex')}\"`;\n const entry = { body, etag };\n statusCache.set(key, entry);\n return entry;\n };\n\n const handleRequest = (req: IncomingMessage, res: ServerResponse): void => {\n baseHeaders(res);\n\n const requestUrl = new URL(req.url ?? '/', 'http://localhost');\n const path = requestUrl.pathname;\n\n // POST /api/control/* is reserved for future write operations (pause,\n // retry, ...); intentionally NOT registered — the v1 surface is\n // read-only (snapshot.readOnly is true and capabilities is empty).\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n respondJson(res, 404, { error: 'Not found' });\n return;\n }\n\n if (path === '/api/status' || path === '/status.json') {\n const sessionId = requestUrl.searchParams.get('session');\n let snapshot: SessionSnapshot | undefined;\n if (sessionId !== null) {\n snapshot = source.get(sessionId);\n if (snapshot === undefined) {\n respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });\n return;\n }\n } else {\n // Back-compat shorthand (US-005): with exactly one active session,\n // GET /api/status with no query string still answers it directly, the\n // same behavior a single-session run always had. Zero or several\n // active sessions is genuinely ambiguous without an id and answers an\n // explicit error instead of guessing.\n const active = source.list();\n if (active.length === 0) {\n respondJson(res, 404, {\n error: 'No active session. Pass ?session=<id> — see GET /api/sessions.',\n });\n return;\n }\n if (active.length > 1) {\n respondJson(res, 409, {\n error: 'Multiple active sessions; specify ?session=<id>.',\n sessions: active.map((entry) => entry.sessionId),\n });\n return;\n }\n snapshot = active[0];\n }\n\n const { body, etag } = statusPayload(snapshot);\n res.setHeader('ETag', etag);\n if (req.headers['if-none-match'] === etag) {\n res.statusCode = 304;\n res.end();\n return;\n }\n respond(res, 200, JSON_TYPE, body);\n return;\n }\n\n if (path === '/api/sessions') {\n // Summary fields for the multi-session dashboard (issue #35): the full\n // SessionSnapshot is already in memory, so the client can render cards\n // from this single list without N× /api/status fetches per poll.\n // issueDescription is truncated — cards only need a short preview.\n respondJson(\n res,\n 200,\n source.list().map((snapshot) => ({\n sessionId: snapshot.sessionId,\n issueNumber: snapshot.issue.number,\n issueTitle: snapshot.issue.title,\n issueDescription: truncateSessionDescription(snapshot.issue.description),\n repositoryName: snapshot.repository.name,\n currentPhase: snapshot.currentPhase,\n progressPercent: snapshot.progress.percent,\n elapsedSeconds: snapshot.elapsedSeconds,\n status: snapshot.status,\n startedAt: snapshot.startedAt,\n updatedAt: snapshot.updatedAt,\n statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? '')}`,\n })),\n );\n return;\n }\n\n if (path === '/api/health') {\n respondJson(res, 200, {\n ok: true,\n uptime: Math.round((Date.now() - startedAtMs) / 1000),\n version,\n refreshSeconds: options.refreshSeconds ?? 5,\n });\n return;\n }\n\n const asset = assets.get(path);\n if (asset) {\n respond(res, 200, asset.contentType, asset.body);\n return;\n }\n\n respondJson(res, 404, { error: 'Not found' });\n };\n\n const server = createServer((req, res) => {\n try {\n handleRequest(req, res);\n } catch (err) {\n try {\n respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });\n } catch {\n // Response already destroyed — nothing to do, never crash the process.\n }\n }\n });\n\n const listening = await new Promise<boolean>((resolve) => {\n const onError = (err: NodeJS.ErrnoException): void => {\n if (err.code === 'EADDRINUSE') {\n warn(\n `Web monitor port ${options.port} is already in use. Continuing without the web server.`,\n );\n } else {\n warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);\n }\n resolve(false);\n };\n server.once('error', onError);\n server.listen(options.port, options.host, () => {\n server.removeListener('error', onError);\n resolve(true);\n });\n });\n\n if (!listening) return null;\n\n // Never keep the process alive on its own, unless explicitly told not to\n // unref: the pipeline ending must end the process. The one exception is the\n // detached `web serve` process (options.unref === false) — it has no other\n // work, so staying alive for as long as the server is bound is the point.\n if (options.unref !== false) {\n server.unref();\n }\n // Post-listen errors must never become uncaught exceptions.\n server.on('error', (err) => {\n warn(`Web monitor server error: ${err.message}`);\n });\n\n const address = server.address() as AddressInfo;\n const port = address.port;\n const displayHost = options.host === '0.0.0.0' ? 'localhost' : options.host;\n const url = `http://${displayHost}:${port}`;\n\n if (options.host === '0.0.0.0') {\n warn('Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.');\n }\n info(`Web monitor running at ${chalk.bold.cyan(url)}`);\n\n let closed = false;\n let closePromise: Promise<void> | null = null;\n\n const doClose = (): Promise<void> =>\n new Promise<void>((resolve) => {\n process.removeListener('SIGINT', onSignal);\n process.removeListener('SIGTERM', onSignal);\n server.close(() => resolve());\n // Drop idle keep-alive connections so close() never hangs.\n server.closeAllConnections();\n });\n\n const close = (): Promise<void> => {\n if (closed) return closePromise ?? Promise.resolve();\n closed = true;\n closePromise = doClose();\n return closePromise;\n };\n\n // `handle.close` is built here, but ensureSingleWebServer/ensureWebMonitor\n // and the `web serve` command each wrap it afterwards (to also remove the\n // lock file, stop the session-directory poller, ...). onSignal below calls\n // `handle.close()` — the *current* value of the property — rather than the\n // `close` closed over here, so a `kill -TERM <pid>` (including `issue-flow\n // web stop`) always runs the fully wrapped shutdown, not just this raw\n // socket close. Capturing `close` directly here was a real bug: the wrapped\n // behavior only ever ran for an *explicit* `handle.close()` call, leaving\n // the lock file (and the poller) behind on every signal-driven shutdown.\n const handle: WebServerHandle = { server, host: options.host, port, url, close };\n\n // Explicit close on SIGINT/SIGTERM, then re-raise so the default\n // termination behavior still applies.\n const onSignal = (signal: NodeJS.Signals): void => {\n void handle.close().finally(() => {\n process.kill(process.pid, signal);\n });\n };\n process.once('SIGINT', onSignal);\n process.once('SIGTERM', onSignal);\n\n return handle;\n}\n"],"mappings":"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","names":["readFile","join","chalk","require","join","readFile","chalk"]}
@@ -6,14 +6,14 @@ import {
6
6
  } from "./chunk-HDV3YDTG.js";
7
7
  import {
8
8
  publishGitState
9
- } from "./chunk-J3PJBZXJ.js";
9
+ } from "./chunk-VOKNTBOO.js";
10
10
  import {
11
11
  EXECUTE_PHASE,
12
12
  elapsedSecondsSince,
13
13
  getPhaseUsageTotals,
14
14
  publishIterationMetrics,
15
15
  publishStoryMetrics
16
- } from "./chunk-QO2MWMAZ.js";
16
+ } from "./chunk-PPLIXZI5.js";
17
17
  import {
18
18
  applyPlaceholders,
19
19
  loadPrompt
@@ -22,7 +22,7 @@ import {
22
22
  createConfig,
23
23
  resolvePaths,
24
24
  validateDependencies
25
- } from "./chunk-BASRRXAM.js";
25
+ } from "./chunk-VYFKOUUR.js";
26
26
  import {
27
27
  allStoriesPass,
28
28
  applyStoryMetrics,
@@ -33,6 +33,7 @@ import {
33
33
  getIcons,
34
34
  getOutputCallback,
35
35
  getSessionPublisher,
36
+ getStoryStageCallback,
36
37
  getStoryUpdateCallback,
37
38
  getTermWidth,
38
39
  hasPendingCorrection,
@@ -49,11 +50,12 @@ import {
49
50
  printSuccess,
50
51
  printWarning,
51
52
  saveTaskPlan,
53
+ selectActiveStory,
52
54
  setLastError,
53
55
  trimErrorMessage,
54
56
  useColor,
55
57
  useUnicode
56
- } from "./chunk-BH7DYADV.js";
58
+ } from "./chunk-V5OHFSW4.js";
57
59
 
58
60
  // src/core/engine.ts
59
61
  import { existsSync } from "fs";
@@ -80,7 +82,7 @@ function printProgressBar(passed, total) {
80
82
  }
81
83
  return `${bar} ${text}`;
82
84
  }
83
- function printIterationHeader(iteration, maxIter, stories) {
85
+ function printIterationHeader(iteration, maxIter, stories, activeStoryId) {
84
86
  const icons = getIcons();
85
87
  const colored = useColor();
86
88
  const cb = getOutputCallback();
@@ -97,10 +99,11 @@ function printIterationHeader(iteration, maxIter, stories) {
97
99
  for (const story of stories) {
98
100
  let icon;
99
101
  let colorFn;
102
+ const isActive = activeStoryId !== void 0 ? story.id === activeStoryId : !story.passes && !foundFirstPending;
100
103
  if (story.passes) {
101
104
  icon = icons.success;
102
105
  colorFn = colored ? chalk.green : (s) => s;
103
- } else if (!foundFirstPending) {
106
+ } else if (isActive) {
104
107
  icon = icons.pending;
105
108
  colorFn = colored ? chalk.yellow : (s) => s;
106
109
  foundFirstPending = true;
@@ -251,6 +254,50 @@ function buildRunSummaryLines(info) {
251
254
  }
252
255
  return lines;
253
256
  }
257
+ function buildQueueSummaryLines(info) {
258
+ const lines = [];
259
+ lines.push(` Branch: ${info.branchName ?? "current"}`);
260
+ lines.push(` Issues: ${info.issues.length}`);
261
+ for (const issue of info.issues) {
262
+ const tokens2 = formatTokens(issue.usage);
263
+ const title = issue.title === "" ? "" : ` ${issue.title}`;
264
+ lines.push(
265
+ ` #${issue.id}${title} \u2014 ${issue.storyCount} stories, ${formatDuration(issue.elapsedSeconds)}` + (tokens2 === "" ? "" : `, ${tokens2}`)
266
+ );
267
+ }
268
+ lines.push(` Duration: ${formatDuration(info.elapsedSeconds)}`);
269
+ const tokens = formatTokens(info.usage);
270
+ if (tokens !== "") {
271
+ lines.push(` Tokens: ${tokens}`);
272
+ }
273
+ if (info.prUrl !== null) {
274
+ lines.push(` PR: ${info.prUrl}`);
275
+ }
276
+ if (info.excluded.length > 0) {
277
+ lines.push(` Skipped: ${info.excluded.map((entry) => `#${entry.id}`).join(", ")}`);
278
+ }
279
+ const review = info.prReview;
280
+ if (review) {
281
+ lines.push(
282
+ ` Review: ${review.recommendation ?? (review.requestedChanges ? "REQUEST_CHANGES" : "unknown")}`
283
+ );
284
+ if (review.reportPath !== null) {
285
+ lines.push(` Report: ${review.reportPath}`);
286
+ }
287
+ }
288
+ return lines;
289
+ }
290
+ function printQueueSummary(info) {
291
+ console.log("");
292
+ if (info.prReview?.requestedChanges === true) {
293
+ printWarning(`Queue finished for issue #${info.queueId}, but the PR review requested changes.`);
294
+ } else {
295
+ printSuccess(`Queue complete for issue #${info.queueId} (${info.issues.length} issues)!`);
296
+ }
297
+ for (const line of buildQueueSummaryLines(info)) {
298
+ console.log(line);
299
+ }
300
+ }
254
301
  function printRunSummary(info) {
255
302
  console.log("");
256
303
  if (info.prReview?.requestedChanges) {
@@ -323,6 +370,13 @@ function newlyCompletedStoryIds(before, after) {
323
370
  const wasPassing = new Map(before.map((story) => [story.id, story.passes]));
324
371
  return after.filter((story) => story.passes && wasPassing.get(story.id) === false).map((story) => story.id);
325
372
  }
373
+ function commitPlaceholders(scope) {
374
+ const suffix = scope === void 0 || scope === "" ? "" : `(${scope})`;
375
+ return {
376
+ __COMMIT_MESSAGE__: `feat${suffix}: [Story ID] - [Story Title]`,
377
+ __FIX_COMMIT_MESSAGE__: `fix${suffix}: address review findings`
378
+ };
379
+ }
326
380
  async function ensureProgressFile(progressFile) {
327
381
  if (!existsSync(progressFile)) {
328
382
  const content = `# Issue Flow Progress Log
@@ -421,15 +475,23 @@ async function runEngine(config, paths) {
421
475
  break;
422
476
  }
423
477
  i++;
424
- getSessionPublisher().publish({ type: "iteration:start", at: isoNow(), iteration: i });
425
478
  plan = await loadTaskPlan(paths.prdFile);
426
479
  const storiesBeforeIteration = plan.userStories;
480
+ const activeStoryId = selectActiveStory(plan.userStories)?.id;
481
+ getSessionPublisher().publish({
482
+ type: "iteration:start",
483
+ at: isoNow(),
484
+ iteration: i,
485
+ storyId: activeStoryId
486
+ });
487
+ getStoryStageCallback()?.(activeStoryId);
427
488
  if (isVerbose()) {
428
- printIterationHeader(i, config.maxIterations, plan.userStories);
489
+ printIterationHeader(i, config.maxIterations, plan.userStories, activeStoryId);
429
490
  }
430
491
  const prompt = applyPlaceholders(promptTemplate, {
431
492
  __PRD_FILE__: paths.prdFile,
432
- __PROGRESS_FILE__: paths.progressFile
493
+ __PROGRESS_FILE__: paths.progressFile,
494
+ ...commitPlaceholders(config.commitScope)
433
495
  });
434
496
  const iterationStartedAt = isoNow();
435
497
  plan = markIssueInProgress(plan, iterationStartedAt);
@@ -589,7 +651,8 @@ async function runExecute(positionalMaxIter, options) {
589
651
  issueNumber: options.issue,
590
652
  maxIterations,
591
653
  retryLimit: options.retryLimit,
592
- retryForever: options.retryForever
654
+ retryForever: options.retryForever,
655
+ commitScope: options.commitScope
593
656
  });
594
657
  const paths = await resolvePaths(config);
595
658
  const exitCode = await runEngine(config, paths);
@@ -607,7 +670,8 @@ async function runExecute(positionalMaxIter, options) {
607
670
  }
608
671
 
609
672
  export {
673
+ printQueueSummary,
610
674
  printRunSummary,
611
675
  runExecute
612
676
  };
613
- //# sourceMappingURL=chunk-TGP6WK65.js.map
677
+ //# sourceMappingURL=chunk-67ZO5ROQ.js.map