issue-flow 0.7.0 → 0.7.1

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 (64) hide show
  1. package/README.md +3 -1
  2. package/dist/{analyze-WRFFG2R5.js → analyze-R25HDGBJ.js} +5 -5
  3. package/dist/{chunk-EWMSQQL2.js → chunk-23WOEBGN.js} +5 -5
  4. package/dist/{chunk-OVFYJLOH.js → chunk-2AVZZAC6.js} +138 -13
  5. package/dist/chunk-2AVZZAC6.js.map +1 -0
  6. package/dist/{chunk-75K5IKAC.js → chunk-ASMCLXQ2.js} +6 -6
  7. package/dist/{chunk-JV7P6UGG.js → chunk-EHVKXFFN.js} +2 -2
  8. package/dist/{chunk-Z3UIGLZG.js → chunk-EM43T462.js} +13 -5
  9. package/dist/{chunk-Z3UIGLZG.js.map → chunk-EM43T462.js.map} +1 -1
  10. package/dist/{chunk-UX6YAVLT.js → chunk-HJ7JT777.js} +6 -6
  11. package/dist/{chunk-2Y3UO7H2.js → chunk-JNEHVQH5.js} +13 -1
  12. package/dist/chunk-JNEHVQH5.js.map +1 -0
  13. package/dist/{chunk-HS4QWZAO.js → chunk-RN2FAMUU.js} +3 -3
  14. package/dist/{chunk-KUDAY57D.js → chunk-TCOS2KFB.js} +2 -2
  15. package/dist/{chunk-CW6UE3NW.js → chunk-WHIBTNYI.js} +3 -3
  16. package/dist/{chunk-YXJPKBPV.js → chunk-YGEWIEAI.js} +10 -9
  17. package/dist/chunk-YGEWIEAI.js.map +1 -0
  18. package/dist/{chunk-VLSZFMLZ.js → chunk-YLXPLHEL.js} +2 -2
  19. package/dist/{chunk-BTBSVBE3.js → chunk-ZAEBDO3Q.js} +6 -6
  20. package/dist/cli.js +13 -13
  21. package/dist/{config-UHQ6XXX2.js → config-SJAQMFWB.js} +9 -3
  22. package/dist/execute-VZ7SHLP4.js +13 -0
  23. package/dist/{generate-DUKQCADD.js → generate-LUK3XTIP.js} +5 -5
  24. package/dist/init-5KXCG36P.js +9 -0
  25. package/dist/plan-R34SAWRK.js +15 -0
  26. package/dist/pr-P4BDQXUQ.js +15 -0
  27. package/dist/pr-review-TOSBE6ZS.js +15 -0
  28. package/dist/prd-RYLULM75.js +15 -0
  29. package/dist/review-ZSC5ELQA.js +13 -0
  30. package/dist/{run-VC647DQI.js → run-SDRQDMSZ.js} +14 -14
  31. package/package.json +1 -1
  32. package/prompts/execute.md +17 -5
  33. package/prompts/plan.md +1 -0
  34. package/prompts/review.md +2 -0
  35. package/dist/chunk-2Y3UO7H2.js.map +0 -1
  36. package/dist/chunk-OVFYJLOH.js.map +0 -1
  37. package/dist/chunk-YXJPKBPV.js.map +0 -1
  38. package/dist/execute-K5B4UMUT.js +0 -13
  39. package/dist/init-W4HBLW7R.js +0 -9
  40. package/dist/plan-NKPU45NP.js +0 -15
  41. package/dist/pr-JYGLLIEF.js +0 -15
  42. package/dist/pr-review-6H6F4XUY.js +0 -15
  43. package/dist/prd-FC2ZCEOX.js +0 -15
  44. package/dist/review-X4NGDZZI.js +0 -13
  45. /package/dist/{analyze-WRFFG2R5.js.map → analyze-R25HDGBJ.js.map} +0 -0
  46. /package/dist/{chunk-EWMSQQL2.js.map → chunk-23WOEBGN.js.map} +0 -0
  47. /package/dist/{chunk-75K5IKAC.js.map → chunk-ASMCLXQ2.js.map} +0 -0
  48. /package/dist/{chunk-JV7P6UGG.js.map → chunk-EHVKXFFN.js.map} +0 -0
  49. /package/dist/{chunk-UX6YAVLT.js.map → chunk-HJ7JT777.js.map} +0 -0
  50. /package/dist/{chunk-HS4QWZAO.js.map → chunk-RN2FAMUU.js.map} +0 -0
  51. /package/dist/{chunk-KUDAY57D.js.map → chunk-TCOS2KFB.js.map} +0 -0
  52. /package/dist/{chunk-CW6UE3NW.js.map → chunk-WHIBTNYI.js.map} +0 -0
  53. /package/dist/{chunk-VLSZFMLZ.js.map → chunk-YLXPLHEL.js.map} +0 -0
  54. /package/dist/{chunk-BTBSVBE3.js.map → chunk-ZAEBDO3Q.js.map} +0 -0
  55. /package/dist/{config-UHQ6XXX2.js.map → config-SJAQMFWB.js.map} +0 -0
  56. /package/dist/{execute-K5B4UMUT.js.map → execute-VZ7SHLP4.js.map} +0 -0
  57. /package/dist/{generate-DUKQCADD.js.map → generate-LUK3XTIP.js.map} +0 -0
  58. /package/dist/{init-W4HBLW7R.js.map → init-5KXCG36P.js.map} +0 -0
  59. /package/dist/{plan-NKPU45NP.js.map → plan-R34SAWRK.js.map} +0 -0
  60. /package/dist/{pr-JYGLLIEF.js.map → pr-P4BDQXUQ.js.map} +0 -0
  61. /package/dist/{pr-review-6H6F4XUY.js.map → pr-review-TOSBE6ZS.js.map} +0 -0
  62. /package/dist/{prd-FC2ZCEOX.js.map → prd-RYLULM75.js.map} +0 -0
  63. /package/dist/{review-X4NGDZZI.js.map → review-ZSC5ELQA.js.map} +0 -0
  64. /package/dist/{run-VC647DQI.js.map → run-SDRQDMSZ.js.map} +0 -0
package/README.md CHANGED
@@ -87,7 +87,7 @@ npx issue-flow run 42 --mode manual
87
87
  npx issue-flow run 42 --web
88
88
  ```
89
89
 
90
- 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, runs correction cycles (re-execute + re-review) up to `maxCorrectionCycles`.
90
+ 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
91
 
92
92
  ### `init` -- Check prerequisites
93
93
 
@@ -216,6 +216,8 @@ The `pipeline` field tracks which phases have completed, enabling resume from an
216
216
  }
217
217
  ```
218
218
 
219
+ The top-level `lastReviewFindings` field (`string | null`) holds the verbatim findings of the most recent failed `review` phase. Non-null overrides the "issue already complete" check even when every user story has `passes: true`, so a correction cycle's re-execute step is guaranteed to run instead of exiting immediately — see `core/engine.ts`.
220
+
219
221
  ## Architecture
220
222
 
