issue-flow 0.11.0 → 0.11.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.
- package/README.md +1 -1
- package/dist/{analyze-7B5TORNK.js → analyze-XV3K25K3.js} +4 -3
- package/dist/analyze-XV3K25K3.js.map +1 -0
- package/dist/{chunk-VCKX262Z.js → chunk-34WNTK3K.js} +4 -3
- package/dist/chunk-34WNTK3K.js.map +1 -0
- package/dist/{chunk-JW7WLSZ6.js → chunk-BXCPXRBR.js} +4 -3
- package/dist/chunk-BXCPXRBR.js.map +1 -0
- package/dist/{chunk-PL6CNMVS.js → chunk-EWDJENFW.js} +29 -8
- package/dist/chunk-EWDJENFW.js.map +1 -0
- package/dist/{chunk-UBIQXCVE.js → chunk-EY6WMHV2.js} +4 -3
- package/dist/chunk-EY6WMHV2.js.map +1 -0
- package/dist/{chunk-MSPUB635.js → chunk-IVHZEYAX.js} +4 -3
- package/dist/chunk-IVHZEYAX.js.map +1 -0
- package/dist/{chunk-MKD2D7WE.js → chunk-KQ4JQN7T.js} +4 -3
- package/dist/chunk-KQ4JQN7T.js.map +1 -0
- package/dist/cli.js +8 -8
- package/dist/{generate-6RA2MFJB.js → generate-TBVTLMOP.js} +4 -3
- package/dist/generate-TBVTLMOP.js.map +1 -0
- package/dist/{plan-KENR6MJ3.js → plan-Y2BYZD24.js} +3 -3
- package/dist/{pr-KDGKG4BP.js → pr-H6GLOSXF.js} +3 -3
- package/dist/{pr-review-CAMAB7KO.js → pr-review-CK5FR6WX.js} +3 -3
- package/dist/{prd-V3MWDIHK.js → prd-4PWLCGH5.js} +3 -3
- package/dist/{review-7LN47SAN.js → review-QGVAHEZF.js} +3 -3
- package/dist/{run-H75MD5BQ.js → run-DBF2VWUN.js} +7 -7
- package/package.json +1 -1
- package/dist/analyze-7B5TORNK.js.map +0 -1
- package/dist/chunk-JW7WLSZ6.js.map +0 -1
- package/dist/chunk-MKD2D7WE.js.map +0 -1
- package/dist/chunk-MSPUB635.js.map +0 -1
- package/dist/chunk-PL6CNMVS.js.map +0 -1
- package/dist/chunk-UBIQXCVE.js.map +0 -1
- package/dist/chunk-VCKX262Z.js.map +0 -1
- package/dist/generate-6RA2MFJB.js.map +0 -1
- /package/dist/{plan-KENR6MJ3.js.map → plan-Y2BYZD24.js.map} +0 -0
- /package/dist/{pr-KDGKG4BP.js.map → pr-H6GLOSXF.js.map} +0 -0
- /package/dist/{pr-review-CAMAB7KO.js.map → pr-review-CK5FR6WX.js.map} +0 -0
- /package/dist/{prd-V3MWDIHK.js.map → prd-4PWLCGH5.js.map} +0 -0
- /package/dist/{review-7LN47SAN.js.map → review-QGVAHEZF.js.map} +0 -0
- /package/dist/{run-H75MD5BQ.js.map → run-DBF2VWUN.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -217,7 +217,7 @@ withGlobalOptions(
|
|
|
217
217
|
}
|
|
218
218
|
throw error;
|
|
219
219
|
}
|
|
220
|
-
const { runGenerate } = await import("./generate-
|
|
220
|
+
const { runGenerate } = await import("./generate-TBVTLMOP.js");
|
|
221
221
|
const code = await runGenerate(options.prompt, target);
|
|
222
222
|
process.exit(code);
|
|
223
223
|
});
|
|
@@ -250,7 +250,7 @@ withUserStoryNumberingOptions(
|
|
|
250
250
|
}
|
|
251
251
|
throw error;
|
|
252
252
|
}
|
|
253
|
-
const { runPipeline } = await import("./run-
|
|
253
|
+
const { runPipeline } = await import("./run-DBF2VWUN.js");
|
|
254
254
|
const code = await runPipeline(
|
|
255
255
|
issues,
|
|
256
256
|
options.mode,
|
|
@@ -272,7 +272,7 @@ withIssueOptions(
|
|
|
272
272
|
program.command("analyze").description("Analyze an issue via Claude Code Headless").argument("<issue>", "Issue number")
|
|
273
273
|
)
|
|
274
274
|
).action(async (issue) => {
|
|
275
|
-
const { runAnalyze } = await import("./analyze-
|
|
275
|
+
const { runAnalyze } = await import("./analyze-XV3K25K3.js");
|
|
276
276
|
const code = await runAnalyze(issue);
|
|
277
277
|
process.exit(code);
|
|
278
278
|
});
|
|
@@ -281,7 +281,7 @@ withIssueOptions(
|
|
|
281
281
|
program.command("prd").description("Generate a PRD from an analyzed issue via Claude Code Headless").argument("<issue>", "Issue number")
|
|
282
282
|
)
|
|
283
283
|
).action(async (issue) => {
|
|
284
|
-
const { runPrd } = await import("./prd-
|
|
284
|
+
const { runPrd } = await import("./prd-4PWLCGH5.js");
|
|
285
285
|
const code = await runPrd(issue);
|
|
286
286
|
process.exit(code);
|
|
287
287
|
});
|
|
@@ -302,7 +302,7 @@ withUserStoryNumberingOptions(
|
|
|
302
302
|
}
|
|
303
303
|
throw error;
|
|
304
304
|
}
|
|
305
|
-
const { runPlan } = await import("./plan-
|
|
305
|
+
const { runPlan } = await import("./plan-Y2BYZD24.js");
|
|
306
306
|
const code = await runPlan(issue, void 0, numbering);
|
|
307
307
|
process.exit(code);
|
|
308
308
|
});
|
|
@@ -335,7 +335,7 @@ withIssueOptions(
|
|
|
335
335
|
program.command("review").description("Validate an issue resolution via Claude Code Headless").argument("<issue>", "Issue number")
|
|
336
336
|
)
|
|
337
337
|
).action(async (issue) => {
|
|
338
|
-
const { runReview } = await import("./review-
|
|
338
|
+
const { runReview } = await import("./review-QGVAHEZF.js");
|
|
339
339
|
const code = await runReview(issue);
|
|
340
340
|
process.exit(code);
|
|
341
341
|
});
|
|
@@ -344,7 +344,7 @@ withIssueOptions(
|
|
|
344
344
|
program.command("pr").description("Create a pull request via Claude Code Headless").argument("<issue>", "Issue number")
|
|
345
345
|
)
|
|
346
346
|
).action(async (issue) => {
|
|
347
|
-
const { runPr } = await import("./pr-
|
|
347
|
+
const { runPr } = await import("./pr-H6GLOSXF.js");
|
|
348
348
|
const code = await runPr(issue);
|
|
349
349
|
process.exit(code);
|
|
350
350
|
});
|
|
@@ -377,7 +377,7 @@ withGlobalOptions(
|
|
|
377
377
|
)
|
|
378
378
|
).action(
|
|
379
379
|
async (pr, options) => {
|
|
380
|
-
const { runPrReview } = await import("./pr-review-
|
|
380
|
+
const { runPrReview } = await import("./pr-review-CK5FR6WX.js");
|
|
381
381
|
const code = await runPrReview(pr, options);
|
|
382
382
|
process.exit(code);
|
|
383
383
|
}
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
localIssueRef
|
|
7
7
|
} from "./chunk-AAY6KM7N.js";
|
|
8
8
|
import {
|
|
9
|
+
DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
9
10
|
runHeadless
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EWDJENFW.js";
|
|
11
12
|
import {
|
|
12
13
|
publishPhaseMetrics,
|
|
13
14
|
resolvePolicyPlaceholders
|
|
@@ -136,7 +137,7 @@ async function draftIssue(promptText) {
|
|
|
136
137
|
const result = await runHeadless({
|
|
137
138
|
prompt,
|
|
138
139
|
maxTurns: 15,
|
|
139
|
-
timeout: getGlobalTimeout() ??
|
|
140
|
+
timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,
|
|
140
141
|
// json (not text) so the CLI reports usage: the envelope's `result` field
|
|
141
142
|
// carries the same assistant text parseIssueDraft() already consumed.
|
|
142
143
|
outputFormat: "json",
|
|
@@ -240,4 +241,4 @@ async function runGenerate(promptText, target) {
|
|
|
240
241
|
export {
|
|
241
242
|
runGenerate
|
|
242
243
|
};
|
|
243
|
-
//# sourceMappingURL=generate-
|
|
244
|
+
//# sourceMappingURL=generate-TBVTLMOP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/issues/draft.ts","../src/issues/label-policy.ts","../src/commands/generate.ts"],"sourcesContent":["import type { IssueDraft } from './types.js';\n\n/**\n * Structured Issue draft emitted by the `generate` prompt.\n *\n * The agent no longer creates the Issue itself: it returns the content and the\n * providers persist it, so the same draft can reach GitHub, the local files, or\n * both. Capturing the result therefore has to be exact — the old approach of\n * fishing an issue URL out of free-form prose only ever worked for GitHub.\n *\n * The block mirrors the `<issue-body>` convention already used by the phase\n * templates, which keeps a multi-line markdown body readable and free of the\n * escaping a JSON payload would demand.\n */\n\n/** Wrapper the agent must emit around the draft. */\nexport const DRAFT_TAG = 'issue-draft';\n\n/** Failure to capture a draft from the agent output. */\nexport class IssueDraftParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'IssueDraftParseError';\n }\n}\n\n/**\n * Last occurrence of `<tag>...</tag>` in `source`, or `null`.\n *\n * The last one wins: an agent that shows the format before filling it in (or\n * restates the draft after a correction) would otherwise have its first,\n * throwaway attempt captured. The closing tag is searched from the end for the\n * same reason a body may legitimately quote one.\n */\nfunction extractTag(source: string, tag: string): string | null {\n const open = `<${tag}>`;\n const close = `</${tag}>`;\n\n const start = source.lastIndexOf(open);\n if (start === -1) return null;\n\n const end = source.lastIndexOf(close);\n if (end === -1 || end < start) return null;\n\n return source.slice(start + open.length, end);\n}\n\nfunction parseLabels(raw: string | null): string[] {\n if (raw === null) return [];\n return raw\n .split(',')\n .map((label) => label.trim())\n .filter((label) => label.length > 0 && label.toLowerCase() !== '(none)');\n}\n\n/**\n * Read the draft the agent produced.\n *\n * @throws IssueDraftParseError when the block is absent or has no usable\n * title/body, so a malformed answer fails loudly instead of creating an empty\n * Issue.\n */\nexport function parseIssueDraft(output: string): IssueDraft {\n const normalized = output.replace(/\\r\\n?/g, '\\n');\n const block = extractTag(normalized, DRAFT_TAG);\n\n if (block === null) {\n throw new IssueDraftParseError(\n `The agent did not emit an <${DRAFT_TAG}> block, so there is no Issue to create. ` +\n `Output was: ${normalized.trim().slice(0, 300) || '(empty)'}`,\n );\n }\n\n const title = (extractTag(block, 'title') ?? '').trim();\n const body = (extractTag(block, 'body') ?? '').trim();\n\n if (title.length === 0) {\n throw new IssueDraftParseError(`The <${DRAFT_TAG}> block has no <title>.`);\n }\n if (body.length === 0) {\n throw new IssueDraftParseError(`The <${DRAFT_TAG}> block has no <body>.`);\n }\n\n // Optional, and deliberately so: a repository with no Issue Types and no\n // templates emits neither, and the draft is exactly what it was before.\n const type = (extractTag(block, 'type') ?? '').trim();\n const template = (extractTag(block, 'template') ?? '').trim();\n\n return {\n title,\n body,\n labels: parseLabels(extractTag(block, 'labels')),\n ...(type === '' || type.toLowerCase() === '(none)' ? {} : { type }),\n ...(template === '' || template.toLowerCase() === '(none)' ? {} : { template }),\n };\n}\n","import type { LabelDefinition } from '../policy/types.js';\nimport { run } from '../utils/shell.js';\n\n/**\n * Reconciling the labels an agent suggested with the ones the repository\n * actually has.\n *\n * A workflow tool has no business changing a repository's taxonomy. Labels are\n * governance: a team that deleted `high`/`medium`/`low` in favour of a native\n * priority field, or `bug`/`enhancement` in favour of Issue Types, made a\n * decision — and a generator that silently recreates them undoes it, quietly and\n * repository-wide.\n *\n * So the default is to **validate, never create**. `policy.issues.allowLabelCreation`\n * restores the old behaviour for repositories that want it.\n */\n\nexport interface LabelReconciliation {\n /** Labels to apply, in the repository's own casing. */\n labels: string[];\n /** Suggested labels the repository does not have. */\n missing: string[];\n}\n\n/**\n * Intersect the suggested labels with the repository's real ones.\n *\n * Matching is case-insensitive and the result carries the **repository's**\n * casing: `gh issue create --label Bug` fails on a repository whose label is\n * `bug`, and an agent has no way to know which one it is.\n *\n * An empty `known` means the labels could not be read at all — no policy, no\n * `gh`, no network. That is not evidence the labels do not exist, so the\n * suggestions pass through untouched and the behaviour is the one that shipped\n * before this layer. Refusing to label an issue because discovery was offline\n * would be the wrong failure.\n */\nexport function reconcileLabels(\n suggested: string[],\n known: LabelDefinition[],\n): LabelReconciliation {\n if (known.length === 0) {\n return { labels: [...suggested], missing: [] };\n }\n\n const canonical = new Map(known.map((label) => [label.name.toLowerCase(), label.name]));\n\n const labels: string[] = [];\n const missing: string[] = [];\n for (const suggestion of suggested) {\n const match = canonical.get(suggestion.trim().toLowerCase());\n if (match === undefined) {\n if (!missing.includes(suggestion)) missing.push(suggestion);\n continue;\n }\n if (!labels.includes(match)) labels.push(match);\n }\n\n return { labels, missing };\n}\n\n/**\n * Create the labels the repository is missing, for\n * `policy.issues.allowLabelCreation: true`.\n *\n * Best-effort per label: one that cannot be created is reported and skipped, so\n * a single failure does not cost the Issue. Returns the labels that now exist.\n */\nexport async function createMissingLabels(\n names: string[],\n warn: (message: string) => void,\n): Promise<string[]> {\n const created: string[] = [];\n\n for (const name of names) {\n const result = await run('gh', ['label', 'create', name]);\n if (result.exitCode === 0) {\n created.push(name);\n continue;\n }\n // gh answers \"already exists\" when a label was created between the\n // discovery and now, which is a success for our purposes.\n const detail = (result.stderr || result.stdout).trim();\n if (/already exists/i.test(detail)) {\n created.push(name);\n continue;\n }\n warn(`Could not create label \"${name}\": ${detail || 'gh label create failed'}`);\n }\n\n return created;\n}\n","import { loadIssuesConfig, loadPolicyConfig } from '../config.js';\nimport { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { ensureProvidersRegistered } from '../issues/bootstrap.js';\nimport { localIssueRef } from '../issues/context.js';\nimport { IssueDraftParseError, parseIssueDraft } from '../issues/draft.js';\nimport { createMissingLabels, reconcileLabels } from '../issues/label-policy.js';\nimport { getProvider } from '../issues/registry.js';\nimport type { Issue, IssueDraft, IssueGenerateTarget } from '../issues/types.js';\nimport { loadRepositoryPolicy } from '../policy/index.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveProjectPaths } from '../storage/resolve.js';\nimport { printError, printInfo, printSuccess, printWarning } from '../ui/logger.js';\n\n/** Human-readable pointer to a created Issue. */\nfunction issueLocation(issue: Issue): string {\n return issue.remoteRef ?? localIssueRef(issue.id);\n}\n\n/**\n * Ask the agent for the Issue content.\n *\n * The agent only drafts: creation belongs to the providers, so the same draft\n * can be persisted to GitHub, to the local files, or to both.\n */\nasync function draftIssue(promptText: string): Promise<IssueDraft> {\n // The duplicate check reads the local issues, which live in the global\n // storage: the path has to be handed over (and allowed) explicitly, since it\n // is outside the working directory the agent is started in.\n const { issuesDir } = await resolveProjectPaths();\n\n const template = await loadPrompt('generate');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __USER_PROMPT__: promptText,\n __LOCAL_ISSUES_DIR__: issuesDir,\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 15,\n timeout: getGlobalTimeout() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseIssueDraft() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [issuesDir],\n statusMessage: 'Drafting issue...',\n });\n // Before the success check: the tokens were spent either way.\n publishPhaseMetrics('generate', result.cost, startedAtMs);\n\n if (!result.success) {\n throw new Error(`Issue creation failed: ${result.error}`);\n }\n\n return parseIssueDraft(result.result);\n}\n\n/**\n * Create the GitHub Issue and mirror it locally.\n *\n * The remote comes first on purpose: it owns the identifier, and the mirror has\n * to reuse it so both copies describe one demand. A remote failure therefore\n * leaves nothing behind, and a mirror failure is reported with the remote\n * reference that already exists.\n */\nasync function createBoth(draft: IssueDraft): Promise<Issue[]> {\n const remote = await getProvider('github').create(draft);\n\n try {\n const mirror = await getProvider('local').create({\n ...draft,\n id: remote.id,\n ...(remote.remoteRef\n ? {\n remote: {\n provider: 'github' as const,\n ref: remote.remoteRef,\n syncedAt: isoNow(),\n syncedContentHash: remote.contentHash,\n },\n }\n : {}),\n });\n return [remote, mirror];\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `GitHub issue created at ${issueLocation(remote)}, but the local mirror was not written: ` +\n `${message}. Nothing else was persisted; re-run with --local to create the mirror.`,\n );\n }\n}\n\nasync function createIssues(target: IssueGenerateTarget, draft: IssueDraft): Promise<Issue[]> {\n if (target === 'both') {\n return createBoth(draft);\n }\n return [await getProvider(target).create(draft)];\n}\n\n/**\n * Reconcile the draft's labels with the ones the repository really has.\n *\n * Two failures are being prevented here, and only one of them is cosmetic.\n * GitHub rejects an issue whose label does not exist, so an invented label costs\n * the whole Issue. And a tool that creates the label instead rewrites a taxonomy\n * the team may have curated deliberately — which is worse, because it succeeds.\n *\n * Never throws: a policy that could not be resolved leaves the draft untouched.\n */\nasync function applyLabelPolicy(draft: IssueDraft): Promise<IssueDraft> {\n let known: Awaited<ReturnType<typeof loadRepositoryPolicy>>['issues']['labels'];\n let allowCreation: boolean;\n try {\n const policy = await loadRepositoryPolicy();\n known = policy.issues.labels;\n const config = await loadPolicyConfig({ projectRoot: policy.root });\n allowCreation = config.issues.allowLabelCreation ?? false;\n } catch {\n return draft;\n }\n\n const { labels, missing } = reconcileLabels(draft.labels, known);\n if (missing.length === 0) {\n return { ...draft, labels };\n }\n\n if (!allowCreation) {\n printWarning(\n `Dropping ${missing.length} label(s) this repository does not have: ${missing.join(', ')}. ` +\n 'Issue Flow does not create labels; set policy.issues.allowLabelCreation to change that.',\n );\n return { ...draft, labels };\n }\n\n const created = await createMissingLabels(missing, printWarning);\n return { ...draft, labels: [...labels, ...created] };\n}\n\nexport async function runGenerate(\n promptText: string,\n target?: IssueGenerateTarget,\n): Promise<number> {\n const config = await loadIssuesConfig();\n const destination = target ?? config.defaultGenerateTarget;\n\n ensureProvidersRegistered();\n\n let draft: IssueDraft;\n try {\n draft = await draftIssue(promptText);\n } catch (err) {\n if (err instanceof IssueDraftParseError) {\n printError(`Could not read the generated Issue draft: ${err.message}`);\n } else {\n printError(err instanceof Error ? err.message : String(err));\n }\n return 1;\n }\n\n const finalDraft = await applyLabelPolicy(draft);\n if (finalDraft.template !== undefined) {\n printInfo(`Following the repository's Issue Template: ${finalDraft.template}`);\n }\n if (finalDraft.type !== undefined) {\n printInfo(`Issue Type: ${finalDraft.type}`);\n }\n\n let created: Issue[];\n try {\n created = await createIssues(destination, finalDraft);\n } catch (err) {\n printError(err instanceof Error ? err.message : String(err));\n return 1;\n }\n\n for (const issue of created) {\n printSuccess(`Issue created (${issue.source}): ${issueLocation(issue)}`);\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runPlan
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EY6WMHV2.js";
|
|
5
5
|
import "./chunk-3BOHTH4V.js";
|
|
6
6
|
import "./chunk-AAY6KM7N.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-EWDJENFW.js";
|
|
8
8
|
import "./chunk-HDV3YDTG.js";
|
|
9
9
|
import "./chunk-OBLVLIUS.js";
|
|
10
10
|
import "./chunk-HQYXVIH6.js";
|
|
@@ -14,4 +14,4 @@ import "./chunk-HC2MT7BX.js";
|
|
|
14
14
|
export {
|
|
15
15
|
runPlan
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=plan-
|
|
17
|
+
//# sourceMappingURL=plan-Y2BYZD24.js.map
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
issueClosesLines,
|
|
4
4
|
multiIssueContext,
|
|
5
5
|
runPr
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-BXCPXRBR.js";
|
|
7
7
|
import "./chunk-3BOHTH4V.js";
|
|
8
8
|
import "./chunk-AAY6KM7N.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-EWDJENFW.js";
|
|
10
10
|
import "./chunk-HDV3YDTG.js";
|
|
11
11
|
import "./chunk-OBLVLIUS.js";
|
|
12
12
|
import "./chunk-HQYXVIH6.js";
|
|
@@ -18,4 +18,4 @@ export {
|
|
|
18
18
|
multiIssueContext,
|
|
19
19
|
runPr
|
|
20
20
|
};
|
|
21
|
-
//# sourceMappingURL=pr-
|
|
21
|
+
//# sourceMappingURL=pr-H6GLOSXF.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
exitCodeFor,
|
|
4
4
|
runPrReview
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-KQ4JQN7T.js";
|
|
6
6
|
import "./chunk-LVKQ2AKT.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-EWDJENFW.js";
|
|
8
8
|
import "./chunk-OBLVLIUS.js";
|
|
9
9
|
import "./chunk-HQYXVIH6.js";
|
|
10
10
|
import "./chunk-47VIN33V.js";
|
|
@@ -14,4 +14,4 @@ export {
|
|
|
14
14
|
exitCodeFor,
|
|
15
15
|
runPrReview
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=pr-review-
|
|
17
|
+
//# sourceMappingURL=pr-review-CK5FR6WX.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runPrd
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IVHZEYAX.js";
|
|
5
5
|
import "./chunk-3BOHTH4V.js";
|
|
6
6
|
import "./chunk-AAY6KM7N.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-EWDJENFW.js";
|
|
8
8
|
import "./chunk-HDV3YDTG.js";
|
|
9
9
|
import "./chunk-OBLVLIUS.js";
|
|
10
10
|
import "./chunk-HQYXVIH6.js";
|
|
@@ -14,4 +14,4 @@ import "./chunk-HC2MT7BX.js";
|
|
|
14
14
|
export {
|
|
15
15
|
runPrd
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=prd-
|
|
17
|
+
//# sourceMappingURL=prd-4PWLCGH5.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runReview
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-34WNTK3K.js";
|
|
5
5
|
import "./chunk-AAY6KM7N.js";
|
|
6
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-EWDJENFW.js";
|
|
7
7
|
import "./chunk-OBLVLIUS.js";
|
|
8
8
|
import "./chunk-HQYXVIH6.js";
|
|
9
9
|
import "./chunk-47VIN33V.js";
|
|
@@ -12,4 +12,4 @@ import "./chunk-HC2MT7BX.js";
|
|
|
12
12
|
export {
|
|
13
13
|
runReview
|
|
14
14
|
};
|
|
15
|
-
//# sourceMappingURL=review-
|
|
15
|
+
//# sourceMappingURL=review-QGVAHEZF.js.map
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
prReviewDir,
|
|
5
5
|
readPrReviewIndex,
|
|
6
6
|
runPrReview
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KQ4JQN7T.js";
|
|
8
8
|
import {
|
|
9
9
|
runPrd
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IVHZEYAX.js";
|
|
11
11
|
import {
|
|
12
12
|
runReview
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-34WNTK3K.js";
|
|
14
14
|
import {
|
|
15
15
|
ensureWebMonitor
|
|
16
16
|
} from "./chunk-GANSDJV7.js";
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
} from "./chunk-LVKQ2AKT.js";
|
|
29
29
|
import {
|
|
30
30
|
runPlan
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-EY6WMHV2.js";
|
|
32
32
|
import {
|
|
33
33
|
runPr
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-BXCPXRBR.js";
|
|
35
35
|
import "./chunk-3BOHTH4V.js";
|
|
36
36
|
import {
|
|
37
37
|
emptyRelations,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
resolveCommandIssue,
|
|
40
40
|
uniqueIds
|
|
41
41
|
} from "./chunk-AAY6KM7N.js";
|
|
42
|
-
import "./chunk-
|
|
42
|
+
import "./chunk-EWDJENFW.js";
|
|
43
43
|
import "./chunk-HDV3YDTG.js";
|
|
44
44
|
import {
|
|
45
45
|
beginUsageScope,
|
|
@@ -1858,4 +1858,4 @@ export {
|
|
|
1858
1858
|
publishStorySeed,
|
|
1859
1859
|
runPipeline
|
|
1860
1860
|
};
|
|
1861
|
-
//# sourceMappingURL=run-
|
|
1861
|
+
//# sourceMappingURL=run-DBF2VWUN.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/analyze.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.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 // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __ANALYSIS_PATH__: analysisPath,\n // Last: the Issue content is substituted in but never scanned again, so a\n // body that happens to contain a placeholder is left untouched.\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 30,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Analyzing issue #${issueNumber}...`,\n });\n // Before the success check: the tokens were spent either way.\n publishPhaseMetrics('analyze', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Analysis failed: ${result.error}`);\n return 1;\n }\n\n // Verify the file was created\n try {\n const content = await readFile(analysisPath, 'utf-8');\n if (content.length < 10) {\n printError('Analysis file was created but appears empty');\n return 1;\n }\n } catch {\n // File wasn't created by headless — save the result as analysis\n printInfo('Headless did not create analysis file; saving output directly');\n await writeFile(analysisPath, result.result, 'utf-8');\n }\n\n // Update pipeline state\n const tasksPath = paths.tasksFile;\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.analyzeCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist yet — that's OK for standalone analyze\n }\n\n printSuccess(`Analysis saved to ${analysisPath}`);\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,iBAAiB;AAY3C,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;AAAA;AAAA,IAGzC,GAAI,MAAM,0BAA0B;AAAA,IACpC,kBAAkB;AAAA,IAClB,mBAAmB;AAAA;AAAA;AAAA,IAGnB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,OAAO;AAAA,IACtD,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAED,sBAAoB,WAAW,OAAO,MAAM,WAAW;AAEvD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,oBAAoB,OAAO,KAAK,EAAE;AAC7C,WAAO;AAAA,EACT;AAGA,MAAI;AACF,UAAM,UAAU,MAAM,SAAS,cAAc,OAAO;AACpD,QAAI,QAAQ,SAAS,IAAI;AACvB,iBAAW,6CAA6C;AACxD,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAEN,cAAU,+DAA+D;AACzE,UAAM,UAAU,cAAc,OAAO,QAAQ,OAAO;AAAA,EACtD;AAGA,QAAM,YAAY,MAAM;AACxB,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,mBAAmB;AACjC,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,eAAa,qBAAqB,YAAY,EAAE;AAChD,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/pr.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { runHeadless } from '../core/headless.js';\nimport { runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { Issue, IssueSource, ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/**\n * Extract a PR URL from headless output.\n */\nfunction parsePrUrl(output: string): string | null {\n const match = output.match(/(https:\\/\\/github\\.com\\/[^\\s]+\\/pull\\/\\d+)/);\n return match?.[1] ?? null;\n}\n\n/**\n * The numeric id inside a PR URL, so later phases (`pr-review`) address the\n * Pull Request without querying GitHub again.\n */\nfunction parsePrNumber(url: string): number | null {\n const match = url.match(/\\/pull\\/(\\d+)/);\n return match?.[1] === undefined ? null : Number(match[1]);\n}\n\n/**\n * The `Closes #N` line for the PR body, empty when the Issue has no remote\n * counterpart: GitHub only understands the reference for Issues it hosts, and\n * an invented `#N` would silently point at an unrelated Issue.\n */\nfunction issueClosesLine(issue: Issue, fallbackId: string): string {\n if (issue.remoteRef === null) {\n return '';\n }\n return `Closes #${issue.number ?? fallbackId}`;\n}\n\n/** One Issue of a queue, as the consolidated Pull Request has to describe it. */\nexport interface PrQueueIssue {\n id: string;\n number: number | null;\n title: string;\n /** `null` when the Issue was never read — see {@link issueClosesLines}. */\n url: string | null;\n /** Origin the Issue came from; `github` is what GitHub can close. */\n source: IssueSource;\n}\n\n/**\n * What a multi-issue queue hands to the `pr` phase.\n *\n * Absent for every standalone run, and that absence is what guarantees the\n * single-issue Pull Request is byte-for-byte the one this command has always\n * produced: the extra placeholders resolve to empty strings.\n */\nexport interface PrQueueContext {\n /** Issues of the queue, in the order they were executed. */\n issues: PrQueueIssue[];\n /** Issues discovered but not executed, with the reason. */\n excluded: { id: string; number: number | null; title: string; reason: string }[];\n /** Anything else worth reporting as pending (unresolved review findings). */\n pending: string[];\n}\n\nexport interface RunPrOptions {\n /** Set only when the Pull Request consolidates a queue. */\n queue?: PrQueueContext;\n}\n\n/** `#51` when the Issue has a number, its raw identifier otherwise. */\nfunction issueRef(entry: { id: string; number: number | null }): string {\n return entry.number === null ? entry.id : `#${entry.number}`;\n}\n\n/**\n * Every `Closes #N` line of a consolidated Pull Request, one per line.\n *\n * Issues that GitHub does not host are skipped for the same reason a\n * single-issue Pull Request skips them — it cannot close what it does not\n * host — and a queue that mixes both origins still gets a valid body.\n *\n * The test is the origin and the number, not the URL: an Issue whose read\n * failed during discovery reaches the queue with `url: null`, and it is still\n * executed and still closed by the queue. Filtering on the URL would leave it\n * out of the very body that is supposed to reference it.\n */\nexport function issueClosesLines(issues: readonly PrQueueIssue[]): string {\n return issues\n .filter((entry) => entry.source === 'github' && entry.number !== null)\n .map((entry) => `Closes #${entry.number}`)\n .join('\\n');\n}\n\n/**\n * The extra instructions the prompt receives when the Pull Request covers a\n * whole queue.\n *\n * Returns `''` for a standalone run, which is what keeps `pr.md` rendering\n * exactly as before for the single-issue path — no empty \"Issues implemented\"\n * section, no redundant ordering of a list of one.\n */\nexport function multiIssueContext(queue: PrQueueContext | undefined): string {\n if (queue === undefined || queue.issues.length <= 1) {\n return '';\n }\n\n const order = queue.issues\n .map(\n (entry, index) => `${index + 1}. ${issueRef(entry)}${entry.title ? ` — ${entry.title}` : ''}`,\n )\n .join('\\n');\n\n const pending = [\n ...queue.excluded.map(\n (entry) => `- ${issueRef(entry)}${entry.title ? ` — ${entry.title}` : ''}: ${entry.reason}`,\n ),\n ...queue.pending.map((note) => `- ${note}`),\n ].join('\\n');\n\n return [\n '',\n 'This Pull Request consolidates several issues implemented on this same branch,',\n 'in this execution order:',\n '',\n order,\n '',\n 'The PR body MUST additionally contain:',\n '- an \"Issues implemented\" section listing every issue above, in that order,',\n ' with one line per issue describing what it delivered;',\n '- a \"Pending\" section with the items below, verbatim, plus anything you find',\n ' unfinished while reviewing the diff. Write \"None\" when the list below is',\n ' empty and you find nothing else;',\n '',\n pending === '' ? '(no known pending items)' : pending,\n '',\n 'The commits of this branch are scoped per issue (`feat(issue-N): …`), which is',\n 'how you tell which change belongs to which issue in `git log`.',\n ].join('\\n');\n}\n\nexport async function runPr(\n issue: string,\n resolvedIssue?: ResolvedIssue,\n options: RunPrOptions = {},\n): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n const tasksPath = paths.tasksFile;\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Get current branch\n let branchName: string;\n try {\n const proc = await execa('git', ['branch', '--show-current'], { reject: false });\n branchName = proc.stdout?.toString().trim() ?? '';\n if (!branchName) {\n printError('Could not determine current branch');\n return 1;\n }\n } catch {\n printError('Failed to get current branch');\n return 1;\n }\n\n const queue = options.queue;\n const consolidating = queue !== undefined && queue.issues.length > 1;\n\n const template = await loadPrompt('pr');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __BRANCH_NAME__: branchName,\n __TASKS_PATH__: tasksPath,\n __ISSUE_CLOSES__: consolidating\n ? issueClosesLines(queue.issues)\n : issueClosesLine(resolution.resolved.issue, issueNumber),\n __MULTI_ISSUE_CONTEXT__: multiIssueContext(queue),\n ...issuePlaceholders(resolution.resolved),\n });\n\n let headlessOutput = '';\n\n const outcome = await runPhaseWithRetry({\n phase: 'pr',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 15,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text parsePrUrl() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Creating PR for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('pr', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `PR creation failed: ${result.error}`,\n };\n }\n\n headlessOutput = result.result;\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `PR creation failed for issue #${issueNumber}`);\n return 1;\n }\n\n const prUrl = parsePrUrl(headlessOutput);\n\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prCreated = true;\n const prNumber = prUrl === null ? null : parsePrNumber(prUrl);\n // Without a URL the PR is still assumed created (the phase succeeded), but\n // there is nothing trustworthy to record: an invented number would send\n // `pr-review` at an unrelated Pull Request.\n if (prUrl !== null && prNumber !== null) {\n plan.pullRequest = {\n number: prNumber,\n url: prUrl,\n headBranch: branchName,\n createdAt: isoNow(),\n };\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n if (prUrl) {\n printSuccess(`PR created: ${prUrl}`);\n } else {\n printSuccess('PR creation completed');\n }\n return 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
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 { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.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 // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\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 startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 40,\n timeout: getGlobalTimeout() ?? 900_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text the parsers below already consumed.\n outputFormat: 'json',\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 // Before the success check: the tokens were spent either way.\n publishPhaseMetrics('pr-review', result.cost, startedAtMs);\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"]}
|
|
@@ -1 +0,0 @@
|
|
|
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 { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.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 // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\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 startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Generating PRD for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('prd', result.cost, startedAtMs);\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;AActB,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;AAAA;AAAA,IAGzC,GAAI,MAAM,0BAA0B;AAAA,IACpC,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,cAAc,KAAK,IAAI;AAC7B,YAAM,SAAS,MAAM,YAAY;AAAA,QAC/B;AAAA,QACA,UAAU;AAAA,QACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,QAG/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,0BAAoB,OAAO,OAAO,MAAM,WAAW;AAEnD,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":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/headless.ts"],"sourcesContent":["import { createInterface } from 'node:readline';\nimport chalk from 'chalk';\nimport { execa } from 'execa';\nimport { createSpinner, ElapsedTimer, formatDuration, getIcons, useColor } from '../ui/logger.js';\nimport { type ClaudeUsage, parseUsage } from './metrics.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { isoNow } from './state-manager.js';\nimport { getOutputCallback, isVerbose } from './verbose.js';\n\nexport interface HeadlessOptions {\n prompt: string;\n maxTurns?: number;\n timeout?: number;\n outputFormat?: 'json' | 'text' | 'stream-json';\n allowedTools?: string[];\n /**\n * Extra directories the headless session may read from and write to, passed\n * through as `--add-dir`. Required whenever a prompt points at a path outside\n * the working tree (e.g. the global issue storage under `~/.issue-flow`),\n * since `claude -p` otherwise refuses the access.\n */\n addDirs?: string[];\n /** Status message displayed as spinner (non-verbose) or header (verbose). */\n statusMessage?: string;\n /** Optional callback for routing verbose output (e.g., through listr2 task.output). When provided, verbose stream events are sent here instead of directly to stderr. */\n onOutput?: (line: string) => void;\n}\n\n/**\n * Token/cost metrics of a headless invocation.\n *\n * Alias of {@link ClaudeUsage} — the name is kept for the existing call sites,\n * but the shape (and the parsing behind it) now lives in core/metrics.ts, so\n * cache tokens and USD cost come along for free.\n */\nexport type HeadlessCost = ClaudeUsage;\n\nexport interface HeadlessResult {\n success: boolean;\n result: string;\n cost: HeadlessCost | null;\n error: string | null;\n}\n\n/* ── argument helpers ───────────────────────────────────────────────────── */\n\n/**\n * Append one `<flag> <value>` pair per value. An empty or absent list leaves\n * `args` untouched, so callers that pass nothing keep the exact same argv.\n */\nfunction pushRepeatedFlag(args: string[], flag: string, values: string[] | undefined): void {\n if (!values || values.length === 0) return;\n for (const value of values) {\n args.push(flag, value);\n }\n}\n\n/* ── verbose stream formatting ──────────────────────────────────────────── */\n\n/**\n * Extract a short context string from a tool_use input object.\n */\nfunction getToolContext(name: string, input: Record<string, unknown>): string {\n switch (name) {\n case 'Read':\n return shortPath(input.file_path as string);\n case 'Write':\n case 'Edit':\n return shortPath(input.file_path as string);\n case 'Glob':\n return (input.pattern as string) ?? '';\n case 'Grep':\n return (input.pattern as string) ?? '';\n case 'Bash': {\n const cmd = (input.command as string) ?? '';\n return cmd.length > 60 ? `${cmd.substring(0, 57)}...` : cmd;\n }\n default:\n return '';\n }\n}\n\n/**\n * Shorten a file path to be relative-friendly.\n */\nfunction shortPath(filePath: string | undefined): string {\n if (!filePath) return '';\n const cwd = process.cwd();\n if (filePath.startsWith(cwd)) {\n return filePath.substring(cwd.length + 1);\n }\n // Show last 2 segments for absolute paths\n const parts = filePath.split('/');\n if (parts.length > 2) {\n return `.../${parts.slice(-2).join('/')}`;\n }\n return filePath;\n}\n\n/**\n * Print a formatted stream event line to stderr.\n */\nfunction printStreamEvent(\n line: string,\n state: { turnCount: number },\n onOutput?: (line: string) => void,\n): void {\n let event: {\n type?: string;\n subtype?: string;\n message?: {\n content?: { type: string; text?: string; name?: string; input?: Record<string, unknown> }[];\n };\n };\n\n try {\n event = JSON.parse(line);\n } catch {\n return;\n }\n\n const icons = getIcons();\n const colored = useColor();\n const emit = onOutput ?? ((msg: string) => process.stderr.write(`${msg}\\n`));\n\n if (event.type === 'assistant' && event.message?.content) {\n state.turnCount++;\n\n for (const block of event.message.content) {\n if (block.type === 'text' && block.text) {\n // Wrap text with connector prefix\n const lines = block.text.split('\\n');\n for (const textLine of lines) {\n if (!textLine.trim()) continue;\n const prefix = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;\n const text = colored ? chalk.dim(textLine) : textLine;\n emit(`${prefix}${text}`);\n }\n }\n\n if (block.type === 'tool_use' && block.name) {\n const context = block.input ? getToolContext(block.name, block.input) : '';\n getSessionPublisher().publish({\n type: 'activity',\n at: isoNow(),\n tool: block.name,\n detail: context || undefined,\n });\n const toolName = block.name.padEnd(12);\n\n const prefix = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;\n const toolIcon = colored ? chalk.cyan(icons.tool) : icons.tool;\n const toolLabel = colored ? chalk.cyan(toolName) : toolName;\n const contextText = context ? (colored ? chalk.dim(context) : context) : '';\n\n emit(`${prefix}${toolIcon} ${toolLabel} ${contextText}`);\n }\n }\n }\n}\n\n/* ── verbose execution ──────────────────────────────────────────────────── */\n\n/**\n * Run headless in verbose mode using stream-json to display real-time progress.\n */\nasync function runHeadlessVerbose(options: {\n prompt: string;\n maxTurns: number;\n timeout: number;\n allowedTools?: string[];\n addDirs?: string[];\n statusMessage?: string;\n onOutput?: (line: string) => void;\n}): Promise<HeadlessResult> {\n const { prompt, maxTurns, timeout, allowedTools, addDirs, statusMessage, onOutput } = options;\n const icons = getIcons();\n const colored = useColor();\n const emit = onOutput ?? ((msg: string) => process.stderr.write(`${msg}\\n`));\n\n // Print header\n if (statusMessage) {\n const msg = colored\n ? chalk.blue(`${icons.start} ${statusMessage}`)\n : `${icons.start} ${statusMessage}`;\n emit(msg);\n }\n\n // Print opening connector\n const connectorLine = colored ? chalk.dim(` ${icons.connector}`) : ` ${icons.connector}`;\n emit(connectorLine);\n\n const startTime = Date.now();\n\n const args: string[] = [\n '-p',\n prompt,\n '--output-format',\n 'stream-json',\n '--verbose',\n '--max-turns',\n String(maxTurns),\n ];\n\n pushRepeatedFlag(args, '--allowedTools', allowedTools);\n pushRepeatedFlag(args, '--add-dir', addDirs);\n\n const subprocess = execa('claude', args, {\n stdin: 'ignore',\n reject: false,\n timeout,\n stripFinalNewline: false,\n });\n\n let resultText = '';\n let isError = false;\n let costData: HeadlessCost | null = null;\n const state = { turnCount: 0 };\n\n if (subprocess.stdout) {\n const rl = createInterface({ input: subprocess.stdout });\n for await (const line of rl) {\n printStreamEvent(line, state, onOutput);\n\n try {\n const event = JSON.parse(line);\n if (event.type === 'result') {\n resultText = event.result ?? '';\n isError = event.is_error === true;\n // Keep the previous metrics when this event carries none, so a\n // malformed trailing result never erases what was already captured.\n costData = parseUsage(event) ?? costData;\n }\n } catch {\n // ignore malformed lines\n }\n }\n }\n\n // Wait for the process to finish\n const proc = await subprocess;\n\n // Close connector with elapsed time\n const elapsedSec = Math.floor((Date.now() - startTime) / 1000);\n const durationStr = formatDuration(elapsedSec);\n const doneLine = colored\n ? chalk.dim(` ${icons.connector} ${chalk.italic(`Done in ${durationStr}`)}`)\n : ` ${icons.connector} Done in ${durationStr}`;\n emit(doneLine);\n emit(connectorLine);\n\n if (proc.exitCode !== 0 && !resultText) {\n const stderr = proc.stderr?.toString() ?? '';\n const isTimeout =\n proc.exitCode === 143 || ('signalName' in proc && proc.signalName === 'SIGTERM');\n const errorMsg = isTimeout\n ? `Headless invocation timed out after ${timeout}ms`\n : stderr || `claude exited with code ${proc.exitCode}`;\n return {\n success: false,\n result: '',\n cost: null,\n error: errorMsg,\n };\n }\n\n return {\n success: !isError,\n result: resultText,\n cost: costData,\n error: isError ? resultText : null,\n };\n}\n\n/* ── standard execution ─────────────────────────────────────────────────── */\n\n/**\n * Invoke Claude Code in headless mode via `claude -p`.\n *\n * Each invocation is an isolated session — no context is shared between calls.\n * Output is parsed as JSON when outputFormat is 'json' (default).\n *\n * When verbose mode is active, uses stream-json to display real-time progress.\n * Otherwise, shows a spinner while waiting.\n */\nexport async function runHeadless(options: HeadlessOptions): Promise<HeadlessResult> {\n const {\n prompt,\n maxTurns = 10,\n timeout = 300_000,\n outputFormat = 'json',\n allowedTools,\n addDirs,\n statusMessage,\n onOutput,\n } = options;\n\n if (isVerbose()) {\n // Use explicit onOutput, fall back to global output callback, or default to stderr\n const effectiveOnOutput = onOutput ?? getOutputCallback();\n return runHeadlessVerbose({\n prompt,\n maxTurns,\n timeout,\n allowedTools,\n addDirs,\n statusMessage,\n onOutput: effectiveOnOutput,\n });\n }\n\n // Non-verbose: use spinner with elapsed timer\n const spinner = statusMessage ? createSpinner(statusMessage).start() : null;\n\n let timer: ElapsedTimer | null = null;\n if (spinner) {\n timer = new ElapsedTimer((elapsed) => {\n spinner.suffixText = useColor() ? chalk.dim(`(${elapsed})`) : `(${elapsed})`;\n }).start();\n }\n\n const args: string[] = [\n '-p',\n prompt,\n '--output-format',\n outputFormat,\n '--max-turns',\n String(maxTurns),\n ];\n\n pushRepeatedFlag(args, '--allowedTools', allowedTools);\n pushRepeatedFlag(args, '--add-dir', addDirs);\n\n try {\n const proc = await execa('claude', args, {\n stdin: 'ignore',\n reject: false,\n timeout,\n stripFinalNewline: false,\n });\n\n const stdout = proc.stdout?.toString() ?? '';\n const stderr = proc.stderr?.toString() ?? '';\n\n if (proc.exitCode !== 0) {\n const elapsed = timer?.stop() ?? 0;\n const dur = useColor()\n ? chalk.dim(` (${formatDuration(elapsed)})`)\n : ` (${formatDuration(elapsed)})`;\n spinner?.fail(`${statusMessage}${dur}`);\n return {\n success: false,\n result: '',\n cost: null,\n error: stderr || stdout || `claude exited with code ${proc.exitCode}`,\n };\n }\n\n const elapsed = timer?.stop() ?? 0;\n const dur = useColor()\n ? chalk.dim(` (${formatDuration(elapsed)})`)\n : ` (${formatDuration(elapsed)})`;\n spinner?.succeed(`${statusMessage}${dur}`);\n\n if (outputFormat === 'json') {\n try {\n const parsed = JSON.parse(stdout);\n return {\n success: true,\n result: parsed.result ?? stdout,\n cost: parseUsage(parsed),\n error: null,\n };\n } catch {\n return {\n success: true,\n result: stdout,\n cost: null,\n error: null,\n };\n }\n }\n\n return {\n success: true,\n result: stdout,\n cost: null,\n error: null,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const catchElapsed = timer?.stop() ?? 0;\n const catchDur = useColor()\n ? chalk.dim(` (${formatDuration(catchElapsed)})`)\n : ` (${formatDuration(catchElapsed)})`;\n spinner?.fail(`${statusMessage}${catchDur}`);\n\n if (message.includes('timed out') || message.includes('ETIMEDOUT')) {\n return {\n success: false,\n result: '',\n cost: null,\n error: `Headless invocation timed out after ${timeout}ms`,\n };\n }\n\n return {\n success: false,\n result: '',\n cost: null,\n error: message,\n };\n }\n}\n"],"mappings":"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","names":["elapsed","dur"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/plan.ts","../src/storage/user-story-numbering.ts"],"sourcesContent":["import { mkdir, readFile } 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 { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { taskPlanSchema } from '../schemas.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport {\n determineUserStoryNumbering,\n formatUserStoryId,\n parseUserStoryNumber,\n} from '../storage/user-story-numbering.js';\nimport { printError, printInfo, printSuccess, printWarning } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/** `--continue` / `--start-us <n>` override of the numbering cascade (issue #36). */\nexport interface PlanUserStoryNumberingOptions {\n continueFlag?: boolean;\n startUs?: number;\n}\n\nexport async function runPlan(\n issue: string,\n resolvedIssue?: ResolvedIssue,\n numbering?: PlanUserStoryNumberingOptions,\n): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Read the PRD\n const prdPath = paths.prdFile;\n let prdContent: string;\n try {\n prdContent = await readFile(prdPath, 'utf-8');\n } catch {\n printError(`PRD not found at ${prdPath}. Run 'issue-flow prd ${issueNumber}' first.`);\n return 1;\n }\n\n const tasksPath = paths.tasksFile;\n\n // Resolve the User Story numbering continuity (issue #36), log where the\n // decision came from — never silently — and persist it into the project's\n // metadata.json for audit before the prompt is even built, so the record on\n // disk always matches what the prompt was told.\n const {\n message: numberingMessage,\n nextUserStoryId,\n decision: { nextNumber },\n } = await determineUserStoryNumbering({\n issueNumber,\n continueFlag: numbering?.continueFlag,\n startUs: numbering?.startUs,\n });\n printInfo(numberingMessage);\n\n const template = await loadPrompt('plan');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __PRD_CONTENT__: prdContent,\n __TASKS_PATH__: tasksPath,\n __NEXT_US_NUMBER__: nextUserStoryId,\n ...issuePlaceholders(resolution.resolved),\n });\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const outcome = await runPhaseWithRetry({\n phase: 'plan',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Converting PRD to task plan for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('plan', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `Task plan generation failed: ${result.error}`,\n };\n }\n\n // Verify the file was created, tolerating a brief FS-visibility lag.\n let rawContent: string;\n try {\n rawContent = await readFileWithGrace(tasksPath);\n } catch {\n return { ok: false, transient: true, error: `tasks.json was not created at ${tasksPath}` };\n }\n\n // Validate JSON structure — a content defect, not a timing issue, but\n // still worth a bounded retry since it's a fresh Claude invocation.\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawContent);\n } catch {\n return { ok: false, transient: true, error: 'tasks.json contains invalid JSON' };\n }\n\n // Validate with zod schema\n const validation = taskPlanSchema.safeParse(parsed);\n if (!validation.success) {\n const issues = validation.error.issues\n .map((i) => ` - ${i.path.join('.')}: ${i.message}`)\n .join('\\n');\n return {\n ok: false,\n transient: true,\n error: `tasks.json does not match expected schema:\\n${issues}`,\n };\n }\n\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `Task plan generation failed for issue #${issueNumber}`);\n return 1;\n }\n\n // Ensure pipeline state reflects completion of this phase\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.jsonCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n\n // The numbering is a prompt instruction, not a programmatic rewrite, so the\n // generated plan can still ignore it. Say so out loud instead of leaving the\n // log and metadata.json claiming a numbering the file on disk contradicts.\n const generatedLowest = plan.userStories\n .map((story) => parseUserStoryNumber(story.id))\n .filter((value): value is number => value !== null)\n .reduce<number | null>(\n (lowest, value) => (lowest === null ? value : Math.min(lowest, value)),\n null,\n );\n if (generatedLowest !== null && generatedLowest < nextNumber) {\n printWarning(\n `The generated plan starts at ${formatUserStoryId(generatedLowest)}, not the requested ` +\n `${nextUserStoryId}. User Story ids may collide with earlier issues of this project — ` +\n `re-run 'issue-flow plan ${issueNumber} --start-us ${nextNumber}' if that matters.`,\n );\n }\n\n printSuccess(`Task plan saved to ${tasksPath} (${plan.userStories.length} stories)`);\n return 0;\n}\n","import { readdir, readFile } from 'node:fs/promises';\nimport { isoNow } from '../core/state-manager.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport { recordUserStoryNumbering } from './compat.js';\nimport { getIssuePaths } from './paths.js';\nimport { type ResolveIssuePathsOptions, resolveProjectPaths } from './resolve.js';\nimport type { UserStoryNumberingDecision } from './schemas.js';\n\n/**\n * Continuity of `US-NNN` numbering across `plan` executions of the same\n * project (issue #36).\n *\n * The User Story `id` field is free-form — `userStorySchema` in\n * `src/schemas.ts` only requires a non-empty string, no `US-NNN` regex is\n * enforced — so every reader here tolerates an id that does not carry a\n * trailing number instead of throwing on it.\n */\n\nconst USER_STORY_ID_PAD = 3;\n\n/** `16` -> `'US-016'`. Numbers past 999 are not padded further. */\nexport function formatUserStoryId(n: number): string {\n return `US-${String(n).padStart(USER_STORY_ID_PAD, '0')}`;\n}\n\n/**\n * Extract the last run of digits from a User Story id, or `null` when none is\n * found. Tolerant by design: an id like `US-001` yields `1`, but so does a\n * free-form one such as `story-1` or `v2-US-007` (the last digit run, `007`,\n * wins over the `2` version prefix) — only an id with no digits at all (e.g.\n * `add-auth`) is ignored.\n */\nexport function parseUserStoryNumber(id: string): number | null {\n const matches = id.match(/\\d+/g);\n if (!matches || matches.length === 0) return null;\n\n const value = Number.parseInt(matches[matches.length - 1], 10);\n return Number.isNaN(value) ? null : value;\n}\n\nexport interface HighestUserStoryNumberResult {\n number: number;\n /** Issue directory name the winning story came from. */\n issueId: string;\n /** Verbatim id of the winning story. */\n storyId: string;\n}\n\n/**\n * Loose read of one `tasks.json`: only cares about `userStories[].id` and\n * tolerates every failure (missing file, invalid JSON, unexpected shape) by\n * returning `null` — a sibling issue mid-migration, or with a corrupt plan,\n * must not abort the whole project scan.\n */\nasync function readUserStoryIds(tasksPath: string): Promise<string[] | null> {\n let raw: string;\n try {\n raw = await readFile(tasksPath, 'utf-8');\n } catch {\n return null;\n }\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !Array.isArray((parsed as { userStories?: unknown }).userStories)\n ) {\n return null;\n }\n\n return (parsed as { userStories: unknown[] }).userStories\n .map((story) =>\n typeof story === 'object' &&\n story !== null &&\n typeof (story as { id?: unknown }).id === 'string'\n ? (story as { id: string }).id\n : null,\n )\n .filter((id): id is string => id !== null);\n } catch {\n return null;\n }\n}\n\nexport interface FindHighestUserStoryNumberOptions extends ResolveIssuePathsOptions {\n /**\n * Issue directory to leave out of the scan — the issue the numbering is\n * being resolved for. Re-planning an issue overwrites its own `tasks.json`,\n * so counting the plan about to be replaced would push the numbering forward\n * on every `plan` re-run of the same issue.\n */\n excludeIssueId?: string;\n}\n\n/**\n * Highest User Story number already used anywhere in the project, scanning\n * every `<issuesDir>/<id>/tasks.json` regardless of which issue produced it —\n * that project-wide scope is what makes `US-NNN` unambiguous across issues.\n * The issue named by `excludeIssueId` is skipped, so re-planning is idempotent.\n *\n * `null` means no readable `tasks.json` was found (the project's first `plan`\n * run ever, or a brand-new project directory). A storage directory that exists\n * but cannot be read throws instead: silently restarting at `US-001` because\n * `~/.issue-flow` was momentarily unreadable is the worst outcome this feature\n * can produce.\n */\nexport async function findHighestUserStoryNumber(\n options: FindHighestUserStoryNumberOptions = {},\n): Promise<HighestUserStoryNumberResult | null> {\n const { excludeIssueId, ...rootOptions } = options;\n const { projectId, issuesDir } = await resolveProjectPaths(rootOptions);\n\n let entries: string[];\n try {\n entries = (await readdir(issuesDir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n // ENOENT is the legitimate \"no history yet\" case. Any other failure\n // (EACCES, EIO, an unavailable mount) must not be reported as an empty\n // history, or the numbering would restart silently.\n if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return null;\n throw new Error(\n `Could not scan the project's User Story history at ${issuesDir}: ` +\n `${(error as Error).message}. Fix the storage directory, or pass --start-us <n> ` +\n 'to set the numbering explicitly.',\n { cause: error },\n );\n }\n\n let best: HighestUserStoryNumberResult | null = null;\n\n for (const issueId of entries) {\n if (excludeIssueId !== undefined && issueId === excludeIssueId) continue;\n // Never hand-build an artifact path (see src/storage/AGENTS.md): asking\n // getIssuePaths() keeps a rename of `tasks.json` a one-file change instead\n // of silently making this scan find nothing.\n const { tasksFile } = getIssuePaths(projectId, issueId, { env: options.env });\n const ids = await readUserStoryIds(tasksFile);\n if (!ids) continue;\n\n for (const id of ids) {\n const value = parseUserStoryNumber(id);\n if (value === null) continue;\n if (best === null || value > best.number) {\n best = { number: value, issueId, storyId: id };\n }\n }\n }\n\n return best;\n}\n\nexport interface ResolveUserStoryNumberingOptions extends ResolveIssuePathsOptions {\n /** Issue the decision is being made for — carried into the audit record. */\n issueNumber: string;\n /** `--continue` was passed explicitly. Purely cosmetic on the log message:\n * the resolved number is identical to the automatic default. */\n continueFlag?: boolean;\n /** `--start-us <n>` was passed. Wins outright over any history. */\n startUs?: number;\n /** Timestamp source, injectable for tests. */\n now?: () => string;\n}\n\nexport interface UserStoryNumberingOutcome {\n decision: UserStoryNumberingDecision;\n /** Ready-to-print explanation of where the decision came from — the CLI\n * must never resolve this silently (issue #36). */\n message: string;\n}\n\n/**\n * Resolve the numbering cascade of #36:\n *\n * 1. `--start-us <n>` wins outright — history is not even scanned, since the\n * whole point is to ignore it.\n * 2. Otherwise the project's history is scanned for the highest number used\n * so far. This is what running with no flag at all does automatically,\n * and what `--continue` names explicitly (the two produce the same\n * number; the flag only changes the log message).\n * 3. No history found (the project's first `plan` ever) starts at `US-001`.\n *\n * Pure with respect to `metadata.json` — this only reads `tasks.json` files,\n * never writes. Persisting the decision is {@link determineUserStoryNumbering}\n * below, via `recordUserStoryNumbering()` in `compat.ts`.\n */\nexport async function resolveUserStoryNumbering(\n options: ResolveUserStoryNumberingOptions,\n): Promise<UserStoryNumberingOutcome> {\n const { issueNumber, continueFlag, startUs, now, ...rootOptions } = options;\n const decidedAt = (now ?? isoNow)();\n\n if (startUs !== undefined) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: startUs,\n source: 'start-us',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `User Story numbering forced to ${formatUserStoryId(startUs)} via --start-us.`,\n };\n }\n\n // The issue being planned is excluded from the scan: `plan` overwrites its\n // own `tasks.json`, so counting the plan it is about to replace would push\n // the numbering forward on every re-run of `plan` for the same issue.\n const highest = await findHighestUserStoryNumber({ ...rootOptions, excludeIssueId: issueNumber });\n\n if (highest === null) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: 1,\n source: 'none',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `Starting User Story numbering at ${formatUserStoryId(1)} (no previous history found for this project).`,\n };\n }\n\n const nextNumber = highest.number + 1;\n const decision: UserStoryNumberingDecision = {\n nextNumber,\n source: 'history',\n issueNumber,\n decidedAt,\n detail: `${highest.storyId} (issue #${highest.issueId})`,\n };\n const via = continueFlag ? ' via --continue' : '';\n return {\n decision,\n message:\n `Continuing User Story numbering from ${formatUserStoryId(nextNumber)}${via} ` +\n `— last used was ${highest.storyId} (issue #${highest.issueId}).`,\n };\n}\n\nexport interface DetermineUserStoryNumberingOptions extends ResolveUserStoryNumberingOptions {\n /** Repository root. Defaults to the git toplevel of the current directory. */\n projectRoot?: string;\n}\n\nexport interface UserStoryNumberingResult extends UserStoryNumberingOutcome {\n /** `decision.nextNumber` formatted as `US-NNN`, ready for the prompt. */\n nextUserStoryId: string;\n}\n\n/**\n * The entry point commands use: resolve the cascade and persist the decision\n * into `metadata.json` in one call, so no call site can log/prompt with one\n * decision and audit a different one.\n */\nexport async function determineUserStoryNumbering(\n options: DetermineUserStoryNumberingOptions,\n): Promise<UserStoryNumberingResult> {\n const { projectRoot: projectRootOption, ...rest } = options;\n const projectRoot = projectRootOption ?? (await getProjectRoot());\n\n const outcome = await resolveUserStoryNumbering({ ...rest, projectRoot });\n await recordUserStoryNumbering(projectRoot, outcome.decision, {\n env: options.env,\n now: options.now,\n });\n\n return { ...outcome, nextUserStoryId: formatUserStoryId(outcome.decision.nextNumber) };\n}\n"],"mappings":"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","names":["readFile","decision","readFile"]}
|