issue-flow 0.11.0 → 0.12.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 +1 -1
- package/dist/{analyze-7B5TORNK.js → analyze-OEET7KGE.js} +17 -11
- package/dist/analyze-OEET7KGE.js.map +1 -0
- package/dist/{apply-WUJHDD7A.js → apply-ZIA3C52S.js} +6 -4
- package/dist/{apply-WUJHDD7A.js.map → apply-ZIA3C52S.js.map} +1 -1
- package/dist/{chunk-6N7LZZ5U.js → chunk-3QHK4YQA.js} +204 -107
- package/dist/chunk-3QHK4YQA.js.map +1 -0
- package/dist/chunk-42PJFMAR.js +605 -0
- package/dist/chunk-42PJFMAR.js.map +1 -0
- package/dist/{chunk-PL6CNMVS.js → chunk-4JSOYE4G.js} +105 -19
- package/dist/chunk-4JSOYE4G.js.map +1 -0
- package/dist/chunk-56KS3TFQ.js +703 -0
- package/dist/chunk-56KS3TFQ.js.map +1 -0
- package/dist/{chunk-AAY6KM7N.js → chunk-73ATPIYX.js} +160 -154
- package/dist/chunk-73ATPIYX.js.map +1 -0
- package/dist/{chunk-MKD2D7WE.js → chunk-A4SLHOW7.js} +70 -13
- package/dist/chunk-A4SLHOW7.js.map +1 -0
- package/dist/chunk-BAUPQXPE.js +173 -0
- package/dist/chunk-BAUPQXPE.js.map +1 -0
- package/dist/{chunk-HC2MT7BX.js → chunk-BYZDPR7N.js} +14 -581
- package/dist/chunk-BYZDPR7N.js.map +1 -0
- package/dist/chunk-CD5ULRVC.js +615 -0
- package/dist/chunk-CD5ULRVC.js.map +1 -0
- package/dist/chunk-DIDUVL6O.js +1365 -0
- package/dist/chunk-DIDUVL6O.js.map +1 -0
- package/dist/{chunk-47VIN33V.js → chunk-EZATO3DW.js} +7 -5
- package/dist/{chunk-47VIN33V.js.map → chunk-EZATO3DW.js.map} +1 -1
- package/dist/{chunk-JW7WLSZ6.js → chunk-FADTCQGM.js} +48 -12
- package/dist/chunk-FADTCQGM.js.map +1 -0
- package/dist/{chunk-GANSDJV7.js → chunk-FJP3UQGD.js} +15 -13
- package/dist/chunk-FJP3UQGD.js.map +1 -0
- package/dist/chunk-KMMASOA6.js +134 -0
- package/dist/chunk-KMMASOA6.js.map +1 -0
- package/dist/{chunk-HQYXVIH6.js → chunk-LAFNLLQP.js} +3 -3
- package/dist/chunk-LWRSMTTZ.js +18 -0
- package/dist/chunk-LWRSMTTZ.js.map +1 -0
- package/dist/{chunk-MSPUB635.js → chunk-M5UCXTIT.js} +15 -12
- package/dist/chunk-M5UCXTIT.js.map +1 -0
- package/dist/chunk-M66SJHQQ.js +206 -0
- package/dist/chunk-M66SJHQQ.js.map +1 -0
- package/dist/{chunk-OBLVLIUS.js → chunk-MJULIKQU.js} +42 -6
- package/dist/chunk-MJULIKQU.js.map +1 -0
- package/dist/{chunk-RKTEV6EG.js → chunk-NS6R37II.js} +4 -4
- package/dist/{run-H75MD5BQ.js → chunk-NTMMF5UD.js} +515 -544
- package/dist/chunk-NTMMF5UD.js.map +1 -0
- package/dist/{chunk-UBIQXCVE.js → chunk-QCPGHBSZ.js} +16 -13
- package/dist/chunk-QCPGHBSZ.js.map +1 -0
- package/dist/{chunk-LVKQ2AKT.js → chunk-TSOLWK7Y.js} +8 -6
- package/dist/chunk-TSOLWK7Y.js.map +1 -0
- package/dist/{chunk-VCKX262Z.js → chunk-XQUSIEKA.js} +13 -10
- package/dist/chunk-XQUSIEKA.js.map +1 -0
- package/dist/chunk-Y5ZDJ4PJ.js +76 -0
- package/dist/chunk-Y5ZDJ4PJ.js.map +1 -0
- package/dist/cli.js +156 -26
- package/dist/cli.js.map +1 -1
- package/dist/{config-4THHOXNN.js → config-PRJGWW7J.js} +15 -3
- package/dist/execute-XBNVL753.js +18 -0
- package/dist/{generate-6RA2MFJB.js → generate-EGB4KV2B.js} +18 -11
- package/dist/generate-EGB4KV2B.js.map +1 -0
- package/dist/init-AJHCXJBP.js +10 -0
- package/dist/operations-BYT626KQ.js +365 -0
- package/dist/operations-BYT626KQ.js.map +1 -0
- package/dist/plan-QOZ3RIO6.js +21 -0
- package/dist/{policy-XQW5NUIE.js → policy-6S4GFC76.js} +6 -4
- package/dist/{policy-XQW5NUIE.js.map → policy-6S4GFC76.js.map} +1 -1
- package/dist/pr-EBBHD2ZX.js +26 -0
- package/dist/pr-review-VVMW5PO4.js +20 -0
- package/dist/prd-JQSZF7PR.js +21 -0
- package/dist/resume-PPCNQVVV.js +221 -0
- package/dist/resume-PPCNQVVV.js.map +1 -0
- package/dist/review-3F6MARWH.js +19 -0
- package/dist/run-RXE5GDQO.js +36 -0
- package/dist/run-RXE5GDQO.js.map +1 -0
- package/dist/state-manager-2EDVDBAR.js +34 -0
- package/dist/state-manager-2EDVDBAR.js.map +1 -0
- package/dist/{web-ANQQTTRD.js → web-EF5WIIGG.js} +13 -8
- package/dist/{web-ANQQTTRD.js.map → web-EF5WIIGG.js.map} +1 -1
- package/package.json +1 -1
- package/prompts/pr.md +11 -4
- package/web/public/app.js +12 -0
- package/dist/analyze-7B5TORNK.js.map +0 -1
- package/dist/chunk-3BOHTH4V.js +0 -58
- package/dist/chunk-3BOHTH4V.js.map +0 -1
- package/dist/chunk-6N7LZZ5U.js.map +0 -1
- package/dist/chunk-AAY6KM7N.js.map +0 -1
- package/dist/chunk-GANSDJV7.js.map +0 -1
- package/dist/chunk-HC2MT7BX.js.map +0 -1
- package/dist/chunk-HDV3YDTG.js +0 -52
- package/dist/chunk-HDV3YDTG.js.map +0 -1
- package/dist/chunk-JW7WLSZ6.js.map +0 -1
- package/dist/chunk-LVKQ2AKT.js.map +0 -1
- package/dist/chunk-MKD2D7WE.js.map +0 -1
- package/dist/chunk-MSPUB635.js.map +0 -1
- package/dist/chunk-OBLVLIUS.js.map +0 -1
- package/dist/chunk-PL6CNMVS.js.map +0 -1
- package/dist/chunk-UBIQXCVE.js.map +0 -1
- package/dist/chunk-VCKX262Z.js.map +0 -1
- package/dist/chunk-VQ5VN7DF.js +0 -1018
- package/dist/chunk-VQ5VN7DF.js.map +0 -1
- package/dist/execute-5OEKLM5X.js +0 -15
- package/dist/generate-6RA2MFJB.js.map +0 -1
- package/dist/init-PRMOTQ7N.js +0 -9
- package/dist/plan-KENR6MJ3.js +0 -17
- package/dist/pr-KDGKG4BP.js +0 -21
- package/dist/pr-review-CAMAB7KO.js +0 -17
- package/dist/prd-V3MWDIHK.js +0 -17
- package/dist/review-7LN47SAN.js +0 -15
- package/dist/run-H75MD5BQ.js.map +0 -1
- /package/dist/{chunk-HQYXVIH6.js.map → chunk-LAFNLLQP.js.map} +0 -0
- /package/dist/{chunk-RKTEV6EG.js.map → chunk-NS6R37II.js.map} +0 -0
- /package/dist/{config-4THHOXNN.js.map → config-PRJGWW7J.js.map} +0 -0
- /package/dist/{execute-5OEKLM5X.js.map → execute-XBNVL753.js.map} +0 -0
- /package/dist/{init-PRMOTQ7N.js.map → init-AJHCXJBP.js.map} +0 -0
- /package/dist/{plan-KENR6MJ3.js.map → plan-QOZ3RIO6.js.map} +0 -0
- /package/dist/{pr-KDGKG4BP.js.map → pr-EBBHD2ZX.js.map} +0 -0
- /package/dist/{pr-review-CAMAB7KO.js.map → pr-review-VVMW5PO4.js.map} +0 -0
- /package/dist/{prd-V3MWDIHK.js.map → prd-JQSZF7PR.js.map} +0 -0
- /package/dist/{review-7LN47SAN.js.map → review-3F6MARWH.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/session-metrics.ts","../src/policy/placeholders.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"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/headless.ts"],"sourcesContent":["import { createInterface } from 'node:readline';\nimport chalk from 'chalk';\nimport { execa } from 'execa';\nimport { createSpinner, ElapsedTimer, formatDuration, getIcons, useColor } from '../ui/logger.js';\nimport { type ClaudeUsage, parseUsage } from './metrics.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { isoNow } from './state-manager.js';\nimport { getOutputCallback, isVerbose } from './verbose.js';\n\nexport interface HeadlessOptions {\n prompt: string;\n maxTurns?: number;\n timeout?: number;\n outputFormat?: 'json' | 'text' | 'stream-json';\n allowedTools?: string[];\n /**\n * Extra directories the headless session may read from and write to, passed\n * through as `--add-dir`. Required whenever a prompt points at a path outside\n * the working tree (e.g. the global issue storage under `~/.issue-flow`),\n * since `claude -p` otherwise refuses the access.\n */\n addDirs?: string[];\n /** Status message displayed as spinner (non-verbose) or header (verbose). */\n statusMessage?: string;\n /** Optional callback for routing verbose output (e.g., through listr2 task.output). When provided, verbose stream events are sent here instead of directly to stderr. */\n onOutput?: (line: string) => void;\n}\n\n/**\n * Token/cost metrics of a headless invocation.\n *\n * Alias of {@link ClaudeUsage} — the name is kept for the existing call sites,\n * but the shape (and the parsing behind it) now lives in core/metrics.ts, so\n * cache tokens and USD cost come along for free.\n */\nexport type HeadlessCost = ClaudeUsage;\n\nexport interface HeadlessResult {\n success: boolean;\n result: string;\n cost: HeadlessCost | null;\n error: string | null;\n}\n\n/* ── argument helpers ───────────────────────────────────────────────────── */\n\n/**\n * Append one `<flag> <value>` pair per value. An empty or absent list leaves\n * `args` untouched, so callers that pass nothing keep the exact same argv.\n */\nfunction pushRepeatedFlag(args: string[], flag: string, values: string[] | undefined): void {\n if (!values || values.length === 0) return;\n for (const value of values) {\n args.push(flag, value);\n }\n}\n\n/* ── verbose stream formatting ──────────────────────────────────────────── */\n\n/**\n * Extract a short context string from a tool_use input object.\n */\nfunction getToolContext(name: string, input: Record<string, unknown>): string {\n switch (name) {\n case 'Read':\n return shortPath(input.file_path as string);\n case 'Write':\n case 'Edit':\n return shortPath(input.file_path as string);\n case 'Glob':\n return (input.pattern as string) ?? '';\n case 'Grep':\n return (input.pattern as string) ?? '';\n case 'Bash': {\n const cmd = (input.command as string) ?? '';\n return cmd.length > 60 ? `${cmd.substring(0, 57)}...` : cmd;\n }\n default:\n return '';\n }\n}\n\n/**\n * Shorten a file path to be relative-friendly.\n */\nfunction shortPath(filePath: string | undefined): string {\n if (!filePath) return '';\n const cwd = process.cwd();\n if (filePath.startsWith(cwd)) {\n return filePath.substring(cwd.length + 1);\n }\n // Show last 2 segments for absolute paths\n const parts = filePath.split('/');\n if (parts.length > 2) {\n return `.../${parts.slice(-2).join('/')}`;\n }\n return filePath;\n}\n\n/**\n * Print a formatted stream event line to stderr.\n */\nfunction printStreamEvent(\n line: string,\n state: { turnCount: number },\n onOutput?: (line: string) => void,\n): void {\n let event: {\n type?: string;\n subtype?: string;\n message?: {\n content?: { type: string; text?: string; name?: string; input?: Record<string, unknown> }[];\n };\n };\n\n try {\n event = JSON.parse(line);\n } catch {\n return;\n }\n\n const icons = getIcons();\n const colored = useColor();\n const emit = onOutput ?? ((msg: string) => process.stderr.write(`${msg}\\n`));\n\n if (event.type === 'assistant' && event.message?.content) {\n state.turnCount++;\n\n for (const block of event.message.content) {\n if (block.type === 'text' && block.text) {\n // Wrap text with connector prefix\n const lines = block.text.split('\\n');\n for (const textLine of lines) {\n if (!textLine.trim()) continue;\n const prefix = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;\n const text = colored ? chalk.dim(textLine) : textLine;\n emit(`${prefix}${text}`);\n }\n }\n\n if (block.type === 'tool_use' && block.name) {\n const context = block.input ? getToolContext(block.name, block.input) : '';\n getSessionPublisher().publish({\n type: 'activity',\n at: isoNow(),\n tool: block.name,\n detail: context || undefined,\n });\n const toolName = block.name.padEnd(12);\n\n const prefix = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;\n const toolIcon = colored ? chalk.cyan(icons.tool) : icons.tool;\n const toolLabel = colored ? chalk.cyan(toolName) : toolName;\n const contextText = context ? (colored ? chalk.dim(context) : context) : '';\n\n emit(`${prefix}${toolIcon} ${toolLabel} ${contextText}`);\n }\n }\n }\n}\n\n/* ── verbose execution ──────────────────────────────────────────────────── */\n\n/**\n * Run headless in verbose mode using stream-json to display real-time progress.\n */\nasync function runHeadlessVerbose(options: {\n prompt: string;\n maxTurns: number;\n timeout: number;\n allowedTools?: string[];\n addDirs?: string[];\n statusMessage?: string;\n onOutput?: (line: string) => void;\n}): Promise<HeadlessResult> {\n const { prompt, maxTurns, timeout, allowedTools, addDirs, statusMessage, onOutput } = options;\n const icons = getIcons();\n const colored = useColor();\n const emit = onOutput ?? ((msg: string) => process.stderr.write(`${msg}\\n`));\n\n // Print header\n if (statusMessage) {\n const msg = colored\n ? chalk.blue(`${icons.start} ${statusMessage}`)\n : `${icons.start} ${statusMessage}`;\n emit(msg);\n }\n\n // Print opening connector\n const connectorLine = colored ? chalk.dim(` ${icons.connector}`) : ` ${icons.connector}`;\n emit(connectorLine);\n\n const startTime = Date.now();\n\n const args: string[] = [\n '-p',\n prompt,\n '--output-format',\n 'stream-json',\n '--verbose',\n '--max-turns',\n String(maxTurns),\n ];\n\n pushRepeatedFlag(args, '--allowedTools', allowedTools);\n pushRepeatedFlag(args, '--add-dir', addDirs);\n\n const subprocess = execa('claude', args, {\n stdin: 'ignore',\n reject: false,\n timeout,\n stripFinalNewline: false,\n });\n\n let resultText = '';\n let isError = false;\n let costData: HeadlessCost | null = null;\n const state = { turnCount: 0 };\n\n if (subprocess.stdout) {\n const rl = createInterface({ input: subprocess.stdout });\n for await (const line of rl) {\n printStreamEvent(line, state, onOutput);\n\n try {\n const event = JSON.parse(line);\n if (event.type === 'result') {\n resultText = event.result ?? '';\n isError = event.is_error === true;\n // Keep the previous metrics when this event carries none, so a\n // malformed trailing result never erases what was already captured.\n costData = parseUsage(event) ?? costData;\n }\n } catch {\n // ignore malformed lines\n }\n }\n }\n\n // Wait for the process to finish\n const proc = await subprocess;\n\n // Close connector with elapsed time\n const elapsedSec = Math.floor((Date.now() - startTime) / 1000);\n const durationStr = formatDuration(elapsedSec);\n const doneLine = colored\n ? chalk.dim(` ${icons.connector} ${chalk.italic(`Done in ${durationStr}`)}`)\n : ` ${icons.connector} Done in ${durationStr}`;\n emit(doneLine);\n emit(connectorLine);\n\n if (proc.exitCode !== 0 && !resultText) {\n const stderr = proc.stderr?.toString() ?? '';\n const isTimeout =\n proc.exitCode === 143 || ('signalName' in proc && proc.signalName === 'SIGTERM');\n const errorMsg = isTimeout\n ? `Headless invocation timed out after ${timeout}ms`\n : stderr || `claude exited with code ${proc.exitCode}`;\n return {\n success: false,\n result: '',\n cost: null,\n error: errorMsg,\n };\n }\n\n return {\n success: !isError,\n result: resultText,\n cost: costData,\n error: isError ? resultText : null,\n };\n}\n\n/* ── standard execution ─────────────────────────────────────────────────── */\n\n/**\n * Invoke Claude Code in headless mode via `claude -p`.\n *\n * Each invocation is an isolated session — no context is shared between calls.\n * Output is parsed as JSON when outputFormat is 'json' (default).\n *\n * When verbose mode is active, uses stream-json to display real-time progress.\n * Otherwise, shows a spinner while waiting.\n */\nexport async function runHeadless(options: HeadlessOptions): Promise<HeadlessResult> {\n const {\n prompt,\n maxTurns = 10,\n timeout = 300_000,\n outputFormat = 'json',\n allowedTools,\n addDirs,\n statusMessage,\n onOutput,\n } = options;\n\n if (isVerbose()) {\n // Use explicit onOutput, fall back to global output callback, or default to stderr\n const effectiveOnOutput = onOutput ?? getOutputCallback();\n return runHeadlessVerbose({\n prompt,\n maxTurns,\n timeout,\n allowedTools,\n addDirs,\n statusMessage,\n onOutput: effectiveOnOutput,\n });\n }\n\n // Non-verbose: use spinner with elapsed timer\n const spinner = statusMessage ? createSpinner(statusMessage).start() : null;\n\n let timer: ElapsedTimer | null = null;\n if (spinner) {\n timer = new ElapsedTimer((elapsed) => {\n spinner.suffixText = useColor() ? chalk.dim(`(${elapsed})`) : `(${elapsed})`;\n }).start();\n }\n\n const args: string[] = [\n '-p',\n prompt,\n '--output-format',\n outputFormat,\n '--max-turns',\n String(maxTurns),\n ];\n\n pushRepeatedFlag(args, '--allowedTools', allowedTools);\n pushRepeatedFlag(args, '--add-dir', addDirs);\n\n try {\n const proc = await execa('claude', args, {\n stdin: 'ignore',\n reject: false,\n timeout,\n stripFinalNewline: false,\n });\n\n const stdout = proc.stdout?.toString() ?? '';\n const stderr = proc.stderr?.toString() ?? '';\n\n if (proc.exitCode !== 0) {\n const elapsed = timer?.stop() ?? 0;\n const dur = useColor()\n ? chalk.dim(` (${formatDuration(elapsed)})`)\n : ` (${formatDuration(elapsed)})`;\n spinner?.fail(`${statusMessage}${dur}`);\n return {\n success: false,\n result: '',\n cost: null,\n error: stderr || stdout || `claude exited with code ${proc.exitCode}`,\n };\n }\n\n const elapsed = timer?.stop() ?? 0;\n const dur = useColor()\n ? chalk.dim(` (${formatDuration(elapsed)})`)\n : ` (${formatDuration(elapsed)})`;\n spinner?.succeed(`${statusMessage}${dur}`);\n\n if (outputFormat === 'json') {\n try {\n const parsed = JSON.parse(stdout);\n return {\n success: true,\n result: parsed.result ?? stdout,\n cost: parseUsage(parsed),\n error: null,\n };\n } catch {\n return {\n success: true,\n result: stdout,\n cost: null,\n error: null,\n };\n }\n }\n\n return {\n success: true,\n result: stdout,\n cost: null,\n error: null,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const catchElapsed = timer?.stop() ?? 0;\n const catchDur = useColor()\n ? chalk.dim(` (${formatDuration(catchElapsed)})`)\n : ` (${formatDuration(catchElapsed)})`;\n spinner?.fail(`${statusMessage}${catchDur}`);\n\n if (message.includes('timed out') || message.includes('ETIMEDOUT')) {\n return {\n success: false,\n result: '',\n cost: null,\n error: `Headless invocation timed out after ${timeout}ms`,\n };\n }\n\n return {\n success: false,\n result: '',\n cost: null,\n error: message,\n };\n }\n}\n"],"mappings":"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","names":["elapsed","dur"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/plan.ts","../src/storage/user-story-numbering.ts"],"sourcesContent":["import { mkdir, readFile } from 'node:fs/promises';\nimport { runHeadless } from '../core/headless.js';\nimport { readFileWithGrace, runPhaseWithRetry } from '../core/phase-runner.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { taskPlanSchema } from '../schemas.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport {\n determineUserStoryNumbering,\n formatUserStoryId,\n parseUserStoryNumber,\n} from '../storage/user-story-numbering.js';\nimport { printError, printInfo, printSuccess, printWarning } from '../ui/logger.js';\nimport { isTransientFailure } from '../utils/retry.js';\n\n/** `--continue` / `--start-us <n>` override of the numbering cascade (issue #36). */\nexport interface PlanUserStoryNumberingOptions {\n continueFlag?: boolean;\n startUs?: number;\n}\n\nexport async function runPlan(\n issue: string,\n resolvedIssue?: ResolvedIssue,\n numbering?: PlanUserStoryNumberingOptions,\n): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const paths = await resolveIssuePaths(issueNumber);\n\n const resolution = await resolveCommandIssue(issueNumber, resolvedIssue);\n if (!resolution.ok) {\n return resolution.code;\n }\n\n // Read the PRD\n const prdPath = paths.prdFile;\n let prdContent: string;\n try {\n prdContent = await readFile(prdPath, 'utf-8');\n } catch {\n printError(`PRD not found at ${prdPath}. Run 'issue-flow prd ${issueNumber}' first.`);\n return 1;\n }\n\n const tasksPath = paths.tasksFile;\n\n // Resolve the User Story numbering continuity (issue #36), log where the\n // decision came from — never silently — and persist it into the project's\n // metadata.json for audit before the prompt is even built, so the record on\n // disk always matches what the prompt was told.\n const {\n message: numberingMessage,\n nextUserStoryId,\n decision: { nextNumber },\n } = await determineUserStoryNumbering({\n issueNumber,\n continueFlag: numbering?.continueFlag,\n startUs: numbering?.startUs,\n });\n printInfo(numberingMessage);\n\n const template = await loadPrompt('plan');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __PRD_CONTENT__: prdContent,\n __TASKS_PATH__: tasksPath,\n __NEXT_US_NUMBER__: nextUserStoryId,\n ...issuePlaceholders(resolution.resolved),\n });\n\n await mkdir(paths.issueDir, { recursive: true });\n\n const outcome = await runPhaseWithRetry({\n phase: 'plan',\n attempt: async () => {\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result`\n // field carries the same assistant text this phase already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep', 'Write'],\n addDirs: [paths.issueDir],\n statusMessage: `Converting PRD to task plan for issue #${issueNumber}...`,\n });\n // One event per attempt; the reducer sums them into the phase total.\n publishPhaseMetrics('plan', result.cost, startedAtMs);\n\n if (!result.success) {\n return {\n ok: false,\n transient: isTransientFailure(1, result.error ?? ''),\n error: `Task plan generation failed: ${result.error}`,\n };\n }\n\n // Verify the file was created, tolerating a brief FS-visibility lag.\n let rawContent: string;\n try {\n rawContent = await readFileWithGrace(tasksPath);\n } catch {\n return { ok: false, transient: true, error: `tasks.json was not created at ${tasksPath}` };\n }\n\n // Validate JSON structure — a content defect, not a timing issue, but\n // still worth a bounded retry since it's a fresh Claude invocation.\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawContent);\n } catch {\n return { ok: false, transient: true, error: 'tasks.json contains invalid JSON' };\n }\n\n // Validate with zod schema\n const validation = taskPlanSchema.safeParse(parsed);\n if (!validation.success) {\n const issues = validation.error.issues\n .map((i) => ` - ${i.path.join('.')}: ${i.message}`)\n .join('\\n');\n return {\n ok: false,\n transient: true,\n error: `tasks.json does not match expected schema:\\n${issues}`,\n };\n }\n\n return { ok: true };\n },\n });\n\n if (!outcome.ok) {\n printError(outcome.error ?? `Task plan generation failed for issue #${issueNumber}`);\n return 1;\n }\n\n // Ensure pipeline state reflects completion of this phase\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.jsonCompleted = true;\n await saveTaskPlan(tasksPath, plan);\n\n // The numbering is a prompt instruction, not a programmatic rewrite, so the\n // generated plan can still ignore it. Say so out loud instead of leaving the\n // log and metadata.json claiming a numbering the file on disk contradicts.\n const generatedLowest = plan.userStories\n .map((story) => parseUserStoryNumber(story.id))\n .filter((value): value is number => value !== null)\n .reduce<number | null>(\n (lowest, value) => (lowest === null ? value : Math.min(lowest, value)),\n null,\n );\n if (generatedLowest !== null && generatedLowest < nextNumber) {\n printWarning(\n `The generated plan starts at ${formatUserStoryId(generatedLowest)}, not the requested ` +\n `${nextUserStoryId}. User Story ids may collide with earlier issues of this project — ` +\n `re-run 'issue-flow plan ${issueNumber} --start-us ${nextNumber}' if that matters.`,\n );\n }\n\n printSuccess(`Task plan saved to ${tasksPath} (${plan.userStories.length} stories)`);\n return 0;\n}\n","import { readdir, readFile } from 'node:fs/promises';\nimport { isoNow } from '../core/state-manager.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport { recordUserStoryNumbering } from './compat.js';\nimport { getIssuePaths } from './paths.js';\nimport { type ResolveIssuePathsOptions, resolveProjectPaths } from './resolve.js';\nimport type { UserStoryNumberingDecision } from './schemas.js';\n\n/**\n * Continuity of `US-NNN` numbering across `plan` executions of the same\n * project (issue #36).\n *\n * The User Story `id` field is free-form — `userStorySchema` in\n * `src/schemas.ts` only requires a non-empty string, no `US-NNN` regex is\n * enforced — so every reader here tolerates an id that does not carry a\n * trailing number instead of throwing on it.\n */\n\nconst USER_STORY_ID_PAD = 3;\n\n/** `16` -> `'US-016'`. Numbers past 999 are not padded further. */\nexport function formatUserStoryId(n: number): string {\n return `US-${String(n).padStart(USER_STORY_ID_PAD, '0')}`;\n}\n\n/**\n * Extract the last run of digits from a User Story id, or `null` when none is\n * found. Tolerant by design: an id like `US-001` yields `1`, but so does a\n * free-form one such as `story-1` or `v2-US-007` (the last digit run, `007`,\n * wins over the `2` version prefix) — only an id with no digits at all (e.g.\n * `add-auth`) is ignored.\n */\nexport function parseUserStoryNumber(id: string): number | null {\n const matches = id.match(/\\d+/g);\n if (!matches || matches.length === 0) return null;\n\n const value = Number.parseInt(matches[matches.length - 1], 10);\n return Number.isNaN(value) ? null : value;\n}\n\nexport interface HighestUserStoryNumberResult {\n number: number;\n /** Issue directory name the winning story came from. */\n issueId: string;\n /** Verbatim id of the winning story. */\n storyId: string;\n}\n\n/**\n * Loose read of one `tasks.json`: only cares about `userStories[].id` and\n * tolerates every failure (missing file, invalid JSON, unexpected shape) by\n * returning `null` — a sibling issue mid-migration, or with a corrupt plan,\n * must not abort the whole project scan.\n */\nasync function readUserStoryIds(tasksPath: string): Promise<string[] | null> {\n let raw: string;\n try {\n raw = await readFile(tasksPath, 'utf-8');\n } catch {\n return null;\n }\n\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n !Array.isArray((parsed as { userStories?: unknown }).userStories)\n ) {\n return null;\n }\n\n return (parsed as { userStories: unknown[] }).userStories\n .map((story) =>\n typeof story === 'object' &&\n story !== null &&\n typeof (story as { id?: unknown }).id === 'string'\n ? (story as { id: string }).id\n : null,\n )\n .filter((id): id is string => id !== null);\n } catch {\n return null;\n }\n}\n\nexport interface FindHighestUserStoryNumberOptions extends ResolveIssuePathsOptions {\n /**\n * Issue directory to leave out of the scan — the issue the numbering is\n * being resolved for. Re-planning an issue overwrites its own `tasks.json`,\n * so counting the plan about to be replaced would push the numbering forward\n * on every `plan` re-run of the same issue.\n */\n excludeIssueId?: string;\n}\n\n/**\n * Highest User Story number already used anywhere in the project, scanning\n * every `<issuesDir>/<id>/tasks.json` regardless of which issue produced it —\n * that project-wide scope is what makes `US-NNN` unambiguous across issues.\n * The issue named by `excludeIssueId` is skipped, so re-planning is idempotent.\n *\n * `null` means no readable `tasks.json` was found (the project's first `plan`\n * run ever, or a brand-new project directory). A storage directory that exists\n * but cannot be read throws instead: silently restarting at `US-001` because\n * `~/.issue-flow` was momentarily unreadable is the worst outcome this feature\n * can produce.\n */\nexport async function findHighestUserStoryNumber(\n options: FindHighestUserStoryNumberOptions = {},\n): Promise<HighestUserStoryNumberResult | null> {\n const { excludeIssueId, ...rootOptions } = options;\n const { projectId, issuesDir } = await resolveProjectPaths(rootOptions);\n\n let entries: string[];\n try {\n entries = (await readdir(issuesDir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch (error) {\n // ENOENT is the legitimate \"no history yet\" case. Any other failure\n // (EACCES, EIO, an unavailable mount) must not be reported as an empty\n // history, or the numbering would restart silently.\n if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return null;\n throw new Error(\n `Could not scan the project's User Story history at ${issuesDir}: ` +\n `${(error as Error).message}. Fix the storage directory, or pass --start-us <n> ` +\n 'to set the numbering explicitly.',\n { cause: error },\n );\n }\n\n let best: HighestUserStoryNumberResult | null = null;\n\n for (const issueId of entries) {\n if (excludeIssueId !== undefined && issueId === excludeIssueId) continue;\n // Never hand-build an artifact path (see src/storage/AGENTS.md): asking\n // getIssuePaths() keeps a rename of `tasks.json` a one-file change instead\n // of silently making this scan find nothing.\n const { tasksFile } = getIssuePaths(projectId, issueId, { env: options.env });\n const ids = await readUserStoryIds(tasksFile);\n if (!ids) continue;\n\n for (const id of ids) {\n const value = parseUserStoryNumber(id);\n if (value === null) continue;\n if (best === null || value > best.number) {\n best = { number: value, issueId, storyId: id };\n }\n }\n }\n\n return best;\n}\n\nexport interface ResolveUserStoryNumberingOptions extends ResolveIssuePathsOptions {\n /** Issue the decision is being made for — carried into the audit record. */\n issueNumber: string;\n /** `--continue` was passed explicitly. Purely cosmetic on the log message:\n * the resolved number is identical to the automatic default. */\n continueFlag?: boolean;\n /** `--start-us <n>` was passed. Wins outright over any history. */\n startUs?: number;\n /** Timestamp source, injectable for tests. */\n now?: () => string;\n}\n\nexport interface UserStoryNumberingOutcome {\n decision: UserStoryNumberingDecision;\n /** Ready-to-print explanation of where the decision came from — the CLI\n * must never resolve this silently (issue #36). */\n message: string;\n}\n\n/**\n * Resolve the numbering cascade of #36:\n *\n * 1. `--start-us <n>` wins outright — history is not even scanned, since the\n * whole point is to ignore it.\n * 2. Otherwise the project's history is scanned for the highest number used\n * so far. This is what running with no flag at all does automatically,\n * and what `--continue` names explicitly (the two produce the same\n * number; the flag only changes the log message).\n * 3. No history found (the project's first `plan` ever) starts at `US-001`.\n *\n * Pure with respect to `metadata.json` — this only reads `tasks.json` files,\n * never writes. Persisting the decision is {@link determineUserStoryNumbering}\n * below, via `recordUserStoryNumbering()` in `compat.ts`.\n */\nexport async function resolveUserStoryNumbering(\n options: ResolveUserStoryNumberingOptions,\n): Promise<UserStoryNumberingOutcome> {\n const { issueNumber, continueFlag, startUs, now, ...rootOptions } = options;\n const decidedAt = (now ?? isoNow)();\n\n if (startUs !== undefined) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: startUs,\n source: 'start-us',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `User Story numbering forced to ${formatUserStoryId(startUs)} via --start-us.`,\n };\n }\n\n // The issue being planned is excluded from the scan: `plan` overwrites its\n // own `tasks.json`, so counting the plan it is about to replace would push\n // the numbering forward on every re-run of `plan` for the same issue.\n const highest = await findHighestUserStoryNumber({ ...rootOptions, excludeIssueId: issueNumber });\n\n if (highest === null) {\n const decision: UserStoryNumberingDecision = {\n nextNumber: 1,\n source: 'none',\n issueNumber,\n decidedAt,\n };\n return {\n decision,\n message: `Starting User Story numbering at ${formatUserStoryId(1)} (no previous history found for this project).`,\n };\n }\n\n const nextNumber = highest.number + 1;\n const decision: UserStoryNumberingDecision = {\n nextNumber,\n source: 'history',\n issueNumber,\n decidedAt,\n detail: `${highest.storyId} (issue #${highest.issueId})`,\n };\n const via = continueFlag ? ' via --continue' : '';\n return {\n decision,\n message:\n `Continuing User Story numbering from ${formatUserStoryId(nextNumber)}${via} ` +\n `— last used was ${highest.storyId} (issue #${highest.issueId}).`,\n };\n}\n\nexport interface DetermineUserStoryNumberingOptions extends ResolveUserStoryNumberingOptions {\n /** Repository root. Defaults to the git toplevel of the current directory. */\n projectRoot?: string;\n}\n\nexport interface UserStoryNumberingResult extends UserStoryNumberingOutcome {\n /** `decision.nextNumber` formatted as `US-NNN`, ready for the prompt. */\n nextUserStoryId: string;\n}\n\n/**\n * The entry point commands use: resolve the cascade and persist the decision\n * into `metadata.json` in one call, so no call site can log/prompt with one\n * decision and audit a different one.\n */\nexport async function determineUserStoryNumbering(\n options: DetermineUserStoryNumberingOptions,\n): Promise<UserStoryNumberingResult> {\n const { projectRoot: projectRootOption, ...rest } = options;\n const projectRoot = projectRootOption ?? (await getProjectRoot());\n\n const outcome = await resolveUserStoryNumbering({ ...rest, projectRoot });\n await recordUserStoryNumbering(projectRoot, outcome.decision, {\n env: options.env,\n now: options.now,\n });\n\n return { ...outcome, nextUserStoryId: formatUserStoryId(outcome.decision.nextNumber) };\n}\n"],"mappings":"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","names":["readFile","decision","readFile"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/review.ts"],"sourcesContent":["import { runHeadless } from '../core/headless.js';\nimport { applyPlaceholders, loadPrompt } from '../core/prompt-resolver.js';\nimport { publishPhaseMetrics } from '../core/session-metrics.js';\nimport { loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { getGlobalTimeout } from '../core/verbose.js';\nimport { issuePlaceholders, resolveCommandIssue } from '../issues/context.js';\nimport type { ResolvedIssue } from '../issues/types.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError, printSuccess } from '../ui/logger.js';\n\nexport interface ReviewResult {\n status: 'PASS' | 'FAIL';\n findings?: string;\n}\n\n/**\n * Parse the <review-result> block from headless output.\n */\nfunction parseReviewResult(output: string): ReviewResult {\n const match = output.match(/<review-result>([\\s\\S]*?)<\\/review-result>/);\n if (!match) {\n // Try to detect PASS/FAIL from the raw output\n if (/STATUS:\\s*PASS/i.test(output)) {\n return { status: 'PASS' };\n }\n if (/STATUS:\\s*FAIL/i.test(output)) {\n const findingsMatch = output.match(/FINDINGS:([\\s\\S]*?)(?:$|<\\/)/);\n return { status: 'FAIL', findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings' };\n }\n // Default to PASS if no explicit status found and no errors\n return { status: 'PASS' };\n }\n\n const block = match[1];\n if (/STATUS:\\s*PASS/i.test(block)) {\n return { status: 'PASS' };\n }\n\n const findingsMatch = block.match(/FINDINGS:([\\s\\S]*)/);\n return {\n status: 'FAIL',\n findings: findingsMatch?.[1]?.trim() ?? 'Unknown findings',\n };\n}\n\nexport async function runReview(issue: string, resolvedIssue?: ResolvedIssue): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n const 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 const template = await loadPrompt('review');\n const prompt = applyPlaceholders(template, {\n // The repository's own conventions. Empty when it declares none, which is\n // what keeps the rendered prompt identical to the pre-policy one.\n ...(await resolvePolicyPlaceholders()),\n __ISSUE_NUMBER__: issueNumber,\n __TASKS_PATH__: tasksPath,\n ...issuePlaceholders(resolution.resolved),\n });\n\n const startedAtMs = Date.now();\n const result = await runHeadless({\n prompt,\n maxTurns: 25,\n timeout: getGlobalTimeout() ?? 300_000,\n // json (not text) so the CLI reports usage: the envelope's `result` field\n // carries the same assistant text parseReviewResult() already consumed.\n outputFormat: 'json',\n allowedTools: ['Bash', 'Read', 'Glob', 'Grep'],\n addDirs: [paths.issueDir],\n statusMessage: `Reviewing issue #${issueNumber} resolution...`,\n });\n // Before the success check: the tokens were spent either way. A correction\n // cycle calls runReview() again and the reducer sums both invocations.\n publishPhaseMetrics('review', result.cost, startedAtMs);\n\n if (!result.success) {\n printError(`Review failed: ${result.error}`);\n return 1;\n }\n\n const review = parseReviewResult(result.result);\n\n if (review.status === 'PASS') {\n // Update pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = true;\n plan.lastReviewFindings = null;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n printSuccess('Review PASSED — implementation meets acceptance criteria');\n return 0;\n }\n\n printError('Review FAILED — issues found:');\n if (review.findings) {\n console.log(review.findings);\n }\n\n // Persist the findings so a correction cycle can act on them: without this,\n // a re-run of execute sees every userStories[].passes already true and\n // exits immediately without attempting any fix (see engine.ts's\n // \"already complete\" guards).\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.pipeline.reviewCompleted = false;\n plan.lastReviewFindings = review.findings ?? 'Unknown findings';\n await saveTaskPlan(tasksPath, plan);\n } catch {\n // tasks.json may not exist\n }\n\n return 1;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,SAAS,kBAAkB,QAA8B;AACvD,QAAM,QAAQ,OAAO,MAAM,4CAA4C;AACvE,MAAI,CAAC,OAAO;AAEV,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,aAAO,EAAE,QAAQ,OAAO;AAAA,IAC1B;AACA,QAAI,kBAAkB,KAAK,MAAM,GAAG;AAClC,YAAMA,iBAAgB,OAAO,MAAM,8BAA8B;AACjE,aAAO,EAAE,QAAQ,QAAQ,UAAUA,iBAAgB,CAAC,GAAG,KAAK,KAAK,mBAAmB;AAAA,IACtF;AAEA,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,QAAQ,MAAM,CAAC;AACrB,MAAI,kBAAkB,KAAK,KAAK,GAAG;AACjC,WAAO,EAAE,QAAQ,OAAO;AAAA,EAC1B;AAEA,QAAM,gBAAgB,MAAM,MAAM,oBAAoB;AACtD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,UAAU,gBAAgB,CAAC,GAAG,KAAK,KAAK;AAAA,EAC1C;AACF;AAEA,eAAsB,UAAU,OAAe,eAAgD;AAC7F,QAAM,cAAc,MAAM,QAAQ,MAAM,EAAE;AAC1C,QAAM,QAAQ,MAAM,kBAAkB,WAAW;AACjD,QAAM,YAAY,MAAM;AAExB,QAAM,aAAa,MAAM,oBAAoB,aAAa,aAAa;AACvE,MAAI,CAAC,WAAW,IAAI;AAClB,WAAO,WAAW;AAAA,EACpB;AAEA,QAAM,WAAW,MAAM,WAAW,QAAQ;AAC1C,QAAM,SAAS,kBAAkB,UAAU;AAAA;AAAA;AAAA,IAGzC,GAAI,MAAM,0BAA0B;AAAA,IACpC,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,IAChB,GAAG,kBAAkB,WAAW,QAAQ;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,KAAK,IAAI;AAC7B,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B;AAAA,IACA,UAAU;AAAA,IACV,SAAS,iBAAiB,KAAK;AAAA;AAAA;AAAA,IAG/B,cAAc;AAAA,IACd,cAAc,CAAC,QAAQ,QAAQ,QAAQ,MAAM;AAAA,IAC7C,SAAS,CAAC,MAAM,QAAQ;AAAA,IACxB,eAAe,oBAAoB,WAAW;AAAA,EAChD,CAAC;AAGD,sBAAoB,UAAU,OAAO,MAAM,WAAW;AAEtD,MAAI,CAAC,OAAO,SAAS;AACnB,eAAW,kBAAkB,OAAO,KAAK,EAAE;AAC3C,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,kBAAkB,OAAO,MAAM;AAE9C,MAAI,OAAO,WAAW,QAAQ;AAE5B,QAAI;AACF,YAAM,OAAO,MAAM,aAAa,SAAS;AACzC,WAAK,SAAS,kBAAkB;AAChC,WAAK,qBAAqB;AAC1B,YAAM,aAAa,WAAW,IAAI;AAAA,IACpC,QAAQ;AAAA,IAER;AAEA,iBAAa,+DAA0D;AACvE,WAAO;AAAA,EACT;AAEA,aAAW,oCAA+B;AAC1C,MAAI,OAAO,UAAU;AACnB,YAAQ,IAAI,OAAO,QAAQ;AAAA,EAC7B;AAMA,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,SAAK,SAAS,kBAAkB;AAChC,SAAK,qBAAqB,OAAO,YAAY;AAC7C,UAAM,aAAa,WAAW,IAAI;AAAA,EACpC,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;","names":["findingsMatch"]}
|