221
223
  ```
@@ -2,17 +2,17 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-HS4QWZAO.js";
5
+ } from "./chunk-RN2FAMUU.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-KUDAY57D.js";
8
+ } from "./chunk-TCOS2KFB.js";
9
9
  import {
10
10
  applyPlaceholders,
11
11
  loadPrompt
12
12
  } from "./chunk-SLHZPB3C.js";
13
13
  import {
14
14
  getIssueDir
15
- } from "./chunk-OVFYJLOH.js";
15
+ } from "./chunk-2AVZZAC6.js";
16
16
  import {
17
17
  getGlobalTimeout,
18
18
  loadTaskPlan,
@@ -20,7 +20,7 @@ import {
20
20
  printInfo,
21
21
  printSuccess,
22
22
  saveTaskPlan
23
- } from "./chunk-2Y3UO7H2.js";
23
+ } from "./chunk-JNEHVQH5.js";
24
24
 
25
25
  // src/commands/analyze.ts
26
26
  import { mkdir, readFile, writeFile } from "fs/promises";
@@ -77,4 +77,4 @@ async function runAnalyze(issue, resolvedIssue) {
77
77
  export {
78
78
  runAnalyze
79
79
  };
80
- //# sourceMappingURL=analyze-WRFFG2R5.js.map
80
+ //# sourceMappingURL=analyze-R25HDGBJ.js.map
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  listPullRequests
4
- } from "./chunk-CW6UE3NW.js";
4
+ } from "./chunk-WHIBTNYI.js";
5
5
  import {
6
6
  runHeadless
7
- } from "./chunk-KUDAY57D.js";
7
+ } from "./chunk-TCOS2KFB.js";
8
8
  import {
9
9
  applyPlaceholders,
10
10
  loadPrompt
@@ -14,7 +14,7 @@ import {
14
14
  getIssueDir,
15
15
  loadPrReviewConfig,
16
16
  run
17
- } from "./chunk-OVFYJLOH.js";
17
+ } from "./chunk-2AVZZAC6.js";
18
18
  import {
19
19
  getGlobalTimeout,
20
20
  getSessionPublisher,
@@ -26,7 +26,7 @@ import {
26
26
  printWarning,
27
27
  saveTaskPlan,
28
28
  writeFileAtomic
29
- } from "./chunk-2Y3UO7H2.js";
29
+ } from "./chunk-JNEHVQH5.js";
30
30
 
31
31
  // src/commands/pr-review.ts
32
32
  import { join as join4 } from "path";
@@ -722,4 +722,4 @@ export {
722
722
  exitCodeFor,
723
723
  runPrReview
724
724
  };
725
- //# sourceMappingURL=chunk-EWMSQQL2.js.map
725
+ //# sourceMappingURL=chunk-23WOEBGN.js.map
@@ -4,12 +4,17 @@ import {
4
4
  prReviewConfigSchema,
5
5
  printWarning,
6
6
  webConfigSchema
7
- } from "./chunk-2Y3UO7H2.js";
7
+ } from "./chunk-JNEHVQH5.js";
8
8
 
9
9
  // src/config.ts
10
10
  import { readFile } from "fs/promises";
11
11
  import { platform } from "os";
12
- import { join as join2 } from "path";
12
+ import { join as join3 } from "path";
13
+
14
+ // src/storage/paths.ts
15
+ import { createHash } from "crypto";
16
+ import { homedir } from "os";
17
+ import { basename, join as join2, resolve } from "path";
13
18
 
14
19
  // src/utils/git.ts
15
20
  import { join } from "path";
@@ -71,6 +76,68 @@ async function getCommitsSince(base) {
71
76
  });
72
77
  }
73
78
 
79
+ // src/storage/paths.ts
80
+ var GLOBAL_DIR_NAME = ".issue-flow";
81
+ var GLOBAL_ROOT_ENV = "ISSUE_FLOW_HOME";
82
+ function getGlobalRoot(options = {}) {
83
+ const env = options.env ?? process.env;
84
+ const override = env[GLOBAL_ROOT_ENV]?.trim();
85
+ if (override) {
86
+ return resolve(override);
87
+ }
88
+ let home;
89
+ try {
90
+ home = homedir();
91
+ } catch {
92
+ home = "";
93
+ }
94
+ if (home.trim() === "") {
95
+ throw new Error(
96
+ `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`
97
+ );
98
+ }
99
+ return join2(home, GLOBAL_DIR_NAME);
100
+ }
101
+
102
+ // src/storage/schemas.ts
103
+ import { z } from "zod";
104
+ var projectMetadataSchema = z.object({
105
+ schemaVersion: z.number().int().positive(),
106
+ projectId: z.string().min(1),
107
+ root: z.string().min(1),
108
+ /** Normalized remote (`host/org/repo`), or null when the project has none. */
109
+ remoteUrl: z.string().nullable(),
110
+ createdAt: z.string().min(1),
111
+ updatedAt: z.string().min(1),
112
+ /** Null until the project is used by a pipeline run. */
113
+ lastAttemptAt: z.string().nullable()
114
+ });
115
+ var webShape = webConfigSchema.shape;
116
+ var globalWebConfigSchema = z.object({
117
+ port: webShape.port.unwrap(),
118
+ host: webShape.host.unwrap(),
119
+ refreshSeconds: webShape.refreshSeconds.unwrap(),
120
+ logLimit: webShape.logLimit.unwrap()
121
+ }).partial();
122
+ var globalRetryConfigSchema = z.object({
123
+ retryLimit: z.number().int().nonnegative(),
124
+ retryForever: z.boolean(),
125
+ backoffBaseSeconds: z.number().nonnegative(),
126
+ backoffMaxSeconds: z.number().nonnegative()
127
+ }).partial();
128
+ var globalCommitConfigSchema = z.object({
129
+ signoff: z.boolean(),
130
+ conventional: z.boolean()
131
+ }).partial();
132
+ var globalConfigSchema = z.object({
133
+ schemaVersion: z.number().int().positive(),
134
+ /** Override for the directory holding `projects/`. */
135
+ storageDir: z.string().min(1),
136
+ web: globalWebConfigSchema,
137
+ retry: globalRetryConfigSchema,
138
+ commit: globalCommitConfigSchema
139
+ }).partial();
140
+
74
141
  // src/config.ts
75
142
  var DEFAULTS = {
76
143
  retryLimit: 10,
@@ -91,21 +158,21 @@ function createConfig(options) {
91
158
  async function resolvePaths(config, scriptDir) {
92
159
  const projectRoot = await getProjectRoot();
93
160
  if (config.issueNumber) {
94
- const issueDir = join2(projectRoot, "issues", config.issueNumber);
161
+ const issueDir = join3(projectRoot, "issues", config.issueNumber);
95
162
  return {
96
- prdFile: join2(issueDir, "tasks.json"),
97
- progressFile: join2(issueDir, "progress.txt"),
98
- archiveDir: join2(issueDir, "archive"),
99
- lastBranchFile: join2(issueDir, ".last-branch"),
163
+ prdFile: join3(issueDir, "tasks.json"),
164
+ progressFile: join3(issueDir, "progress.txt"),
165
+ archiveDir: join3(issueDir, "archive"),
166
+ lastBranchFile: join3(issueDir, ".last-branch"),
100
167
  projectRoot
101
168
  };
102
169
  }
103
170
  const base = scriptDir ?? projectRoot;
104
171
  return {
105
- prdFile: join2(base, "prd.json"),
106
- progressFile: join2(base, "progress.txt"),
107
- archiveDir: join2(base, "archive"),
108
- lastBranchFile: join2(base, ".last-branch"),
172
+ prdFile: join3(base, "prd.json"),
173
+ progressFile: join3(base, "progress.txt"),
174
+ archiveDir: join3(base, "archive"),
175
+ lastBranchFile: join3(base, ".last-branch"),
109
176
  projectRoot
110
177
  };
111
178
  }
@@ -145,7 +212,7 @@ async function readProjectConfigFile(projectRoot, warn) {
145
212
  return null;
146
213
  }
147
214
  }
148
- const filePath = join2(root, PROJECT_CONFIG_FILENAME);
215
+ const filePath = join3(root, PROJECT_CONFIG_FILENAME);
149
216
  let raw;
150
217
  try {
151
218
  raw = await readFile(filePath, "utf-8");
@@ -165,6 +232,61 @@ async function readProjectConfigFile(projectRoot, warn) {
165
232
  }
166
233
  return parsed;
167
234
  }
235
+ var GLOBAL_CONFIG_FILENAME = "config.json";
236
+ async function loadGlobalConfig(options = {}) {
237
+ const warn = options.warn ?? printWarning;
238
+ let root;
239
+ try {
240
+ root = options.globalRoot ?? getGlobalRoot({ env: options.env ?? process.env });
241
+ } catch (err) {
242
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${err.message}`);
243
+ return {};
244
+ }
245
+ const filePath = join3(root, GLOBAL_CONFIG_FILENAME);
246
+ let raw;
247
+ try {
248
+ raw = await readFile(filePath, "utf-8");
249
+ } catch (err) {
250
+ if (err.code !== "ENOENT") {
251
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${err.message}`);
252
+ }
253
+ return {};
254
+ }
255
+ let parsed;
256
+ try {
257
+ parsed = JSON.parse(raw);
258
+ } catch {
259
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: invalid JSON.`);
260
+ return {};
261
+ }
262
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
263
+ warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: expected a JSON object.`);
264
+ return {};
265
+ }
266
+ const config = {};
267
+ for (const [key, value] of Object.entries(parsed)) {
268
+ const result = globalConfigSchema.safeParse({ [key]: value });
269
+ if (!result.success) {
270
+ warn(
271
+ `Ignoring "${key}" key of ${GLOBAL_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? "invalid value"}.`
272
+ );
273
+ continue;
274
+ }
275
+ Object.assign(config, result.data);
276
+ }
277
+ return config;
278
+ }
279
+ function mergeConfigLayers(layers) {
280
+ const merged = {};
281
+ for (const layer of [layers.defaults, layers.global, layers.project, layers.env, layers.cli]) {
282
+ for (const [key, value] of Object.entries(layer ?? {})) {
283
+ if (value !== void 0) {
284
+ merged[key] = value;
285
+ }
286
+ }
287
+ }
288
+ return merged;
289
+ }
168
290
  var webCliOverrides = {};
169
291
  function setWebCliOverrides(overrides) {
170
292
  webCliOverrides = overrides;
@@ -320,10 +442,13 @@ export {
320
442
  validateDependencies,
321
443
  PROJECT_CONFIG_FILENAME,
322
444
  WEB_CONFIG_FILENAME,
445
+ GLOBAL_CONFIG_FILENAME,
446
+ loadGlobalConfig,
447
+ mergeConfigLayers,
323
448
  setWebCliOverrides,
324
449
  loadWebConfig,
325
450
  setIssuesCliOverrides,
326
451
  loadIssuesConfig,
327
452
  loadPrReviewConfig
328
453
  };
329
- //# sourceMappingURL=chunk-OVFYJLOH.js.map
454
+ //# sourceMappingURL=chunk-2AVZZAC6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/storage/paths.ts","../src/utils/git.ts","../src/utils/shell.ts","../src/storage/schemas.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { platform } from 'node:os';\nimport { join } from 'node:path';\nimport type { IssuesConfig } from './issues/types.js';\nimport {\n issuesConfigSchema,\n type PrReviewConfig,\n prReviewConfigSchema,\n type WebConfig,\n webConfigSchema,\n} from './schemas.js';\nimport { getGlobalRoot } from './storage/paths.js';\nimport { type GlobalConfig, globalConfigSchema } from './storage/schemas.js';\nimport type { EngineConfig, ResolvedPaths } from './types.js';\nimport { printWarning } from './ui/logger.js';\nimport { getProjectRoot } from './utils/git.js';\nimport { run } from './utils/shell.js';\n\n/**\n * Default configuration values — matching the Bash script exactly.\n */\nexport const DEFAULTS = {\n retryLimit: 10,\n retryForever: false,\n backoffBaseSeconds: 30,\n backoffMaxSeconds: 900,\n} as const;\n\n/**\n * Create a EngineConfig with defaults merged with provided options.\n */\nexport function createConfig(options: Partial<EngineConfig>): EngineConfig {\n return {\n issueNumber: options.issueNumber,\n maxIterations: options.maxIterations,\n retryLimit: options.retryLimit ?? DEFAULTS.retryLimit,\n retryForever: options.retryForever ?? DEFAULTS.retryForever,\n backoffBaseSeconds: options.backoffBaseSeconds ?? DEFAULTS.backoffBaseSeconds,\n backoffMaxSeconds: options.backoffMaxSeconds ?? DEFAULTS.backoffMaxSeconds,\n };\n}\n\n/**\n * Resolve file paths based on issue number and project root.\n *\n * With --issue N:\n * prdFile = {projectRoot}/issues/{N}/tasks.json\n * progressFile = {projectRoot}/issues/{N}/progress.txt\n *\n * Standalone:\n * prdFile = {projectRoot}/prd.json\n * progressFile = {projectRoot}/progress.txt\n */\nexport async function resolvePaths(\n config: EngineConfig,\n scriptDir?: string,\n): Promise<ResolvedPaths> {\n const projectRoot = await getProjectRoot();\n\n if (config.issueNumber) {\n const issueDir = join(projectRoot, 'issues', config.issueNumber);\n return {\n prdFile: join(issueDir, 'tasks.json'),\n progressFile: join(issueDir, 'progress.txt'),\n archiveDir: join(issueDir, 'archive'),\n lastBranchFile: join(issueDir, '.last-branch'),\n projectRoot,\n };\n }\n\n // Standalone mode — use scriptDir if available, otherwise projectRoot\n const base = scriptDir ?? projectRoot;\n return {\n prdFile: join(base, 'prd.json'),\n progressFile: join(base, 'progress.txt'),\n archiveDir: join(base, 'archive'),\n lastBranchFile: join(base, '.last-branch'),\n projectRoot,\n };\n}\n\n/**\n * Return a platform-appropriate install hint for a given package.\n */\nexport function getInstallHint(pkg: string): string {\n const os = platform();\n\n if (os === 'darwin') {\n return `brew install ${pkg}`;\n }\n if (os === 'linux') {\n return `apt install ${pkg} (or your distro's package manager)`;\n }\n if (os === 'win32') {\n return `winget install ${pkg} (or choco install ${pkg})`;\n }\n\n return `install ${pkg} using your system package manager`;\n}\n\n/**\n * Validate that required external dependencies are available.\n * Returns an array of error messages (empty if all deps are found).\n */\nexport async function validateDependencies(): Promise<string[]> {\n const errors: string[] = [];\n\n // Check git\n const gitResult = await run('git', ['--version']);\n if (gitResult.exitCode !== 0) {\n errors.push(` - git (install with: ${getInstallHint('git')})`);\n }\n\n // Check claude\n const claudeResult = await run('claude', ['--version']);\n if (claudeResult.exitCode !== 0) {\n errors.push(' - claude (install with: npm install -g @anthropic-ai/claude-code)');\n }\n\n // Note: jq is NOT required — the TypeScript CLI handles JSON natively\n\n return errors;\n}\n\n// ── Per-project configuration file ──────────────────────────────────────────\n\n/** Optional per-project configuration file, read from the project root. */\nexport const PROJECT_CONFIG_FILENAME = '.issue-flow.json';\n\n/** Historical alias kept for the web monitoring call sites. */\nexport const WEB_CONFIG_FILENAME = PROJECT_CONFIG_FILENAME;\n\n/**\n * Read and parse .issue-flow.json from the project root.\n *\n * Never throws: an absent file, unreadable path, invalid JSON or a non-object\n * root all degrade to `null` (with a warning for the malformed cases), so every\n * consumer falls back to its own defaults.\n */\nasync function readProjectConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Record<string, unknown> | null> {\n let root = projectRoot;\n if (root === undefined) {\n try {\n root = await getProjectRoot();\n } catch {\n return null;\n }\n }\n\n const filePath = join(root, PROJECT_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch {\n // The file is entirely optional — absence is the common case.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: invalid JSON.`);\n return null;\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: expected a JSON object.`);\n return null;\n }\n\n return parsed as Record<string, unknown>;\n}\n\n// ── Global configuration file ───────────────────────────────────────────────\n\n/** Machine-wide configuration file, read from the global storage root. */\nexport const GLOBAL_CONFIG_FILENAME = 'config.json';\n\nexport interface LoadGlobalConfigOptions {\n /** Environment source, forwarded to getGlobalRoot(). Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory holding config.json. Defaults to getGlobalRoot(). */\n globalRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\n/**\n * Read and parse `~/.issue-flow/config.json`, the preferences a user sets once\n * for every project.\n *\n * Never throws, exactly like readProjectConfigFile(): an absent file, an\n * unreadable path, invalid JSON, a non-object root or an invalid key all\n * degrade to \"no global preference\" so the caller falls back to the layers\n * below. Absence is silent — it is the common case; every other failure warns,\n * because a preference the user did write is being dropped.\n *\n * Validation happens key by key on purpose: a typo in `retry` must not cost the\n * user their `web` settings. Unknown keys are dropped without a warning, which\n * is what keeps a file written by a newer release readable here.\n */\nexport async function loadGlobalConfig(\n options: LoadGlobalConfigOptions = {},\n): Promise<GlobalConfig> {\n const warn = options.warn ?? printWarning;\n\n let root: string;\n try {\n root = options.globalRoot ?? getGlobalRoot({ env: options.env ?? process.env });\n } catch (err: unknown) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n return {};\n }\n\n const filePath = join(root, GLOBAL_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: invalid JSON.`);\n return {};\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: expected a JSON object.`);\n return {};\n }\n\n const config: GlobalConfig = {};\n for (const [key, value] of Object.entries(parsed)) {\n const result = globalConfigSchema.safeParse({ [key]: value });\n if (!result.success) {\n warn(\n `Ignoring \"${key}\" key of ${GLOBAL_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n continue;\n }\n // An unknown key parses successfully into an empty object and disappears\n // here, which is the retro-compatible behaviour we want.\n Object.assign(config, result.data);\n }\n\n return config;\n}\n\n/**\n * The configuration sources of the engine, ordered from the lowest to the\n * highest precedence:\n *\n * defaults < config.json (global) < .issue-flow.json (project) < env < CLI\n */\nexport interface ConfigLayers<T extends object> {\n /** Hard-coded fallbacks, e.g. DEFAULTS or the values baked into a schema. */\n defaults?: Partial<T>;\n /** ~/.issue-flow/config.json, via loadGlobalConfig(). */\n global?: Partial<T>;\n /** The matching key of .issue-flow.json in the project root. */\n project?: Partial<T>;\n /** ISSUE_FLOW_* environment variables. */\n env?: Partial<T>;\n /** CLI flags. */\n cli?: Partial<T>;\n}\n\n/**\n * Merge configuration layers following the documented precedence.\n *\n * Pure and shallow: a layer only participates with the keys it actually\n * carries, so an absent key never erases the layer below it. `undefined` counts\n * as absent — that is what lets a layer be built by assigning only the values\n * that were really provided.\n *\n * Because the merge is shallow, nested objects (`web`, `retry`) are replaced\n * whole rather than merged field by field; callers that need per-field\n * precedence inside a nested key must flatten it into its own merge.\n *\n * Caveat for the `project` layer: it must be the *raw* set of keys the user\n * wrote, not the output of a schema that materializes defaults. In zod 4 a\n * `.default()` survives `.partial()`, so parsing the project file with\n * `webConfigSchema.partial()` yields every default and would make the project\n * layer swallow the global one.\n */\nexport function mergeConfigLayers<T extends object>(layers: ConfigLayers<T>): Partial<T> {\n const merged: Record<string, unknown> = {};\n\n for (const layer of [layers.defaults, layers.global, layers.project, layers.env, layers.cli]) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n }\n\n return merged as Partial<T>;\n}\n\n// ── Web monitoring configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts. Highest-precedence\n * source consumed by loadWebConfig().\n */\nlet webCliOverrides: Partial<WebConfig> = {};\n\nexport function setWebCliOverrides(overrides: Partial<WebConfig>): void {\n webCliOverrides = overrides;\n}\n\nexport interface LoadWebConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setWebCliOverrides(). */\n cli?: Partial<WebConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nconst FALSY_ENV_VALUES = new Set(['', '0', 'false', 'no', 'off']);\n\nfunction parseBooleanEnv(value: string): boolean {\n return !FALSY_ENV_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction readNumberEnv(\n env: NodeJS.ProcessEnv,\n name: string,\n warn: (message: string) => void,\n): number | undefined {\n const raw = env[name];\n if (raw === undefined) {\n return undefined;\n }\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`Ignoring ${name}=\"${raw}\": not a number.`);\n return undefined;\n }\n return parsed;\n}\n\nfunction readWebConfigEnv(\n env: NodeJS.ProcessEnv,\n warn: (message: string) => void,\n): Partial<WebConfig> {\n const layer: Partial<WebConfig> = {};\n if (env.ISSUE_FLOW_WEB !== undefined) {\n layer.enabled = parseBooleanEnv(env.ISSUE_FLOW_WEB);\n }\n const port = readNumberEnv(env, 'ISSUE_FLOW_WEB_PORT', warn);\n if (port !== undefined) {\n layer.port = port;\n }\n if (env.ISSUE_FLOW_WEB_HOST !== undefined) {\n layer.host = env.ISSUE_FLOW_WEB_HOST;\n }\n const refresh = readNumberEnv(env, 'ISSUE_FLOW_WEB_REFRESH', warn);\n if (refresh !== undefined) {\n layer.refreshSeconds = refresh;\n }\n const logLimit = readNumberEnv(env, 'ISSUE_FLOW_WEB_LOG_LIMIT', warn);\n if (logLimit !== undefined) {\n layer.logLimit = logLimit;\n }\n return layer;\n}\n\nasync function readWebConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<WebConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const web = file?.web;\n if (web === undefined) {\n return {};\n }\n\n const result = webConfigSchema.partial().safeParse(web);\n if (!result.success) {\n warn(\n `Ignoring \"web\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the web monitoring configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning.\n */\nexport async function loadWebConfig(options: LoadWebConfigOptions = {}): Promise<WebConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? webCliOverrides;\n const env = options.env ?? process.env;\n\n const fileLayer = await readWebConfigFile(options.projectRoot, warn);\n const envLayer = readWebConfigEnv(env, warn);\n const merged = { ...fileLayer, ...envLayer, ...cli };\n\n const result = webConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid web monitoring configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return webConfigSchema.parse({});\n}\n\n// ── Issue provider configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts (--local, --github,\n * --prefer-local, --prefer-github, --ask). Highest-precedence source consumed\n * by loadIssuesConfig().\n */\nlet issuesCliOverrides: Partial<IssuesConfig> = {};\n\nexport function setIssuesCliOverrides(overrides: Partial<IssuesConfig>): void {\n issuesCliOverrides = overrides;\n}\n\nexport interface LoadIssuesConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setIssuesCliOverrides(). */\n cli?: Partial<IssuesConfig>;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nasync function readIssuesConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<IssuesConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const issues = file?.issues;\n if (issues === undefined) {\n return {};\n }\n\n const result = issuesConfigSchema.partial().safeParse(issues);\n if (!result.success) {\n warn(\n `Ignoring \"issues\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the Issue provider configuration with the documented precedence:\n * CLI flag > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning, and the defaults reproduce the GitHub-only behaviour.\n */\nexport async function loadIssuesConfig(\n options: LoadIssuesConfigOptions = {},\n): Promise<IssuesConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? issuesCliOverrides;\n\n const fileLayer = await readIssuesConfigFile(options.projectRoot, warn);\n const merged = { ...fileLayer, ...cli };\n\n const result = issuesConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid Issue provider configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return issuesConfigSchema.parse({});\n}\n\n// ── PR review configuration ─────────────────────────────────────────────────\n\nexport interface LoadPrReviewConfigOptions {\n /** Highest-precedence layer, for a future `--publisher` flag. */\n cli?: Partial<PrReviewConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nfunction readPrReviewConfigEnv(env: NodeJS.ProcessEnv): Partial<PrReviewConfig> {\n const publisher = env.ISSUE_FLOW_PR_REVIEW_PUBLISHER;\n // An unknown value is not dropped here: it goes through the schema below, so\n // the user gets the same warning a bad config file would produce.\n return publisher === undefined ? {} : { publisher: publisher as PrReviewConfig['publisher'] };\n}\n\nasync function readPrReviewConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<PrReviewConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const prReview = file?.prReview;\n if (prReview === undefined) {\n return {};\n }\n\n const result = prReviewConfigSchema.partial().safeParse(prReview);\n if (!result.success) {\n warn(\n `Ignoring \"prReview\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the `pr-review` configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: an absent, malformed or unknown value degrades to the default\n * publisher with a warning, so a typo in .issue-flow.json costs a warning\n * rather than the review.\n */\nexport async function loadPrReviewConfig(\n options: LoadPrReviewConfigOptions = {},\n): Promise<PrReviewConfig> {\n const warn = options.warn ?? printWarning;\n const env = options.env ?? process.env;\n\n const fileLayer = await readPrReviewConfigFile(options.projectRoot, warn);\n const envLayer = readPrReviewConfigEnv(env);\n const merged = { ...fileLayer, ...envLayer, ...(options.cli ?? {}) };\n\n const result = prReviewConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid PR review configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return prReviewConfigSchema.parse({});\n}\n","import { createHash } from 'node:crypto';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\n\n/** Directory created under the user's home when no override is provided. */\nexport const GLOBAL_DIR_NAME = '.issue-flow';\n\n/** Environment variable that relocates the whole global storage tree. */\nexport const GLOBAL_ROOT_ENV = 'ISSUE_FLOW_HOME';\n\n/** Directory under the global root holding one folder per project. */\nexport const PROJECTS_DIR_NAME = 'projects';\n\n/** Directory under a project holding one folder per issue. */\nexport const ISSUES_DIR_NAME = 'issues';\n\nexport interface GetGlobalRootOptions {\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve the root directory of the global storage tree (`~/.issue-flow`).\n *\n * Every path under the global storage MUST be derived from this function: it is\n * the single seam where `ISSUE_FLOW_HOME` takes effect, which is what lets\n * tests, CI and sandboxes point the whole tree at a temporary directory instead\n * of touching the real `$HOME`. A call site that joins `homedir()` by hand\n * silently opts out of that isolation.\n *\n * Pure and synchronous: it never creates directories nor touches the\n * filesystem — callers decide when (and whether) the directory should exist.\n */\nexport function getGlobalRoot(options: GetGlobalRootOptions = {}): string {\n const env = options.env ?? process.env;\n\n const override = env[GLOBAL_ROOT_ENV]?.trim();\n if (override) {\n // A relative override is resolved against the CWD so that every consumer\n // gets an absolute path, exactly as in the homedir() branch below.\n return resolve(override);\n }\n\n // The only failure mode: an environment where the home directory cannot be\n // determined (some containers and CI images). The override is the escape\n // hatch, so the error names it.\n let home: string;\n try {\n home = homedir();\n } catch {\n home = '';\n }\n if (home.trim() === '') {\n throw new Error(\n `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`,\n );\n }\n\n return join(home, GLOBAL_DIR_NAME);\n}\n\n/** Maximum length of the human-readable half of a project id. */\nconst SLUG_MAX_LENGTH = 32;\n\n/** Number of hex characters kept from the sha256 of the seed. */\nconst HASH_LENGTH = 12;\n\n/** Used when the repository name has no character that survives sanitization. */\nconst FALLBACK_SLUG = 'project';\n\n/**\n * Reduce an arbitrary repository name to a path-safe slug: lowercase, only\n * `[a-z0-9-]`, runs of separators collapsed, truncated to {@link SLUG_MAX_LENGTH}.\n */\nfunction slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, SLUG_MAX_LENGTH)\n .replace(/-+$/g, '');\n\n return slug === '' ? FALLBACK_SLUG : slug;\n}\n\n/**\n * Derive a stable identifier for a project, in the form `<slug>-<hash12>`.\n *\n * The seed is canonical rather than local, so the same repository yields the\n * same id on any machine: `remote:<normalizedRemote>` whenever an `origin`\n * remote exists, falling back to `path:<absoluteProjectRoot>` when it does not.\n * That is what makes moving or renaming the local folder harmless for a cloned\n * repository — the history stays attached to the remote identity.\n *\n * The `remote:` / `path:` prefix is part of the hashed seed, so a project\n * identified by path can never collide with one identified by remote even if\n * the two strings were to coincide. The slug is only cosmetic (it makes the\n * directory recognizable); the hash carries the identity.\n *\n * Known limitation of the path fallback: for a repository with no `origin`\n * remote, the absolute path *is* the identity. Moving or renaming that folder\n * produces a different id, and the previous history is left behind under the\n * old one. Configuring a remote before adopting the global storage avoids it.\n */\nexport async function getProjectId(projectRoot: string): Promise<string> {\n const remote = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n return projectIdFromRemote(remote, projectRoot);\n}\n\n/**\n * Pure half of {@link getProjectId}: derive the id once the normalized remote\n * (or `null`) is already known.\n *\n * Exported so a caller that already resolved the remote for another reason\n * (e.g. {@link resolveStorageMode} in `compat.ts`, which also persists it into\n * `metadata.json`) can compute the id without shelling out to git a second\n * time.\n */\nexport function projectIdFromRemote(remote: string | null, projectRoot: string): string {\n let seed: string;\n let name: string;\n if (remote) {\n // `host/org/repo` -> the repository name is the last path segment.\n seed = `remote:${remote}`;\n name = remote.split('/').pop() ?? '';\n } else {\n const absolute = resolve(projectRoot);\n seed = `path:${absolute}`;\n name = basename(absolute);\n }\n\n const hash = createHash('sha256').update(seed).digest('hex').slice(0, HASH_LENGTH);\n\n return `${slugify(name)}-${hash}`;\n}\n\n/**\n * Absolute directory of a project inside the global storage tree.\n *\n * `projectId` is expected to come from {@link getProjectId}, which already\n * guarantees a single safe path segment.\n */\nexport function getProjectDir(projectId: string, options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);\n}\n\n/**\n * Every artifact of one issue, resolved from a single place.\n *\n * Note that `prdFile` is the requirements document (`prd.md`) and `tasksFile`\n * is the task plan (`tasks.json`) — unlike `ResolvedPaths.prdFile` in\n * `types.ts`, which historically points at `tasks.json`.\n */\nexport interface IssuePaths {\n issueDir: string;\n issueFile: string;\n metadataFile: string;\n prdFile: string;\n tasksFile: string;\n progressFile: string;\n analysisFile: string;\n sessionFile: string;\n lastBranchFile: string;\n archiveDir: string;\n prReviewDir: string;\n}\n\n/**\n * Issue identifiers become path segments, so anything that could escape the\n * issues directory is rejected before it reaches `join` — the same discipline\n * as `normalizeId()` in `issues/providers/local.ts`.\n *\n * Non-numeric identifiers (`auth-refactor`, `pr-184`) are first-class here:\n * the local provider already supports them, so the global storage must too.\n */\nfunction normalizeIssueNumber(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n\n if (normalized.length === 0) {\n throw new Error('Issue identifier cannot be empty');\n }\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n\n return normalized;\n}\n\n/**\n * Resolve every path of a single issue under the global storage.\n *\n * This is the only place allowed to know the layout of an issue directory:\n * call sites ask for the artifact they need instead of joining names by hand,\n * so renaming or adding an artifact stays a one-file change.\n *\n * Pure and synchronous: nothing here creates directories or touches the\n * filesystem.\n */\nexport function getIssuePaths(\n projectId: string,\n issueNumber: string | number,\n options: GetGlobalRootOptions = {},\n): IssuePaths {\n const issueDir = join(\n getProjectDir(projectId, options),\n ISSUES_DIR_NAME,\n normalizeIssueNumber(issueNumber),\n );\n\n return {\n issueDir,\n issueFile: join(issueDir, 'issue.md'),\n metadataFile: join(issueDir, 'metadata.json'),\n prdFile: join(issueDir, 'prd.md'),\n tasksFile: join(issueDir, 'tasks.json'),\n progressFile: join(issueDir, 'progress.txt'),\n analysisFile: join(issueDir, 'analysis.md'),\n sessionFile: join(issueDir, 'session.json'),\n lastBranchFile: join(issueDir, '.last-branch'),\n archiveDir: join(issueDir, 'archive'),\n prReviewDir: join(issueDir, 'pr-review'),\n };\n}\n","import { join } from 'node:path';\nimport { type ExecResult, run } from './shell.js';\n\n/**\n * Get the root directory of the current git repository.\n * Throws if not inside a git repository.\n */\nexport async function getProjectRoot(): Promise<string> {\n const result = await run('git', ['rev-parse', '--show-toplevel']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Not inside a git repository. Please run issue-flow from within a git project.',\n );\n }\n\n return result.stdout.trim();\n}\n\n/**\n * Resolve `issues/<issueNumber>/`, always anchored to the git repository\n * root — never to `process.cwd()`.\n *\n * Every command must go through this instead of `join('issues', issueNumber)`:\n * a plain CWD-relative join means the pipeline resolves a different directory\n * depending on which subdirectory of the repo the command was launched from,\n * which is exactly what let `plan` write to `<subdir>/issues/<N>/` while\n * `execute` (already anchored to the project root via `resolvePaths()` in\n * config.ts) looked for it at `<repoRoot>/issues/<N>/` and failed. Anchoring\n * every command the same way makes the outcome identical regardless of CWD,\n * so there is nothing left for a user to get wrong by running from a\n * subdirectory.\n */\nexport async function getIssueDir(issueNumber: string): Promise<string> {\n return join(await getProjectRoot(), 'issues', issueNumber);\n}\n\n/**\n * Get the current git branch name.\n * Returns an empty string if in detached HEAD state.\n */\nexport async function getCurrentBranch(): Promise<string> {\n const result = await run('git', ['branch', '--show-current']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Failed to detect git branch. Ensure git is installed and you are inside a repository.',\n );\n }\n\n return result.stdout.trim();\n}\n\nexport interface CommitInfo {\n hash: string;\n subject: string;\n}\n\n/**\n * Get the base branch of the repository: the remote HEAD (origin/HEAD) when\n * available, otherwise the first of main/master that exists locally.\n * Never throws; defaults to 'main'.\n */\nexport async function getBaseBranch(): Promise<string> {\n const remoteHead = await run('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);\n if (remoteHead.exitCode === 0) {\n const name = remoteHead.stdout.trim().replace(/^origin\\//, '');\n if (name) return name;\n }\n\n for (const candidate of ['main', 'master']) {\n const check = await run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${candidate}`]);\n if (check.exitCode === 0) return candidate;\n }\n\n return 'main';\n}\n\n/**\n * Get the URL of the `origin` remote.\n * Never throws: a missing remote, a non-zero exit code, an empty stdout or a\n * git binary that cannot even be spawned all resolve to `null` — the same\n * discipline as {@link getBaseBranch}, so callers can treat \"no remote\" as an\n * ordinary state instead of an error path.\n *\n * `cwd` selects which repository is queried; omitting it falls back to\n * `process.cwd()`, which is almost never what a caller resolving a specific\n * `projectRoot` wants — pass it explicitly whenever one is available.\n */\nexport async function getRemoteUrl(cwd?: string): Promise<string | null> {\n let result: ExecResult;\n try {\n result = await run('git', ['remote', 'get-url', 'origin'], { cwd });\n } catch {\n return null;\n }\n\n if (result.exitCode !== 0) return null;\n\n const url = result.stdout.trim();\n return url === '' ? null : url;\n}\n\n/**\n * Reduce a git remote URL to a canonical `host/path` identity, so that every\n * way of addressing the same repository collapses to the same string:\n *\n * ```\n * https://github.com/org/repo.git -> github.com/org/repo\n * git@github.com:org/repo.git -> github.com/org/repo\n * ssh://git@github.com:22/org/repo.git -> github.com/org/repo\n * https://user:token@github.com/org/repo -> github.com/org/repo\n * https://github.com/Org/Repo/ -> github.com/org/repo\n * ```\n *\n * Pure function: it never shells out, so it is testable without a git\n * repository. Returns `null` when no host and path can be extracted.\n *\n * Known limitation: both host and path are lowercased. Hostnames are\n * case-insensitive by definition, but repository paths are case-sensitive on\n * some self-hosted servers — so `host/org/Repo` and `host/org/repo` normalize\n * to the same identity even if that server considers them distinct. This is a\n * deliberate trade: matching the same repository across clones (where users\n * routinely type the wrong case) matters more than telling apart two repos\n * whose paths differ only by case.\n */\nexport function normalizeRemoteUrl(url: string | null | undefined): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (trimmed === '') return null;\n\n let authority: string;\n let path: string;\n\n const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.exec(trimmed);\n if (scheme) {\n const rest = trimmed.slice(scheme[0].length);\n const slash = rest.indexOf('/');\n if (slash === -1) return null;\n authority = rest.slice(0, slash);\n path = rest.slice(slash + 1);\n } else {\n // scp-like syntax: [user@]host:path — everything after the first colon is\n // the path, which is how git itself reads it (no port is possible here).\n const colon = trimmed.indexOf(':');\n if (colon === -1) return null;\n authority = trimmed.slice(0, colon);\n path = trimmed.slice(colon + 1);\n }\n\n // Drop embedded credentials (user, user:token) and the ssh user.\n const at = authority.lastIndexOf('@');\n if (at !== -1) authority = authority.slice(at + 1);\n\n const host = authority.replace(/:\\d+$/, '').toLowerCase();\n if (host === '') return null;\n\n const normalizedPath = path\n .replace(/[?#].*$/, '')\n .replace(/\\/+/g, '/')\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\.git$/i, '')\n .replace(/\\/+$/, '')\n .toLowerCase();\n if (normalizedPath === '') return null;\n\n return `${host}/${normalizedPath}`;\n}\n\n/**\n * List commits on HEAD that are not on the given base branch (base..HEAD),\n * most recent first. Never throws; returns [] when the range cannot be\n * resolved (e.g. unknown base branch or shallow clone).\n */\nexport async function getCommitsSince(base: string): Promise<CommitInfo[]> {\n const result = await run('git', ['log', '--pretty=format:%h%x09%s', `${base}..HEAD`]);\n if (result.exitCode !== 0) return [];\n\n return result.stdout\n .split('\\n')\n .filter((line) => line.trim() !== '')\n .map((line) => {\n const tab = line.indexOf('\\t');\n return tab === -1\n ? { hash: line.trim(), subject: '' }\n : { hash: line.slice(0, tab), subject: line.slice(tab + 1) };\n });\n}\n","import { type Options as ExecaOptions, execa } from 'execa';\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Execute a command with arguments and capture its output.\n * Uses execa which calls execFile internally (no shell injection risk).\n * Does not throw on non-zero exit codes.\n */\nexport async function run(\n command: string,\n args: string[] = [],\n options?: ExecaOptions,\n): Promise<ExecResult> {\n const result = await execa(command, args, {\n reject: false,\n ...options,\n });\n\n return {\n stdout: result.stdout?.toString() ?? '',\n stderr: result.stderr?.toString() ?? '',\n exitCode: result.exitCode ?? 1,\n };\n}\n","import { z } from 'zod';\nimport { webConfigSchema } from '../schemas.js';\n\n/**\n * Zod schemas for the files written under the global storage tree\n * (`~/.issue-flow`): the per-project `metadata.json` and the global\n * `config.json`.\n *\n * They live here rather than in `src/schemas.ts` so that file stays focused on\n * the pipeline domain (task plans, Issue metadata, session snapshots) and the\n * storage layer keeps owning its own formats.\n */\n\n/** Version stamped on files written by this release of the storage layer. */\nexport const STORAGE_SCHEMA_VERSION = 1;\n\n/**\n * `~/.issue-flow/projects/<project-id>/metadata.json`.\n *\n * Deliberately **not** `.strict()`: a newer release may add fields (dashboard\n * history, counters) and an older one must still be able to read the file\n * instead of rejecting it. Unknown keys are dropped on parse, never fatal.\n *\n * `root` is the last known local checkout — informative only. Identity lives in\n * `projectId` (see `getProjectId()` in `paths.ts`), so moving the folder of a\n * project that has a remote updates `root` without changing the id.\n */\nexport const projectMetadataSchema = z.object({\n schemaVersion: z.number().int().positive(),\n projectId: z.string().min(1),\n root: z.string().min(1),\n /** Normalized remote (`host/org/repo`), or null when the project has none. */\n remoteUrl: z.string().nullable(),\n createdAt: z.string().min(1),\n updatedAt: z.string().min(1),\n /** Null until the project is used by a pipeline run. */\n lastAttemptAt: z.string().nullable(),\n});\n\n/**\n * Global counterpart of the `web` key of `.issue-flow.json`, restricted to the\n * settings that make sense machine-wide: `enabled` and `includeLogs` stay a\n * per-project decision.\n *\n * The validation of each field is reused from `webConfigSchema` through\n * `.unwrap()`, which strips the `.default()` and keeps the constraints. Picking\n * and calling `.partial()` would not be enough: in zod 4 an optional field\n * still materializes its default when the key is absent\n * (`z.object({ port: z.number().default(3737) }).partial().parse({})` yields\n * `{ port: 3737 }`), and a materialized default in this layer would override\n * the project layer during the merge.\n */\nconst webShape = webConfigSchema.shape;\nexport const globalWebConfigSchema = z\n .object({\n port: webShape.port.unwrap(),\n host: webShape.host.unwrap(),\n refreshSeconds: webShape.refreshSeconds.unwrap(),\n logLimit: webShape.logLimit.unwrap(),\n })\n .partial();\n\n/** Global retry preferences, mirroring `DEFAULTS` in `config.ts`. */\nexport const globalRetryConfigSchema = z\n .object({\n retryLimit: z.number().int().nonnegative(),\n retryForever: z.boolean(),\n backoffBaseSeconds: z.number().nonnegative(),\n backoffMaxSeconds: z.number().nonnegative(),\n })\n .partial();\n\n/** Global commit preferences. Consumed by later issues, not by this release. */\nexport const globalCommitConfigSchema = z\n .object({\n signoff: z.boolean(),\n conventional: z.boolean(),\n })\n .partial();\n\n/**\n * `~/.issue-flow/config.json`.\n *\n * **Every key is optional and no key carries a `.default()`.** This is a design\n * constraint, not an omission: the global file is an intermediate precedence\n * layer (CLI > env > `.issue-flow.json` > `config.json` > defaults), so a\n * default materialized here would be indistinguishable from a value the user\n * actually wrote and would silently override the project layer during the\n * merge. Same reason `readWebConfigFile()` parses with\n * `webConfigSchema.partial()`. Defaults belong to the final layer only.\n */\nexport const globalConfigSchema = z\n .object({\n schemaVersion: z.number().int().positive(),\n /** Override for the directory holding `projects/`. */\n storageDir: z.string().min(1),\n web: globalWebConfigSchema,\n retry: globalRetryConfigSchema,\n commit: globalCommitConfigSchema,\n })\n .partial();\n\nexport type ProjectMetadata = z.infer<typeof projectMetadataSchema>;\nexport type GlobalWebConfig = z.infer<typeof globalWebConfigSchema>;\nexport type GlobalRetryConfig = z.infer<typeof globalRetryConfigSchema>;\nexport type GlobalCommitConfig = z.infer<typeof globalCommitConfigSchema>;\nexport type GlobalConfig = z.infer<typeof globalConfigSchema>;\n"],"mappings":"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","names":["join","join","join","join"]}
@@ -2,24 +2,24 @@
2
2
  import {
3
3
  readFileWithGrace,
4
4
  runPhaseWithRetry
5
- } from "./chunk-JV7P6UGG.js";
5
+ } from "./chunk-EHVKXFFN.js";
6
6
  import {
7
7
  issuePlaceholders,
8
8
  resolveCommandIssue
9
- } from "./chunk-HS4QWZAO.js";
9
+ } from "./chunk-RN2FAMUU.js";
10
10
  import {
11
11
  isTransientFailure
12
12
  } from "./chunk-HDV3YDTG.js";
13
13
  import {
14
14
  runHeadless
15
- } from "./chunk-KUDAY57D.js";
15
+ } from "./chunk-TCOS2KFB.js";
16
16
  import {
17
17
  applyPlaceholders,
18
18
  loadPrompt
19
19
  } from "./chunk-SLHZPB3C.js";
20
20
  import {
21
21
  getIssueDir
22
- } from "./chunk-OVFYJLOH.js";
22
+ } from "./chunk-2AVZZAC6.js";
23
23
  import {
24
24
  getGlobalTimeout,
25
25
  loadTaskPlan,
@@ -27,7 +27,7 @@ import {
27
27
  printSuccess,
28
28
  saveTaskPlan,
29
29
  taskPlanSchema
30
- } from "./chunk-2Y3UO7H2.js";
30
+ } from "./chunk-JNEHVQH5.js";
31
31
 
