issue-flow 0.7.1 → 0.7.2
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.
- package/README.md +11 -7
- package/dist/{analyze-R25HDGBJ.js → analyze-XMEULTT2.js} +10 -10
- package/dist/analyze-XMEULTT2.js.map +1 -0
- package/dist/{chunk-23WOEBGN.js → chunk-6UBA7CSJ.js} +21 -18
- package/dist/chunk-6UBA7CSJ.js.map +1 -0
- package/dist/{chunk-ZAEBDO3Q.js → chunk-CYU6RSDJ.js} +13 -10
- package/dist/chunk-CYU6RSDJ.js.map +1 -0
- package/dist/{chunk-RN2FAMUU.js → chunk-DKMBLNGO.js} +70 -21
- package/dist/chunk-DKMBLNGO.js.map +1 -0
- package/dist/{chunk-2AVZZAC6.js → chunk-G3TJHMAS.js} +331 -26
- package/dist/chunk-G3TJHMAS.js.map +1 -0
- package/dist/{chunk-TCOS2KFB.js → chunk-GOZT2YP5.js} +18 -15
- package/dist/chunk-GOZT2YP5.js.map +1 -0
- package/dist/{chunk-EM43T462.js → chunk-MWUF4LMF.js} +8 -8
- package/dist/chunk-MWUF4LMF.js.map +1 -0
- package/dist/{chunk-WHIBTNYI.js → chunk-PRVWJZV4.js} +2 -2
- package/dist/{chunk-HJ7JT777.js → chunk-RGCU2KE6.js} +8 -8
- package/dist/chunk-RGCU2KE6.js.map +1 -0
- package/dist/{chunk-YGEWIEAI.js → chunk-WQ4SUZ4P.js} +3 -3
- package/dist/{chunk-ASMCLXQ2.js → chunk-WUITOU5B.js} +10 -10
- package/dist/chunk-WUITOU5B.js.map +1 -0
- package/dist/cli.js +11 -11
- package/dist/{config-SJAQMFWB.js → config-2NDV7Y72.js} +2 -2
- package/dist/{execute-VZ7SHLP4.js → execute-BOQPTRH4.js} +4 -4
- package/dist/{generate-LUK3XTIP.js → generate-YEQRH36H.js} +10 -6
- package/dist/generate-YEQRH36H.js.map +1 -0
- package/dist/{plan-R34SAWRK.js → plan-4I47CQM5.js} +5 -5
- package/dist/{pr-P4BDQXUQ.js → pr-F672VPMM.js} +5 -5
- package/dist/pr-review-GKP4TJRJ.js +15 -0
- package/dist/{prd-RYLULM75.js → prd-3QME4DYX.js} +5 -5
- package/dist/review-RG2UIPOB.js +13 -0
- package/dist/{run-SDRQDMSZ.js → run-EFXDXSB4.js} +26 -21
- package/dist/run-EFXDXSB4.js.map +1 -0
- package/package.json +1 -1
- package/prompts/generate.md +1 -1
- package/prompts/prd.md +1 -1
- package/dist/analyze-R25HDGBJ.js.map +0 -1
- package/dist/chunk-23WOEBGN.js.map +0 -1
- package/dist/chunk-2AVZZAC6.js.map +0 -1
- package/dist/chunk-ASMCLXQ2.js.map +0 -1
- package/dist/chunk-EM43T462.js.map +0 -1
- package/dist/chunk-HJ7JT777.js.map +0 -1
- package/dist/chunk-RN2FAMUU.js.map +0 -1
- package/dist/chunk-TCOS2KFB.js.map +0 -1
- package/dist/chunk-ZAEBDO3Q.js.map +0 -1
- package/dist/generate-LUK3XTIP.js.map +0 -1
- package/dist/pr-review-TOSBE6ZS.js +0 -15
- package/dist/review-ZSC5ELQA.js +0 -13
- package/dist/run-SDRQDMSZ.js.map +0 -1
- /package/dist/{chunk-WHIBTNYI.js.map → chunk-PRVWJZV4.js.map} +0 -0
- /package/dist/{chunk-YGEWIEAI.js.map → chunk-WQ4SUZ4P.js.map} +0 -0
- /package/dist/{config-SJAQMFWB.js.map → config-2NDV7Y72.js.map} +0 -0
- /package/dist/{execute-VZ7SHLP4.js.map → execute-BOQPTRH4.js.map} +0 -0
- /package/dist/{plan-R34SAWRK.js.map → plan-4I47CQM5.js.map} +0 -0
- /package/dist/{pr-P4BDQXUQ.js.map → pr-F672VPMM.js.map} +0 -0
- /package/dist/{pr-review-TOSBE6ZS.js.map → pr-review-GKP4TJRJ.js.map} +0 -0
- /package/dist/{prd-RYLULM75.js.map → prd-3QME4DYX.js.map} +0 -0
- /package/dist/{review-ZSC5ELQA.js.map → review-RG2UIPOB.js.map} +0 -0
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ Verifies that `claude`, `gh` (authenticated), and `git` (inside a repo) are avai
|
|
|
103
103
|
npx issue-flow analyze 42
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce a structured analysis saved to
|
|
106
|
+
Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce a structured analysis saved to `~/.issue-flow/…/issues/42/analysis.md` (see [Pipeline State](#pipeline-state)).
|
|
107
107
|
|
|
108
108
|
### `prd` -- Generate a PRD
|
|
109
109
|
|
|
@@ -111,7 +111,7 @@ Invokes Claude headlessly to fetch issue data, analyze the codebase, and produce
|
|
|
111
111
|
npx issue-flow prd 42
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Generates a Product Requirements Document from the issue analysis. Reads `
|
|
114
|
+
Generates a Product Requirements Document from the issue analysis. Reads `analysis.md` from the same directory as context if available. Saves to `~/.issue-flow/…/issues/42/prd.md`.
|
|
115
115
|
|
|
116
116
|
### `plan` -- Convert PRD to task plan
|
|
117
117
|
|
|
@@ -119,7 +119,7 @@ Generates a Product Requirements Document from the issue analysis. Reads `issues
|
|
|
119
119
|
npx issue-flow plan 42
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
Converts the PRD into a structured
|
|
122
|
+
Converts the PRD into a structured `~/.issue-flow/…/issues/42/tasks.json` with ordered user stories, acceptance criteria, and pipeline state. Validates the output with zod schemas.
|
|
123
123
|
|
|
124
124
|
### `execute` -- Run the story execution loop
|
|
125
125
|
|
|
@@ -133,7 +133,7 @@ Runs the iterative agent loop. Each iteration is a fresh Claude instance that pi
|
|
|
133
133
|
|
|
134
134
|
| Flag | Description |
|
|
135
135
|
|------|-------------|
|
|
136
|
-
| `--issue N` | Issue number -- reads artifacts from
|
|
136
|
+
| `--issue N` | Issue number -- reads artifacts from `~/.issue-flow/…/issues/N/` |
|
|
137
137
|
| `--max-iterations N` | Stop after N iterations (default: unlimited) |
|
|
138
138
|
| `--retry-limit N` | Retry transient Claude failures up to N consecutive times (default: 10) |
|
|
139
139
|
| `--retry-forever` | Retry transient Claude failures indefinitely |
|
|
@@ -185,22 +185,26 @@ Each setting resolves with the precedence **CLI flag > environment variable > `.
|
|
|
185
185
|
| `--web-log-limit <n>` | `ISSUE_FLOW_WEB_LOG_LIMIT` | `web.logLimit` | `200` |
|
|
186
186
|
| `--web-no-logs` | -- | `web.includeLogs` | logs included |
|
|
187
187
|
|
|
188
|
-
Monitoring never affects the pipeline: with `--web` off the behavior is byte-for-byte identical, a busy port just skips the server with a warning, and killing the server mid-run has no effect on the execution. While enabled, the snapshot served at `/api/status` is also persisted to
|
|
188
|
+
Monitoring never affects the pipeline: with `--web` off the behavior is byte-for-byte identical, a busy port just skips the server with a warning, and killing the server mid-run has no effect on the execution. While enabled, the snapshot served at `/api/status` is also persisted to `~/.issue-flow/…/issues/N/session.json` -- outside your working tree, so there is nothing to add to `.gitignore`.
|
|
189
189
|
|
|
190
190
|
For the full documentation (endpoints, `session.json` format, Tailscale setup), see the [root README](../../README.md#web-monitoring).
|
|
191
191
|
|
|
192
192
|
## Pipeline State
|
|
193
193
|
|
|
194
|
-
Each issue's state is tracked in
|
|
194
|
+
Each issue's state is tracked in a directory of its own under `~/.issue-flow`, keyed by a deterministic project id and the issue identifier:
|
|
195
195
|
|
|
196
196
|
```
|
|
197
|
-
issues/42/
|
|
197
|
+
~/.issue-flow/projects/<project-id>/issues/42/
|
|
198
198
|
analysis.md # Issue analysis
|
|
199
199
|
prd.md # Product requirements
|
|
200
200
|
tasks.json # Task plan with pipeline state and user stories
|
|
201
201
|
progress.txt # Execution log
|
|
202
|
+
session.json # Live session snapshot (web monitoring only)
|
|
203
|
+
pr-review/ # PR review reports and index
|
|
202
204
|
```
|
|
203
205
|
|
|
206
|
+
Nothing is written to your repository. A `<projectRoot>/issues/` tree left by an earlier release is copied here automatically on first use and then treated as read-only -- see the [root README](../../README.md#global-storage) for the full layout, the project id derivation and the migration.
|
|
207
|
+
|
|
204
208
|
The `pipeline` field tracks which phases have completed, enabling resume from any point:
|
|
205
209
|
|
|
206
210
|
```json
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
import {
|
|
3
3
|
issuePlaceholders,
|
|
4
4
|
resolveCommandIssue
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DKMBLNGO.js";
|
|
6
6
|
import {
|
|
7
7
|
runHeadless
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GOZT2YP5.js";
|
|
9
9
|
import {
|
|
10
10
|
applyPlaceholders,
|
|
11
11
|
loadPrompt
|
|
12
12
|
} from "./chunk-SLHZPB3C.js";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
resolveIssuePaths
|
|
15
|
+
} from "./chunk-G3TJHMAS.js";
|
|
16
16
|
import {
|
|
17
17
|
getGlobalTimeout,
|
|
18
18
|
loadTaskPlan,
|
|
@@ -24,16 +24,15 @@ import {
|
|
|
24
24
|
|
|
25
25
|
// src/commands/analyze.ts
|
|
26
26
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
27
|
-
import { join } from "path";
|
|
28
27
|
async function runAnalyze(issue, resolvedIssue) {
|
|
29
28
|
const issueNumber = issue.replace(/^#/, "");
|
|
30
|
-
const
|
|
31
|
-
const analysisPath =
|
|
29
|
+
const paths = await resolveIssuePaths(issueNumber);
|
|
30
|
+
const analysisPath = paths.analysisFile;
|
|
32
31
|
const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);
|
|
33
32
|
if (!resolution.ok) {
|
|
34
33
|
return resolution.code;
|
|
35
34
|
}
|
|
36
|
-
await mkdir(issueDir, { recursive: true });
|
|
35
|
+
await mkdir(paths.issueDir, { recursive: true });
|
|
37
36
|
const template = await loadPrompt("analyze");
|
|
38
37
|
const prompt = applyPlaceholders(template, {
|
|
39
38
|
__ISSUE_NUMBER__: issueNumber,
|
|
@@ -48,6 +47,7 @@ async function runAnalyze(issue, resolvedIssue) {
|
|
|
48
47
|
timeout: getGlobalTimeout() ?? 3e5,
|
|
49
48
|
outputFormat: "text",
|
|
50
49
|
allowedTools: ["Bash", "Read", "Glob", "Grep", "Write"],
|
|
50
|
+
addDirs: [paths.issueDir],
|
|
51
51
|
statusMessage: `Analyzing issue #${issueNumber}...`
|
|
52
52
|
});
|
|
53
53
|
if (!result.success) {
|
|
@@ -64,7 +64,7 @@ async function runAnalyze(issue, resolvedIssue) {
|
|
|
64
64
|
printInfo("Headless did not create analysis file; saving output directly");
|
|
65
65
|
await writeFile(analysisPath, result.result, "utf-8");
|
|
66
66
|
}
|
|
67
|
-
const tasksPath =
|
|
67
|
+
const tasksPath = paths.tasksFile;
|
|
68
68
|
try {
|
|
69
69
|
const plan = await loadTaskPlan(tasksPath);
|
|
70
70
|
plan.pipeline.analyzeCompleted = true;
|
|
@@ -77,4 +77,4 @@ async function runAnalyze(issue, resolvedIssue) {
|
|
|
77
77
|
export {
|
|
78
78
|
runAnalyze
|
|
79
79
|
};
|
|
80
|
-
//# sourceMappingURL=analyze-
|
|
80
|
+
//# sourceMappingURL=analyze-XMEULTT2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/analyze.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printInfo, printSuccess } from '../ui/logger.js';\n\nexport async function runAnalyze(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const analysisPath = paths.analysisFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const template = await loadPrompt('analyze');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __ANALYSIS_PATH__: analysisPath,\n // Last: the Issue content is substituted in but never scanned again, so a\n // body that happens to contain a placeholder is left untouched.\n ...issuePlaceholders(resolution.resolved),\n });\n\n const result = await runHeadless({\n prompt,\n maxTurns: 30,\n timeout: getGlobalTimeout() ?? 300_000,\n outputFormat: 'text',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Analyzing issue #${issueNumber}...`,\n });\n\n if (!result.success) {\n printError(`Analysis failed: ${result.error}`);\n return 1;\n }\n\n // Verify the file was created\n try {\n const content = await readFile(analysisPath, 'utf-8');\n if (content.length < 10) {\n printError('Analysis file was created but appears empty');\n return 1;\n }\n } catch {\n // File wasn't created by headless — save the result as analysis\n printInfo('Headless did not create analysis file; saving output directly');\n await writeFile(analysisPath, result.result, 'utf-8');\n }\n\n // Update pipeline state\n const tasksPath = paths.tasksFile;\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.analyzeCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist yet — that's OK for standalone analyze\n }\n\n printSuccess(`Analysis saved to ${analysisPath}`);\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,iBAAiB;AAU3C,eAAsB,WAAW,OAAe,eAAgD;AAC9F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,eAAe,MAAM;AAE3B,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,MAAM,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE/C,QAAM,WAAW,MAAM,WAAW,SAAS;AAC3C,QAAM,SAAS,kBAAkB,UAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,mBAAmB;AAAA;AAAA;AAAA,IAGnB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,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,QAAQ,OAAO;AAAA,IACtD,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,oBAAoB,OAAO,KAAK,EAAE;AAC7C,WAAO;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,cAAc,OAAO;AACpD,QAAI,QAAQ,SAAS,IAAI;AACvB,iBAAW,6CAA6C;AACxD,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAEN,cAAU,+DAA+D;AACzE,UAAM,UAAU,cAAc,OAAO,QAAQ,OAAO;AAAA,EACtD;AAGA,QAAM,YAAY,MAAM;AACxB,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,mBAAmB;AACjC,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,eAAa,qBAAqB,YAAY,EAAE;AAChD,SAAO;AACT;","names":[]}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
listPullRequests
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PRVWJZV4.js";
|
|
5
5
|
import {
|
|
6
6
|
runHeadless
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GOZT2YP5.js";
|
|
8
8
|
import {
|
|
9
9
|
applyPlaceholders,
|
|
10
10
|
loadPrompt
|
|
11
11
|
} from "./chunk-SLHZPB3C.js";
|
|
12
12
|
import {
|
|
13
13
|
getCurrentBranch,
|
|
14
|
-
getIssueDir,
|
|
15
14
|
loadPrReviewConfig,
|
|
15
|
+
resolveIssuePaths,
|
|
16
16
|
run
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-G3TJHMAS.js";
|
|
18
18
|
import {
|
|
19
19
|
getGlobalTimeout,
|
|
20
20
|
getSessionPublisher,
|
|
@@ -29,10 +29,9 @@ import {
|
|
|
29
29
|
} from "./chunk-JNEHVQH5.js";
|
|
30
30
|
|
|
31
31
|
// src/commands/pr-review.ts
|
|
32
|
-
import { join as
|
|
32
|
+
import { join as join3 } from "path";
|
|
33
33
|
|
|
34
34
|
// src/core/pr-review/discovery.ts
|
|
35
|
-
import { join } from "path";
|
|
36
35
|
import { createInterface } from "readline";
|
|
37
36
|
var PrDiscoveryError = class extends Error {
|
|
38
37
|
exitCode;
|
|
@@ -51,7 +50,7 @@ var SOURCE_LABELS = {
|
|
|
51
50
|
};
|
|
52
51
|
async function loadPlanPullRequest(issue) {
|
|
53
52
|
try {
|
|
54
|
-
const plan = await loadTaskPlan(
|
|
53
|
+
const plan = await loadTaskPlan((await resolveIssuePaths(issue)).tasksFile);
|
|
55
54
|
return plan.pullRequest ?? null;
|
|
56
55
|
} catch {
|
|
57
56
|
return null;
|
|
@@ -257,11 +256,11 @@ async function discover(sources, issue, warn) {
|
|
|
257
256
|
|
|
258
257
|
// src/core/pr-review/publisher.ts
|
|
259
258
|
import { mkdir } from "fs/promises";
|
|
260
|
-
import { join as
|
|
259
|
+
import { join as join2 } from "path";
|
|
261
260
|
|
|
262
261
|
// src/core/pr-review/report.ts
|
|
263
262
|
import { readdir, readFile } from "fs/promises";
|
|
264
|
-
import { join
|
|
263
|
+
import { join } from "path";
|
|
265
264
|
var RECOMMENDATIONS = {
|
|
266
265
|
APPROVE: "APPROVE",
|
|
267
266
|
APPROVE_WITH_SUGGESTIONS: "APPROVE_WITH_SUGGESTIONS",
|
|
@@ -458,7 +457,7 @@ function buildReportMarkdown(input) {
|
|
|
458
457
|
}
|
|
459
458
|
async function prReviewDir(opts) {
|
|
460
459
|
const slug = opts.issue === void 0 || opts.issue === "" ? `pr-${opts.pullRequest}` : opts.issue.replace(/^#/, "");
|
|
461
|
-
return
|
|
460
|
+
return (await resolveIssuePaths(slug)).prReviewDir;
|
|
462
461
|
}
|
|
463
462
|
function reportFileName(pullRequest, round) {
|
|
464
463
|
return `pr-${pullRequest}-round-${round}.md`;
|
|
@@ -466,7 +465,7 @@ function reportFileName(pullRequest, round) {
|
|
|
466
465
|
var REPORT_FILE = /^pr-(\d+)-round-(\d+)\.md$/;
|
|
467
466
|
async function readPrReviewIndex(dir) {
|
|
468
467
|
try {
|
|
469
|
-
const parsed = JSON.parse(await readFile(
|
|
468
|
+
const parsed = JSON.parse(await readFile(join(dir, "index.json"), "utf-8"));
|
|
470
469
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
471
470
|
const index = parsed;
|
|
472
471
|
if (!Array.isArray(index.rounds) || typeof index.pullRequest !== "object") return null;
|
|
@@ -510,7 +509,7 @@ var LocalReportPublisher = class {
|
|
|
510
509
|
async publish(report) {
|
|
511
510
|
await mkdir(this.dir, { recursive: true });
|
|
512
511
|
const fileName = reportFileName(report.pullRequest.number, report.round);
|
|
513
|
-
await writeFileAtomic(
|
|
512
|
+
await writeFileAtomic(join2(this.dir, fileName), report.markdown);
|
|
514
513
|
const existing = await readPrReviewIndex(this.dir);
|
|
515
514
|
const index = {
|
|
516
515
|
schemaVersion: 1,
|
|
@@ -525,7 +524,7 @@ var LocalReportPublisher = class {
|
|
|
525
524
|
reportPath: fileName,
|
|
526
525
|
findings: report.findings
|
|
527
526
|
});
|
|
528
|
-
await writeFileAtomic(
|
|
527
|
+
await writeFileAtomic(join2(this.dir, "index.json"), `${JSON.stringify(updated, null, 2)}
|
|
529
528
|
`);
|
|
530
529
|
}
|
|
531
530
|
};
|
|
@@ -627,12 +626,13 @@ async function runPrReview(prArg, opts = {}) {
|
|
|
627
626
|
);
|
|
628
627
|
return 1;
|
|
629
628
|
}
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
const
|
|
629
|
+
const issuePaths = issue === void 0 ? null : await resolveIssuePaths(issue);
|
|
630
|
+
const issueDir = issuePaths?.issueDir ?? null;
|
|
631
|
+
const tasksPath = issuePaths?.tasksFile ?? null;
|
|
632
|
+
const prdPath = issuePaths?.prdFile ?? null;
|
|
633
633
|
const dir = await prReviewDir({ issue, pullRequest: target.number });
|
|
634
634
|
const round = await resolveRound(dir, target.number, explicitRound);
|
|
635
|
-
const reportPath =
|
|
635
|
+
const reportPath = join3(dir, reportFileName(target.number, round));
|
|
636
636
|
const template = await loadPrompt("pr-review");
|
|
637
637
|
const prompt = applyPlaceholders(template, {
|
|
638
638
|
__PR_NUMBER__: String(target.number),
|
|
@@ -648,6 +648,9 @@ async function runPrReview(prArg, opts = {}) {
|
|
|
648
648
|
timeout: getGlobalTimeout() ?? 9e5,
|
|
649
649
|
outputFormat: "text",
|
|
650
650
|
allowedTools: ["Bash", "Read", "Glob", "Grep"],
|
|
651
|
+
// With an Issue, `dir` sits under `issueDir`, so the parent covers both the
|
|
652
|
+
// report destination and the tasks.json/prd.md the prompt points at.
|
|
653
|
+
addDirs: issueDir === null ? [dir] : [issueDir],
|
|
651
654
|
statusMessage: `Reviewing Pull Request #${target.number} (round ${round})...`
|
|
652
655
|
});
|
|
653
656
|
if (!result.success) {
|
|
@@ -722,4 +725,4 @@ export {
|
|
|
722
725
|
exitCodeFor,
|
|
723
726
|
runPrReview
|
|
724
727
|
};
|
|
725
|
-
//# sourceMappingURL=chunk-
|
|
728
|
+
//# sourceMappingURL=chunk-6UBA7CSJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/pr-review.ts","../src/core/pr-review/discovery.ts","../src/core/pr-review/publisher.ts","../src/core/pr-review/report.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { loadPrReviewConfig } from '../config.js';\nimport { runHeadless } from '../core/headless.js';\nimport {\n PrDiscoveryError,\n type ResolvedPullRequest,\n resolvePullRequest,\n} from '../core/pr-review/discovery.js';\nimport { createPrReviewPublisher, type PrReviewPublisher } from '../core/pr-review/publisher.js';\nimport {\n buildReportMarkdown,\n type PrReviewPullRequest,\n parseFindings,\n parsePrReviewResult,\n prReviewDir,\n reportFileName,\n resolveRound,\n} from '../core/pr-review/report.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport type { PrReviewRecommendation } from '../types.js';\nimport { printError, printInfo, printSuccess } from '../ui/logger.js';\nimport { run } from '../utils/shell.js';\n\n/**\n * The `pr-review` phase: review a Pull Request as a whole.\n *\n * It follows the five steps of `review.ts` — resolve the target, build the\n * prompt, run headless, parse deterministically, persist state — with two\n * differences that come from reviewing a Pull Request instead of an Issue: the\n * target has to be discovered (`core/pr-review/discovery.ts`) and the outcome\n * is an artifact on disk, written through a `PrReviewPublisher`.\n *\n * The phase is intended to be read-only: Write/Edit are not allowed, and the\n * prompt forbids commits and `gh pr review|comment|merge`. Bash remains\n * available for `gh`/`git` inspection, so the restriction is policy plus tool\n * allow-list — not a sandbox that can stop every write.\n */\n\n/** Which verdicts make the command fail. */\nexport type PrReviewFailOn = 'request-changes' | 'suggestions' | 'none';\n\nconst FAIL_ON_LEVELS: PrReviewFailOn[] = ['request-changes', 'suggestions', 'none'];\nconst DEFAULT_FAIL_ON: PrReviewFailOn = 'request-changes';\n\n/** Placeholder value for context the run does not have. */\nconst NO_ISSUE = 'none';\nconst NO_PATH = '(none)';\n\nexport interface PrReviewOptions {\n /** Issue the Pull Request belongs to; enables plan lookup and state writes. */\n issue?: string;\n /** `--round <n>`: rewrite one specific round instead of appending a new one. */\n round?: string | number;\n /** Skip the discovery confirmation (`--yes`, and every call from `run`). */\n yes?: boolean;\n failOn?: string;\n /** Injection point for tests and for future publishers (GitHub). */\n publisher?: PrReviewPublisher;\n}\n\n/**\n * Exit code for a parsed verdict. `1` is reserved for execution failures, so a\n * review that ran to completion answers only `0` or `2`.\n */\nexport function exitCodeFor(\n recommendation: PrReviewRecommendation,\n failOn: PrReviewFailOn,\n): number {\n if (failOn === 'none') {\n return 0;\n }\n if (recommendation === 'REQUEST_CHANGES') {\n return 2;\n }\n return failOn === 'suggestions' && recommendation === 'APPROVE_WITH_SUGGESTIONS' ? 2 : 0;\n}\n\nfunction parseFailOn(value: string | undefined): PrReviewFailOn | null {\n if (value === undefined || value === '') {\n return DEFAULT_FAIL_ON;\n }\n const normalized = value.trim().toLowerCase();\n return FAIL_ON_LEVELS.find((level) => level === normalized) ?? null;\n}\n\n/** Metadata `gh` knows and the discovery may not — title, URL, head revision. */\ninterface GhPullRequest {\n title?: string;\n url?: string;\n headRefName?: string;\n headRefOid?: string;\n}\n\n/**\n * Best-effort `gh pr view`. A failure here costs the report a title and the\n * index a head SHA; it never costs the review, so nothing is thrown.\n */\nasync function fetchPullRequestMetadata(number: number): Promise<GhPullRequest | null> {\n try {\n const result = await run('gh', [\n 'pr',\n 'view',\n String(number),\n '--json',\n 'title,url,headRefName,headRefOid',\n ]);\n if (result.exitCode !== 0) {\n return null;\n }\n const parsed: unknown = JSON.parse(result.stdout);\n return typeof parsed === 'object' && parsed !== null ? (parsed as GhPullRequest) : null;\n } catch {\n return null;\n }\n}\n\nfunction mergePullRequest(\n target: ResolvedPullRequest,\n meta: GhPullRequest | null,\n): PrReviewPullRequest {\n return {\n number: target.number,\n url: target.url ?? meta?.url ?? null,\n title: target.title ?? meta?.title ?? null,\n headBranch: target.headBranch ?? meta?.headRefName ?? null,\n };\n}\n\ninterface PersistInput {\n tasksPath: string;\n pullRequestNumber: number;\n round: number;\n /** null when the verdict could not be parsed. */\n recommendation: PrReviewRecommendation | null;\n at: string;\n}\n\n/**\n * Record the round on the plan. Only `prReviewCompleted` and `prReview` are\n * touched: the other phases own the rest of `pipeline`.\n *\n * Best-effort like `review.ts`: reviewing a Pull Request with no associated\n * Issue (or whose plan is gone) is a supported use of the command, not a\n * failure — the report on disk is the deliverable either way.\n */\nasync function persistState(input: PersistInput): Promise<void> {\n try {\n const plan = await loadTaskPlan(input.tasksPath);\n plan.pipeline.prReviewCompleted =\n input.recommendation === 'APPROVE' || input.recommendation === 'APPROVE_WITH_SUGGESTIONS';\n plan.prReview = {\n // Preserved, never turned on here: opting the pipeline in is `run\n // --pr-review`'s decision, not a side effect of a manual review.\n enabled: plan.prReview?.enabled ?? false,\n pullRequestNumber: input.pullRequestNumber,\n // `--round <n>` rewrites an earlier round, which must not shrink the count.\n rounds: Math.max(plan.prReview?.rounds ?? 0, input.round),\n lastRecommendation: input.recommendation ?? plan.prReview?.lastRecommendation,\n lastReviewedAt: input.at,\n };\n await saveTaskPlan(input.tasksPath, plan);\n } catch {\n // tasks.json may not exist.\n }\n}\n\nexport async function runPrReview(prArg?: string, opts: PrReviewOptions = {}): Promise<number> {\n const failOn = parseFailOn(opts.failOn);\n if (failOn === null) {\n printError(`Invalid --fail-on value '${opts.failOn}'. Expected ${FAIL_ON_LEVELS.join(', ')}.`);\n return 1;\n }\n\n const explicitRound = opts.round === undefined ? undefined : Number(opts.round);\n if (explicitRound !== undefined && (!Number.isInteger(explicitRound) || explicitRound < 1)) {\n printError(`Invalid --round value '${opts.round}'. Expected a positive integer.`);\n return 1;\n }\n\n const issueRef = opts.issue?.replace(/^#/, '').trim();\n const issue = issueRef === undefined || issueRef === '' ? undefined : issueRef;\n\n let target: ResolvedPullRequest;\n try {\n target = await resolvePullRequest(prArg, { issue, yes: opts.yes });\n } catch (err) {\n if (err instanceof PrDiscoveryError) {\n printError(err.message);\n return err.exitCode;\n }\n printError(\n `Could not determine which Pull Request to review: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n // With no Issue there is nothing to resolve: the placeholders take NO_PATH\n // and the report lands under the synthetic `pr-<N>` slug resolved below.\n const issuePaths = issue === undefined ? null : await resolveIssuePaths(issue);\n const issueDir = issuePaths?.issueDir ?? null;\n const tasksPath = issuePaths?.tasksFile ?? null;\n const prdPath = issuePaths?.prdFile ?? null;\n\n const dir = await prReviewDir({ issue, pullRequest: target.number });\n const round = await resolveRound(dir, target.number, explicitRound);\n const reportPath = join(dir, reportFileName(target.number, round));\n\n const template = await loadPrompt('pr-review');\n const prompt = applyPlaceholders(template, {\n __PR_NUMBER__: String(target.number),\n __ISSUE_NUMBER__: issue ?? NO_ISSUE,\n __TASKS_PATH__: tasksPath ?? NO_PATH,\n __PRD_PATH__: prdPath ?? NO_PATH,\n __REPORT_PATH__: reportPath,\n __ROUND__: String(round),\n });\n\n const result = await runHeadless({\n prompt,\n maxTurns: 40,\n timeout: getGlobalTimeout() ?? 900_000,\n outputFormat: 'text',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n // With an Issue, `dir` sits under `issueDir`, so the parent covers both the\n // report destination and the tasks.json/prd.md the prompt points at.\n addDirs: issueDir === null ? [dir] : [issueDir],\n statusMessage: `Reviewing Pull Request #${target.number} (round ${round})...`,\n });\n\n if (!result.success) {\n printError(`PR review failed: ${result.error}`);\n return 1;\n }\n\n const parsed = parsePrReviewResult(result.result);\n const recommendation = parsed.ok ? parsed.result.recommendation : null;\n const blockers = parsed.ok ? parsed.result.blockers : [];\n\n const meta = await fetchPullRequestMetadata(target.number);\n const pullRequest = mergePullRequest(target, meta);\n const at = isoNow();\n const headSha = meta?.headRefOid ?? null;\n\n const report = {\n pullRequest,\n round,\n at,\n headSha,\n recommendation,\n blockers,\n findings: parseFindings(result.result),\n markdown: buildReportMarkdown({\n pullRequest,\n round,\n at,\n headSha,\n recommendation,\n body: result.result,\n parseError: parsed.ok ? null : parsed.error,\n }),\n };\n\n // Which publisher runs is configuration (`prReview.publisher`), never a code\n // path here: the command only ever holds the interface.\n const publisher =\n opts.publisher ?? createPrReviewPublisher(dir, (await loadPrReviewConfig()).publisher);\n try {\n await publisher.publish(report);\n } catch (err) {\n printError(\n `Could not publish the review report: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n if (tasksPath !== null) {\n await persistState({\n tasksPath,\n pullRequestNumber: target.number,\n round,\n recommendation,\n at,\n });\n }\n\n printInfo(`Report: ${reportPath}`);\n\n // A verdict that could not be read is an execution failure, never an\n // approval — the raw output is in the report for whoever has to look.\n if (!parsed.ok || recommendation === null) {\n printError(`PR review could not be parsed: ${parsed.ok ? 'no recommendation' : parsed.error}`);\n return 1;\n }\n\n if (recommendation === 'REQUEST_CHANGES') {\n printError(`PR review #${target.number}: REQUEST_CHANGES`);\n for (const blocker of blockers) {\n console.log(` - ${blocker}`);\n }\n return exitCodeFor(recommendation, failOn);\n }\n\n printSuccess(\n recommendation === 'APPROVE'\n ? `PR review #${target.number}: APPROVE`\n : `PR review #${target.number}: APPROVE_WITH_SUGGESTIONS — no blockers, improvements suggested`,\n );\n return exitCodeFor(recommendation, failOn);\n}\n","import { createInterface } from 'node:readline';\nimport { resolveIssuePaths } from '../../storage/resolve.js';\nimport type { PullRequestRef } from '../../types.js';\nimport { printInfo, printWarning } from '../../ui/logger.js';\nimport { getCurrentBranch } from '../../utils/git.js';\nimport { listPullRequests } from '../session-git.js';\nimport { getSessionPublisher } from '../session-publisher.js';\nimport type { SessionPullRequest } from '../session-state.js';\nimport { loadTaskPlan } from '../state-manager.js';\n\n/**\n * Which Pull Request the `pr-review` phase should look at.\n *\n * The order is deterministic and the search never falls back to \"some PR of\n * this repository\": a review pointed at an unrelated Pull Request is worse\n * than no review at all, so an exhausted search is an actionable error.\n */\n\n/** Origin of the resolved Pull Request, in precedence order. */\nexport type PullRequestSource = 'argument' | 'session' | 'plan' | 'branch';\n\nexport interface ResolvedPullRequest {\n number: number;\n /** Only known when the origin carried it; never synthesized. */\n url: string | null;\n title: string | null;\n headBranch: string | null;\n source: PullRequestSource;\n}\n\n/**\n * Failure to settle on a Pull Request. Carries the exit code the CLI should\n * return, mirroring IssueResolutionError (`issues/resolver.ts`).\n */\nexport class PrDiscoveryError extends Error {\n readonly exitCode: number;\n\n constructor(message: string, exitCode = 1) {\n super(message);\n this.name = 'PrDiscoveryError';\n this.exitCode = exitCode;\n }\n}\n\n/** Data sources, injectable for tests — the pattern of `GitStateSources`. */\nexport interface PrDiscoverySources {\n /** PRs already known to the active session publisher (`--web` runs). */\n sessionPullRequests(): SessionPullRequest[];\n /** `plan.pullRequest` persisted by the `pr` phase, or null. */\n planPullRequest(issue: string): Promise<PullRequestRef | null>;\n currentBranch(): Promise<string>;\n branchPullRequests(branch: string): Promise<SessionPullRequest[]>;\n}\n\nexport interface ResolvePullRequestOptions {\n /** Issue the pipeline is working on; enables the `plan` source. */\n issue?: string;\n /** `--yes`, or any non-interactive caller (`run`): skip the confirmation. */\n yes?: boolean;\n /**\n * Whether a prompt may be shown. Defaults to a real TTY on both ends outside\n * CI, which is what keeps the confirmation from blocking a headless run.\n */\n interactive?: boolean;\n /** Input stream for the confirmation. Defaults to process.stdin. */\n stdin?: NodeJS.ReadableStream;\n /** Output stream for the confirmation. Defaults to process.stdout. */\n stdout?: NodeJS.WritableStream;\n /** Informational sink. Defaults to printInfo. */\n info?: (message: string) => void;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n sources?: Partial<PrDiscoverySources>;\n}\n\n/** How many invalid answers are tolerated before the prompt gives up. */\nconst MAX_PROMPT_ATTEMPTS = 3;\n\nconst SOURCE_LABELS: Record<PullRequestSource, string> = {\n argument: 'the command argument',\n session: 'the current session',\n plan: 'tasks.json',\n branch: 'the current branch',\n};\n\n/**\n * `plan.pullRequest` of the issue's global tasks.json. Best-effort: a missing\n * plan, an invalid one or a plan written before the `pr` phase ran is simply\n * \"no candidate\", never a failure — the next source still gets its turn.\n */\nasync function loadPlanPullRequest(issue: string): Promise<PullRequestRef | null> {\n try {\n const plan = await loadTaskPlan((await resolveIssuePaths(issue)).tasksFile);\n return plan.pullRequest ?? null;\n } catch {\n return null;\n }\n}\n\nconst defaultSources: PrDiscoverySources = {\n sessionPullRequests: () => getSessionPublisher().snapshot().pullRequests,\n planPullRequest: loadPlanPullRequest,\n currentBranch: getCurrentBranch,\n branchPullRequests: listPullRequests,\n};\n\n/**\n * The most recent of several Pull Requests: the highest number, which does not\n * depend on the order the source returned them.\n */\nexport function mostRecent(pullRequests: SessionPullRequest[]): SessionPullRequest | null {\n let latest: SessionPullRequest | null = null;\n for (const pr of pullRequests) {\n if (latest === null || pr.number > latest.number) {\n latest = pr;\n }\n }\n return latest;\n}\n\n/**\n * The PR number inside an explicit argument: `184`, `#184` or a GitHub URL.\n * Returns null for anything else, so a typo fails loudly instead of being\n * coerced into a number that addresses an unrelated Pull Request.\n */\nfunction parsePrArgument(arg: string): number | null {\n const trimmed = arg.trim();\n const fromUrl = trimmed.match(/\\/pull\\/(\\d+)/);\n if (fromUrl?.[1] !== undefined) {\n return Number(fromUrl[1]);\n }\n const plain = trimmed.match(/^#?(\\d+)$/);\n return plain?.[1] === undefined ? null : Number(plain[1]);\n}\n\nfunction isInteractiveByDefault(): boolean {\n const ci = process.env.CI;\n const inCi = ci !== undefined && ci !== '' && ci !== '0' && ci.toLowerCase() !== 'false';\n return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !inCi;\n}\n\n/** Human-readable one-liner for logs and the confirmation prompt. */\nfunction describe(pr: ResolvedPullRequest): string {\n const parts = [`#${pr.number}`];\n if (pr.title !== null) parts.push(`\"${pr.title}\"`);\n if (pr.headBranch !== null) parts.push(`(${pr.headBranch})`);\n return parts.join(' ');\n}\n\n/**\n * Ask for confirmation, defaulting to yes on an empty answer.\n *\n * Lines are queued rather than read on demand for the same reason as\n * `issues/resolver.ts`: `rl.question` drops input that arrives before the\n * prompt is written, which silently loses a piped answer. EOF answers null,\n * which the caller treats as a refusal — never as consent.\n */\nasync function confirm(\n query: string,\n stdin: NodeJS.ReadableStream,\n stdout: NodeJS.WritableStream,\n warn: (message: string) => void,\n): Promise<boolean> {\n const rl = createInterface({ input: stdin, output: stdout });\n const queued: string[] = [];\n let pending: ((line: string | null) => void) | null = null;\n let closed = false;\n\n rl.on('line', (line: string) => {\n if (pending !== null) {\n const resolve = pending;\n pending = null;\n resolve(line);\n return;\n }\n queued.push(line);\n });\n rl.on('close', () => {\n closed = true;\n if (pending !== null) {\n const resolve = pending;\n pending = null;\n resolve(null);\n }\n });\n\n const ask = (): Promise<string | null> =>\n new Promise<string | null>((resolve) => {\n const buffered = queued.shift();\n if (buffered !== undefined) {\n resolve(buffered);\n return;\n }\n if (closed) {\n resolve(null);\n return;\n }\n stdout.write(query);\n pending = resolve;\n });\n\n try {\n for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {\n const answer = await ask();\n if (answer === null) {\n return false;\n }\n const normalized = answer.trim().toLowerCase();\n if (normalized === '' || normalized === 'y' || normalized === 'yes') {\n return true;\n }\n if (normalized === 'n' || normalized === 'no') {\n return false;\n }\n warn(`Invalid answer: '${answer.trim()}'. Enter y or n.`);\n }\n return false;\n } finally {\n rl.close();\n }\n}\n\n/**\n * Settle on the Pull Request to review, in this order:\n *\n * 1. the explicit argument;\n * 2. `plan.pullRequest`, written by the `pr` phase (when an issue is known);\n * 3. the PRs the active in-memory session publisher already knows about\n * (populated during `run --web` via `publishGitState` — not by reading\n * `session.json` from disk);\n * 4. the PRs of the current branch, via `gh`;\n * 5. an actionable failure.\n *\n * The plan is preferred over the session: after `pr`, `plan.pullRequest` is the\n * authoritative record of what this pipeline opened, while the session may\n * still list older PRs for the same branch (`gh pr list --state all`).\n *\n * Sources 2–4 were discovered rather than requested, so an interactive\n * terminal is asked to confirm before a long (and paid) review runs against\n * the wrong Pull Request. Anywhere else — `--yes`, CI, a pipe or the `run`\n * command — the discovery is only logged.\n */\nexport async function resolvePullRequest(\n prArg?: string,\n opts: ResolvePullRequestOptions = {},\n): Promise<ResolvedPullRequest> {\n const info = opts.info ?? printInfo;\n const warn = opts.warn ?? printWarning;\n const sources: PrDiscoverySources = { ...defaultSources, ...opts.sources };\n\n if (prArg !== undefined && prArg.trim() !== '') {\n const number = parsePrArgument(prArg);\n if (number === null) {\n throw new PrDiscoveryError(\n `Invalid Pull Request reference '${prArg.trim()}'. ` +\n 'Use a number (e.g. 184) or a GitHub Pull Request URL.',\n );\n }\n const url = prArg.trim().includes('/pull/') ? prArg.trim() : null;\n return { number, url, title: null, headBranch: null, source: 'argument' };\n }\n\n const discovered = await discover(sources, opts.issue, warn);\n if (discovered === null) {\n throw new PrDiscoveryError(\n 'No Pull Request found for this branch, in tasks.json or in the current session. ' +\n 'Run `issue-flow pr-review <number>` with the Pull Request number.',\n );\n }\n\n const interactive = opts.yes === true ? false : (opts.interactive ?? isInteractiveByDefault());\n if (!interactive) {\n info(\n `Reviewing Pull Request ${describe(discovered)}, found in ${SOURCE_LABELS[discovered.source]}.`,\n );\n return discovered;\n }\n\n info(`Pull Request found in ${SOURCE_LABELS[discovered.source]}:`);\n info(` number: #${discovered.number}`);\n info(` title: ${discovered.title ?? '(unknown)'}`);\n info(` branch: ${discovered.headBranch ?? '(unknown)'}`);\n\n const accepted = await confirm(\n `Review Pull Request #${discovered.number}? (Y/n): `,\n opts.stdin ?? process.stdin,\n opts.stdout ?? process.stdout,\n warn,\n );\n if (!accepted) {\n throw new PrDiscoveryError(\n `Cancelled: Pull Request #${discovered.number} was not confirmed. ` +\n 'Run `issue-flow pr-review <number>` to review a specific Pull Request.',\n );\n }\n return discovered;\n}\n\n/**\n * Sources 2–4, in order, stopping at the first hit. Every source is allowed to\n * fail on its own: a broken `gh` or an unreadable plan must not mask a\n * candidate another source could still provide.\n */\nasync function discover(\n sources: PrDiscoverySources,\n issue: string | undefined,\n warn: (message: string) => void,\n): Promise<ResolvedPullRequest | null> {\n if (issue !== undefined && issue !== '') {\n try {\n const fromPlan = await sources.planPullRequest(issue);\n if (fromPlan !== null) {\n return {\n number: fromPlan.number,\n url: fromPlan.url,\n title: null,\n headBranch: fromPlan.headBranch,\n source: 'plan',\n };\n }\n } catch {\n // An unreadable plan is \"no candidate\", never a hard failure.\n }\n }\n\n let sessionPrs: SessionPullRequest[] = [];\n try {\n sessionPrs = sources.sessionPullRequests();\n } catch {\n // The monitoring snapshot is an optimization, never a requirement.\n }\n const fromSession = mostRecent(sessionPrs);\n if (fromSession !== null) {\n return {\n number: fromSession.number,\n url: fromSession.url,\n title: fromSession.title,\n headBranch: null,\n source: 'session',\n };\n }\n\n let branch = '';\n try {\n branch = await sources.currentBranch();\n } catch (err) {\n warn(\n `Could not detect the current branch: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n if (branch === '') {\n return null;\n }\n\n let branchPrs: SessionPullRequest[] = [];\n try {\n branchPrs = await sources.branchPullRequests(branch);\n } catch (err) {\n warn(\n `Could not list Pull Requests for branch '${branch}': ${err instanceof Error ? err.message : String(err)}`,\n );\n return null;\n }\n const fromBranch = mostRecent(branchPrs);\n if (fromBranch === null) {\n return null;\n }\n return {\n number: fromBranch.number,\n url: fromBranch.url,\n title: fromBranch.title,\n headBranch: branch,\n source: 'branch',\n };\n}\n","import { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { PrReviewRecommendation } from '../../types.js';\nimport { writeFileAtomic } from '../../utils/fs.js';\nimport {\n type PrReviewFinding,\n type PrReviewIndex,\n type PrReviewPullRequest,\n readPrReviewIndex,\n reportFileName,\n upsertRound,\n} from './report.js';\n\n/**\n * Where the outcome of a review round goes.\n *\n * The phase itself only knows this interface, so pointing the results at\n * GitHub later (a review comment, a check run) is a new implementation and a\n * configuration change — not a change to the phase.\n */\n\n/** One finished review round, ready to be published. */\nexport interface PrReviewReport {\n pullRequest: PrReviewPullRequest;\n round: number;\n at: string;\n headSha: string | null;\n /** null when the agent output could not be parsed. */\n recommendation: PrReviewRecommendation | null;\n blockers: string[];\n findings: PrReviewFinding[];\n /** The full Markdown report, already assembled by `buildReportMarkdown()`. */\n markdown: string;\n}\n\nexport interface PrReviewPublisher {\n publish(report: PrReviewReport): Promise<void>;\n}\n\n/**\n * Writes `pr-<N>-round-<k>.md` plus `index.json` under the artifact directory.\n *\n * Rounds are additive: an existing index is loaded and merged instead of being\n * replaced, so round N+1 never erases the trail of the rounds before it. Both\n * files go through `writeFileAtomic()`, the mechanism `saveTaskPlan()` uses —\n * an interrupted write leaves the previous artifacts intact rather than a\n * truncated report.\n *\n * This publisher never writes to GitHub.\n */\nexport class LocalReportPublisher implements PrReviewPublisher {\n constructor(private readonly dir: string) {}\n\n async publish(report: PrReviewReport): Promise<void> {\n await mkdir(this.dir, { recursive: true });\n\n const fileName = reportFileName(report.pullRequest.number, report.round);\n await writeFileAtomic(join(this.dir, fileName), report.markdown);\n\n const existing = await readPrReviewIndex(this.dir);\n const index: PrReviewIndex = {\n schemaVersion: 1,\n pullRequest: report.pullRequest,\n rounds: existing?.rounds ?? [],\n };\n\n const updated = upsertRound(index, {\n round: report.round,\n at: report.at,\n recommendation: report.recommendation,\n headSha: report.headSha,\n reportPath: fileName,\n findings: report.findings,\n });\n\n await writeFileAtomic(join(this.dir, 'index.json'), `${JSON.stringify(updated, null, 2)}\\n`);\n }\n}\n\n/** Publishers selectable by configuration. v1 ships only the local one. */\nexport type PrReviewPublisherKind = 'local';\n\nconst PUBLISHERS: Record<PrReviewPublisherKind, (dir: string) => PrReviewPublisher> = {\n local: (dir) => new LocalReportPublisher(dir),\n};\n\n/**\n * The publisher the phase should use. Going through the factory is what keeps\n * the command bound to `PrReviewPublisher` alone: adding a GitHub adapter is a\n * new entry here plus a configuration key, never a change to the phase.\n */\nexport function createPrReviewPublisher(\n dir: string,\n kind: PrReviewPublisherKind = 'local',\n): PrReviewPublisher {\n return PUBLISHERS[kind](dir);\n}\n","import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { resolveIssuePaths } from '../../storage/resolve.js';\nimport type { PrReviewRecommendation } from '../../types.js';\n\n/**\n * Everything the `pr-review` phase produces out of a headless run: the parsed\n * verdict, the Markdown report and the structured index.\n *\n * The Markdown is what a human reads; `index.json` is what an automation reads.\n * Keeping both means a future GitHub publisher never has to re-parse Markdown\n * to know what the review concluded.\n */\n\n/** The Pull Request a report is about. Fields the source never knew stay null. */\nexport interface PrReviewPullRequest {\n number: number;\n url: string | null;\n title: string | null;\n headBranch: string | null;\n}\n\nexport type FindingSeverity = 'blocker' | 'high' | 'medium' | 'low';\n\nexport interface PrReviewFinding {\n severity: FindingSeverity;\n file: string | null;\n line: number | null;\n title: string;\n}\n\nexport interface PrReviewResult {\n recommendation: PrReviewRecommendation;\n /** Items listed under `BLOCKERS:`; empty when the review found none. */\n blockers: string[];\n}\n\n/**\n * Parsing outcome. A failure is explicit rather than an `APPROVE` default:\n * silently approving a Pull Request because the agent's output was malformed\n * is the one failure mode this phase must never have.\n */\nexport type PrReviewParse = { ok: true; result: PrReviewResult } | { ok: false; error: string };\n\nexport interface PrReviewRoundEntry {\n round: number;\n at: string;\n /** null when the output could not be parsed — never coerced to APPROVE. */\n recommendation: PrReviewRecommendation | null;\n headSha: string | null;\n /** Relative to the directory holding index.json. */\n reportPath: string;\n findings: PrReviewFinding[];\n}\n\nexport interface PrReviewIndex {\n schemaVersion: 1;\n pullRequest: PrReviewPullRequest;\n rounds: PrReviewRoundEntry[];\n}\n\nconst RECOMMENDATIONS: Record<string, PrReviewRecommendation> = {\n APPROVE: 'APPROVE',\n APPROVE_WITH_SUGGESTIONS: 'APPROVE_WITH_SUGGESTIONS',\n REQUEST_CHANGES: 'REQUEST_CHANGES',\n};\n\n/**\n * Read the `<pr-review-result>` block. Tolerant in the spirit of\n * `parseReviewResult()` (`commands/review.ts`): without the block, a bare\n * `RECOMMENDATION:` line in the raw output still counts. Anything else — no\n * recommendation at all, or one outside the enum — is a parse failure.\n */\nexport function parsePrReviewResult(output: string): PrReviewParse {\n const block = output.match(/<pr-review-result>([\\s\\S]*?)<\\/pr-review-result>/);\n const scope = block?.[1] ?? output;\n\n const line = scope.match(/RECOMMENDATION:\\s*([^\\n\\r]+)/i);\n if (line?.[1] === undefined) {\n return {\n ok: false,\n error: block\n ? 'The <pr-review-result> block has no RECOMMENDATION line.'\n : 'No <pr-review-result> block and no RECOMMENDATION line in the output.',\n };\n }\n\n // Markdown emphasis around the verdict is stripped, but never the inner\n // underscores of APPROVE_WITH_SUGGESTIONS.\n const raw = line[1]\n .trim()\n .replace(/[*`]/g, '')\n .replace(/^_+|[_.]+$/g, '')\n .trim();\n const normalized = raw.toUpperCase().replace(/[\\s-]+/g, '_');\n const recommendation = RECOMMENDATIONS[normalized];\n if (recommendation === undefined) {\n return {\n ok: false,\n error:\n `Unknown recommendation '${raw}'. ` +\n 'Expected APPROVE, APPROVE_WITH_SUGGESTIONS or REQUEST_CHANGES.',\n };\n }\n\n return { ok: true, result: { recommendation, blockers: parseBlockers(scope) } };\n}\n\n/** Items under `BLOCKERS:`, up to the end of the block or the next label. */\nfunction parseBlockers(scope: string): string[] {\n const match = scope.match(/BLOCKERS:([\\s\\S]*)/i);\n if (match?.[1] === undefined) return [];\n\n return match[1]\n .split('\\n')\n .map((entry) => entry.trim())\n .filter((entry) => /^[-*]\\s+/.test(entry))\n .map((entry) => entry.replace(/^[-*]\\s+/, '').trim())\n .filter((entry) => entry !== '' && !/^(none|nenhum|n\\/a)\\.?$/i.test(entry));\n}\n\n/** The sections every report carries, in order. */\nexport const REPORT_SECTIONS = [\n 'Executive summary',\n 'Strengths',\n 'Issues found',\n 'Suggested improvements',\n 'Architectural observations',\n 'Risks identified',\n 'Required before merge',\n 'Final recommendation',\n] as const;\n\nexport type ReportSection = (typeof REPORT_SECTIONS)[number];\n\n/**\n * Headings that map onto a canonical section. The agent writes prose, so the\n * mapping is deliberately forgiving — an unmatched heading is not dropped, it\n * lands in \"Additional notes\".\n */\nconst SECTION_ALIASES: Record<ReportSection, string[]> = {\n 'Executive summary': ['executive summary', 'summary', 'overview', 'resumo executivo', 'resumo'],\n Strengths: ['strengths', 'positives', 'highlights', 'what went well', 'pontos positivos'],\n 'Issues found': ['issues found', 'issues', 'problems', 'findings', 'problemas encontrados'],\n 'Suggested improvements': [\n 'suggested improvements',\n 'suggestions',\n 'improvements',\n 'sugestoes de melhoria',\n 'sugestoes',\n ],\n 'Architectural observations': [\n 'architectural observations',\n 'architecture',\n 'architectural notes',\n 'observacoes arquiteturais',\n 'arquitetura',\n ],\n 'Risks identified': ['risks identified', 'risks', 'riscos identificados', 'riscos'],\n 'Required before merge': [\n 'required before merge',\n 'blockers',\n 'must fix before merge',\n 'itens obrigatorios antes do merge',\n ],\n 'Final recommendation': [\n 'final recommendation',\n 'recommendation',\n 'verdict',\n 'recomendacao final',\n 'recomendacao',\n ],\n};\n\n/** lowercase, unaccented, without list numbering or trailing punctuation. */\nfunction normalizeHeading(heading: string): string {\n return heading\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/^\\d+[.)]\\s*/, '')\n .replace(/[*_`#]/g, '')\n .replace(/[.:;!?]+$/, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction matchSection(heading: string): ReportSection | null {\n const normalized = normalizeHeading(heading);\n for (const section of REPORT_SECTIONS) {\n if (SECTION_ALIASES[section].includes(normalized)) return section;\n }\n return null;\n}\n\ninterface SplitBody {\n sections: Map<ReportSection, string>;\n /** Preamble and unrecognized headings, preserved verbatim. */\n extra: string;\n}\n\n/**\n * Split the agent's Markdown by heading. Content that matches no canonical\n * section is kept rather than discarded: the report is the record of the\n * review, so nothing the agent wrote may be lost in the rewrite.\n */\nfunction splitBody(body: string): SplitBody {\n const sections = new Map<ReportSection, string>();\n const extra: string[] = [];\n let current: ReportSection | null = null;\n let buffer: string[] = [];\n\n const flush = () => {\n const content = buffer.join('\\n').trim();\n buffer = [];\n if (content === '') return;\n if (current === null) {\n extra.push(content);\n return;\n }\n const previous = sections.get(current);\n sections.set(current, previous === undefined ? content : `${previous}\\n\\n${content}`);\n };\n\n for (const line of body.split('\\n')) {\n const heading = line.match(/^#{1,6}\\s+(.*)$/);\n if (heading?.[1] === undefined) {\n buffer.push(line);\n continue;\n }\n flush();\n const section = matchSection(heading[1]);\n current = section;\n if (section === null) {\n buffer.push(line);\n }\n }\n flush();\n\n return { sections, extra: extra.join('\\n\\n').trim() };\n}\n\nconst FINDING_LINE = /^[-*]\\s*\\[(\\w+)\\]\\s+(.+)$/;\nconst FINDING_LOCATION = /^(\\S+?)(?::(\\d+))?\\s+[—–-]\\s+(.+)$/;\n\nconst SEVERITY_ALIASES: Record<string, FindingSeverity> = {\n blocker: 'blocker',\n blocking: 'blocker',\n critical: 'blocker',\n high: 'high',\n major: 'high',\n medium: 'medium',\n moderate: 'medium',\n low: 'low',\n minor: 'low',\n nit: 'low',\n};\n\n/**\n * Structured findings out of the report body, from list items written as\n * `- [severity] path/to/file.ts:12 — Title` under \"Issues found\" and\n * \"Required before merge\" (the format the prompt asks for). Items in any other\n * shape stay in the Markdown only: the index is a convenience for automations,\n * never the authoritative record.\n */\nexport function parseFindings(body: string): PrReviewFinding[] {\n const { sections } = splitBody(body);\n const scopes = [sections.get('Issues found'), sections.get('Required before merge')];\n const findings: PrReviewFinding[] = [];\n\n for (const scope of scopes) {\n if (scope === undefined) continue;\n for (const line of scope.split('\\n')) {\n const item = line.trim().match(FINDING_LINE);\n if (item?.[1] === undefined || item[2] === undefined) continue;\n\n const severity = SEVERITY_ALIASES[item[1].toLowerCase()];\n // Unknown markers stay in the Markdown only — inventing `medium` would\n // mislead any automation that keys off index.json severities.\n if (severity === undefined) continue;\n\n const rest = item[2].trim();\n const location = rest.match(FINDING_LOCATION);\n if (location?.[1] === undefined || location[3] === undefined) {\n findings.push({ severity, file: null, line: null, title: rest });\n continue;\n }\n findings.push({\n severity,\n file: location[1],\n line: location[2] === undefined ? null : Number(location[2]),\n title: location[3].trim(),\n });\n }\n }\n\n return dedupeFindings(findings);\n}\n\n/** A finding listed both as an issue and as a merge blocker is still one finding. */\nfunction dedupeFindings(findings: PrReviewFinding[]): PrReviewFinding[] {\n const seen = new Set<string>();\n return findings.filter((finding) => {\n const key = `${finding.file ?? ''}:${finding.line ?? ''}:${finding.title}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n}\n\nexport interface BuildReportInput {\n pullRequest: PrReviewPullRequest;\n round: number;\n at: string;\n headSha?: string | null;\n /** null when parsing failed. */\n recommendation: PrReviewRecommendation | null;\n /** Raw headless output. */\n body: string;\n /** Set when `parsePrReviewResult()` failed, so the raw output is kept. */\n parseError?: string | null;\n}\n\n/**\n * Assemble the Markdown report: a metadata header, the canonical sections in a\n * fixed order and — when parsing failed — the raw output verbatim, so a\n * malformed run is still auditable instead of being reduced to an exit code.\n */\nexport function buildReportMarkdown(input: BuildReportInput): string {\n const pr = input.pullRequest;\n const parts: string[] = [`# Pull Request Review — #${pr.number} (round ${input.round})`, ''];\n\n const heading = pr.url === null ? `#${pr.number}` : `[#${pr.number}](${pr.url})`;\n parts.push(`- **Pull Request:** ${heading}${pr.title === null ? '' : ` — ${pr.title}`}`);\n if (pr.headBranch !== null) parts.push(`- **Head branch:** ${pr.headBranch}`);\n if (input.headSha != null) parts.push(`- **Head SHA:** ${input.headSha}`);\n parts.push(`- **Round:** ${input.round}`);\n parts.push(`- **Reviewed at:** ${input.at}`);\n parts.push(`- **Recommendation:** ${input.recommendation ?? 'UNPARSED'}`);\n parts.push('');\n\n if (input.parseError != null && input.parseError !== '') {\n parts.push(`> Verdict could not be parsed: ${input.parseError}`, '');\n }\n\n const { sections, extra } = splitBody(input.body);\n for (const section of REPORT_SECTIONS) {\n parts.push(`## ${section}`, '', sections.get(section) ?? '_Not reported._', '');\n }\n\n if (input.parseError != null && input.parseError !== '') {\n parts.push('## Raw agent output', '', '```text', input.body.trim(), '```', '');\n } else if (extra !== '') {\n parts.push('## Additional notes', '', extra, '');\n }\n\n return `${parts.join('\\n').trimEnd()}\\n`;\n}\n\n/**\n * The global `issues/<N>/pr-review/`, or `issues/pr-<N>/pr-review/` with no\n * issue.\n *\n * A Pull Request with no associated Issue still needs somewhere to keep its\n * rounds, so it gets the synthetic `pr-<N>` identifier — `getIssuePaths()`\n * accepts non-numeric identifiers, so it is a first-class issue directory as\n * far as the storage layer is concerned.\n */\nexport async function prReviewDir(opts: { issue?: string; pullRequest: number }): Promise<string> {\n const slug =\n opts.issue === undefined || opts.issue === ''\n ? `pr-${opts.pullRequest}`\n : opts.issue.replace(/^#/, '');\n return (await resolveIssuePaths(slug)).prReviewDir;\n}\n\nexport function reportFileName(pullRequest: number, round: number): string {\n return `pr-${pullRequest}-round-${round}.md`;\n}\n\nconst REPORT_FILE = /^pr-(\\d+)-round-(\\d+)\\.md$/;\n\n/**\n * Read `index.json`. Never throws: a missing or corrupt index means \"no rounds\n * recorded yet\", which the round resolution below cross-checks against the\n * report files actually on disk.\n */\nexport async function readPrReviewIndex(dir: string): Promise<PrReviewIndex | null> {\n try {\n const parsed: unknown = JSON.parse(await readFile(join(dir, 'index.json'), 'utf-8'));\n if (typeof parsed !== 'object' || parsed === null) return null;\n const index = parsed as Partial<PrReviewIndex>;\n if (!Array.isArray(index.rounds) || typeof index.pullRequest !== 'object') return null;\n return {\n schemaVersion: 1,\n pullRequest: index.pullRequest as PrReviewPullRequest,\n rounds: index.rounds,\n };\n } catch {\n return null;\n }\n}\n\n/** Rounds already written for this Pull Request, from the report file names. */\nasync function roundsOnDisk(dir: string, pullRequest: number): Promise<number[]> {\n try {\n return (await readdir(dir))\n .map((name) => name.match(REPORT_FILE))\n .filter((match): match is RegExpMatchArray => match !== null)\n .filter((match) => Number(match[1]) === pullRequest)\n .map((match) => Number(match[2]));\n } catch {\n return [];\n }\n}\n\n/**\n * Which round to write: the explicit `--round <n>`, which rewrites that round,\n * or one past the highest round seen. Both the index and the files on disk are\n * consulted so a corrupt index can never overwrite an existing report.\n */\nexport async function resolveRound(\n dir: string,\n pullRequest: number,\n explicit?: number,\n): Promise<number> {\n if (explicit !== undefined) return explicit;\n\n const index = await readPrReviewIndex(dir);\n const known = [\n ...(index?.rounds ?? []).map((entry) => entry.round),\n ...(await roundsOnDisk(dir, pullRequest)),\n ];\n return known.length === 0 ? 1 : Math.max(...known) + 1;\n}\n\n/**\n * Add or replace a round. Additive by construction: writing round N+1 keeps\n * every earlier entry, and `--round n` replaces exactly one.\n */\nexport function upsertRound(index: PrReviewIndex, entry: PrReviewRoundEntry): PrReviewIndex {\n const rounds = index.rounds.filter((existing) => existing.round !== entry.round);\n rounds.push(entry);\n rounds.sort((a, b) => a.round - b.round);\n return { ...index, rounds };\n}\n"],"mappings":"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","names":["join","join","join","join"]}
|
|
@@ -6,20 +6,20 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
issuePlaceholders,
|
|
8
8
|
resolveCommandIssue
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DKMBLNGO.js";
|
|
10
10
|
import {
|
|
11
11
|
isTransientFailure
|
|
12
12
|
} from "./chunk-HDV3YDTG.js";
|
|
13
13
|
import {
|
|
14
14
|
runHeadless
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-GOZT2YP5.js";
|
|
16
16
|
import {
|
|
17
17
|
applyPlaceholders,
|
|
18
18
|
loadPrompt
|
|
19
19
|
} from "./chunk-SLHZPB3C.js";
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
} from "./chunk-
|
|
21
|
+
resolveIssuePaths
|
|
22
|
+
} from "./chunk-G3TJHMAS.js";
|
|
23
23
|
import {
|
|
24
24
|
getGlobalTimeout,
|
|
25
25
|
loadTaskPlan,
|
|
@@ -30,20 +30,22 @@ import {
|
|
|
30
30
|
|
|
31
31
|
// src/commands/prd.ts
|
|
32
32
|
import { mkdir } from "fs/promises";
|
|
33
|
-
import { join } from "path";
|
|
34
33
|
async function runPrd(issue, resolvedIssue) {
|
|
35
34
|
const issueNumber = issue.replace(/^#/, "");
|
|
36
|
-
const
|
|
37
|
-
const prdPath =
|
|
35
|
+
const paths = await resolveIssuePaths(issueNumber);
|
|
36
|
+
const prdPath = paths.prdFile;
|
|
38
37
|
const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);
|
|
39
38
|
if (!resolution.ok) {
|
|
40
39
|
return resolution.code;
|
|
41
40
|
}
|
|
42
|
-
await mkdir(issueDir, { recursive: true });
|
|
41
|
+
await mkdir(paths.issueDir, { recursive: true });
|
|
43
42
|
const template = await loadPrompt("prd");
|
|
44
43
|
const prompt = applyPlaceholders(template, {
|
|
45
44
|
__ISSUE_NUMBER__: issueNumber,
|
|
46
45
|
__PRD_PATH__: prdPath,
|
|
46
|
+
// Absolute: the analysis lives in the global storage, outside the working
|
|
47
|
+
// directory the agent is started in.
|
|
48
|
+
__ANALYSIS_PATH__: paths.analysisFile,
|
|
47
49
|
...issuePlaceholders(resolution.resolved)
|
|
48
50
|
});
|
|
49
51
|
const outcome = await runPhaseWithRetry({
|
|
@@ -55,6 +57,7 @@ async function runPrd(issue, resolvedIssue) {
|
|
|
55
57
|
timeout: getGlobalTimeout() ?? 3e5,
|
|
56
58
|
outputFormat: "text",
|
|
57
59
|
allowedTools: ["Bash", "Read", "Glob", "Grep", "Write"],
|
|
60
|
+
addDirs: [paths.issueDir],
|
|
58
61
|
statusMessage: `Generating PRD for issue #${issueNumber}...`
|
|
59
62
|
});
|
|
60
63
|
if (!result.success) {
|
|
@@ -79,7 +82,7 @@ async function runPrd(issue, resolvedIssue) {
|
|
|
79
82
|
printError(outcome.error ?? `PRD generation failed for issue #${issueNumber}`);
|
|
80
83
|
return 1;
|
|
81
84
|
}
|
|
82
|
-
const tasksPath =
|
|
85
|
+
const tasksPath = paths.tasksFile;
|
|
83
86
|
try {
|
|
84
87
|
const plan = await loadTaskPlan(tasksPath);
|
|
85
88
|
plan.pipeline.prdCompleted = true;
|
|
@@ -93,4 +96,4 @@ async function runPrd(issue, resolvedIssue) {
|
|
|
93
96
|
export {
|
|
94
97
|
runPrd
|
|
95
98
|
};
|
|
96
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-CYU6RSDJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/prd.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { runHeadless } from '../core/headless.js';\nimport { readFileWithGrace, runPhaseWithRetry } from '../core/phase-runner.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 { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\nexport async function runPrd(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const prdPath = paths.prdFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const template = await loadPrompt('prd');\n const prompt = applyPlaceholders(template, {\n __ISSUE_NUMBER__: issueNumber,\n __PRD_PATH__: prdPath,\n // Absolute: the analysis lives in the global storage, outside the working\n // directory the agent is started in.\n __ANALYSIS_PATH__: paths.analysisFile,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const outcome = await runPhaseWithRetry({\n phase: 'prd',\n attempt: async () => {\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n outputFormat: 'text',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Generating PRD for issue #${issueNumber}...`,\n });\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `PRD generation failed: ${result.error}`,\n };\n }\n\n // Verify the file was created. A brief grace period absorbs a lag\n // between the Write tool completing and this process seeing it; if it\n // still isn't there, treat it as recoverable and let the phase retry.\n try {\n const content = await readFileWithGrace(prdPath);\n if (content.length < 10) {\n return { ok: false, transient: true, error: 'PRD file was created but appears empty' };\n }\n } catch {\n return { ok: false, transient: true, error: `PRD file was not created at ${prdPath}` };\n }\n\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `PRD generation failed for issue #${issueNumber}`);\n return 1;\n }\n\n // Update pipeline state\n const tasksPath = paths.tasksFile;\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prdCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist yet\n }\n\n printSuccess(`PRD saved to ${prdPath}`);\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AAYtB,eAAsB,OAAO,OAAe,eAAgD;AAC1F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,UAAU,MAAM;AAEtB,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,MAAM,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE/C,QAAM,WAAW,MAAM,WAAW,KAAK;AACvC,QAAM,SAAS,kBAAkB,UAAU;AAAA,IACzC,kBAAkB;AAAA,IAClB,cAAc;AAAA;AAAA;AAAA,IAGd,mBAAmB,MAAM;AAAA,IACzB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,UAAU,MAAM,kBAAkB;AAAA,IACtC,OAAO;AAAA,IACP,SAAS,YAAY;AACnB,YAAM,SAAS,MAAM,YAAY;AAAA,QAC/B;AAAA,QACA,UAAU;AAAA,QACV,SAAS,iBAAiB,KAAK;AAAA,QAC/B,cAAc;AAAA,QACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;AAAA,QACtD,SAAS,CAAC,MAAM,QAAQ;AAAA,QACxB,eAAe,6BAA6B,WAAW;AAAA,MACzD,CAAC;AAED,UAAI,CAAC,OAAO,SAAS;AACnB,eAAO;AAAA,UACL,IAAI;AAAA,UACJ,WAAW,mBAAmB,GAAG,OAAO,SAAS,EAAE;AAAA,UACnD,OAAO,0BAA0B,OAAO,KAAK;AAAA,QAC/C;AAAA,MACF;AAKA,UAAI;AACF,cAAM,UAAU,MAAM,kBAAkB,OAAO;AAC/C,YAAI,QAAQ,SAAS,IAAI;AACvB,iBAAO,EAAE,IAAI,OAAO,WAAW,MAAM,OAAO,yCAAyC;AAAA,QACvF;AAAA,MACF,QAAQ;AACN,eAAO,EAAE,IAAI,OAAO,WAAW,MAAM,OAAO,+BAA+B,OAAO,GAAG;AAAA,MACvF;AAEA,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,EACF,CAAC;AAED,MAAI,CAAC,QAAQ,IAAI;AACf,eAAW,QAAQ,SAAS,oCAAoC,WAAW,EAAE;AAC7E,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,MAAM;AACxB,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,eAAe;AAC7B,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,eAAa,gBAAgB,OAAO,EAAE;AACtC,SAAO;AACT;","names":[]}
|