issue-flow 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -274
- package/dist/agent-OX3KPHE4.js +30 -0
- package/dist/{analyze-OEET7KGE.js → analyze-WIND2SEB.js} +31 -16
- package/dist/analyze-WIND2SEB.js.map +1 -0
- package/dist/{apply-ZIA3C52S.js → apply-JNOI2LR4.js} +18 -7
- package/dist/{apply-ZIA3C52S.js.map → apply-JNOI2LR4.js.map} +1 -1
- package/dist/bench-B4VZON7E.js +910 -0
- package/dist/bench-B4VZON7E.js.map +1 -0
- package/dist/{chunk-FADTCQGM.js → chunk-2IQ6M4ZE.js} +94 -32
- package/dist/chunk-2IQ6M4ZE.js.map +1 -0
- package/dist/chunk-3BWZO4YI.js +141 -0
- package/dist/chunk-3BWZO4YI.js.map +1 -0
- package/dist/chunk-3WFZETIM.js +163 -0
- package/dist/chunk-3WFZETIM.js.map +1 -0
- package/dist/chunk-67YIU2VB.js +362 -0
- package/dist/chunk-67YIU2VB.js.map +1 -0
- package/dist/{chunk-QCPGHBSZ.js → chunk-6BENHR2I.js} +63 -21
- package/dist/chunk-6BENHR2I.js.map +1 -0
- package/dist/{chunk-BYZDPR7N.js → chunk-6QYMSNVA.js} +194 -235
- package/dist/chunk-6QYMSNVA.js.map +1 -0
- package/dist/{chunk-Y5ZDJ4PJ.js → chunk-76MMPJ53.js} +20 -10
- package/dist/chunk-76MMPJ53.js.map +1 -0
- package/dist/chunk-7Z4Y3VKM.js +26 -0
- package/dist/chunk-7Z4Y3VKM.js.map +1 -0
- package/dist/{chunk-3QHK4YQA.js → chunk-B72RQZBV.js} +346 -119
- package/dist/chunk-B72RQZBV.js.map +1 -0
- package/dist/chunk-BEZ5BA5U.js +625 -0
- package/dist/chunk-BEZ5BA5U.js.map +1 -0
- package/dist/chunk-BRCNQXYQ.js +74 -0
- package/dist/chunk-BRCNQXYQ.js.map +1 -0
- package/dist/chunk-CGKLE6WR.js +1203 -0
- package/dist/chunk-CGKLE6WR.js.map +1 -0
- package/dist/chunk-CRGIQFUQ.js +83 -0
- package/dist/chunk-CRGIQFUQ.js.map +1 -0
- package/dist/{chunk-NS6R37II.js → chunk-F6TOYSYY.js} +141 -12
- package/dist/chunk-F6TOYSYY.js.map +1 -0
- package/dist/chunk-G5EK2XRN.js +25 -0
- package/dist/chunk-G5EK2XRN.js.map +1 -0
- package/dist/chunk-HAKDLR6N.js +134 -0
- package/dist/chunk-HAKDLR6N.js.map +1 -0
- package/dist/chunk-J3SOTUVI.js +298 -0
- package/dist/chunk-J3SOTUVI.js.map +1 -0
- package/dist/chunk-JN6D3MY6.js +30 -0
- package/dist/chunk-JN6D3MY6.js.map +1 -0
- package/dist/{chunk-LAFNLLQP.js → chunk-KMRFRXIT.js} +5 -5
- package/dist/chunk-KT7JKOXB.js +72 -0
- package/dist/chunk-KT7JKOXB.js.map +1 -0
- package/dist/chunk-L6HSKTQE.js +77 -0
- package/dist/chunk-L6HSKTQE.js.map +1 -0
- package/dist/chunk-LBGHI4S2.js +1264 -0
- package/dist/chunk-LBGHI4S2.js.map +1 -0
- package/dist/chunk-LEW4QM74.js +180 -0
- package/dist/chunk-LEW4QM74.js.map +1 -0
- package/dist/{chunk-FJP3UQGD.js → chunk-LFEQIZLB.js} +115 -20
- package/dist/chunk-LFEQIZLB.js.map +1 -0
- package/dist/chunk-OOVG2KBY.js +77 -0
- package/dist/chunk-OOVG2KBY.js.map +1 -0
- package/dist/{chunk-56KS3TFQ.js → chunk-OR43364P.js} +115 -160
- package/dist/chunk-OR43364P.js.map +1 -0
- package/dist/chunk-P3LRUNC6.js +16 -0
- package/dist/chunk-P3LRUNC6.js.map +1 -0
- package/dist/{chunk-TSOLWK7Y.js → chunk-PBTA3QQX.js} +8 -7
- package/dist/{chunk-TSOLWK7Y.js.map → chunk-PBTA3QQX.js.map} +1 -1
- package/dist/{chunk-MJULIKQU.js → chunk-PHW2YV6N.js} +23 -47
- package/dist/chunk-PHW2YV6N.js.map +1 -0
- package/dist/{chunk-73ATPIYX.js → chunk-PZTWU277.js} +36 -71
- package/dist/chunk-PZTWU277.js.map +1 -0
- package/dist/{chunk-M5UCXTIT.js → chunk-QD4VOILZ.js} +22 -18
- package/dist/chunk-QD4VOILZ.js.map +1 -0
- package/dist/chunk-QJ5UZAH5.js +2718 -0
- package/dist/chunk-QJ5UZAH5.js.map +1 -0
- package/dist/chunk-QK7WUO7A.js +283 -0
- package/dist/chunk-QK7WUO7A.js.map +1 -0
- package/dist/{chunk-A4SLHOW7.js → chunk-QZVKRDJY.js} +27 -19
- package/dist/chunk-QZVKRDJY.js.map +1 -0
- package/dist/chunk-REO3PXXO.js +71 -0
- package/dist/chunk-REO3PXXO.js.map +1 -0
- package/dist/{chunk-EZATO3DW.js → chunk-RVO7L4FX.js} +594 -17
- package/dist/chunk-RVO7L4FX.js.map +1 -0
- package/dist/{chunk-XQUSIEKA.js → chunk-SZMHCEYV.js} +63 -14
- package/dist/chunk-SZMHCEYV.js.map +1 -0
- package/dist/chunk-TYRNTSPJ.js +243 -0
- package/dist/chunk-TYRNTSPJ.js.map +1 -0
- package/dist/chunk-UJ3L2MS7.js +238 -0
- package/dist/chunk-UJ3L2MS7.js.map +1 -0
- package/dist/chunk-V7MO5EDS.js +120 -0
- package/dist/chunk-V7MO5EDS.js.map +1 -0
- package/dist/chunk-WB2X37MO.js +415 -0
- package/dist/chunk-WB2X37MO.js.map +1 -0
- package/dist/{chunk-NTMMF5UD.js → chunk-X44FKNKP.js} +525 -252
- package/dist/chunk-X44FKNKP.js.map +1 -0
- package/dist/chunk-XEYH6ZZI.js +289 -0
- package/dist/chunk-XEYH6ZZI.js.map +1 -0
- package/dist/cli.js +267 -44
- package/dist/cli.js.map +1 -1
- package/dist/{config-PRJGWW7J.js → config-HMFMCY2M.js} +31 -5
- package/dist/contract-JH2WBIO6.js +8 -0
- package/dist/conventions-NJF2OPVQ.js +146 -0
- package/dist/conventions-NJF2OPVQ.js.map +1 -0
- package/dist/diagnostics-THH42ZMP.js +23 -0
- package/dist/execute-KFNNZZHI.js +35 -0
- package/dist/{generate-EGB4KV2B.js → generate-FW5C6NP7.js} +37 -20
- package/dist/{generate-EGB4KV2B.js.map → generate-FW5C6NP7.js.map} +1 -1
- package/dist/git-W72QQ6GT.js +33 -0
- package/dist/init-HTN4V67Q.js +24 -0
- package/dist/{operations-BYT626KQ.js → operations-BCFOBH42.js} +27 -15
- package/dist/{operations-BYT626KQ.js.map → operations-BCFOBH42.js.map} +1 -1
- package/dist/permissions-4AKNQZHK.js +18 -0
- package/dist/plan-SIZLGA4V.js +34 -0
- package/dist/{policy-6S4GFC76.js → policy-DT6Q6ZVM.js} +20 -6
- package/dist/policy-DT6Q6ZVM.js.map +1 -0
- package/dist/pr-OMWCGUOW.js +39 -0
- package/dist/pr-review-N24CZPKR.js +32 -0
- package/dist/pr-review-N24CZPKR.js.map +1 -0
- package/dist/prd-5OFTZAHJ.js +33 -0
- package/dist/prd-5OFTZAHJ.js.map +1 -0
- package/dist/ps-RFORTCUD.js +93 -0
- package/dist/ps-RFORTCUD.js.map +1 -0
- package/dist/recorder-KCCPMKQG.js +32 -0
- package/dist/recorder-KCCPMKQG.js.map +1 -0
- package/dist/registry-GLPXOMFG.js +15 -0
- package/dist/registry-GLPXOMFG.js.map +1 -0
- package/dist/{resume-PPCNQVVV.js → resume-M5RLKP4I.js} +55 -34
- package/dist/{resume-PPCNQVVV.js.map → resume-M5RLKP4I.js.map} +1 -1
- package/dist/review-YM4JAABY.js +35 -0
- package/dist/review-YM4JAABY.js.map +1 -0
- package/dist/routing-NNT5WKGX.js +94 -0
- package/dist/routing-NNT5WKGX.js.map +1 -0
- package/dist/run-PZUKHPQK.js +54 -0
- package/dist/run-PZUKHPQK.js.map +1 -0
- package/dist/session-publisher-T7EI3CIP.js +11 -0
- package/dist/session-publisher-T7EI3CIP.js.map +1 -0
- package/dist/{state-manager-2EDVDBAR.js → state-manager-GQH32C44.js} +4 -2
- package/dist/state-manager-GQH32C44.js.map +1 -0
- package/dist/usage-V2CHE5PY.js +177 -0
- package/dist/usage-V2CHE5PY.js.map +1 -0
- package/dist/{web-EF5WIIGG.js → web-EA3KGNFN.js} +45 -30
- package/dist/web-EA3KGNFN.js.map +1 -0
- package/package.json +6 -2
- package/prompts/execute.md +7 -8
- package/prompts/plan.md +4 -5
- package/prompts/pr.md +2 -2
- package/web/AGENTS.md +152 -13
- package/web/public/app.css +380 -109
- package/web/public/app.js +525 -25
- package/web/public/index.html +68 -1
- package/dist/analyze-OEET7KGE.js.map +0 -1
- package/dist/chunk-3QHK4YQA.js.map +0 -1
- package/dist/chunk-42PJFMAR.js +0 -605
- package/dist/chunk-42PJFMAR.js.map +0 -1
- package/dist/chunk-4JSOYE4G.js +0 -368
- package/dist/chunk-4JSOYE4G.js.map +0 -1
- package/dist/chunk-56KS3TFQ.js.map +0 -1
- package/dist/chunk-73ATPIYX.js.map +0 -1
- package/dist/chunk-A4SLHOW7.js.map +0 -1
- package/dist/chunk-BAUPQXPE.js +0 -173
- package/dist/chunk-BAUPQXPE.js.map +0 -1
- package/dist/chunk-BYZDPR7N.js.map +0 -1
- package/dist/chunk-CD5ULRVC.js +0 -615
- package/dist/chunk-CD5ULRVC.js.map +0 -1
- package/dist/chunk-DIDUVL6O.js +0 -1365
- package/dist/chunk-DIDUVL6O.js.map +0 -1
- package/dist/chunk-EZATO3DW.js.map +0 -1
- package/dist/chunk-FADTCQGM.js.map +0 -1
- package/dist/chunk-FJP3UQGD.js.map +0 -1
- package/dist/chunk-LWRSMTTZ.js +0 -18
- package/dist/chunk-LWRSMTTZ.js.map +0 -1
- package/dist/chunk-M5UCXTIT.js.map +0 -1
- package/dist/chunk-M66SJHQQ.js +0 -206
- package/dist/chunk-M66SJHQQ.js.map +0 -1
- package/dist/chunk-MJULIKQU.js.map +0 -1
- package/dist/chunk-NS6R37II.js.map +0 -1
- package/dist/chunk-NTMMF5UD.js.map +0 -1
- package/dist/chunk-QCPGHBSZ.js.map +0 -1
- package/dist/chunk-XQUSIEKA.js.map +0 -1
- package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
- package/dist/execute-XBNVL753.js +0 -18
- package/dist/init-AJHCXJBP.js +0 -10
- package/dist/plan-QOZ3RIO6.js +0 -21
- package/dist/policy-6S4GFC76.js.map +0 -1
- package/dist/pr-EBBHD2ZX.js +0 -26
- package/dist/pr-review-VVMW5PO4.js +0 -20
- package/dist/prd-JQSZF7PR.js +0 -21
- package/dist/review-3F6MARWH.js +0 -19
- package/dist/run-RXE5GDQO.js +0 -36
- package/dist/web-EF5WIIGG.js.map +0 -1
- /package/dist/{config-PRJGWW7J.js.map → agent-OX3KPHE4.js.map} +0 -0
- /package/dist/{chunk-LAFNLLQP.js.map → chunk-KMRFRXIT.js.map} +0 -0
- /package/dist/{execute-XBNVL753.js.map → config-HMFMCY2M.js.map} +0 -0
- /package/dist/{init-AJHCXJBP.js.map → contract-JH2WBIO6.js.map} +0 -0
- /package/dist/{plan-QOZ3RIO6.js.map → diagnostics-THH42ZMP.js.map} +0 -0
- /package/dist/{pr-EBBHD2ZX.js.map → execute-KFNNZZHI.js.map} +0 -0
- /package/dist/{pr-review-VVMW5PO4.js.map → git-W72QQ6GT.js.map} +0 -0
- /package/dist/{prd-JQSZF7PR.js.map → init-HTN4V67Q.js.map} +0 -0
- /package/dist/{review-3F6MARWH.js.map → permissions-4AKNQZHK.js.map} +0 -0
- /package/dist/{run-RXE5GDQO.js.map → plan-SIZLGA4V.js.map} +0 -0
- /package/dist/{state-manager-2EDVDBAR.js.map → pr-OMWCGUOW.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/pr.ts"],"sourcesContent":["import { execa } from 'execa';\nimport { DEFAULT_HEADLESS_TIMEOUT_MS, runHeadless } from '../core/headless.js';\nimport { runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { listPullRequests } from '../core/session-git.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\n/**\n * Adopt the Pull Request that is already open for this branch, if there is one.\n *\n * \"Already open\" is the only safe reading: a *closed* Pull Request for the same\n * branch is a decision someone made, and reopening or reusing it would undo it.\n * A merged one means the work is done and a new branch is the answer.\n *\n * Returns `true` when the phase has nothing left to do. Never throws — a `gh`\n * that cannot answer leaves the phase to run exactly as it did before.\n */\nasync function adoptExistingPullRequest(\n branchName: string,\n tasksPath: string,\n issueNumber: string,\n): Promise<boolean> {\n let open: Awaited<ReturnType<typeof listPullRequests>>;\n try {\n open = await listPullRequests(branchName, { state: 'open' });\n } catch {\n return false;\n }\n\n const existing = open[0];\n if (existing === undefined) return false;\n\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.prCreated = true;\n plan.pullRequest = {\n number: existing.number,\n url: existing.url,\n headBranch: branchName,\n // The Pull Request predates this attempt; what is recorded is when this\n // run adopted it, which is the only timestamp this process can vouch for.\n createdAt: isoNow(),\n };\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // No plan yet: the adoption still stands, there is just nowhere to note it.\n }\n\n printSuccess(\n `Pull Request already open for ${branchName}: ${existing.url}. Adopting it instead of opening a second one for issue #${issueNumber}.`,\n );\n return true;\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 // Idempotence, before anything is invoked.\n //\n // `runPhaseWithRetry` retries this phase up to three times, and the timeout\n // that used to end a run is now correctly classified as transient — so an\n // attempt that created the Pull Request and *then* timed out would have the\n // next attempt create a second one. Asking GitHub what already exists is the\n // cheap half of the fix; the expensive half would be undoing a duplicate.\n const adopted = await adoptExistingPullRequest(branchName, tasksPath, issueNumber);\n if (adopted) return 0;\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() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\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/web/lock.ts","../src/web/server.ts"],"sourcesContent":["import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { isoNow } from '../core/state-manager.js';\n// The liveness probe is shared with `storage/lock.ts`, which generalised this\n// module's guard into run ownership: one definition of \"is that pid alive\",\n// with one place to get the EPERM case right.\nimport { isProcessAlive } from '../storage/lock.js';\nimport { type GetGlobalRootOptions, getGlobalRoot } from '../storage/paths.js';\nimport { type WebLock, webLockSchema } from '../storage/schemas.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport { startWebServer, type WebServerHandle, type WebServerOptions } from './server.js';\n\n/**\n * Single-instance guard for the web monitoring server (`~/.issue-flow/web.lock`).\n *\n * A machine runs at most one monitoring server. Two entry points build on top\n * of the same lock:\n *\n * - `ensureSingleWebServer` binds (or reuses) a server **in the calling\n * process**. It is the low-level primitive, used by the `web serve` command\n * (the standalone process that ends up owning the lock) and by this\n * module's own tests. Nothing else should call it directly.\n * - `ensureWebMonitor` (US-002) is what `run`/`execute` call: it reuses an\n * active instance exactly like `ensureSingleWebServer`, but when none\n * exists it spawns `web serve` as a **detached** child instead of binding\n * locally, so the server outlives the pipeline process. Falls back to the\n * legacy in-process `startWebServer` (US-006) when the global storage tree\n * itself is unreachable.\n */\n\n/** Lock file name, sibling of `config.json` at the global storage root. */\nexport const WEB_LOCK_FILENAME = 'web.lock';\n\n/** How long a health probe against a candidate instance waits before giving up. */\nconst HEALTH_PROBE_TIMEOUT_MS = 1000;\n\n/**\n * A process that loses the atomic lock creation retries this many times,\n * waiting for the winner to finish starting up, before giving up.\n */\nconst LOCK_CLAIM_RETRIES = 5;\nconst LOCK_CLAIM_RETRY_DELAY_MS = 100;\n\nexport function getWebLockFile(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), WEB_LOCK_FILENAME);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** 0.0.0.0 is not a connectable address; probe loopback instead, same mapping\n * `startWebServer` uses to build its own display URL. */\nfunction probeUrl(host: string, port: number): string {\n const probeHost = host === '0.0.0.0' ? '127.0.0.1' : host;\n return `http://${probeHost}:${port}/api/health`;\n}\n\nasync function probeHealth(host: string, port: number): Promise<boolean> {\n try {\n const res = await fetch(probeUrl(host, port), {\n signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/** Malformed content (partial write, older/newer incompatible shape) reads as\n * \"no lock\" — the same degrade-to-absent behavior as `loadGlobalConfig()`. */\nexport async function readWebLock(lockFile: string): Promise<WebLock | null> {\n let raw: string;\n try {\n raw = await readFile(lockFile, 'utf-8');\n } catch {\n return null;\n }\n try {\n const result = webLockSchema.safeParse(JSON.parse(raw));\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nexport async function removeWebLock(lockFile: string): Promise<void> {\n try {\n await rm(lockFile, { force: true });\n } catch {\n /* best-effort cleanup */\n }\n}\n\n/**\n * A lock is only trustworthy when both signals agree: the owning process is\n * alive AND it answers `/api/health`. A dead pid is the obvious stale case;\n * a live pid that does not answer covers a process that still exists but\n * crashed before binding, was killed -9 leaving a zombie-adjacent state, or\n * whose port was reclaimed by something else entirely. Either way the lock is\n * removed so the next attempt starts clean instead of looping on it forever.\n */\nexport async function detectActiveInstance(lockFile: string): Promise<WebLock | null> {\n const lock = await readWebLock(lockFile);\n if (lock === null) return null;\n\n if (isProcessAlive(lock.pid) && (await probeHealth(lock.host, lock.port))) {\n return lock;\n }\n\n await removeWebLock(lockFile);\n return null;\n}\n\n/**\n * Atomic exclusive create: `wx` fails with EEXIST when another process won\n * the race, which is what lets two concurrent invocations agree on a single\n * winner without a separate lock-of-the-lock.\n */\nasync function claimWebLock(lockFile: string, lock: WebLock): Promise<boolean> {\n try {\n await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: 'utf-8', flag: 'wx' });\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;\n throw err;\n }\n}\n\n/** Handle returned when reusing an already-running instance instead of\n * binding a new one — there is no local `Server` to hold, and closing it\n * must not tear down a server another process owns. */\nfunction reusedHandle(lock: WebLock): WebServerHandle {\n const displayHost = lock.host === '0.0.0.0' ? 'localhost' : lock.host;\n return {\n host: lock.host,\n port: lock.port,\n url: `http://${displayHost}:${lock.port}`,\n close: async () => {},\n };\n}\n\n/**\n * Start the web monitoring server, first checking whether a live instance\n * already owns `web.lock` and reusing it instead of binding a second one.\n *\n * The lock is claimed only *after* a successful bind, never before: claiming\n * it with the port the server is about to (but hasn't yet) listen on would\n * make that lock briefly answer no health probe at all, and a concurrent\n * invocation would misread that gap as \"stale\" and delete a perfectly good\n * lock out from under its owner. Binding first also means two invocations\n * racing for the same fixed port never both reach the claim step — the OS\n * itself lets only one `listen()` succeed. The `wx` claim then exists for the\n * remaining race: two invocations that *both* manage to bind (only possible\n * with an ephemeral port) still agree on exactly one winner. The loser closes\n * the server it just opened and defers to whichever lock exists.\n *\n * Returns null exactly when `startWebServer` would: nothing here may affect\n * the pipeline, so a lock we cannot resolve after every retry degrades to \"no\n * server\" rather than throwing.\n */\nexport async function ensureSingleWebServer(\n options: WebServerOptions,\n lockOptions: GetGlobalRootOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n const lockFile = getWebLockFile(lockOptions);\n\n for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const handle = await startWebServer(options);\n if (handle === null) {\n // Could not bind. Most likely another instance's listen() won a race\n // on the same fixed port between our detection above and this call —\n // its lock should be visible shortly. Anything else (port taken by an\n // unrelated process, invalid host, ...) fails the same way on the next\n // retry too, and the loop's own limit stops it from spinning forever.\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n let claimed: boolean;\n try {\n claimed = await claimWebLock(lockFile, {\n pid: process.pid,\n port: handle.port,\n host: handle.host,\n startedAt: isoNow(),\n });\n } catch (err) {\n await handle.close();\n throw err;\n }\n\n if (!claimed) {\n // We bound successfully (an ephemeral port let both of us through),\n // but another invocation's lock got written first. We are not the\n // canonical instance: close what we just opened and defer to it.\n await handle.close();\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n await removeWebLock(lockFile);\n };\n return handle;\n }\n\n warn('Could not determine the web monitor lock ownership. Continuing without the web server.');\n return null;\n}\n\n/** Bounded wait for a freshly spawned `web serve` child to claim the lock. */\nconst DETACHED_CLAIM_TIMEOUT_MS = 5000;\nconst DETACHED_CLAIM_POLL_MS = 150;\n\nexport interface EnsureWebMonitorOptions extends GetGlobalRootOptions {\n /** Injectable for tests. Defaults to `node:child_process`'s `spawn`. */\n spawn?: (\n command: string,\n args: string[],\n options: { detached: true; stdio: 'ignore' },\n ) => ChildProcess;\n /** Node executable to spawn. Defaults to `process.execPath`. */\n execPath?: string;\n /** CLI entry script re-invoked as `<entry> web serve ...`. Defaults to `process.argv[1]`. */\n entryScript?: string;\n /** How long to wait for the spawned instance's lock to become healthy. */\n claimTimeoutMs?: number;\n claimPollIntervalMs?: number;\n}\n\n/**\n * Entry point for `run`/`execute`: get a web monitor without binding one in\n * this process (US-002).\n *\n * 1. Reuse a live, healthy instance exactly like `ensureSingleWebServer`.\n * 2. Otherwise, spawn `<node> <cli> web serve --port … --host … [--refresh …]`\n * detached (`{ detached: true, stdio: 'ignore' }`) and `unref()`ed, so it\n * survives this process exiting and never pipes its own stdio back here.\n * 3. Poll the lock file (bounded) until the spawned instance claims it, then\n * return a *reused* handle for it — this process never owns that `Server`,\n * the same shape `ensureSingleWebServer`'s reuse path already returns.\n *\n * Two failure modes degrade gracefully rather than affecting the pipeline:\n * the global storage tree being unreachable (no resolvable home directory)\n * falls back to `startWebServer` bound right here, exactly the pre-US-001\n * behavior (US-006); a spawn or claim failure with the storage tree otherwise\n * healthy just returns null, same contract `startWebServer` itself has.\n */\nexport async function ensureWebMonitor(\n options: WebServerOptions,\n monitorOptions: EnsureWebMonitorOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n\n let lockFile: string;\n try {\n lockFile = getWebLockFile(monitorOptions);\n } catch (err) {\n warn(\n `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`,\n );\n return startWebServer(options);\n }\n\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const execPath = monitorOptions.execPath ?? process.execPath;\n const entryScript = monitorOptions.entryScript ?? process.argv[1];\n if (entryScript === undefined) {\n warn('Web monitor: could not determine the CLI entry point to spawn a detached server.');\n return null;\n }\n\n const args = [\n entryScript,\n 'web',\n 'serve',\n '--port',\n String(options.port),\n '--host',\n options.host,\n ];\n if (options.refreshSeconds !== undefined) {\n args.push('--refresh', String(options.refreshSeconds));\n }\n\n const spawnFn = monitorOptions.spawn ?? nodeSpawn;\n try {\n const child = spawnFn(execPath, args, { detached: true, stdio: 'ignore' });\n child.unref();\n } catch (err) {\n warn(\n `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`,\n );\n return null;\n }\n\n const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;\n const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const claimed = await detectActiveInstance(lockFile);\n if (claimed !== null) {\n info(`Web monitor running at ${chalk.bold.cyan(reusedHandle(claimed).url)}`);\n return reusedHandle(claimed);\n }\n await sleep(pollMs);\n }\n\n warn(\n 'Web monitor: the detached server did not start in time. Continuing without the web server.',\n );\n return null;\n}\n","import { createHash } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { resolvePackageDir } from '../core/prompt-resolver.js';\nimport {\n NullPublisher,\n type SessionPublisher,\n type SessionSnapshot,\n} from '../core/session-state.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport type { SessionDirectoryHandle } from './session-directory.js';\n\n/**\n * HTTP server for the web monitoring mode. Plain node:http — no new runtime\n * dependencies. Serves the publisher's in-memory snapshot (it never re-reads\n * issues/N/session.json) and the static UI assets.\n *\n * Resilience contract: nothing here may ever affect the pipeline. Listen\n * failures (EADDRINUSE included) log a warning and the execution continues\n * without a server; the server is unref()'d so it cannot keep the process\n * alive; request handling is wrapped so a handler error answers 500 instead\n * of crashing the process.\n */\n\nconst require = createRequire(import.meta.url);\n\n/** Max length of `issueDescription` on GET /api/sessions (dashboard preview). */\nexport const SESSION_LIST_DESCRIPTION_MAX = 280;\n\nfunction readPackageVersion(): string {\n try {\n return (require('../../package.json') as { version: string }).version;\n } catch {\n return '0.0.0';\n }\n}\n\n/** Collapse whitespace and truncate for the sessions list payload. */\nexport function truncateSessionDescription(\n text: string | null | undefined,\n max = SESSION_LIST_DESCRIPTION_MAX,\n): string | null {\n if (text === null || text === undefined) return null;\n const normalized = text.replace(/\\s+/g, ' ').trim();\n if (normalized.length === 0) return normalized;\n if (normalized.length <= max) return normalized;\n return `${normalized.slice(0, max - 1).trimEnd()}…`;\n}\n\nexport interface WebServerOptions {\n /**\n * Legacy single-session mode (pre-US-003, still used by the US-006\n * fallback): serves this publisher's in-memory snapshot directly, with no\n * directory scan and no lock file involved. Mutually exclusive with\n * `sessions` — when both are given, `sessions` wins.\n */\n publisher?: SessionPublisher;\n /**\n * Multi-session mode: sessions discovered by polling the global storage\n * tree (`web/session-directory.ts`). This is what every current entry\n * point (`web serve`) passes; `publisher` only exists for the legacy path.\n */\n sessions?: SessionDirectoryHandle;\n port: number;\n host: string;\n /** Suggested UI polling interval, exposed via /api/health. */\n refreshSeconds?: number;\n /** Package version reported by /api/health. Default: read from package.json. */\n version?: string;\n /** Directory holding index.html/app.css/app.js. Default: auto-resolved. */\n publicDir?: string;\n /** Info logger. Default: printInfo. */\n info?: (message: string) => void;\n /** Warning logger. Default: printWarning. */\n warn?: (message: string) => void;\n /**\n * Whether to `unref()` the underlying socket so it never keeps the process\n * alive on its own. Default `true`, matching the historical contract for a\n * server bound inline in the pipeline process. The standalone `web serve`\n * process (the detached server itself) passes `false`: it has nothing else\n * to do, so staying alive for as long as the server is bound *is* the job.\n */\n unref?: boolean;\n}\n\nexport interface WebServerHandle {\n /**\n * Absent when this handle represents an existing instance reused through\n * `ensureSingleWebServer` (`web/lock.ts`) instead of one bound locally.\n */\n server?: Server;\n /** Host the server is bound to. */\n host: string;\n /** Actual bound port (relevant when options.port is 0). */\n port: number;\n /** Human-facing access URL. */\n url: string;\n /** Close the server and release signal handlers. Idempotent, never rejects. */\n close(): Promise<void>;\n}\n\ninterface StaticAsset {\n body: string;\n contentType: string;\n}\n\nconst JSON_TYPE = 'application/json; charset=utf-8';\n\n/**\n * Uniform view over \"whatever sessions this server knows about\", so the route\n * handlers below never care whether they are backed by a single in-memory\n * publisher (legacy mode) or by the global directory scan (US-003/US-004).\n */\ninterface SessionSource {\n /** Every session currently considered active, in no particular order. */\n list(): SessionSnapshot[];\n /** A specific session by id, or undefined when it is not currently active. */\n get(sessionId: string): SessionSnapshot | undefined;\n}\n\nfunction publisherSessionSource(publisher: SessionPublisher): SessionSource {\n return {\n list: () => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === null ? [] : [snapshot];\n },\n get: (sessionId) => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === sessionId ? snapshot : undefined;\n },\n };\n}\n\nfunction directorySessionSource(handle: SessionDirectoryHandle): SessionSource {\n return {\n list: () => handle.sessions().map((entry) => entry.snapshot),\n get: (sessionId) => handle.getSession(sessionId)?.snapshot,\n };\n}\n\nfunction resolveSessionSource(options: WebServerOptions): SessionSource {\n if (options.sessions) return directorySessionSource(options.sessions);\n return publisherSessionSource(options.publisher ?? new NullPublisher());\n}\n\nconst STATIC_ROUTES: Record<string, { file: string; contentType: string }> = {\n '/': { file: 'index.html', contentType: 'text/html; charset=utf-8' },\n '/app.css': { file: 'app.css', contentType: 'text/css; charset=utf-8' },\n '/app.js': { file: 'app.js', contentType: 'text/javascript; charset=utf-8' },\n};\n\n/** Assets are read once at startup; missing files simply 404. */\nasync function loadStaticAssets(publicDir: string | null): Promise<Map<string, StaticAsset>> {\n const assets = new Map<string, StaticAsset>();\n if (publicDir === null) return assets;\n for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {\n try {\n const body = await readFile(join(publicDir, file), 'utf-8');\n assets.set(route, { body, contentType });\n } catch {\n // Asset not present (e.g. UI not built yet) — route answers 404.\n }\n }\n return assets;\n}\n\n/** Headers applied to every response, including 304s and errors. */\nfunction baseHeaders(res: ServerResponse): void {\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('Referrer-Policy', 'no-referrer');\n res.setHeader('X-Robots-Tag', 'noindex, nofollow');\n}\n\nfunction respond(res: ServerResponse, status: number, contentType: string, body: string): void {\n res.statusCode = status;\n res.setHeader('Content-Type', contentType);\n res.end(body);\n}\n\nfunction respondJson(res: ServerResponse, status: number, payload: unknown): void {\n respond(res, status, JSON_TYPE, JSON.stringify(payload));\n}\n\n/**\n * Start the monitoring HTTP server. Returns null when the server could not\n * listen (port in use, invalid host, ...) — the pipeline continues without\n * monitoring, it is never brought down by the server.\n */\nexport async function startWebServer(options: WebServerOptions): Promise<WebServerHandle | null> {\n const info = options.info ?? printInfo;\n const warn = options.warn ?? printWarning;\n const version = options.version ?? readPackageVersion();\n const startedAtMs = Date.now();\n\n // The UI ships at the package root as web/public/ (sibling of prompts/),\n // resolved the same way from src/ and from the published dist/ layout.\n const assets = await loadStaticAssets(\n options.publicDir ?? resolvePackageDir(join('web', 'public')),\n );\n\n const source = resolveSessionSource(options);\n\n // JSON serialization memoized per session id: an unchanged poll answers 304\n // with an empty body. Content-hashed rather than counter-based, unlike the\n // pre-multi-session version — a directory-backed session has no in-process\n // publisher to hand out a monotonic version(), and a hash works uniformly\n // for both sources.\n const statusCache = new Map<string, { body: string; etag: string }>();\n const statusPayload = (snapshot: SessionSnapshot): { body: string; etag: string } => {\n const key = snapshot.sessionId ?? '';\n const body = JSON.stringify(snapshot);\n const cached = statusCache.get(key);\n if (cached !== undefined && cached.body === body) return cached;\n const etag = `\"${createHash('sha1').update(body).digest('hex')}\"`;\n const entry = { body, etag };\n statusCache.set(key, entry);\n return entry;\n };\n\n const handleRequest = (req: IncomingMessage, res: ServerResponse): void => {\n baseHeaders(res);\n\n const requestUrl = new URL(req.url ?? '/', 'http://localhost');\n const path = requestUrl.pathname;\n\n // POST /api/control/* is reserved for future write operations (pause,\n // retry, ...); intentionally NOT registered — the v1 surface is\n // read-only (snapshot.readOnly is true and capabilities is empty).\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n respondJson(res, 404, { error: 'Not found' });\n return;\n }\n\n if (path === '/api/status' || path === '/status.json') {\n const sessionId = requestUrl.searchParams.get('session');\n let snapshot: SessionSnapshot | undefined;\n if (sessionId !== null) {\n snapshot = source.get(sessionId);\n if (snapshot === undefined) {\n respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });\n return;\n }\n } else {\n // Back-compat shorthand (US-005): with exactly one active session,\n // GET /api/status with no query string still answers it directly, the\n // same behavior a single-session run always had. Zero or several\n // active sessions is genuinely ambiguous without an id and answers an\n // explicit error instead of guessing.\n const active = source.list();\n if (active.length === 0) {\n respondJson(res, 404, {\n error: 'No active session. Pass ?session=<id> — see GET /api/sessions.',\n });\n return;\n }\n if (active.length > 1) {\n respondJson(res, 409, {\n error: 'Multiple active sessions; specify ?session=<id>.',\n sessions: active.map((entry) => entry.sessionId),\n });\n return;\n }\n snapshot = active[0];\n }\n\n const { body, etag } = statusPayload(snapshot);\n res.setHeader('ETag', etag);\n if (req.headers['if-none-match'] === etag) {\n res.statusCode = 304;\n res.end();\n return;\n }\n respond(res, 200, JSON_TYPE, body);\n return;\n }\n\n if (path === '/api/sessions') {\n // Summary fields for the multi-session dashboard (issue #35): the full\n // SessionSnapshot is already in memory, so the client can render cards\n // from this single list without N× /api/status fetches per poll.\n // issueDescription is truncated — cards only need a short preview.\n respondJson(\n res,\n 200,\n source.list().map((snapshot) => ({\n sessionId: snapshot.sessionId,\n issueNumber: snapshot.issue.number,\n issueTitle: snapshot.issue.title,\n issueDescription: truncateSessionDescription(snapshot.issue.description),\n repositoryName: snapshot.repository.name,\n currentPhase: snapshot.currentPhase,\n progressPercent: snapshot.progress.percent,\n elapsedSeconds: snapshot.elapsedSeconds,\n status: snapshot.status,\n startedAt: snapshot.startedAt,\n updatedAt: snapshot.updatedAt,\n // Resilience fields, for a card that has to answer \"is this still\n // moving\" during a six-hour run. `updatedAt` is already the last\n // activity; these two say how hard the run has had to work for it.\n retries: snapshot.execution.retries,\n correctionCycle: snapshot.execution.correctionCycle,\n statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? '')}`,\n })),\n );\n return;\n }\n\n if (path === '/api/health') {\n respondJson(res, 200, {\n ok: true,\n uptime: Math.round((Date.now() - startedAtMs) / 1000),\n version,\n refreshSeconds: options.refreshSeconds ?? 5,\n });\n return;\n }\n\n const asset = assets.get(path);\n if (asset) {\n respond(res, 200, asset.contentType, asset.body);\n return;\n }\n\n respondJson(res, 404, { error: 'Not found' });\n };\n\n const server = createServer((req, res) => {\n try {\n handleRequest(req, res);\n } catch (err) {\n try {\n respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });\n } catch {\n // Response already destroyed — nothing to do, never crash the process.\n }\n }\n });\n\n const listening = await new Promise<boolean>((resolve) => {\n const onError = (err: NodeJS.ErrnoException): void => {\n if (err.code === 'EADDRINUSE') {\n warn(\n `Web monitor port ${options.port} is already in use. Continuing without the web server.`,\n );\n } else {\n warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);\n }\n resolve(false);\n };\n server.once('error', onError);\n server.listen(options.port, options.host, () => {\n server.removeListener('error', onError);\n resolve(true);\n });\n });\n\n if (!listening) return null;\n\n // Never keep the process alive on its own, unless explicitly told not to\n // unref: the pipeline ending must end the process. The one exception is the\n // detached `web serve` process (options.unref === false) — it has no other\n // work, so staying alive for as long as the server is bound is the point.\n if (options.unref !== false) {\n server.unref();\n }\n // Post-listen errors must never become uncaught exceptions.\n server.on('error', (err) => {\n warn(`Web monitor server error: ${err.message}`);\n });\n\n const address = server.address() as AddressInfo;\n const port = address.port;\n const displayHost = options.host === '0.0.0.0' ? 'localhost' : options.host;\n const url = `http://${displayHost}:${port}`;\n\n if (options.host === '0.0.0.0') {\n warn('Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.');\n }\n info(`Web monitor running at ${chalk.bold.cyan(url)}`);\n\n let closed = false;\n let closePromise: Promise<void> | null = null;\n\n const doClose = (): Promise<void> =>\n new Promise<void>((resolve) => {\n process.removeListener('SIGINT', onSignal);\n process.removeListener('SIGTERM', onSignal);\n server.close(() => resolve());\n // Drop idle keep-alive connections so close() never hangs.\n server.closeAllConnections();\n });\n\n const close = (): Promise<void> => {\n if (closed) return closePromise ?? Promise.resolve();\n closed = true;\n closePromise = doClose();\n return closePromise;\n };\n\n // `handle.close` is built here, but ensureSingleWebServer/ensureWebMonitor\n // and the `web serve` command each wrap it afterwards (to also remove the\n // lock file, stop the session-directory poller, ...). onSignal below calls\n // `handle.close()` — the *current* value of the property — rather than the\n // `close` closed over here, so a `kill -TERM <pid>` (including `issue-flow\n // web stop`) always runs the fully wrapped shutdown, not just this raw\n // socket close. Capturing `close` directly here was a real bug: the wrapped\n // behavior only ever ran for an *explicit* `handle.close()` call, leaving\n // the lock file (and the poller) behind on every signal-driven shutdown.\n const handle: WebServerHandle = { server, host: options.host, port, url, close };\n\n // Explicit close on SIGINT/SIGTERM, then re-raise so the default\n // termination behavior still applies.\n const onSignal = (signal: NodeJS.Signals): void => {\n void handle.close().finally(() => {\n process.kill(process.pid, signal);\n });\n };\n process.once('SIGINT', onSignal);\n process.once('SIGTERM', onSignal);\n\n return handle;\n}\n"],"mappings":"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","names":["readFile","join","chalk","require","join","readFile","chalk"]}
|
package/dist/chunk-LWRSMTTZ.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
classify
|
|
4
|
-
} from "./chunk-DIDUVL6O.js";
|
|
5
|
-
|
|
6
|
-
// src/utils/retry.ts
|
|
7
|
-
function isTransientFailure(exitCode, output) {
|
|
8
|
-
return classify({ source: "agent", exitCode, stdout: output }).retryable;
|
|
9
|
-
}
|
|
10
|
-
function sleep(seconds) {
|
|
11
|
-
return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
isTransientFailure,
|
|
16
|
-
sleep
|
|
17
|
-
};
|
|
18
|
-
//# sourceMappingURL=chunk-LWRSMTTZ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/retry.ts"],"sourcesContent":["import { classify } from '../resilience/errors.js';\n\n/**\n * Determine if an agent invocation failure is transient (retryable).\n *\n * This is a thin adapter over `resilience/errors.ts:classify()` — the taxonomy\n * is the single source of truth, and this signature survives only because\n * `core/engine.ts` and the phase commands are written against it. A caller\n * that has more evidence than an exit code and a blob of text (an `errno`, an\n * HTTP status, `timedOut`) should call `classify()` directly and keep it.\n */\nexport function isTransientFailure(exitCode: number, output: string): boolean {\n return classify({ source: 'agent', exitCode, stdout: output }).retryable;\n}\n\n/**\n * Calculate retry delay using exponential backoff.\n *\n * delay = baseSeconds * 2^(attempt-1), capped at maxSeconds\n *\n * @param attempt - The retry attempt number (1-based)\n * @param baseSeconds - Base delay in seconds (default: 30)\n * @param maxSeconds - Maximum delay in seconds (default: 900)\n */\nexport function retryDelaySeconds(\n attempt: number,\n baseSeconds: number = 30,\n maxSeconds: number = 900,\n): number {\n const delay = baseSeconds * 2 ** (attempt - 1);\n return Math.min(delay, maxSeconds);\n}\n\n/**\n * Sleep for a given number of seconds.\n */\nexport function sleep(seconds: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, seconds * 1000));\n}\n"],"mappings":";;;;;;AAWO,SAAS,mBAAmB,UAAkB,QAAyB;AAC5E,SAAO,SAAS,EAAE,QAAQ,SAAS,UAAU,QAAQ,OAAO,CAAC,EAAE;AACjE;AAuBO,SAAS,MAAM,SAAgC;AACpD,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,UAAU,GAAI,CAAC;AACrE;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/prd.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { DEFAULT_HEADLESS_TIMEOUT_MS, 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() ?? DEFAULT_HEADLESS_TIMEOUT_MS,\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":[]}
|
package/dist/chunk-M66SJHQQ.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/core/shutdown.ts
|
|
4
|
-
var SHUTDOWN_GRACE_MS = 15e3;
|
|
5
|
-
var GRACE_POLL_MS = 100;
|
|
6
|
-
var EXIT_CODES = { SIGINT: 130, SIGTERM: 143 };
|
|
7
|
-
var controller = null;
|
|
8
|
-
var installed = false;
|
|
9
|
-
var shuttingDown = false;
|
|
10
|
-
var forced = false;
|
|
11
|
-
var hooks = /* @__PURE__ */ new Set();
|
|
12
|
-
var children = /* @__PURE__ */ new Set();
|
|
13
|
-
function getShutdownSignal() {
|
|
14
|
-
controller ??= new AbortController();
|
|
15
|
-
return controller.signal;
|
|
16
|
-
}
|
|
17
|
-
function onShutdown(hook) {
|
|
18
|
-
hooks.add(hook);
|
|
19
|
-
return () => {
|
|
20
|
-
hooks.delete(hook);
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
function registerChild(child) {
|
|
24
|
-
children.add(child);
|
|
25
|
-
return () => {
|
|
26
|
-
children.delete(child);
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
function installShutdownHandlers(options = {}) {
|
|
30
|
-
if (installed) return;
|
|
31
|
-
installed = true;
|
|
32
|
-
const listen = options.onSignal ?? ((signal, handler) => {
|
|
33
|
-
process.on(signal, handler);
|
|
34
|
-
});
|
|
35
|
-
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
36
|
-
listen(signal, () => {
|
|
37
|
-
void beginShutdown(signal, options);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async function beginShutdown(reason, options = {}) {
|
|
42
|
-
const exit = options.exit ?? ((code) => process.exit(code));
|
|
43
|
-
const notify = options.notify ?? ((message) => process.stderr.write(`${message}
|
|
44
|
-
`));
|
|
45
|
-
if (shuttingDown) {
|
|
46
|
-
forced = true;
|
|
47
|
-
killChildren("SIGKILL");
|
|
48
|
-
exit(EXIT_CODES[reason]);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
shuttingDown = true;
|
|
52
|
-
getShutdownSignal();
|
|
53
|
-
controller?.abort();
|
|
54
|
-
notify(
|
|
55
|
-
`
|
|
56
|
-
Interrupted. Saving a checkpoint and stopping the agent (press ${reason === "SIGINT" ? "Ctrl+C" : "the signal"} again to quit now)\u2026`
|
|
57
|
-
);
|
|
58
|
-
await runHooks("checkpoint", reason, notify);
|
|
59
|
-
await terminateChildren(options.graceMs ?? SHUTDOWN_GRACE_MS);
|
|
60
|
-
await runHooks("close", reason, notify);
|
|
61
|
-
exit(EXIT_CODES[reason]);
|
|
62
|
-
}
|
|
63
|
-
async function runHooks(phase, reason, notify) {
|
|
64
|
-
for (const hook of [...hooks]) {
|
|
65
|
-
if (hook.phase !== phase) continue;
|
|
66
|
-
try {
|
|
67
|
-
await hook.run(reason);
|
|
68
|
-
} catch (err) {
|
|
69
|
-
notify(
|
|
70
|
-
`issue-flow: a shutdown step failed: ${err instanceof Error ? err.message : String(err)}`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function killChildren(signal) {
|
|
76
|
-
for (const child of [...children]) {
|
|
77
|
-
try {
|
|
78
|
-
child.kill(signal);
|
|
79
|
-
} catch {
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
async function terminateChildren(graceMs) {
|
|
84
|
-
if (children.size === 0) return;
|
|
85
|
-
const pending = [...children];
|
|
86
|
-
let alive = pending.length;
|
|
87
|
-
for (const child of pending) {
|
|
88
|
-
void child.done.then(
|
|
89
|
-
() => {
|
|
90
|
-
alive--;
|
|
91
|
-
},
|
|
92
|
-
() => {
|
|
93
|
-
alive--;
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
killChildren("SIGTERM");
|
|
98
|
-
const deadline = Date.now() + graceMs;
|
|
99
|
-
while (alive > 0 && !forced && Date.now() < deadline) {
|
|
100
|
-
await sleep(GRACE_POLL_MS);
|
|
101
|
-
}
|
|
102
|
-
if (alive > 0) killChildren("SIGKILL");
|
|
103
|
-
}
|
|
104
|
-
function sleep(ms) {
|
|
105
|
-
return new Promise((resolve) => {
|
|
106
|
-
const timer = setTimeout(resolve, ms);
|
|
107
|
-
timer.unref?.();
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// src/core/watchdog.ts
|
|
112
|
-
var DEFAULT_INACTIVITY_TIMEOUT_MS = 6e5;
|
|
113
|
-
var CHECK_DIVISOR = 10;
|
|
114
|
-
var MIN_CHECK_MS = 250;
|
|
115
|
-
var MAX_CHECK_MS = 5e3;
|
|
116
|
-
function checkIntervalFor(timeoutMs) {
|
|
117
|
-
return Math.min(MAX_CHECK_MS, Math.max(MIN_CHECK_MS, Math.floor(timeoutMs / CHECK_DIVISOR)));
|
|
118
|
-
}
|
|
119
|
-
function createWatchdog(options = {}) {
|
|
120
|
-
const timeoutMs = options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
|
|
121
|
-
const clock = options.clock ?? Date.now;
|
|
122
|
-
const start = options.setInterval ?? ((handler, ms) => setInterval(handler, ms));
|
|
123
|
-
const clear = options.clearInterval ?? ((timer2) => clearInterval(timer2));
|
|
124
|
-
const state = { stalled: false, silentMs: 0 };
|
|
125
|
-
if (timeoutMs <= 0) {
|
|
126
|
-
return {
|
|
127
|
-
beat: () => {
|
|
128
|
-
},
|
|
129
|
-
stop: () => {
|
|
130
|
-
},
|
|
131
|
-
get stalled() {
|
|
132
|
-
return state.stalled;
|
|
133
|
-
},
|
|
134
|
-
get silentMs() {
|
|
135
|
-
return state.silentMs;
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
let lastBeat = clock();
|
|
140
|
-
let timer = null;
|
|
141
|
-
let killTimer = null;
|
|
142
|
-
const stop = () => {
|
|
143
|
-
if (timer !== null) {
|
|
144
|
-
clear(timer);
|
|
145
|
-
timer = null;
|
|
146
|
-
}
|
|
147
|
-
if (killTimer !== null) {
|
|
148
|
-
clearTimeout(killTimer);
|
|
149
|
-
killTimer = null;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
const declareStalled = () => {
|
|
153
|
-
state.stalled = true;
|
|
154
|
-
state.silentMs = clock() - lastBeat;
|
|
155
|
-
stop();
|
|
156
|
-
options.onStall?.(state.silentMs);
|
|
157
|
-
const child = options.child;
|
|
158
|
-
if (child === void 0) return;
|
|
159
|
-
child.kill("SIGTERM");
|
|
160
|
-
killTimer = setTimeout(() => {
|
|
161
|
-
child.kill("SIGKILL");
|
|
162
|
-
}, options.graceMs ?? SHUTDOWN_GRACE_MS);
|
|
163
|
-
killTimer.unref?.();
|
|
164
|
-
void child.done.then(
|
|
165
|
-
() => {
|
|
166
|
-
if (killTimer !== null) {
|
|
167
|
-
clearTimeout(killTimer);
|
|
168
|
-
killTimer = null;
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
() => {
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
};
|
|
175
|
-
timer = start(() => {
|
|
176
|
-
if (state.stalled) return;
|
|
177
|
-
if (clock() - lastBeat > timeoutMs) declareStalled();
|
|
178
|
-
}, checkIntervalFor(timeoutMs));
|
|
179
|
-
timer.unref?.();
|
|
180
|
-
return {
|
|
181
|
-
beat: () => {
|
|
182
|
-
lastBeat = clock();
|
|
183
|
-
},
|
|
184
|
-
stop,
|
|
185
|
-
get stalled() {
|
|
186
|
-
return state.stalled;
|
|
187
|
-
},
|
|
188
|
-
get silentMs() {
|
|
189
|
-
return state.silentMs;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
function describeStall(silentMs) {
|
|
194
|
-
return `claude produced no output for ${Math.round(silentMs / 1e3)}s and was stopped (stalled)`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export {
|
|
198
|
-
getShutdownSignal,
|
|
199
|
-
onShutdown,
|
|
200
|
-
registerChild,
|
|
201
|
-
installShutdownHandlers,
|
|
202
|
-
DEFAULT_INACTIVITY_TIMEOUT_MS,
|
|
203
|
-
createWatchdog,
|
|
204
|
-
describeStall
|
|
205
|
-
};
|
|
206
|
-
//# sourceMappingURL=chunk-M66SJHQQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/shutdown.ts","../src/core/watchdog.ts"],"sourcesContent":["/**\n * Graceful shutdown: what a `Ctrl+C` does to a run that has been going for\n * hours.\n *\n * Before this module there was no `SIGINT` handler anywhere in the pipeline —\n * the only one in the project belonged to the web server — so an interrupt\n * killed the process mid-phase: nothing was marked as paused, no `session:end`\n * was published (leaving `session.json` on `running` forever), and the `claude`\n * child was orphaned rather than asked to stop. Resumption then worked by\n * accident, because a stale `in_progress` happens to be treated as \"do this\n * one first\".\n *\n * The sequence here is fixed, and the order is the whole design:\n *\n * 1. **stop accepting new work** — the process-wide `AbortSignal` fires, which\n * is the same signal every retry backoff already waits on, so a run sitting\n * in a two-minute delay stops in that instant instead of two minutes later;\n * 2. **write the checkpoint** — while the child is still alive and the state is\n * still coherent. Checkpointing after the kill would race the very writes it\n * is trying to capture;\n * 3. **ask the child to stop** — `SIGTERM`, then a grace period, then\n * `SIGKILL`. An agent given no chance to finish its sentence leaves a\n * half-written file behind;\n * 4. **close the surfaces** — the journal and the snapshot last, so everything\n * the previous steps published is on disk before the process ends.\n *\n * A second interrupt during the grace ends the process immediately: someone who\n * pressed `Ctrl+C` twice has said what they want, and waiting fifteen more\n * seconds to be polite is not it.\n */\n\n/** How long a child has to exit on its own before it is killed outright. */\nexport const SHUTDOWN_GRACE_MS = 15_000;\n\n/** How often the grace period checks whether the child is gone. */\nconst GRACE_POLL_MS = 100;\n\nexport type ShutdownReason = 'SIGINT' | 'SIGTERM';\n\n/** Exit codes by convention: 128 + the signal number. */\nconst EXIT_CODES: Readonly<Record<ShutdownReason, number>> = { SIGINT: 130, SIGTERM: 143 };\n\n/**\n * A child process the shutdown may terminate.\n *\n * Structural on purpose: an execa subprocess satisfies it without this module\n * depending on execa, and a test satisfies it with two lines.\n */\nexport interface TerminableChild {\n kill(signal?: NodeJS.Signals): boolean;\n /** Resolves once the process has exited, however it exited. Never rejects. */\n done: Promise<unknown>;\n}\n\nexport type ShutdownPhase =\n | /** Persist state. Runs while the child is still alive. */ 'checkpoint'\n | /** Flush and close output surfaces. Runs last. */ 'close';\n\nexport interface ShutdownHook {\n phase: ShutdownPhase;\n run: (reason: ShutdownReason) => void | Promise<void>;\n}\n\nexport interface InstallShutdownOptions {\n /** Grace given to a child between SIGTERM and SIGKILL. */\n graceMs?: number;\n /** Injectable exit, so a test can observe the code without dying. */\n exit?: (code: number) => void;\n /** Where the \"press again\" notice goes. Defaults to stderr. */\n notify?: (message: string) => void;\n /** Injectable signal source, so a test never touches the real process. */\n onSignal?: (signal: ShutdownReason, handler: () => void) => void;\n}\n\nlet controller: AbortController | null = null;\nlet installed = false;\nlet shuttingDown = false;\nlet forced = false;\nconst hooks = new Set<ShutdownHook>();\nconst children = new Set<TerminableChild>();\n\n/**\n * The process-wide abort signal.\n *\n * Created on first use rather than at install time, so a caller that only needs\n * the signal — a retry backoff, say — works identically whether or not handlers\n * were ever installed. That is what keeps every existing test path unchanged.\n */\nexport function getShutdownSignal(): AbortSignal {\n controller ??= new AbortController();\n return controller.signal;\n}\n\n/** Whether a shutdown has begun. New work must not be started after this. */\nexport function isShuttingDown(): boolean {\n return shuttingDown;\n}\n\n/** Register a hook. Returns the function that removes it again. */\nexport function onShutdown(hook: ShutdownHook): () => void {\n hooks.add(hook);\n return () => {\n hooks.delete(hook);\n };\n}\n\n/**\n * Register a child so the shutdown can ask it to stop.\n *\n * The returned function deregisters it, and **must** be called when the child\n * finishes normally: a set that only grows would have the shutdown signalling\n * pids that belong to something else entirely by then.\n */\nexport function registerChild(child: TerminableChild): () => void {\n children.add(child);\n return () => {\n children.delete(child);\n };\n}\n\n/**\n * Install the `SIGINT`/`SIGTERM` handlers, once per process.\n *\n * Idempotent by design: several entry points may call it, and a second set of\n * handlers would run the whole sequence twice — two checkpoints, two kills, two\n * exits.\n */\nexport function installShutdownHandlers(options: InstallShutdownOptions = {}): void {\n if (installed) return;\n installed = true;\n\n const listen =\n options.onSignal ??\n ((signal: ShutdownReason, handler: () => void) => {\n process.on(signal, handler);\n });\n\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n listen(signal, () => {\n void beginShutdown(signal, options);\n });\n }\n}\n\n/**\n * Run the shutdown sequence. Exported for the callers that need to trigger it\n * without a signal (a fatal condition, a test).\n */\nexport async function beginShutdown(\n reason: ShutdownReason,\n options: InstallShutdownOptions = {},\n): Promise<void> {\n const exit = options.exit ?? ((code: number) => process.exit(code));\n const notify = options.notify ?? ((message: string) => process.stderr.write(`${message}\\n`));\n\n if (shuttingDown) {\n // Second interrupt. The person has said what they want.\n forced = true;\n killChildren('SIGKILL');\n exit(EXIT_CODES[reason]);\n return;\n }\n\n shuttingDown = true;\n // 1. Stop accepting new work, and cut every pending backoff short.\n getShutdownSignal();\n controller?.abort();\n notify(\n `\\nInterrupted. Saving a checkpoint and stopping the agent (press ${reason === 'SIGINT' ? 'Ctrl+C' : 'the signal'} again to quit now)…`,\n );\n\n // 2. Checkpoint, while the child is still alive and the state is coherent.\n await runHooks('checkpoint', reason, notify);\n\n // 3. Ask the child to stop, then insist.\n await terminateChildren(options.graceMs ?? SHUTDOWN_GRACE_MS);\n\n // 4. Close the output surfaces last, so everything above is on disk.\n await runHooks('close', reason, notify);\n\n exit(EXIT_CODES[reason]);\n}\n\nasync function runHooks(\n phase: ShutdownPhase,\n reason: ShutdownReason,\n notify: (message: string) => void,\n): Promise<void> {\n for (const hook of [...hooks]) {\n if (hook.phase !== phase) continue;\n try {\n await hook.run(reason);\n } catch (err) {\n // A hook that fails must not stop the ones after it: the journal still\n // has to be closed even if the checkpoint could not be written.\n notify(\n `issue-flow: a shutdown step failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n}\n\nfunction killChildren(signal: NodeJS.Signals): void {\n for (const child of [...children]) {\n try {\n child.kill(signal);\n } catch {\n // Already gone. Nothing to do.\n }\n }\n}\n\n/**\n * `SIGTERM`, then wait, then `SIGKILL`.\n *\n * The wait ends early when every child has exited, and immediately when a\n * second interrupt arrives — polling rather than racing the `done` promises so\n * that a child which never settles cannot hold the process open forever.\n */\nasync function terminateChildren(graceMs: number): Promise<void> {\n if (children.size === 0) return;\n\n const pending = [...children];\n let alive = pending.length;\n for (const child of pending) {\n void child.done.then(\n () => {\n alive--;\n },\n () => {\n alive--;\n },\n );\n }\n\n killChildren('SIGTERM');\n\n const deadline = Date.now() + graceMs;\n while (alive > 0 && !forced && Date.now() < deadline) {\n await sleep(GRACE_POLL_MS);\n }\n\n if (alive > 0) killChildren('SIGKILL');\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n timer.unref?.();\n });\n}\n\n/** Drop every handler, hook and child. For tests only. */\nexport function resetShutdownState(): void {\n controller = null;\n installed = false;\n shuttingDown = false;\n forced = false;\n hooks.clear();\n children.clear();\n}\n","import { SHUTDOWN_GRACE_MS, type TerminableChild } from './shutdown.js';\n\n/**\n * Inactivity watchdog: telling a long task from a stuck one.\n *\n * The pipeline had exactly one instrument for a hung agent — the absolute\n * timeout — and it cannot tell the difference between a phase that has been\n * working hard for fourteen minutes and a phase that has produced nothing for\n * fourteen minutes. The execute loop did not even have that: it runs with\n * `timeout: 0` by design, because its budget is iterations rather than seconds,\n * so a `claude` that hangs without printing anything hangs forever.\n *\n * What separates the two cases is *output*. An agent that is working emits\n * events continuously; one that is stuck emits nothing. So the watchdog watches\n * the stream, not the clock: every event is a heartbeat, and only the **absence**\n * of heartbeats for longer than the limit is a stall.\n *\n * The absolute timeout stays exactly where it was. This is a second, tighter\n * instrument, not a replacement: the ceiling still bounds a task that keeps\n * talking and never finishes.\n */\n\n/** Default silence before a child is considered stuck. */\nexport const DEFAULT_INACTIVITY_TIMEOUT_MS = 600_000;\n\nexport interface WatchdogOptions {\n /** Silence tolerated before the child is stopped. `0` disables the watchdog. */\n inactivityTimeoutMs?: number;\n /** The process to stop. Omitted for a watchdog that only reports. */\n child?: TerminableChild;\n /** Grace between `SIGTERM` and `SIGKILL`. Defaults to the shutdown's. */\n graceMs?: number;\n /** Called once, when the silence limit is crossed. */\n onStall?: (silentMs: number) => void;\n /** Injectable clock, so a test drives the silence without waiting for it. */\n clock?: () => number;\n /** Injectable timer factory, so a test can use fake timers deliberately. */\n setInterval?: (handler: () => void, ms: number) => NodeJS.Timeout;\n clearInterval?: (timer: NodeJS.Timeout) => void;\n}\n\nexport interface Watchdog {\n /** Record a sign of life. Called once per event of the stream. */\n beat(): void;\n /** Whether the watchdog decided the child was stuck. */\n readonly stalled: boolean;\n /** How long the child had been silent when it was judged. */\n readonly silentMs: number;\n /** Stop watching. Idempotent; always call it when the child finishes. */\n stop(): void;\n}\n\n/** How often the silence is measured, as a fraction of the limit. */\nconst CHECK_DIVISOR = 10;\nconst MIN_CHECK_MS = 250;\nconst MAX_CHECK_MS = 5_000;\n\nfunction checkIntervalFor(timeoutMs: number): number {\n return Math.min(MAX_CHECK_MS, Math.max(MIN_CHECK_MS, Math.floor(timeoutMs / CHECK_DIVISOR)));\n}\n\n/**\n * Watch a stream for silence and stop the child when it goes quiet.\n *\n * `inactivityTimeoutMs: 0` returns an inert watchdog: `beat()` is a no-op and\n * nothing is ever killed. That is the documented \"off\" switch, and it is what\n * keeps this feature opt-out without a second code path anywhere else.\n */\nexport function createWatchdog(options: WatchdogOptions = {}): Watchdog {\n const timeoutMs = options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;\n const clock = options.clock ?? Date.now;\n const start = options.setInterval ?? ((handler, ms) => setInterval(handler, ms));\n const clear = options.clearInterval ?? ((timer) => clearInterval(timer));\n\n const state = { stalled: false, silentMs: 0 };\n\n if (timeoutMs <= 0) {\n return {\n beat: () => {},\n stop: () => {},\n get stalled() {\n return state.stalled;\n },\n get silentMs() {\n return state.silentMs;\n },\n };\n }\n\n let lastBeat = clock();\n let timer: NodeJS.Timeout | null = null;\n let killTimer: NodeJS.Timeout | null = null;\n\n const stop = (): void => {\n if (timer !== null) {\n clear(timer);\n timer = null;\n }\n if (killTimer !== null) {\n clearTimeout(killTimer);\n killTimer = null;\n }\n };\n\n const declareStalled = (): void => {\n state.stalled = true;\n state.silentMs = clock() - lastBeat;\n stop();\n options.onStall?.(state.silentMs);\n\n const child = options.child;\n if (child === undefined) return;\n\n // The same courtesy the shutdown extends: ask first, insist after. An agent\n // killed outright mid-write leaves a half-written file behind.\n child.kill('SIGTERM');\n killTimer = setTimeout(() => {\n child.kill('SIGKILL');\n }, options.graceMs ?? SHUTDOWN_GRACE_MS);\n killTimer.unref?.();\n void child.done.then(\n () => {\n if (killTimer !== null) {\n clearTimeout(killTimer);\n killTimer = null;\n }\n },\n () => {},\n );\n };\n\n timer = start(() => {\n if (state.stalled) return;\n if (clock() - lastBeat > timeoutMs) declareStalled();\n }, checkIntervalFor(timeoutMs));\n timer.unref?.();\n\n return {\n beat: () => {\n lastBeat = clock();\n },\n stop,\n get stalled() {\n return state.stalled;\n },\n get silentMs() {\n return state.silentMs;\n },\n };\n}\n\n/**\n * The message a stalled invocation reports.\n *\n * The wording is a contract, exactly like the timeout's: `classify()` reads it\n * as a last resort, and `stalled` has to survive the trip through a plain\n * string so the phase gets its retries.\n */\nexport function describeStall(silentMs: number): string {\n return `claude produced no output for ${Math.round(silentMs / 1000)}s and was stopped (stalled)`;\n}\n"],"mappings":"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","names":["timer"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/session-metrics.ts","../src/policy/placeholders.ts","../src/core/stream.ts"],"sourcesContent":["import { type ClaudeUsage, hasUsageData, sumUsage } from './metrics.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { isoNow } from './state-manager.js';\n\n/**\n * Publication helper for the single-invocation phases (analyze, generate, prd,\n * plan, review, pr, pr-review).\n *\n * Each of them owns its own `runHeadless` call, so the metrics are published\n * from inside the command rather than from the `instrumentedRunners` wrapper in\n * run.ts: the wrapper only sees `() => Promise<void>` and never touches the\n * HeadlessResult. Publishing here also covers standalone invocations\n * (`issue-flow prd 42`), which never go through the pipeline wrapper at all.\n */\n\n/** Whole seconds elapsed since a `Date.now()` mark, never negative. */\nexport function elapsedSecondsSince(startedAtMs: number): number {\n return Math.max(0, Math.round((Date.now() - startedAtMs) / 1000));\n}\n\n/**\n * Process-owned totals, accumulated alongside every published event.\n *\n * The session snapshot cannot be the source of the terminal summary: with web\n * monitoring off the publisher is a `NullPublisher` and its snapshot stays\n * empty, so the numbers would silently vanish exactly in the default mode.\n * These counters live in the process and are therefore independent of any\n * publisher.\n *\n * Only phase- and iteration-scoped usage is recorded. Story metrics are a\n * rateio of an iteration already counted here — adding them would double the\n * totals, the same rule the reducer follows.\n */\ninterface UsageAccumulator {\n all: ClaudeUsage;\n byPhase: Map<string, ClaudeUsage>;\n}\n\nfunction createAccumulator(): UsageAccumulator {\n return { all: {}, byPhase: new Map() };\n}\n\n/**\n * Stack of active accumulators, innermost last.\n *\n * The bottom entry is the process total and always exists. A multi-issue run\n * pushes one accumulator per issue (and one for the whole queue) so the terminal\n * summary of issue B never inherits what issue A spent — the leak `core/CLAUDE.md`\n * warned about when it stated that the module-level counters were safe \"only\n * under the current one-issue-per-process model\".\n *\n * Every publication feeds **all** active accumulators, so a scope is a view of\n * the same events rather than a redirection of them.\n */\nconst scopes: UsageAccumulator[] = [createAccumulator()];\n\n/** Innermost active scope: what `run` and the engine report right now. */\nfunction current(): UsageAccumulator {\n return scopes[scopes.length - 1] as UsageAccumulator;\n}\n\nfunction recordRunUsage(phase: string, usage: ClaudeUsage): void {\n for (const scope of scopes) {\n scope.all = sumUsage(scope.all, usage);\n scope.byPhase.set(phase, sumUsage(scope.byPhase.get(phase), usage));\n }\n}\n\n/** Everything the innermost active scope has spent, across all phases. */\nexport function getRunUsageTotals(): ClaudeUsage {\n return { ...current().all };\n}\n\n/** What a single phase has spent so far. Empty when the phase reported nothing. */\nexport function getPhaseUsageTotals(phase: string): ClaudeUsage {\n return { ...(current().byPhase.get(phase) ?? {}) };\n}\n\n/** A usage scope opened by {@link beginUsageScope}. */\nexport interface UsageScope {\n /** Everything published while this scope was open. */\n totals(): ClaudeUsage;\n /** What one phase published while this scope was open. */\n phaseTotals(phase: string): ClaudeUsage;\n /** Close the scope. Idempotent, and safe to call out of order. */\n end(): ClaudeUsage;\n}\n\n/**\n * Open a nested usage scope, for a run that processes several issues in one\n * process.\n *\n * The scope accumulates only what is published while it is open, which is what\n * lets a queue report a per-issue cost and a consolidated total from the same\n * stream of events. Closing it never discards anything: the outer scopes were\n * fed in parallel.\n */\nexport function beginUsageScope(): UsageScope {\n const accumulator = createAccumulator();\n scopes.push(accumulator);\n let open = true;\n\n return {\n totals: () => ({ ...accumulator.all }),\n phaseTotals: (phase: string) => ({ ...(accumulator.byPhase.get(phase) ?? {}) }),\n end: () => {\n if (open) {\n const index = scopes.indexOf(accumulator);\n // Never pop the process total, even if a caller double-ends a scope.\n if (index > 0) scopes.splice(index, 1);\n open = false;\n }\n return { ...accumulator.all };\n },\n };\n}\n\n/** Test seam: the counters are module state and must be cleared between runs. */\nexport function resetRunUsageTotals(): void {\n scopes.splice(0, scopes.length, createAccumulator());\n}\n\n/**\n * Publish one `metrics:update` event of scope `phase` for a headless\n * invocation.\n *\n * A no-op when the CLI reported no usage at all: an event with every field\n * undefined would bump the snapshot version without adding information, and the\n * reducer would leave every counter at null anyway.\n *\n * A command with more than one invocation (the retrying phases, or `review`\n * inside a correction cycle) calls this once per invocation — the reducer's\n * summing is what turns them into the phase total.\n *\n * Never throws and never returns a value: publishing metrics must not be able\n * to change a command's exit code.\n */\nexport function publishPhaseMetrics(\n phase: string,\n usage: ClaudeUsage | null | undefined,\n startedAtMs?: number,\n): void {\n if (usage == null || !hasUsageData(usage)) return;\n\n recordRunUsage(phase, usage);\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'phase',\n phase,\n ...usage,\n // Informational only: a phase's durationSeconds keeps coming exclusively\n // from phase:start/phase:end, so the reducer ignores this for scope 'phase'.\n durationSeconds: startedAtMs === undefined ? undefined : elapsedSecondsSince(startedAtMs),\n });\n}\n\n/** The execute loop is the only producer of iteration- and story-scoped metrics. */\nexport const EXECUTE_PHASE = 'execute';\n\n/**\n * Publish one `metrics:update` event of scope `iteration` for a pass of the\n * execute loop.\n *\n * Lands on the `execute` phase and on the issue-wide aggregate, exactly like a\n * phase-scoped event: an iteration *is* a slice of the execute phase. Failed\n * iterations (transient or fatal) publish too — those tokens were spent.\n *\n * A no-op when the CLI reported no usage. Never throws.\n */\nexport function publishIterationMetrics(\n iteration: number,\n usage: ClaudeUsage | null | undefined,\n durationSeconds?: number,\n): void {\n if (usage == null || !hasUsageData(usage)) return;\n\n recordRunUsage(EXECUTE_PHASE, usage);\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'iteration',\n phase: EXECUTE_PHASE,\n iteration,\n ...usage,\n // Informational only: the reducer keeps a phase's durationSeconds coming\n // exclusively from phase:start/phase:end.\n durationSeconds,\n });\n}\n\n/**\n * Publish one `metrics:update` event of scope `story`.\n *\n * The values are the rateio of the iteration that completed the story, so the\n * reducer deliberately keeps them off the phase and the aggregate — the\n * iteration event of the same cycle already counted them there.\n *\n * A no-op when there is neither usage nor a duration to report. Never throws.\n */\nexport function publishStoryMetrics(\n storyId: string,\n usage: ClaudeUsage | null | undefined,\n durationSeconds?: number,\n): void {\n if (!hasUsageData(usage) && durationSeconds === undefined) return;\n\n getSessionPublisher().publish({\n type: 'metrics:update',\n at: isoNow(),\n scope: 'story',\n storyId,\n ...(usage ?? {}),\n durationSeconds,\n });\n}\n","import { loadPolicyConfig } from '../config.js';\nimport { getBaseBranch } from '../utils/git.js';\nimport { loadRepositoryPolicy } from './resolve.js';\nimport type { IssueTemplate, PolicyDocument, RepositoryPolicy } from './types.js';\n\n/**\n * Projection of a {@link RepositoryPolicy} onto the prompt placeholders.\n *\n * The prompts receive a **summary**, never the raw files. Dumping\n * `CONTRIBUTING.md` and three `AGENTS.md` into every prompt would cost tokens on\n * every single run and drown the actual instruction — and it would duplicate,\n * inside Issue Flow, a rule that already lives in the repository.\n *\n * The whole projection is empty when the repository declares nothing, which is\n * what lets a prompt render byte for byte as it did before this layer existed.\n */\n\n/** Every placeholder this module produces. Ordered as they appear in a prompt. */\nexport const POLICY_PLACEHOLDER_KEYS = [\n '__REPO_POLICY__',\n '__REPO_ISSUE_TEMPLATES__',\n '__REPO_LABELS__',\n '__REPO_ISSUE_TYPES__',\n '__REPO_PR_TEMPLATE__',\n '__REPO_BASE_BRANCH__',\n '__REPO_CONVENTIONS__',\n '__REPO_DOCS__',\n] as const;\n\nexport type PolicyPlaceholderKey = (typeof POLICY_PLACEHOLDER_KEYS)[number];\n\n/** Every placeholder empty — the projection of \"this repository declares nothing\". */\nexport function emptyPolicyPlaceholders(): Record<string, string> {\n return Object.fromEntries(POLICY_PLACEHOLDER_KEYS.map((key) => [key, ''])) as Record<\n string,\n string\n >;\n}\n\n/**\n * Rough token count of a rendered section.\n *\n * Four characters per token is the usual English/Portuguese approximation and is\n * deliberately crude: the budget exists to stop a policy from swallowing the\n * prompt, not to bill anyone. Erring high is the safe direction, so the estimate\n * rounds up.\n */\nexport function estimateTokens(text: string): number {\n return Math.ceil(text.length / 4);\n}\n\n// ── Section renderers ───────────────────────────────────────────────────────\n\nfunction renderIssueTemplates(templates: IssueTemplate[]): string {\n if (templates.length === 0) return '';\n\n const lines = templates.map((template) => {\n const parts = [`- **${template.name ?? template.path}** (\\`${template.path}\\`)`];\n if (template.type !== null) parts.push(`type: ${template.type}`);\n if (template.labels.length > 0) parts.push(`labels: ${template.labels.join(', ')}`);\n if (template.title !== null) parts.push(`title prefix: \\`${template.title}\\``);\n if (template.about !== null) parts.push(template.about);\n return parts.join(' — ');\n });\n\n return lines.join('\\n');\n}\n\nfunction renderLabels(policy: RepositoryPolicy): string {\n if (policy.issues.labels.length === 0) return '';\n\n return policy.issues.labels\n .map((label) =>\n label.description === null ? `- ${label.name}` : `- ${label.name} — ${label.description}`,\n )\n .join('\\n');\n}\n\nfunction renderConventions(policy: RepositoryPolicy): string {\n const rows: string[] = [];\n if (policy.issues.titleConvention !== null) {\n rows.push(`- Issue title: ${policy.issues.titleConvention}`);\n }\n if (policy.pullRequests.titleConvention !== null) {\n rows.push(`- Pull Request title: ${policy.pullRequests.titleConvention}`);\n }\n if (policy.git.branchConvention !== null) {\n rows.push(`- Branch: ${policy.git.branchConvention}`);\n }\n if (policy.git.commitConvention !== null) {\n rows.push(`- Commit: ${policy.git.commitConvention}`);\n }\n return rows.join('\\n');\n}\n\n/**\n * Paths of the applicable documents — **never their content**.\n *\n * The agent has `Read` and fetches what it needs. Embedding the documents would\n * multiply the cost of every run and, worse, freeze a copy of the repository's\n * own rule inside a prompt.\n */\nfunction renderDocs(docs: PolicyDocument[]): string {\n if (docs.length === 0) return '';\n\n return docs\n .map((doc) => {\n const scope = doc.scope === '' ? '' : ` (applies to \\`${doc.scope}/\\`)`;\n return `- \\`${doc.path}\\`${scope}`;\n })\n .join('\\n');\n}\n\n// ── The budgeted summary ────────────────────────────────────────────────────\n\ninterface Section {\n /** Heading inside the summary. */\n title: string;\n body: string;\n /**\n * Kept whole even when the budget is blown. The essentials are the taxonomy\n * an agent cannot guess: it can re-read a document, but it cannot invent the\n * label a repository actually uses.\n */\n essential: boolean;\n /** Shown in place of the body once the budget is exhausted. */\n pointer: string;\n}\n\n/**\n * Compose `__REPO_POLICY__` within `budgetTokens`.\n *\n * Sections are added whole, in priority order, while they fit. A section that\n * does not fit is replaced **whole** by a pointer to where the agent can read it\n * — nothing is ever cut mid-sentence, because a summary truncated mid-rule is\n * worse than one that says where the rule lives.\n */\nexport function renderPolicySummary(policy: RepositoryPolicy, budgetTokens: number): string {\n const sections: Section[] = [\n {\n title: 'Base branch',\n body: policy.pullRequests.baseBranch ?? '',\n essential: true,\n pointer: '',\n },\n {\n title: 'Conventions',\n body: renderConventions(policy),\n essential: true,\n pointer: '',\n },\n {\n title: 'Issue Types',\n body: policy.issues.types.join(', '),\n essential: true,\n pointer: '',\n },\n {\n title: 'Issue Templates',\n body: renderIssueTemplates(policy.issues.templates),\n essential: true,\n pointer: 'Run `issue-flow policy --json` to read the templates in full.',\n },\n {\n title: 'Labels',\n body: renderLabels(policy),\n essential: true,\n pointer: 'Run `gh label list` to read the labels of this repository.',\n },\n {\n title: 'Pull Request template',\n body: policy.pullRequests.templates[0]?.path ?? '',\n essential: false,\n pointer: '',\n },\n {\n title: 'Policy documents',\n body: renderDocs(policy.docs),\n essential: false,\n pointer: '',\n },\n ];\n\n const rendered: string[] = [];\n let used = 0;\n\n for (const section of sections) {\n if (section.body.trim() === '') continue;\n\n const block = `### ${section.title}\\n\\n${section.body}`;\n const cost = estimateTokens(block);\n\n if (used + cost <= budgetTokens) {\n rendered.push(block);\n used += cost;\n continue;\n }\n\n // Over budget. An essential section keeps its slot as a pointer; a\n // non-essential one simply drops out.\n if (section.essential && section.pointer !== '') {\n const fallback = `### ${section.title}\\n\\n${section.pointer}`;\n rendered.push(fallback);\n used += estimateTokens(fallback);\n }\n }\n\n return rendered.join('\\n\\n');\n}\n\n/** True when the policy carries nothing a prompt could act on. */\nexport function isEmptyPolicy(policy: RepositoryPolicy): boolean {\n return (\n policy.issues.templates.length === 0 &&\n policy.issues.types.length === 0 &&\n policy.issues.labels.length === 0 &&\n policy.issues.titleConvention === null &&\n policy.pullRequests.templates.length === 0 &&\n policy.pullRequests.baseBranch === null &&\n policy.pullRequests.titleConvention === null &&\n policy.git.branchConvention === null &&\n policy.git.commitConvention === null &&\n policy.docs.length === 0\n );\n}\n\nexport interface PolicyPlaceholderOptions {\n /** Token budget for `__REPO_POLICY__`. Defaults to the resolved configuration. */\n budgetTokens?: number;\n}\n\n/**\n * Project a resolved policy onto the prompt placeholders.\n *\n * A `null` policy — or one that carries nothing — yields every placeholder\n * empty, which is what makes the rendered prompt identical to the pre-policy\n * one. This is the contract the whole feature rests on.\n */\nexport function policyPlaceholders(\n policy: RepositoryPolicy | null,\n options: PolicyPlaceholderOptions = {},\n): Record<string, string> {\n if (policy === null || !policy.enabled || isEmptyPolicy(policy)) {\n return emptyPolicyPlaceholders();\n }\n\n const budget = options.budgetTokens ?? DEFAULT_POLICY_CONTEXT_BUDGET;\n\n return {\n __REPO_POLICY__: renderPolicySummary(policy, budget),\n __REPO_ISSUE_TEMPLATES__: renderIssueTemplates(policy.issues.templates),\n __REPO_LABELS__: renderLabels(policy),\n __REPO_ISSUE_TYPES__: policy.issues.types.join(', '),\n // The one place a whole file is projected verbatim: a Pull Request body has\n // to *be* the template, so a summary of it would be useless.\n __REPO_PR_TEMPLATE__: policy.pullRequests.template ?? '',\n __REPO_BASE_BRANCH__: policy.pullRequests.baseBranch ?? '',\n __REPO_CONVENTIONS__: renderConventions(policy),\n __REPO_DOCS__: renderDocs(policy.docs),\n };\n}\n\n/** Default token budget for `__REPO_POLICY__`, overridable via `policy.contextBudget`. */\nexport const DEFAULT_POLICY_CONTEXT_BUDGET = 1500;\n\n// ── Conventions that always have a value ────────────────────────────────────\n\n/** Branch naming Issue Flow has always used, when the repository declares none. */\nexport const DEFAULT_BRANCH_CONVENTION = 'issue/{N}-{slug}';\n\n/**\n * Placeholders that must **never** render empty, because a prompt puts them\n * inside a command it asks the agent to run.\n *\n * `__BASE_BRANCH__` is the reason this group exists separately from the policy\n * projection. `prompts/pr.md` used to spell `main` three times — in\n * `git log main..HEAD`, in `git diff main...HEAD` and in `gh pr create --base\n * main`. In a repository whose base is `develop` that is not a missing feature\n * but an active defect: `main` often *exists* there too, so nothing fails — the\n * agent simply reviews the wrong diff and opens the Pull Request against the\n * wrong target.\n *\n * The resolution order is the repository's declared or discovered base, then\n * `getBaseBranch()`, whose own fallback is `main`. So a repository on `main`\n * renders exactly the text it rendered before.\n */\nexport function conventionPlaceholders(\n policy: RepositoryPolicy | null,\n baseBranchFallback: string,\n): Record<string, string> {\n return {\n __BASE_BRANCH__: policy?.pullRequests.baseBranch ?? baseBranchFallback,\n __BRANCH_CONVENTION__: policy?.git.branchConvention ?? DEFAULT_BRANCH_CONVENTION,\n // The one member of this group that may legitimately render empty: it gates\n // a conditional section rather than sitting inside a command.\n __COMMIT_CONVENTION__: policy?.git.commitConvention ?? '',\n };\n}\n\nexport interface ResolvePolicyPlaceholdersOptions {\n /** Repository root. Defaults to the git project root. */\n root?: string;\n /** Subdirectory the policy applies to, for monorepos. */\n scope?: string | null;\n}\n\n/**\n * Resolve the repository policy and project it, in one call.\n *\n * This is what every command uses. It **never throws and never warns**: a\n * repository that is not a git checkout, a discovery that fails, a `gh` that is\n * not installed — all of them yield the empty projection, and the command runs\n * exactly as it did before.\n *\n * The resolution itself is cached per `(root, scope)` by `loadRepositoryPolicy`,\n * so a pipeline that renders eight prompts still discovers once.\n */\nexport async function resolvePolicyPlaceholders(\n options: ResolvePolicyPlaceholdersOptions = {},\n): Promise<Record<string, string>> {\n let policy: RepositoryPolicy | null = null;\n let projection = emptyPolicyPlaceholders();\n try {\n policy = await loadRepositoryPolicy({ root: options.root, scope: options.scope ?? null });\n const config = await loadPolicyConfig({ projectRoot: policy.root });\n projection = policyPlaceholders(policy, { budgetTokens: config.contextBudget });\n } catch {\n // Discovery is an enrichment; the conventions below still have to resolve.\n policy = null;\n }\n\n let fallbackBase = 'main';\n try {\n fallbackBase = await getBaseBranch();\n } catch {\n // getBaseBranch() does not throw today, but its contract is \"never fails\",\n // and a base branch that cannot be resolved must not cost the whole prompt.\n }\n\n return { ...projection, ...conventionPlaceholders(policy, fallbackBase) };\n}\n","import { createInterface } from 'node:readline';\nimport type { Readable } from 'node:stream';\nimport { type ClaudeUsage, parseUsage } from './metrics.js';\n\n/**\n * Reading the CLI's `stream-json` output.\n *\n * The project used to ask for two different formats depending on whether the\n * user passed `--verbose`: `stream-json` when it had something to print, and a\n * single `json` envelope otherwise. The envelope is one write at the very end,\n * which means the non-verbose path — the common one, and the one that runs\n * unattended for hours — had **no signal at all** while the agent worked. An\n * agent that hung produced exactly the same observable behaviour as one that\n * was thinking.\n *\n * So the stream is now always what is requested, and only the *rendering*\n * differs: verbose prints each event, non-verbose feeds a spinner and a\n * watchdog heartbeat. This module is the half both share — the parsing, the\n * final result and the usage — so neither can drift from the other.\n */\n\n/** What a finished stream yielded. */\nexport interface StreamOutcome {\n /** Assistant text of the `result` event; `''` when none arrived. */\n result: string;\n /** Whether the `result` event reported an error. */\n isError: boolean;\n usage: ClaudeUsage | null;\n /** How many lines were read. `0` means the CLI printed nothing at all. */\n events: number;\n /**\n * Everything the stream printed, joined back together.\n *\n * The fallback for a CLI build that ignores `--output-format` and prints\n * plain text: the stream is consumed by the time the process finishes, so\n * `result.stdout` is empty and this is the only copy of what it said.\n */\n raw: string;\n}\n\nexport interface ReadStreamOptions {\n /** Called for every line, parsed or not. This is the watchdog's heartbeat. */\n onLine?: (line: string) => void;\n /** Called for every event that parsed, after `onLine`. */\n onEvent?: (event: Record<string, unknown>) => void;\n}\n\n/**\n * Consume `stream-json` from `stdout` until it ends.\n *\n * Malformed lines are skipped rather than fatal: the CLI interleaves its own\n * diagnostics with the stream, and a run must not die because one line was not\n * JSON. They still count as activity — a process writing anything is a process\n * that is alive, which is the only question the watchdog is asking.\n */\nexport async function readClaudeStream(\n stdout: Readable,\n options: ReadStreamOptions = {},\n): Promise<StreamOutcome> {\n const outcome: StreamOutcome = { result: '', isError: false, usage: null, events: 0, raw: '' };\n const collected: string[] = [];\n\n const lines = createInterface({ input: stdout });\n for await (const line of lines) {\n options.onLine?.(line);\n if (line.trim() === '') continue;\n outcome.events++;\n collected.push(line);\n\n let event: unknown;\n try {\n event = JSON.parse(line);\n } catch {\n continue;\n }\n if (typeof event !== 'object' || event === null) continue;\n\n const record = event as Record<string, unknown>;\n options.onEvent?.(record);\n\n if (record.type === 'result') {\n outcome.result = typeof record.result === 'string' ? record.result : '';\n outcome.isError = record.is_error === true;\n // Keep the previous metrics when this event carries none, so a malformed\n // trailing result never erases what was already captured.\n outcome.usage = parseUsage(record) ?? outcome.usage;\n }\n }\n\n outcome.raw = collected.join('\\n');\n return outcome;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/init.ts"],"sourcesContent":["import { execa } from 'execa';\nimport type { IssueSource } from '../issues/types.js';\nimport type { ScaffoldActionKind, ScaffoldPlan } from '../scaffold/plan.js';\nimport { printError, printInfo, printSuccess, printWarning } from '../ui/logger.js';\n\n/** Stable identifier of a check, used to decide whether it blocks. */\ntype CheckKey = 'claude' | 'gh' | 'git';\n\ninterface CheckResult {\n key: CheckKey;\n name: string;\n passed: boolean;\n detail: string;\n hint?: string;\n}\n\nasync function checkClaude(): Promise<CheckResult> {\n try {\n const proc = await execa('claude', ['--version'], { reject: false, timeout: 10_000 });\n if (proc.exitCode === 0) {\n const version = proc.stdout?.toString().trim() ?? 'unknown';\n return { key: 'claude', name: 'claude CLI', passed: true, detail: version };\n }\n return {\n key: 'claude',\n name: 'claude CLI',\n passed: false,\n detail: 'claude command failed',\n hint: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code',\n };\n } catch {\n return {\n key: 'claude',\n name: 'claude CLI',\n passed: false,\n detail: 'claude not found',\n hint: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code',\n };\n }\n}\n\nasync function checkGh(): Promise<CheckResult> {\n try {\n const proc = await execa('gh', ['--version'], { reject: false, timeout: 10_000 });\n if (proc.exitCode !== 0) {\n return {\n key: 'gh',\n name: 'gh CLI',\n passed: false,\n detail: 'gh command failed',\n hint: 'Install GitHub CLI: https://cli.github.com/',\n };\n }\n const version = proc.stdout?.toString().split('\\n')[0]?.trim() ?? 'unknown';\n\n // Check auth status\n const auth = await execa('gh', ['auth', 'status'], { reject: false, timeout: 10_000 });\n if (auth.exitCode !== 0) {\n return {\n key: 'gh',\n name: 'gh CLI',\n passed: false,\n detail: `${version} (not authenticated)`,\n hint: 'Run: gh auth login',\n };\n }\n return { key: 'gh', name: 'gh CLI', passed: true, detail: `${version} (authenticated)` };\n } catch {\n return {\n key: 'gh',\n name: 'gh CLI',\n passed: false,\n detail: 'gh not found',\n hint: 'Install GitHub CLI: https://cli.github.com/',\n };\n }\n}\n\nasync function checkGit(): Promise<CheckResult> {\n try {\n const proc = await execa('git', ['--version'], { reject: false, timeout: 10_000 });\n if (proc.exitCode !== 0) {\n return {\n key: 'git',\n name: 'git',\n passed: false,\n detail: 'git command failed',\n hint: 'Install git: https://git-scm.com/',\n };\n }\n const version = proc.stdout?.toString().trim() ?? 'unknown';\n\n // Check if current directory is a git repo\n const repo = await execa('git', ['rev-parse', '--is-inside-work-tree'], {\n reject: false,\n timeout: 5_000,\n });\n if (repo.exitCode !== 0) {\n return {\n key: 'git',\n name: 'git',\n passed: false,\n detail: `${version} (not a git repository)`,\n hint: 'Run this command inside a git repository',\n };\n }\n return { key: 'git', name: 'git', passed: true, detail: `${version} (inside repo)` };\n } catch {\n return {\n key: 'git',\n name: 'git',\n passed: false,\n detail: 'git not found',\n hint: 'Install git: https://git-scm.com/',\n };\n }\n}\n\nexport interface InitOptions {\n /** Write the missing files instead of only reporting them. */\n apply?: boolean;\n /** Emit the plan as JSON — the bridge the initialization skill reads. */\n json?: boolean;\n /** Subdirectory the conventions apply to, in a monorepo. */\n scope?: string;\n /** Skip the convention report entirely and only check prerequisites. */\n checkOnly?: boolean;\n}\n\nconst ACTION_ICON: Record<ScaffoldActionKind, string> = {\n create: '+',\n keep: '=',\n review: '!',\n};\n\n/**\n * Render the plan for a human.\n *\n * `keep` lines are printed, not hidden: the most valuable thing this report\n * says is usually *what it is not going to touch*, and a list of only the\n * missing files would read as if the repository had nothing.\n */\nfunction renderPlan(plan: ScaffoldPlan, apply: boolean): void {\n const creates = plan.actions.filter((a) => a.kind === 'create');\n const reviews = plan.actions.filter((a) => a.kind === 'review');\n\n console.log('');\n printInfo(`Repository conventions (${plan.root})\\n`);\n\n for (const item of plan.actions) {\n console.log(` ${ACTION_ICON[item.kind]} ${item.path}`);\n console.log(` ${item.reason}`);\n }\n\n if (plan.notes.length > 0) {\n console.log('');\n for (const note of plan.notes) {\n printWarning(note);\n }\n }\n\n console.log('');\n if (creates.length === 0 && reviews.length === 0) {\n printSuccess('This repository already declares everything Issue Flow would add.');\n return;\n }\n if (creates.length === 0) {\n printSuccess('Nothing to create.');\n return;\n }\n if (!apply) {\n printInfo(`${creates.length} file(s) would be created. Re-run with --apply to write them.`);\n }\n}\n\n/**\n * Verify the prerequisites of the pipeline and report the repository's\n * conventions.\n *\n * `source` is the Issue origin the run is headed for. `gh` only blocks when\n * that origin is GitHub: no other origin shells out to it, so a missing or\n * unauthenticated gh is reported as a warning instead of failing the\n * environment. `claude` and `git` stay blocking for every origin, and the\n * default ('github') keeps the previous behaviour byte for byte.\n *\n * The convention report is additive: the prerequisite checks run first and still\n * decide the exit code, so an existing script calling `issue-flow init` sees the\n * same pass/fail it always did. Nothing is written without `--apply`.\n */\nexport async function runInit(\n source: IssueSource = 'github',\n options: InitOptions = {},\n): Promise<number> {\n const json = options.json === true;\n\n if (!json) {\n printInfo('Checking prerequisites...\\n');\n }\n\n const results = await Promise.all([checkClaude(), checkGh(), checkGit()]);\n const isBlocking = (r: CheckResult): boolean => r.key !== 'gh' || source === 'github';\n\n if (!json) {\n for (const r of results) {\n if (r.passed) {\n printSuccess(`${r.name}: ${r.detail}`);\n } else if (isBlocking(r)) {\n printError(`${r.name}: ${r.detail}`);\n if (r.hint) {\n console.log(` ${r.hint}`);\n }\n } else {\n printWarning(`${r.name}: ${r.detail} (not required for ${source} issues)`);\n }\n }\n }\n\n const allPassed = results.filter(isBlocking).every((r) => r.passed);\n\n if (!json) {\n console.log('');\n if (allPassed) {\n printSuccess('All prerequisites met. Ready to run the pipeline.');\n } else {\n printError('Some prerequisites are missing. Please fix the issues above.');\n }\n }\n\n if (options.checkOnly === true) {\n return allPassed ? 0 : 1;\n }\n\n // The conventions half. It never changes the exit code: a repository missing\n // a template is not a broken environment, and failing here would break every\n // script that treats `init` as a prerequisite gate.\n let plan: ScaffoldPlan;\n try {\n const { planRepositoryScaffold } = await import('../scaffold/apply.js');\n plan = await planRepositoryScaffold({ scope: options.scope ?? null });\n } catch (err) {\n if (json) {\n console.log(\n JSON.stringify(\n { schemaVersion: 1, error: err instanceof Error ? err.message : String(err) },\n null,\n 2,\n ),\n );\n return allPassed ? 0 : 1;\n }\n printWarning(\n `Could not inspect the repository conventions: ${err instanceof Error ? err.message : String(err)}`,\n );\n return allPassed ? 0 : 1;\n }\n\n let applied: { written: string[]; skipped: string[] } | null = null;\n if (options.apply === true) {\n const { applyScaffoldPlan } = await import('../scaffold/apply.js');\n applied = await applyScaffoldPlan(plan);\n }\n\n if (json) {\n console.log(\n JSON.stringify(\n {\n schemaVersion: 1,\n prerequisites: results.map((r) => ({\n key: r.key,\n passed: r.passed,\n detail: r.detail,\n blocking: isBlocking(r),\n })),\n root: plan.root,\n // Content is omitted: a skill decides from the actions, and the bodies\n // would make the payload unreadable for no gain.\n actions: plan.actions.map(({ path, kind, tier, reason }) => ({\n path,\n kind,\n tier,\n reason,\n })),\n notes: plan.notes,\n applied,\n },\n null,\n 2,\n ),\n );\n return allPassed ? 0 : 1;\n }\n\n renderPlan(plan, options.apply === true);\n\n if (applied !== null) {\n console.log('');\n for (const path of applied.written) {\n printSuccess(`Created ${path}`);\n }\n for (const path of applied.skipped) {\n printWarning(`Skipped ${path}: it already exists.`);\n }\n if (applied.written.length === 0) {\n printInfo('Nothing was written — the repository already had everything.');\n }\n }\n\n return allPassed ? 0 : 1;\n}\n"],"mappings":"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","names":[]}
|