32
32
  // src/commands/plan.ts
33
33
  import { mkdir, readFile } from "fs/promises";
@@ -113,4 +113,4 @@ ${issues}`
113
113
  export {
114
114
  runPlan
115
115
  };
116
- //# sourceMappingURL=chunk-75K5IKAC.js.map
116
+ //# sourceMappingURL=chunk-ASMCLXQ2.js.map
@@ -7,7 +7,7 @@ import {
7
7
  getSessionPublisher,
8
8
  isoNow,
9
9
  printRetry
10
- } from "./chunk-2Y3UO7H2.js";
10
+ } from "./chunk-JNEHVQH5.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-JV7P6UGG.js.map
58
+ //# sourceMappingURL=chunk-EHVKXFFN.js.map
@@ -2,24 +2,24 @@
2
2
  import {
3
3
  issuePlaceholders,
4
4
  resolveCommandIssue
5
- } from "./chunk-HS4QWZAO.js";
5
+ } from "./chunk-RN2FAMUU.js";
6
6
  import {
7
7
  runHeadless
8
- } from "./chunk-KUDAY57D.js";
8
+ } from "./chunk-TCOS2KFB.js";
9
9
  import {
10
10
  applyPlaceholders,
11
11
  loadPrompt
12
12
  } from "./chunk-SLHZPB3C.js";
13
13
  import {
14
14
  getIssueDir
15
- } from "./chunk-OVFYJLOH.js";
15
+ } from "./chunk-2AVZZAC6.js";
16
16
  import {
17
17
  getGlobalTimeout,
18
18
  loadTaskPlan,
19
19
  printError,
20
20
  printSuccess,
21
21
  saveTaskPlan
22
- } from "./chunk-2Y3UO7H2.js";
22
+ } from "./chunk-JNEHVQH5.js";
23
23
 
24
24
  // src/commands/review.ts
25
25
  import { join } from "path";
@@ -76,6 +76,7 @@ async function runReview(issue, resolvedIssue) {
76
76
  try {
77
77
  const plan = await loadTaskPlan(tasksPath);
78
78
  plan.pipeline.reviewCompleted = true;
79
+ plan.lastReviewFindings = null;
79
80
  await saveTaskPlan(tasksPath, plan);
80
81
  } catch {
81
82
  }
@@ -86,10 +87,17 @@ async function runReview(issue, resolvedIssue) {
86
87
  if (review.findings) {
87
88
  console.log(review.findings);
88
89
  }
90
+ try {
91
+ const plan = await loadTaskPlan(tasksPath);
92
+ plan.pipeline.reviewCompleted = false;
93
+ plan.lastReviewFindings = review.findings ?? "Unknown findings";
94
+ await saveTaskPlan(tasksPath, plan);
95
+ } catch {
96
+ }
89
97
  return 1;
90
98
  }
91
99
 
92
100
  export {
93
101
  runReview
94
102
  };
95
- //# sourceMappingURL=chunk-Z3UIGLZG.js.map
103
+ //# sourceMappingURL=chunk-EM43T462.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.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 { printError, printSuccess } from '../ui/logger.js';\nimport { getIssueDir } from '../utils/git.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 issueDir = await getIssueDir(issueNumber);\n const tasksPath = join(issueDir, 'tasks.json');\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 result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n outputFormat: 'text',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\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 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 return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,YAAY;AAkBrB,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,WAAW,MAAM,YAAY,WAAW;AAC9C,QAAM,YAAY,KAAK,UAAU,YAAY;AAE7C,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,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA,IAC/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,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,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;AACA,SAAO;AACT;","names":["findingsMatch"]}
1
+ {"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.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 { printError, printSuccess } from '../ui/logger.js';\nimport { getIssueDir } from '../utils/git.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 issueDir = await getIssueDir(issueNumber);\n const tasksPath = join(issueDir, 'tasks.json');\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 result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n outputFormat: 'text',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\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":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,YAAY;AAkBrB,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,WAAW,MAAM,YAAY,WAAW;AAC9C,QAAM,YAAY,KAAK,UAAU,YAAY;AAE7C,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,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA,IAC/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,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"]}
@@ -1,24 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runPhaseWithRetry
4
- } from "./chunk-JV7P6UGG.js";
4
+ } from "./chunk-EHVKXFFN.js";
5
5
  import {
6
6
  issuePlaceholders,
7
7
  resolveCommandIssue
8
- } from "./chunk-HS4QWZAO.js";
8
+ } from "./chunk-RN2FAMUU.js";
9
9
  import {
10
10
  isTransientFailure
11
11
  } from "./chunk-HDV3YDTG.js";
12
12
  import {
13
13
  runHeadless
14
- } from "./chunk-KUDAY57D.js";
14
+ } from "./chunk-TCOS2KFB.js";
15
15
  import {
16
16
  applyPlaceholders,
17
17
  loadPrompt
18
18
  } from "./chunk-SLHZPB3C.js";
19
19
  import {
20
20
  getIssueDir
21
- } from "./chunk-OVFYJLOH.js";
21
+ } from "./chunk-2AVZZAC6.js";
22
22
  import {
23
23
  getGlobalTimeout,
24
24
  isoNow,
@@ -26,7 +26,7 @@ import {
26
26
  printError,
27
27
  printSuccess,
28
28
  saveTaskPlan
29
- } from "./chunk-2Y3UO7H2.js";
29
+ } from "./chunk-JNEHVQH5.js";
30
30
 
31
31
  // src/commands/pr.ts
32
32
  import { join } from "path";
@@ -127,4 +127,4 @@ async function runPr(issue, resolvedIssue) {
127
127
  export {
128
128
  runPr
129
129
  };
130
- //# sourceMappingURL=chunk-UX6YAVLT.js.map
130
+ //# sourceMappingURL=chunk-HJ7JT777.js.map
@@ -74,6 +74,13 @@ var taskPlanSchema = z.object({
74
74
  lastError: lastErrorSchema.nullable(),
75
75
  correctionCycle: z.number().int().min(0),
76
76
  maxCorrectionCycles: z.number().int().min(0),
77
+ /**
78
+ * Findings from the most recent failed review, verbatim. Non-null means the
79
+ * issue has a pending correction even if every userStories[].passes is
80
+ * already true — the execute phase must address these before the field is
81
+ * cleared back to null. See core/engine.ts's early-return guards.
82
+ */
83
+ lastReviewFindings: z.string().nullable().optional().default(null),
77
84
  pipeline: pipelineStateSchema,
78
85
  pullRequest: pullRequestRefSchema.optional(),
79
86
  prReview: prReviewStateSchema.optional(),
@@ -630,9 +637,13 @@ function initializeState(plan) {
630
637
  lastError: plan.lastError ?? null,
631
638
  correctionCycle: plan.correctionCycle ?? 0,
632
639
  maxCorrectionCycles: plan.maxCorrectionCycles ?? 3,
640
+ lastReviewFindings: plan.lastReviewFindings ?? null,
633
641
  pipeline: plan.pipeline ? { ...defaultPipeline, ...plan.pipeline } : defaultPipeline
634
642
  };
635
643
  }
644
+ function hasPendingCorrection(plan) {
645
+ return Boolean(plan.lastReviewFindings);
646
+ }
636
647
  function allStoriesPass(plan) {
637
648
  if (plan.userStories.length === 0) {
638
649
  return false;
@@ -880,6 +891,7 @@ export {
880
891
  loadTaskPlan,
881
892
  saveTaskPlan,
882
893
  initializeState,
894
+ hasPendingCorrection,
883
895
  allStoriesPass,
884
896
  markIssueInProgress,
885
897
  markIssueCompleted,
@@ -907,4 +919,4 @@ export {
907
919
  formatDuration,
908
920
  ElapsedTimer
909
921
  };
910
- //# sourceMappingURL=chunk-2Y3UO7H2.js.map
922
+ //# sourceMappingURL=chunk-JNEHVQH5.js.map