issue-flow 0.7.5 → 0.7.7
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/dist/{analyze-TNFTUMOO.js → analyze-2RVGY5OK.js} +10 -8
- package/dist/{analyze-TNFTUMOO.js.map → analyze-2RVGY5OK.js.map} +1 -1
- package/dist/{chunk-5IRBHUYO.js → chunk-3E6KASSE.js} +11 -9
- package/dist/{chunk-5IRBHUYO.js.map → chunk-3E6KASSE.js.map} +1 -1
- package/dist/{chunk-A42R36BX.js → chunk-6IOPYFUM.js} +10 -8
- package/dist/{chunk-A42R36BX.js.map → chunk-6IOPYFUM.js.map} +1 -1
- package/dist/{chunk-JECNPBBO.js → chunk-6L2BPOE3.js} +11 -9
- package/dist/{chunk-JECNPBBO.js.map → chunk-6L2BPOE3.js.map} +1 -1
- package/dist/{chunk-DWF46N65.js → chunk-6UTVPKNF.js} +11 -9
- package/dist/{chunk-DWF46N65.js.map → chunk-6UTVPKNF.js.map} +1 -1
- package/dist/{chunk-6S2XT3JE.js → chunk-EOTULFWV.js} +10 -8
- package/dist/{chunk-6S2XT3JE.js.map → chunk-EOTULFWV.js.map} +1 -1
- package/dist/{chunk-CD7BMWXW.js → chunk-EYVXLRRD.js} +3 -3
- package/dist/{chunk-OE7RYTUT.js → chunk-G37JNQKB.js} +2 -2
- package/dist/chunk-JOK7VXRV.js +447 -0
- package/dist/chunk-JOK7VXRV.js.map +1 -0
- package/dist/{chunk-DF7SUHT5.js → chunk-KHTLN54U.js} +2 -2
- package/dist/{chunk-D56VWHSM.js → chunk-OPIDR2DC.js} +2 -2
- package/dist/{chunk-5NLLEWJD.js → chunk-RUTDK6ZF.js} +12 -2
- package/dist/{chunk-5NLLEWJD.js.map → chunk-RUTDK6ZF.js.map} +1 -1
- package/dist/chunk-SLHZPB3C.js +50 -0
- package/dist/chunk-SLHZPB3C.js.map +1 -0
- package/dist/{chunk-BMRBQ23Q.js → chunk-WGDLUPSF.js} +9 -7
- package/dist/{chunk-BMRBQ23Q.js.map → chunk-WGDLUPSF.js.map} +1 -1
- package/dist/{chunk-5Q2TMW7K.js → chunk-WO6ERABE.js} +2 -1
- package/dist/{chunk-QLVAEDVD.js → chunk-XPW46TI5.js} +3 -3
- package/dist/{chunk-I7FTA5FI.js → chunk-ZYBYMEWG.js} +2 -47
- package/dist/chunk-ZYBYMEWG.js.map +1 -0
- package/dist/cli.js +28 -13
- package/dist/cli.js.map +1 -1
- package/dist/{config-U3FC4WH6.js → config-OAZG7KIK.js} +3 -3
- package/dist/execute-AOEM52CD.js +14 -0
- package/dist/{generate-RHPOWGDX.js → generate-BEJF3SVJ.js} +10 -8
- package/dist/{generate-RHPOWGDX.js.map → generate-BEJF3SVJ.js.map} +1 -1
- package/dist/init-TBC3WI74.js +9 -0
- package/dist/plan-YDZYB2BO.js +16 -0
- package/dist/pr-SIXMJIZF.js +16 -0
- package/dist/pr-review-AY56ZPJR.js +16 -0
- package/dist/prd-MBSSBLGO.js +16 -0
- package/dist/review-2MDDHZ4X.js +14 -0
- package/dist/{run-Z6ZDXZB6.js → run-SEB6F3W4.js} +23 -196
- package/dist/run-SEB6F3W4.js.map +1 -0
- package/dist/web-3SBYL7QJ.js +188 -0
- package/dist/web-3SBYL7QJ.js.map +1 -0
- package/package.json +1 -1
- package/web/public/app.css +87 -0
- package/web/public/app.js +80 -0
- package/web/public/index.html +11 -0
- package/dist/chunk-I7FTA5FI.js.map +0 -1
- package/dist/execute-BNBA77WJ.js +0 -13
- package/dist/init-IF72GR7O.js +0 -9
- package/dist/plan-FITZZXZQ.js +0 -15
- package/dist/pr-GO244D7M.js +0 -15
- package/dist/pr-review-XY6VL62M.js +0 -15
- package/dist/prd-2Y6RMVTF.js +0 -15
- package/dist/review-ITKXJMX3.js +0 -13
- package/dist/run-Z6ZDXZB6.js.map +0 -1
- /package/dist/{chunk-CD7BMWXW.js.map → chunk-EYVXLRRD.js.map} +0 -0
- /package/dist/{chunk-OE7RYTUT.js.map → chunk-G37JNQKB.js.map} +0 -0
- /package/dist/{chunk-DF7SUHT5.js.map → chunk-KHTLN54U.js.map} +0 -0
- /package/dist/{chunk-D56VWHSM.js.map → chunk-OPIDR2DC.js.map} +0 -0
- /package/dist/{chunk-5Q2TMW7K.js.map → chunk-WO6ERABE.js.map} +0 -0
- /package/dist/{chunk-QLVAEDVD.js.map → chunk-XPW46TI5.js.map} +0 -0
- /package/dist/{config-U3FC4WH6.js.map → config-OAZG7KIK.js.map} +0 -0
- /package/dist/{execute-BNBA77WJ.js.map → execute-AOEM52CD.js.map} +0 -0
- /package/dist/{init-IF72GR7O.js.map → init-TBC3WI74.js.map} +0 -0
- /package/dist/{plan-FITZZXZQ.js.map → plan-YDZYB2BO.js.map} +0 -0
- /package/dist/{pr-GO244D7M.js.map → pr-SIXMJIZF.js.map} +0 -0
- /package/dist/{pr-review-XY6VL62M.js.map → pr-review-AY56ZPJR.js.map} +0 -0
- /package/dist/{prd-2Y6RMVTF.js.map → prd-MBSSBLGO.js.map} +0 -0
- /package/dist/{review-ITKXJMX3.js.map → review-2MDDHZ4X.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/run.ts","../src/core/pipeline.ts","../src/ui/pipeline-renderer.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { execa } from 'execa';\nimport { loadIssuesConfig, loadWebConfig } from '../config.js';\nimport {\n PIPELINE_PHASES,\n PIPELINE_PHASES_NO_BRANCH,\n PIPELINE_PHASES_WITH_PR_REVIEW,\n PipelineManager,\n type PipelinePhase,\n} from '../core/pipeline.js';\nimport { mostRecent } from '../core/pr-review/discovery.js';\nimport {\n type PrReviewRoundEntry,\n prReviewDir,\n readPrReviewIndex,\n} from '../core/pr-review/report.js';\nimport { listPullRequests, publishGitState } from '../core/session-git.js';\nimport { getRunUsageTotals } from '../core/session-metrics.js';\nimport { setSessionPublisher } from '../core/session-publisher.js';\nimport { FilePublisher, NullPublisher, type SessionPublisher } from '../core/session-state.js';\nimport { isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { isVerbose } from '../core/verbose.js';\nimport { resolveCommandIssue } from '../issues/context.js';\nimport { getProvider } from '../issues/registry.js';\nimport type { Issue } from '../issues/types.js';\nimport type { IssuePaths } from '../storage/paths.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport type { UserStory } from '../types.js';\nimport { printError, printInfo, printWarning } from '../ui/logger.js';\nimport { runPipelineWithRenderer } from '../ui/pipeline-renderer.js';\nimport { printRunSummary, type RunSummaryPrReview } from '../ui/summary.js';\nimport { ensureWebMonitor } from '../web/lock.js';\nimport { runExecute } from './execute.js';\nimport { runInit } from './init.js';\nimport { runPlan } from './plan.js';\nimport { runPr } from './pr.js';\nimport { runPrReview } from './pr-review.js';\nimport { runPrd } from './prd.js';\nimport { runReview } from './review.js';\n\n/**\n * Numeric form of an identifier, or `null` when the origin uses a non-numeric\n * one. Published as-is in session.json: a local id like 'auth-refactor' has no\n * number, and reporting it as 0 would claim an Issue that does not exist.\n */\nfunction toIssueNumber(id: string): number | null {\n const parsed = Number.parseInt(id, 10);\n return Number.isNaN(parsed) ? null : parsed;\n}\n\n/**\n * Seed the snapshot with the stories a `tasks.json` already holds on disk, so\n * the monitor shows the plan instead of \"no user story yet\" until the first\n * execute iteration republishes them.\n *\n * Must be called **after** `session:start`, which resets the snapshot through\n * `createInitialSnapshot()`. An empty plan publishes nothing: the event would\n * bump the publisher's version without adding any content.\n *\n * Returns whether anything was published.\n */\nexport function publishStorySeed(\n publisher: SessionPublisher,\n stories: readonly UserStory[],\n at: string,\n): boolean {\n if (stories.length === 0) return false;\n publisher.publish({ type: 'stories:update', at, stories: [...stories] });\n return true;\n}\n\n/**\n * Publish the Issue's structural data (title, description, labels, state) so\n * the panel shows what is being implemented without a detour through GitHub.\n *\n * Same window as the story seed: right after `session:start`, which resets the\n * snapshot. The data comes from the `ResolvedIssue` the run already holds — no\n * extra provider call — and the description goes out whole, untruncated.\n */\nexport function publishIssueDetails(publisher: SessionPublisher, issue: Issue, at: string): void {\n publisher.publish({\n type: 'issue:update',\n at,\n number: issue.number,\n // Left undefined (rather than null) when the origin has no remote, so the\n // reducer keeps whatever URL session:start already published.\n url: issue.remoteRef ?? undefined,\n title: issue.title,\n description: issue.body,\n labels: issue.labels,\n state: issue.state,\n });\n}\n\n/** Runnable phase lists (excluding 'init' which is handled separately). */\nconst RUNNABLE_PHASES: PipelinePhase[] = ['prd', 'plan', 'execute', 'review', 'pr'];\nconst RUNNABLE_PHASES_NO_BRANCH: PipelinePhase[] = ['prd', 'plan', 'execute', 'review'];\nconst RUNNABLE_PHASES_WITH_PR_REVIEW: PipelinePhase[] = [...RUNNABLE_PHASES, 'pr-review'];\n\n/**\n * What the `pr-review` phase left behind, for the steps that run after it: the\n * automatic issue close, the highlighted warning and the final summary.\n *\n * Same shape the summary consumes: `requestedChanges` drives the close\n * suppression and is true on exit code 2 even when the plan is gone.\n */\ntype PrReviewOutcome = RunSummaryPrReview;\n\n/**\n * Recover the verdict and the report path the `pr-review` phase produced.\n *\n * Best-effort by design: the exit code already tells whether changes were\n * requested, so a missing plan or index costs the summary a detail, never the\n * decision to keep the issue open.\n */\nasync function readPrReviewOutcome(\n issue: string,\n tasksPath: string,\n requestedChanges: boolean,\n): Promise<PrReviewOutcome> {\n const outcome: PrReviewOutcome = { requestedChanges, recommendation: null, reportPath: null };\n try {\n const plan = await loadTaskPlan(tasksPath);\n outcome.recommendation = plan.prReview?.lastRecommendation ?? null;\n\n const pullRequest = plan.prReview?.pullRequestNumber;\n if (pullRequest !== undefined) {\n const dir = await prReviewDir({ issue, pullRequest });\n const index = await readPrReviewIndex(dir);\n const last = index?.rounds.reduce<PrReviewRoundEntry | null>(\n (latest, entry) => (latest === null || entry.round > latest.round ? entry : latest),\n null,\n );\n if (last) {\n outcome.reportPath = join(dir, last.reportPath);\n outcome.recommendation ??= last.recommendation;\n }\n }\n } catch {\n /* non-critical */\n }\n return outcome;\n}\n\nexport async function runPipeline(\n issue: string,\n mode: string,\n from?: string,\n noBranch?: boolean,\n prReview?: boolean,\n): Promise<number> {\n const issueNumber = issue.replace(/^#/, '');\n // Resolved once, at the top: every phase that runs below shares the process\n // cache, so the git call and the legacy migration happen a single time for\n // the whole run instead of once per phase.\n const paths = await resolveIssuePaths(issueNumber);\n\n const webConfig = await loadWebConfig();\n let publisher: SessionPublisher = new NullPublisher();\n if (webConfig.enabled) {\n // resolveIssuePaths never creates directories, and a run may well be the\n // first thing to touch this issue's global folder — so the writer creates\n // it. Only under --web: with monitoring off the pipeline still creates\n // nothing at all (US-009).\n await mkdir(paths.issueDir, { recursive: true });\n publisher = new FilePublisher(paths.sessionFile, {\n logLimit: webConfig.logLimit,\n includeLogs: webConfig.includeLogs,\n });\n }\n setSessionPublisher(publisher);\n\n // A null handle (port in use, ...) means the pipeline runs without a server.\n // ensureWebMonitor reuses an already-running, healthy instance instead of\n // binding a second one (US-001), or spawns it detached when none exists\n // (US-002) — either way the returned handle never owns a local server that\n // this process would need to close.\n if (webConfig.enabled) {\n await ensureWebMonitor({\n publisher,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n });\n }\n\n let exitCode = 1;\n try {\n exitCode = await runPipelinePhases(\n issueNumber,\n paths,\n mode,\n publisher,\n from,\n noBranch,\n prReview,\n );\n return exitCode;\n } finally {\n publisher.publish({\n type: 'session:end',\n at: isoNow(),\n status: exitCode === 0 ? 'completed' : 'failed',\n });\n // The web monitor is no longer this process's to close (US-002): it is a\n // detached, single machine-wide instance meant to outlive the pipeline\n // and serve other invocations. Only this run's own publication ends here.\n await publisher.close();\n setSessionPublisher(undefined);\n }\n}\n\nasync function runPipelinePhases(\n issueNumber: string,\n paths: IssuePaths,\n mode: string,\n publisher: SessionPublisher,\n from?: string,\n noBranch?: boolean,\n prReview?: boolean,\n): Promise<number> {\n const tasksPath = paths.tasksFile;\n const sessionId = randomUUID();\n\n // Refined with the provider's own number once the Issue is resolved.\n let publishedIssueNumber = toIssueNumber(issueNumber);\n\n const publishSessionStart = (\n phases: readonly string[],\n at: string,\n info?: { issueUrl?: string; branch?: string },\n ): void => {\n publisher.publish({\n type: 'session:start',\n at,\n sessionId,\n issueNumber: publishedIssueNumber,\n issueUrl: info?.issueUrl,\n branch: info?.branch,\n phases: [...phases],\n environment: { node: process.version, platform: process.platform },\n });\n };\n\n printInfo(`Starting pipeline for issue #${issueNumber} (mode: ${mode})`);\n\n // Loaded before the checks so init knows which origin the user is heading\n // for: with a local one, a missing gh must not fail the environment.\n const issuesConfig = await loadIssuesConfig();\n\n // Phase 1: Init check\n printInfo('Running prerequisite checks...');\n const sessionStartedAt = isoNow();\n const initCode = await runInit(issuesConfig.preferredProvider);\n if (initCode !== 0) {\n publishSessionStart(PIPELINE_PHASES, sessionStartedAt);\n publisher.publish({ type: 'phase:start', at: sessionStartedAt, phase: 'init' });\n publisher.publish({\n type: 'phase:end',\n at: isoNow(),\n phase: 'init',\n success: false,\n error: 'Prerequisites not met',\n });\n printError('Prerequisites not met. Fix the issues above and try again.');\n return 1;\n }\n\n // The origin is settled once, here, and the decision travels to every phase.\n // Resolving per phase would query the providers five times and could ask the\n // user about the same divergence five times.\n const resolution = await resolveCommandIssue(issueNumber, undefined, { config: issuesConfig });\n if (!resolution.ok) {\n return resolution.code;\n }\n const resolvedIssue = resolution.resolved;\n publishedIssueNumber = resolvedIssue.issue.number;\n\n // Resolve noBranch mode: persisted value takes precedence on resume\n let effectiveNoBranch = noBranch ?? false;\n // Resolve pr-review mode: flag > persisted value > default (off). Unlike\n // --no-branch, the flag wins: the phase adds a step at the end instead of\n // changing what the earlier phases did, so opting in on resume is safe.\n let effectivePrReview = prReview ?? false;\n let planIssueUrl: string | undefined;\n let planBranch: string | undefined;\n // Kept from the same read as planIssueUrl/planBranch — seeding the snapshot\n // must not cost a second trip to disk.\n let planStories: UserStory[] = [];\n try {\n const existingPlan = await loadTaskPlan(tasksPath);\n const persistedNoBranch = existingPlan.noBranch ?? false;\n planIssueUrl = existingPlan.issueUrl || undefined;\n planBranch = existingPlan.branchName || undefined;\n planStories = existingPlan.userStories;\n effectivePrReview = prReview ?? existingPlan.prReview?.enabled ?? false;\n\n // Only warn when the user explicitly passed a flag that conflicts with the persisted value\n if (noBranch !== undefined && noBranch !== persistedNoBranch) {\n if (persistedNoBranch) {\n printWarning(\n 'This pipeline was started with --no-branch. Ignoring current flag; using persisted mode.',\n );\n } else {\n printWarning(\n 'This pipeline was started without --no-branch. Ignoring current flag; using persisted mode.',\n );\n }\n }\n\n // Persisted mode wins on resume\n effectiveNoBranch = persistedNoBranch;\n } catch {\n // No tasks.json yet — use the CLI flag as-is\n }\n\n // The CLI rejects --pr-review with --no-branch, but the persisted no-branch\n // mode can only be known here. Without a pr phase there is no Pull Request\n // to review, so the opt-in is dropped instead of failing a resumed run.\n if (effectiveNoBranch && effectivePrReview) {\n printWarning(\n 'This pipeline runs with --no-branch and opens no PR. Skipping the pr-review phase.',\n );\n effectivePrReview = false;\n }\n\n // Persist the opt-in as soon as we know it, not only after the `plan` phase.\n // A mid-pipeline `--pr-review` (e.g. `--from pr --pr-review`) never re-enters\n // the plan runner, and without `enabled: true` a later resume without the flag\n // would drop the phase even when `prReviewCompleted` is still false.\n if (effectivePrReview) {\n try {\n const plan = await loadTaskPlan(tasksPath);\n if (plan.prReview?.enabled !== true) {\n plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };\n await saveTaskPlan(tasksPath, plan);\n }\n } catch {\n // No tasks.json yet — the plan runner persists it after creating the file.\n }\n }\n\n const activePhases = effectiveNoBranch\n ? PIPELINE_PHASES_NO_BRANCH\n : effectivePrReview\n ? PIPELINE_PHASES_WITH_PR_REVIEW\n : PIPELINE_PHASES;\n const phaseOrder = effectiveNoBranch\n ? RUNNABLE_PHASES_NO_BRANCH\n : effectivePrReview\n ? RUNNABLE_PHASES_WITH_PR_REVIEW\n : RUNNABLE_PHASES;\n\n // The phase list is only known after resolving --no-branch, so the init\n // phase (which already ran) is published retroactively with real timestamps.\n publishSessionStart(activePhases, sessionStartedAt, {\n issueUrl: planIssueUrl ?? resolvedIssue.issue.remoteRef ?? undefined,\n branch: planBranch,\n });\n // Right after session:start (which resets the snapshot) and before any phase\n // event, so the first /api/status poll already answers with the Issue and\n // the plan.\n publishIssueDetails(publisher, resolvedIssue.issue, sessionStartedAt);\n publishStorySeed(publisher, planStories, sessionStartedAt);\n publisher.publish({ type: 'phase:start', at: sessionStartedAt, phase: 'init' });\n publisher.publish({ type: 'phase:end', at: isoNow(), phase: 'init', success: true });\n await publishGitState(publisher);\n\n // Determine starting phase\n let startPhase: PipelinePhase = 'prd';\n if (from) {\n if (!(activePhases as readonly string[]).includes(from)) {\n const validPhases = activePhases.filter((p) => p !== 'init').join(', ');\n // Check if the phase exists in the full set but is excluded by --no-branch\n if (effectiveNoBranch && (PIPELINE_PHASES as readonly string[]).includes(from)) {\n printError(\n `The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`,\n );\n } else {\n printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);\n }\n return 1;\n }\n startPhase = from as PipelinePhase;\n } else {\n // Try to auto-resume from pipeline state\n try {\n const plan = await loadTaskPlan(tasksPath);\n const mgr = new PipelineManager(plan, tasksPath, activePhases);\n const nextPhase = mgr.getNextPhase();\n if (nextPhase && nextPhase !== 'init') {\n startPhase = nextPhase;\n printInfo(`Resuming from phase: ${startPhase}`);\n }\n } catch {\n // No tasks.json yet — start from beginning\n }\n }\n\n // Validate resume prerequisites if starting from a later phase\n if (from) {\n try {\n const plan = await loadTaskPlan(tasksPath);\n const mgr = new PipelineManager(plan, tasksPath, activePhases);\n if (!mgr.canResume(startPhase)) {\n printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);\n return 1;\n }\n } catch {\n if (startPhase !== 'prd') {\n printError(`Cannot resume from ${startPhase}: no pipeline state found`);\n return 1;\n }\n }\n }\n\n const startIdx = phaseOrder.indexOf(startPhase);\n\n // Build phase runner functions that throw on failure\n const makeRunner = (fn: () => Promise<number>, phase: string) => async () => {\n const code = await fn();\n if (code !== 0) {\n throw new Error(`Phase ${phase} failed with exit code ${code}`);\n }\n };\n\n const runners: Record<string, () => Promise<void>> = {\n prd: makeRunner(() => runPrd(issueNumber, resolvedIssue), 'prd'),\n plan: async () => {\n await makeRunner(() => runPlan(issueNumber, resolvedIssue), 'plan')();\n // Persist the phase-selection modes into the newly created tasks.json\n if (effectiveNoBranch || effectivePrReview) {\n try {\n const plan = await loadTaskPlan(tasksPath);\n if (effectiveNoBranch) {\n plan.noBranch = true;\n }\n if (effectivePrReview) {\n plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n /* non-critical: tasks.json may not exist yet if plan phase didn't create it */\n }\n }\n },\n execute: makeRunner(() => runExecute(undefined, { issue: issueNumber }), 'execute'),\n review: async () => {\n // Read maxCorrectionCycles\n let maxCycles = 3;\n try {\n const plan = await loadTaskPlan(tasksPath);\n maxCycles = plan.maxCorrectionCycles;\n } catch {\n /* use default */\n }\n\n let code = await runReview(issueNumber, resolvedIssue);\n\n // Auto-correction loop on failure\n let cycle = 0;\n while (code !== 0 && cycle < maxCycles) {\n cycle++;\n printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);\n publisher.publish({ type: 'correction:cycle', at: isoNow(), cycle, maxCycles });\n\n // Update correction cycle in tasks.json\n try {\n const plan = await loadTaskPlan(tasksPath);\n plan.correctionCycle = cycle;\n await saveTaskPlan(tasksPath, plan);\n } catch {\n /* non-critical */\n }\n\n // Re-execute\n const execCode = await runExecute(undefined, { issue: issueNumber });\n if (execCode !== 0) {\n throw new Error('Correction execution failed');\n }\n\n // Re-review\n code = await runReview(issueNumber, resolvedIssue);\n }\n\n if (code !== 0) {\n throw new Error(`Review failed after ${maxCycles} correction cycles`);\n }\n },\n pr: makeRunner(() => runPr(issueNumber, resolvedIssue), 'pr'),\n };\n\n // Filled by the pr-review runner; read after the pipeline finishes. Held in a\n // box because a `let` written only inside a closure keeps its initial `null`\n // narrowing at the read site.\n const reviewBox: { outcome: PrReviewOutcome | null } = { outcome: null };\n if (effectivePrReview) {\n runners['pr-review'] = async () => {\n // `yes` because the run is autonomous: the phase must never stop to ask\n // which Pull Request it is reviewing.\n const code = await runPrReview(undefined, { issue: issueNumber, yes: true });\n if (code === 1) {\n throw new Error('Phase pr-review failed with exit code 1');\n }\n // Exit code 2 is a verdict, not a failure: the review ran, the report is\n // on disk and the pipeline keeps going.\n reviewBox.outcome = await readPrReviewOutcome(issueNumber, tasksPath, code === 2);\n };\n }\n\n // Publish phase:start/phase:end around every runner without touching the\n // listr2 renderer (pipeline-renderer.ts stays publication-free). Commit/PR\n // enrichment happens only at these boundaries (and at iteration end, in\n // engine.ts) — never per HTTP request.\n const instrumentedRunners = Object.fromEntries(\n Object.entries(runners).map(([phase, fn]) => [\n phase,\n async () => {\n publisher.publish({ type: 'phase:start', at: isoNow(), phase });\n try {\n await fn();\n await publishGitState(publisher);\n publisher.publish({ type: 'phase:end', at: isoNow(), phase, success: true });\n } catch (err) {\n await publishGitState(publisher);\n publisher.publish({\n type: 'phase:end',\n at: isoNow(),\n phase,\n success: false,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n },\n ]),\n );\n\n // Run pipeline with listr2 renderer — startup header printed above, summary below\n const result = await runPipelineWithRenderer({\n phases: phaseOrder,\n startIndex: startIdx,\n verbose: isVerbose(),\n runners: instrumentedRunners,\n tasksPath,\n });\n\n if (!result.success) {\n printError(`Phase ${result.failedPhase} failed`);\n return 1;\n }\n\n // A PR review asking for changes is not a pipeline failure, but the work is\n // not done either: the warning is highlighted and the issue stays open.\n const review = reviewBox.outcome;\n if (review?.requestedChanges) {\n console.log('');\n printWarning('PR review requested changes — the Pull Request is not ready to merge.');\n if (review.reportPath !== null) {\n console.log(` Report: ${review.reportPath}`);\n }\n }\n\n // Close the issue through whoever owns it. A provider without close() (a\n // read-only origin) has nothing to do here, so the step is simply skipped.\n // REQUEST_CHANGES also leaves the local plan unfinished: marking\n // `issueStatus: completed` while `prReviewCompleted` is false would lie to\n // every tool that keys off the local status.\n if (review?.requestedChanges) {\n printInfo('Issue left open until the review blockers are addressed.');\n } else {\n try {\n const provider = getProvider(resolvedIssue.source);\n if (provider.close !== undefined) {\n printInfo('Closing issue...');\n await provider.close(issueNumber);\n }\n } catch {\n printWarning('Failed to close issue automatically');\n }\n }\n\n // Get branch and story count\n let branchName = 'unknown';\n try {\n const proc = await execa('git', ['branch', '--show-current'], { reject: false });\n branchName = proc.stdout?.toString().trim() ?? 'unknown';\n } catch {\n /* non-critical */\n }\n\n let storyCount = 0;\n let planPrUrl: string | null = null;\n try {\n const plan = await loadTaskPlan(tasksPath);\n storyCount = plan.userStories.length;\n // The `pr` phase records the Pull Request it opened; trusting it spares a\n // round-trip to the GitHub CLI below.\n planPrUrl = plan.pullRequest?.url ?? null;\n\n plan.lastAttemptAt = isoNow();\n if (!review?.requestedChanges) {\n plan.issueStatus = 'completed';\n plan.completedAt = isoNow();\n }\n await saveTaskPlan(tasksPath, plan);\n } catch {\n /* non-critical */\n }\n\n // Get PR URL for summary. Only GitHub-hosted Issues have a pull request to\n // look up, and --no-branch never opens one. Falling back to the GitHub CLI\n // uses the branch actually checked out: a query without a head branch would\n // return an unrelated Pull Request.\n let prUrl = 'unknown';\n if (!effectiveNoBranch && resolvedIssue.source === 'github') {\n if (planPrUrl !== null) {\n prUrl = planPrUrl;\n } else if (branchName !== 'unknown' && branchName !== '') {\n try {\n const latest = mostRecent(await listPullRequests(branchName));\n if (latest !== null) {\n prUrl = latest.url;\n }\n } catch {\n /* non-critical */\n }\n }\n }\n\n printRunSummary({\n issueNumber,\n branchName,\n noBranch: effectiveNoBranch,\n storyCount,\n elapsedSeconds: result.overallElapsedSeconds,\n prUrl,\n prReview: review,\n // Process-owned counters: the session snapshot is empty whenever web\n // monitoring is off, so it cannot be the source of these totals.\n usage: getRunUsageTotals(),\n });\n\n return 0;\n}\n","import type { PipelineState, TaskPlan } from '../types.js';\nimport { loadTaskPlan, saveTaskPlan } from './state-manager.js';\n\n/**\n * Ordered pipeline phases. Each phase must complete before the next can start.\n *\n * `pr-review` is deliberately NOT here: it is opt-in via `--pr-review`. Adding\n * it to the default set would make every existing `tasks.json` (which has no\n * `prReviewCompleted`) look unfinished, so `getNextPhase()` would resume into a\n * phase the user never asked for.\n */\nexport const PIPELINE_PHASES = ['init', 'prd', 'plan', 'execute', 'review', 'pr'] as const;\n\n/**\n * The default set plus the opt-in `pr-review` phase, used when the user passes\n * `--pr-review` (or the flag was persisted in `plan.prReview.enabled`).\n */\nexport const PIPELINE_PHASES_WITH_PR_REVIEW = [...PIPELINE_PHASES, 'pr-review'] as const;\n\nexport type PipelinePhase = (typeof PIPELINE_PHASES_WITH_PR_REVIEW)[number];\n\n/**\n * Pipeline phases excluding the 'pr' phase, for --no-branch mode.\n * `pr-review` is excluded too: without a branch there is no PR to review.\n */\nexport const PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(\n (p): p is Exclude<PipelinePhase, 'pr' | 'pr-review'> => p !== 'pr',\n);\n\n/**\n * Map pipeline phases to their corresponding PipelineState field.\n * 'init' has no persisted state — it's a runtime-only check.\n */\nconst PHASE_TO_FIELD: Record<PipelinePhase, keyof PipelineState | null> = {\n init: null,\n prd: 'prdCompleted',\n plan: 'jsonCompleted',\n execute: 'executionCompleted',\n review: 'reviewCompleted',\n pr: 'prCreated',\n 'pr-review': 'prReviewCompleted',\n};\n\nexport class PipelineManager {\n private tasksJsonPath: string;\n private plan: TaskPlan;\n private activePhases: readonly PipelinePhase[];\n\n constructor(plan: TaskPlan, tasksJsonPath: string, activePhases?: readonly PipelinePhase[]) {\n this.plan = plan;\n this.tasksJsonPath = tasksJsonPath;\n this.activePhases = activePhases ?? PIPELINE_PHASES;\n }\n\n /**\n * Reload state from disk.\n */\n async reload(): Promise<void> {\n this.plan = await loadTaskPlan(this.tasksJsonPath);\n }\n\n /**\n * Check if a phase is complete.\n */\n isPhaseComplete(phase: PipelinePhase): boolean {\n const field = PHASE_TO_FIELD[phase];\n if (field === null) return true; // init is always \"complete\" after running\n return this.plan.pipeline?.[field] ?? false;\n }\n\n /**\n * Get the first incomplete phase in the pipeline.\n * Returns null if all phases are complete.\n */\n getNextPhase(): PipelinePhase | null {\n for (const phase of this.activePhases) {\n if (!this.isPhaseComplete(phase)) {\n return phase;\n }\n }\n return null;\n }\n\n /**\n * Get the current phase (alias for getNextPhase).\n */\n getCurrentPhase(): PipelinePhase | null {\n return this.getNextPhase();\n }\n\n /**\n * Check whether we can resume from a specific phase.\n * All prerequisite phases must be complete.\n */\n canResume(fromPhase: PipelinePhase): boolean {\n const idx = this.activePhases.indexOf(fromPhase);\n if (idx < 0) return false;\n\n // All active phases before fromPhase must be complete\n for (let i = 0; i < idx; i++) {\n if (!this.isPhaseComplete(this.activePhases[i])) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Mark a phase as complete and persist to disk.\n */\n async markPhaseComplete(phase: PipelinePhase): Promise<void> {\n const field = PHASE_TO_FIELD[phase];\n if (field === null) return; // init has no persisted state\n\n this.plan = {\n ...this.plan,\n pipeline: {\n ...(this.plan.pipeline ?? {\n prdCompleted: false,\n jsonCompleted: false,\n executionCompleted: false,\n reviewCompleted: false,\n prCreated: false,\n }),\n [field]: true,\n },\n };\n\n await saveTaskPlan(this.tasksJsonPath, this.plan);\n }\n}\n","import { Listr, type ListrTask, PRESET_TIMER, PRESET_TIMESTAMP } from 'listr2';\nimport type { PipelinePhase } from '../core/pipeline.js';\nimport { loadTaskPlan } from '../core/state-manager.js';\nimport { setOutputCallback, setStoryUpdateCallback } from '../core/verbose.js';\nimport type { UserStory } from '../types.js';\n\n/** Minimal interface for the listr2 task wrapper properties we use. */\ninterface TaskContext {\n title: string;\n output: string;\n newListr(tasks: ListrTask[], options?: Record<string, unknown>): Listr;\n}\n\n/**\n * Result returned after the pipeline renderer finishes.\n */\nexport interface PipelineResult {\n success: boolean;\n failedPhase?: string;\n overallElapsedSeconds: number;\n}\n\n/**\n * Options for runPipelineWithRenderer.\n */\nexport interface PipelineRendererOptions {\n /** All pipeline phases in order */\n phases: PipelinePhase[];\n /** Index of the first phase to run (phases before this are skipped) */\n startIndex: number;\n /** Whether verbose mode is enabled */\n verbose: boolean;\n /** Map of phase name to its async runner function */\n runners: Record<string, () => Promise<void>>;\n /** Path to tasks.json — enables execute-phase subtask progress when set */\n tasksPath?: string;\n}\n\n/**\n * Phase labels for display.\n *\n * Typed as `Record<string, string>` on purpose (phases arrive as plain strings\n * here), which also means adding a phase to `PipelinePhase` does *not* break\n * the build when its label is missing — it silently falls back to the\n * capitalized phase name. Hence `phaseLabel()` below being covered by a test.\n */\nconst PHASE_LABELS: Record<string, string> = {\n prd: 'PRD',\n plan: 'Plan',\n execute: 'Execute',\n review: 'Review',\n pr: 'PR',\n 'pr-review': 'PR Review',\n};\n\n/**\n * Display label for a pipeline phase, falling back to the capitalized phase\n * name for anything not in the table.\n */\nexport function phaseLabel(phase: string): string {\n return PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);\n}\n\n/**\n * Select the appropriate listr2 renderer based on environment.\n *\n * - TTY + not verbose -> 'default' (animated spinners)\n * - TTY + verbose -> 'verbose' (sequential lines)\n * - Non-TTY / CI -> 'simple' (plain timestamped output)\n *\n * NO_COLOR is handled by listr2's own color support and the useColor() utility,\n * not by switching renderers — a TTY with NO_COLOR still benefits from the\n * default renderer's layout and spinners.\n */\nfunction selectRenderer(verbose: boolean): 'default' | 'verbose' | 'simple' {\n const isTTY = !!process.stdout.isTTY;\n const isCI = !!process.env.CI;\n\n if (!isTTY || isCI) {\n return 'simple';\n }\n\n return verbose ? 'verbose' : 'default';\n}\n\n/**\n * Build the execute phase task with dynamic subtasks for each user story.\n *\n * Stories that already pass are displayed as skipped. Pending stories wait for\n * the engine to complete them via the story update callback. The parent task\n * title is updated with aggregate progress (e.g., \"Execute (3/5 stories passing)\").\n */\nfunction buildExecutePhaseTask(runner: () => Promise<void>, tasksPath: string, verbose: boolean) {\n return async (_ctx: unknown, task: TaskContext) => {\n let stories: UserStory[];\n try {\n const plan = await loadTaskPlan(tasksPath);\n stories = [...plan.userStories].sort((a, b) => a.priority - b.priority);\n } catch {\n // If we can't read the plan, fall back to running without subtasks\n setOutputCallback((line: string) => {\n task.output = line;\n });\n try {\n await runner();\n } finally {\n setOutputCallback(undefined);\n }\n return;\n }\n\n const totalStories = stories.length;\n const initialPassed = stories.filter((s) => s.passes).length;\n task.title = `Execute (${initialPassed}/${totalStories} stories passing)`;\n\n // Create promise resolvers for pending stories\n const resolvers = new Map<string, { resolve: () => void; reject: (err: Error) => void }>();\n\n // Set up story update callback — engine calls this after each iteration\n setStoryUpdateCallback((updatedStories: UserStory[]) => {\n const passed = updatedStories.filter((s) => s.passes).length;\n task.title = `Execute (${passed}/${totalStories} stories passing)`;\n\n for (const s of updatedStories) {\n if (s.passes && resolvers.has(s.id)) {\n resolvers.get(s.id)!.resolve();\n resolvers.delete(s.id);\n }\n }\n });\n\n // Build subtask definitions for each story\n const subtaskDefs = stories.map((story) => ({\n title: `${story.id}: ${story.title}`,\n skip: story.passes ? 'already passing' : false,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n task: async () => {\n await new Promise<void>((resolve, reject) => {\n resolvers.set(story.id, { resolve, reject });\n });\n },\n rendererOptions: {\n timer: PRESET_TIMER,\n },\n }));\n\n // Engine runner subtask — drives the execution loop\n const engineSubtask = {\n title: 'Running engine',\n task: async (_c: unknown, engineTask: TaskContext) => {\n setOutputCallback((line: string) => {\n engineTask.output = line;\n });\n try {\n await runner();\n } finally {\n setOutputCallback(undefined);\n setStoryUpdateCallback(undefined);\n // Resolve any remaining pending stories (engine finished successfully)\n for (const [, { resolve }] of resolvers) {\n resolve();\n }\n resolvers.clear();\n }\n },\n rendererOptions: {\n timer: PRESET_TIMER,\n outputBar: verbose ? Infinity : false,\n persistentOutput: false,\n bottomBar: verbose ? Infinity : 3,\n },\n };\n\n // Run engine + story subtasks concurrently:\n // - Engine subtask runs the actual execution loop\n // - Story subtasks resolve as the engine completes each story\n // Cleanup of setStoryUpdateCallback is handled in engineSubtask's finally block\n //\n // exitOnError must stay true: the per-story subtasks never reject (they\n // only ever get resolve()d, either by the story-update callback or by\n // engineSubtask's finally block once the engine is done), so the only way\n // this nested Listr can fail is engineSubtask itself throwing. With\n // exitOnError: false that failure was swallowed here — the outer \"execute\"\n // phase task then resolved successfully even though the engine failed,\n // and the pipeline moved on to `review` with 0 stories passing instead of\n // stopping. exitOnError: true lets the failure propagate out of\n // task.newListr(...) so the outer phase list (which does have\n // exitOnError: true) aborts before ever starting `review`.\n return task.newListr([engineSubtask, ...subtaskDefs], {\n concurrent: true,\n exitOnError: true,\n rendererOptions: {\n timer: PRESET_TIMER,\n collapseSkips: false,\n },\n });\n };\n}\n\n/**\n * Run the pipeline phases through listr2 for single-writer terminal output.\n *\n * Phases before startIndex are displayed as \"skipped\".\n * Each running phase shows an animated spinner with elapsed time.\n * Completed phases show a checkmark with final duration.\n * The execute phase shows per-story subtask progress when tasksPath is provided.\n */\nexport async function runPipelineWithRenderer(\n options: PipelineRendererOptions,\n): Promise<PipelineResult> {\n const { phases, startIndex, verbose, runners, tasksPath } = options;\n const overallStart = Date.now();\n let currentPhase: string | undefined;\n const renderer = selectRenderer(verbose);\n const isSimple = renderer === 'simple';\n\n const tasks = new Listr(\n phases.map((phase, index) => {\n const label = phaseLabel(phase);\n const isExecutePhase = phase === 'execute' && tasksPath;\n\n return {\n title: label,\n skip: index < startIndex ? 'skipped' : false,\n task: isExecutePhase\n ? async (_ctx: unknown, task: TaskContext) => {\n currentPhase = phase;\n const runner = runners[phase];\n if (!runner) {\n throw new Error(`No runner defined for phase: ${phase}`);\n }\n return buildExecutePhaseTask(runner, tasksPath, verbose)(_ctx, task);\n }\n : async (_ctx: unknown, task: TaskContext) => {\n currentPhase = phase;\n const runner = runners[phase];\n if (!runner) {\n throw new Error(`No runner defined for phase: ${phase}`);\n }\n // Route all output through task.output so listr2 controls rendering\n setOutputCallback((line: string) => {\n task.output = line;\n });\n try {\n await runner();\n } finally {\n setOutputCallback(undefined);\n }\n },\n rendererOptions: {\n timer: PRESET_TIMER,\n outputBar: verbose ? Infinity : false,\n persistentOutput: false,\n },\n };\n }),\n {\n renderer,\n rendererOptions: {\n timer: PRESET_TIMER,\n collapseSkips: false,\n ...(isSimple ? { timestamp: PRESET_TIMESTAMP } : {}),\n },\n exitOnError: true,\n },\n );\n\n let failedPhase: string | undefined;\n\n try {\n await tasks.run();\n } catch {\n failedPhase = currentPhase ?? phases[startIndex];\n } finally {\n // Ensure global callbacks are always cleaned up, even on unexpected errors\n setOutputCallback(undefined);\n setStoryUpdateCallback(undefined);\n }\n\n const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1000);\n\n return {\n success: !failedPhase,\n failedPhase,\n overallElapsedSeconds,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureSingleWebServer,
|
|
4
|
+
getWebLockFile,
|
|
5
|
+
readWebLock,
|
|
6
|
+
removeWebLock
|
|
7
|
+
} from "./chunk-JOK7VXRV.js";
|
|
8
|
+
import "./chunk-SLHZPB3C.js";
|
|
9
|
+
import {
|
|
10
|
+
ISSUES_DIR_NAME,
|
|
11
|
+
PROJECTS_DIR_NAME,
|
|
12
|
+
getGlobalRoot,
|
|
13
|
+
loadWebConfig
|
|
14
|
+
} from "./chunk-RUTDK6ZF.js";
|
|
15
|
+
import {
|
|
16
|
+
printError,
|
|
17
|
+
printInfo,
|
|
18
|
+
printWarning,
|
|
19
|
+
sessionSnapshotSchema
|
|
20
|
+
} from "./chunk-WO6ERABE.js";
|
|
21
|
+
|
|
22
|
+
// src/web/session-directory.ts
|
|
23
|
+
import { readdir, readFile, stat as statFile } from "fs/promises";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
26
|
+
var DEFAULT_STALE_AFTER_MS = 3e4;
|
|
27
|
+
async function listSubdirectories(dir) {
|
|
28
|
+
try {
|
|
29
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
30
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function discoverSessionFiles(root) {
|
|
36
|
+
const projectsDir = join(root, PROJECTS_DIR_NAME);
|
|
37
|
+
const projectIds = await listSubdirectories(projectsDir);
|
|
38
|
+
const perProject = await Promise.all(
|
|
39
|
+
projectIds.map(async (projectId) => {
|
|
40
|
+
const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);
|
|
41
|
+
const issueIds = await listSubdirectories(issuesDir);
|
|
42
|
+
return issueIds.map((issueId) => join(issuesDir, issueId, "session.json"));
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
return perProject.flat();
|
|
46
|
+
}
|
|
47
|
+
async function readSessionFile(filePath) {
|
|
48
|
+
let raw;
|
|
49
|
+
let mtimeMs;
|
|
50
|
+
try {
|
|
51
|
+
const [content, stat] = await Promise.all([readFile(filePath, "utf-8"), statFile(filePath)]);
|
|
52
|
+
raw = content;
|
|
53
|
+
mtimeMs = stat.mtimeMs;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));
|
|
59
|
+
if (!parsed.success || parsed.data.sessionId === null) return null;
|
|
60
|
+
return { snapshot: parsed.data, updatedAtMs: mtimeMs };
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function watchSessionDirectory(options = {}) {
|
|
66
|
+
const root = getGlobalRoot(options);
|
|
67
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
68
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
69
|
+
const warn = options.onWarn;
|
|
70
|
+
let sessions = /* @__PURE__ */ new Map();
|
|
71
|
+
let warned = false;
|
|
72
|
+
async function scan() {
|
|
73
|
+
let files;
|
|
74
|
+
try {
|
|
75
|
+
files = await discoverSessionFiles(root);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (!warned) {
|
|
78
|
+
warned = true;
|
|
79
|
+
warn?.(
|
|
80
|
+
`issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${err instanceof Error ? err.message : String(err)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const next = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const filePath of files) {
|
|
88
|
+
const result = await readSessionFile(filePath);
|
|
89
|
+
if (result === null) continue;
|
|
90
|
+
if (now - result.updatedAtMs > staleAfterMs) continue;
|
|
91
|
+
const sessionId = result.snapshot.sessionId;
|
|
92
|
+
next.set(sessionId, {
|
|
93
|
+
issueDir: join(filePath, ".."),
|
|
94
|
+
filePath,
|
|
95
|
+
snapshot: result.snapshot,
|
|
96
|
+
updatedAtMs: result.updatedAtMs
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
sessions = next;
|
|
100
|
+
}
|
|
101
|
+
void scan();
|
|
102
|
+
const timer = setInterval(() => void scan(), pollIntervalMs);
|
|
103
|
+
timer.unref();
|
|
104
|
+
return {
|
|
105
|
+
sessions: () => [...sessions.values()],
|
|
106
|
+
getSession: (sessionId) => sessions.get(sessionId),
|
|
107
|
+
refresh: scan,
|
|
108
|
+
close: () => clearInterval(timer)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/commands/web.ts
|
|
113
|
+
var STOP_POLL_INTERVAL_MS = 100;
|
|
114
|
+
var STOP_TIMEOUT_MS = 5e3;
|
|
115
|
+
function sleep(ms) {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
118
|
+
async function runWebServe(options) {
|
|
119
|
+
const cli = {};
|
|
120
|
+
if (options.port !== void 0) cli.port = options.port;
|
|
121
|
+
if (options.host !== void 0) cli.host = options.host;
|
|
122
|
+
if (options.refresh !== void 0) cli.refreshSeconds = options.refresh;
|
|
123
|
+
const webConfig = await loadWebConfig({ cli });
|
|
124
|
+
const sessions = watchSessionDirectory();
|
|
125
|
+
const noop = () => {
|
|
126
|
+
};
|
|
127
|
+
const handle = await ensureSingleWebServer({
|
|
128
|
+
sessions,
|
|
129
|
+
port: webConfig.port,
|
|
130
|
+
host: webConfig.host,
|
|
131
|
+
refreshSeconds: webConfig.refreshSeconds,
|
|
132
|
+
unref: false,
|
|
133
|
+
info: noop,
|
|
134
|
+
warn: noop
|
|
135
|
+
});
|
|
136
|
+
if (handle === null) {
|
|
137
|
+
sessions.close();
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
if (handle.server === void 0) {
|
|
141
|
+
sessions.close();
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
const originalClose = handle.close;
|
|
145
|
+
handle.close = async () => {
|
|
146
|
+
await originalClose();
|
|
147
|
+
sessions.close();
|
|
148
|
+
};
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
async function runWebStop() {
|
|
152
|
+
const lockFile = getWebLockFile();
|
|
153
|
+
const lock = await readWebLock(lockFile);
|
|
154
|
+
if (lock === null) {
|
|
155
|
+
printInfo("No web monitor is currently running.");
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
process.kill(lock.pid, "SIGTERM");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (err.code === "ESRCH") {
|
|
162
|
+
printWarning(
|
|
163
|
+
"The web monitor lock referenced a process that is no longer running; removing it."
|
|
164
|
+
);
|
|
165
|
+
await removeWebLock(lockFile);
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
printError(
|
|
169
|
+
`Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`
|
|
170
|
+
);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
174
|
+
while (Date.now() < deadline) {
|
|
175
|
+
if (await readWebLock(lockFile) === null) {
|
|
176
|
+
printInfo(`Web monitor (pid ${lock.pid}) stopped.`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
await sleep(STOP_POLL_INTERVAL_MS);
|
|
180
|
+
}
|
|
181
|
+
printWarning("Sent the stop signal, but the web monitor did not confirm shutdown in time.");
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
runWebServe,
|
|
186
|
+
runWebStop
|
|
187
|
+
};
|
|
188
|
+
//# sourceMappingURL=web-3SBYL7QJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile, stat as statFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport { sessionSnapshotSchema } from '../schemas.js';\nimport {\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n} from '../storage/paths.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` throttles writes to ~1s and force-flushes on every\n * phase/session boundary, so a live run's file is never quiet for long; this\n * threshold only needs to be comfortably above one throttle window plus one\n * poll interval to avoid flapping a session in and out on timing alone.\n */\nexport const DEFAULT_STALE_AFTER_MS = 30_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, 'session.json'));\n }),\n );\n\n return perProject.flat();\n}\n\ninterface ReadResult {\n snapshot: ValidatedSessionSnapshot;\n updatedAtMs: number;\n}\n\n/**\n * Read and validate one `session.json`. Anything short of a fully valid file\n * (missing, mid-write, corrupted, written by an incompatible future version)\n * reads as \"not currently a session\" — the directory scan must never fail\n * because one file is momentarily inconsistent (`FilePublisher` writes via a\n * temp file + rename, but a reader can still race a concurrent write).\n */\nasync function readSessionFile(filePath: string): Promise<ReadResult | null> {\n let raw: string;\n let mtimeMs: number;\n try {\n const [content, stat] = await Promise.all([readFile(filePath, 'utf-8'), statFile(filePath)]);\n raw = content;\n mtimeMs = stat.mtimeMs;\n } catch {\n return null;\n }\n\n try {\n const parsed = sessionSnapshotSchema.safeParse(JSON.parse(raw));\n if (!parsed.success || parsed.data.sessionId === null) return null;\n return { snapshot: parsed.data, updatedAtMs: mtimeMs };\n } catch {\n return null;\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { printError, printInfo, printWarning } from '../ui/logger.js';\nimport { ensureSingleWebServer, getWebLockFile, readWebLock, removeWebLock } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nconst STOP_POLL_INTERVAL_MS = 100;\nconst STOP_TIMEOUT_MS = 5000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const lockFile = getWebLockFile();\n const lock = await readWebLock(lockFile);\n if (lock === null) {\n printInfo('No web monitor is currently running.');\n return 0;\n }\n\n try {\n process.kill(lock.pid, 'SIGTERM');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ESRCH') {\n printWarning(\n 'The web monitor lock referenced a process that is no longer running; removing it.',\n );\n await removeWebLock(lockFile);\n return 0;\n }\n printError(\n `Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if ((await readWebLock(lockFile)) === null) {\n printInfo(`Web monitor (pid ${lock.pid}) stopped.`);\n return 0;\n }\n await sleep(STOP_POLL_INTERVAL_MS);\n }\n\n printWarning('Sent the stop signal, but the web monitor did not confirm shutdown in time.');\n return 0;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/web/public/app.css
CHANGED
|
@@ -234,6 +234,93 @@ select {
|
|
|
234
234
|
margin: 0 0 4px;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/* ---- Resumo da issue ---- */
|
|
238
|
+
|
|
239
|
+
.issue-summary-title {
|
|
240
|
+
margin: 0 0 8px;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.issue-summary-meta {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-wrap: wrap;
|
|
247
|
+
align-items: center;
|
|
248
|
+
gap: 10px;
|
|
249
|
+
margin-bottom: 8px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.badge-row {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-wrap: wrap;
|
|
255
|
+
gap: 6px;
|
|
256
|
+
margin-bottom: 8px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.label-badge {
|
|
260
|
+
background: var(--idle-bg);
|
|
261
|
+
color: var(--fg);
|
|
262
|
+
text-transform: none;
|
|
263
|
+
font-weight: 500;
|
|
264
|
+
letter-spacing: normal;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.state-open {
|
|
268
|
+
background: var(--run-bg);
|
|
269
|
+
color: var(--run);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.state-closed {
|
|
273
|
+
background: var(--ok-bg);
|
|
274
|
+
color: var(--ok);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.state-unknown {
|
|
278
|
+
background: var(--idle-bg);
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.issue-description {
|
|
283
|
+
margin: 0;
|
|
284
|
+
white-space: pre-wrap;
|
|
285
|
+
word-break: break-word;
|
|
286
|
+
max-height: 220px;
|
|
287
|
+
overflow-y: auto;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* ---- User stories: status e dependências ---- */
|
|
291
|
+
|
|
292
|
+
.story-meta {
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-wrap: wrap;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
margin-top: 4px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.story-status-backlog {
|
|
301
|
+
background: var(--idle-bg);
|
|
302
|
+
color: var(--muted);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.story-status-in_progress {
|
|
306
|
+
background: var(--run-bg);
|
|
307
|
+
color: var(--run);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.story-status-in_review {
|
|
311
|
+
background: var(--warn-bg);
|
|
312
|
+
color: var(--warn);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.story-status-done {
|
|
316
|
+
background: var(--ok-bg);
|
|
317
|
+
color: var(--ok);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.story-deps {
|
|
321
|
+
font-size: 0.84rem;
|
|
322
|
+
}
|
|
323
|
+
|
|
237
324
|
/* ---- Progresso ---- */
|
|
238
325
|
|
|
239
326
|
.progress-row {
|
package/web/public/app.js
CHANGED
|
@@ -25,6 +25,13 @@
|
|
|
25
25
|
failed: '✗',
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
const STORY_STATUS_LABELS = {
|
|
29
|
+
backlog: 'backlog',
|
|
30
|
+
in_progress: 'em andamento',
|
|
31
|
+
in_review: 'em revisão',
|
|
32
|
+
done: 'concluída',
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
const els = {
|
|
29
36
|
banner: document.getElementById('banner-disconnected'),
|
|
30
37
|
issueLink: document.getElementById('issue-link'),
|
|
@@ -35,6 +42,8 @@
|
|
|
35
42
|
refreshSelect: document.getElementById('refresh-select'),
|
|
36
43
|
alerts: document.getElementById('alerts'),
|
|
37
44
|
alertsBody: document.getElementById('alerts-body'),
|
|
45
|
+
issueSummary: document.getElementById('issue-summary'),
|
|
46
|
+
repository: document.getElementById('repository'),
|
|
38
47
|
progressBar: document.getElementById('progress-bar'),
|
|
39
48
|
progressPercent: document.getElementById('progress-percent'),
|
|
40
49
|
progressCounters: document.getElementById('progress-counters'),
|
|
@@ -266,6 +275,8 @@
|
|
|
266
275
|
if (!snapshot) return;
|
|
267
276
|
renderHeader(snapshot);
|
|
268
277
|
renderAlerts(snapshot);
|
|
278
|
+
renderIssueSummary(snapshot);
|
|
279
|
+
renderRepository(snapshot);
|
|
269
280
|
renderProgress(snapshot);
|
|
270
281
|
renderNow(snapshot);
|
|
271
282
|
renderPhases(snapshot);
|
|
@@ -341,6 +352,61 @@
|
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
|
|
355
|
+
// snapshot.issue vem sempre como objeto (schema não o torna nulável); só os
|
|
356
|
+
// campos individuais podem ser null/undefined — session.json antigo, issue
|
|
357
|
+
// local sem remote, etc. Cada leitura abaixo trata isso individualmente.
|
|
358
|
+
function renderIssueSummary(snapshot) {
|
|
359
|
+
clear(els.issueSummary);
|
|
360
|
+
const issue = snapshot.issue || {};
|
|
361
|
+
|
|
362
|
+
const heading = el('p', 'issue-summary-title');
|
|
363
|
+
heading.appendChild(
|
|
364
|
+
el('span', 'mono', issue.number !== null && issue.number !== undefined ? '#' + issue.number : '—'),
|
|
365
|
+
);
|
|
366
|
+
heading.appendChild(document.createTextNode(' ' + (issue.title || 'Sem título')));
|
|
367
|
+
els.issueSummary.appendChild(heading);
|
|
368
|
+
|
|
369
|
+
const meta = el('div', 'issue-summary-meta');
|
|
370
|
+
const state = issue.state || null;
|
|
371
|
+
const stateClass = state === 'open' ? 'state-open' : state === 'closed' ? 'state-closed' : 'state-unknown';
|
|
372
|
+
meta.appendChild(el('span', 'badge ' + stateClass, state || 'estado desconhecido'));
|
|
373
|
+
meta.appendChild(el('span', 'muted', 'Prioridade: Não definida'));
|
|
374
|
+
els.issueSummary.appendChild(meta);
|
|
375
|
+
|
|
376
|
+
const labels = issue.labels || [];
|
|
377
|
+
if (labels.length > 0) {
|
|
378
|
+
const labelRow = el('div', 'badge-row');
|
|
379
|
+
for (const label of labels) {
|
|
380
|
+
labelRow.appendChild(el('span', 'badge label-badge', label));
|
|
381
|
+
}
|
|
382
|
+
els.issueSummary.appendChild(labelRow);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
els.issueSummary.appendChild(el('p', 'issue-description', issue.description || 'Sem descrição.'));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function metaRow(grid, label, value, className) {
|
|
389
|
+
grid.appendChild(el('dt', null, label));
|
|
390
|
+
const dd = el('dd', className, value);
|
|
391
|
+
grid.appendChild(dd);
|
|
392
|
+
return dd;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// snapshot.repository é coletado de forma tolerante a falhas (sem remote,
|
|
396
|
+
// sem commits, git ausente): cada campo pode ser null independentemente dos
|
|
397
|
+
// demais, nunca o objeto inteiro.
|
|
398
|
+
function renderRepository(snapshot) {
|
|
399
|
+
clear(els.repository);
|
|
400
|
+
const repo = snapshot.repository || {};
|
|
401
|
+
const grid = el('dl', 'now-grid');
|
|
402
|
+
metaRow(grid, 'Repositório', repo.name || '—');
|
|
403
|
+
metaRow(grid, 'Branch', repo.branch || '—', 'mono');
|
|
404
|
+
metaRow(grid, 'Commit', repo.headCommit || '—', 'mono');
|
|
405
|
+
const rootDd = metaRow(grid, 'Diretório', repo.root || '—', 'mono');
|
|
406
|
+
if (repo.root) rootDd.title = repo.root;
|
|
407
|
+
els.repository.appendChild(grid);
|
|
408
|
+
}
|
|
409
|
+
|
|
344
410
|
function renderProgress(snapshot) {
|
|
345
411
|
const progress = snapshot.progress;
|
|
346
412
|
els.progressBar.value = progress.percent;
|
|
@@ -452,6 +518,20 @@
|
|
|
452
518
|
title.appendChild(el('span', 'story-id', story.id));
|
|
453
519
|
title.appendChild(document.createTextNode(story.title));
|
|
454
520
|
main.appendChild(title);
|
|
521
|
+
|
|
522
|
+
// status/dependencies vêm com default no schema (backlog/[]), mas
|
|
523
|
+
// session.json gravado antes de #29 pode chegar sem eles.
|
|
524
|
+
const storyStatus = story.status !== null && story.status !== undefined ? story.status : 'backlog';
|
|
525
|
+
const meta = el('div', 'story-meta');
|
|
526
|
+
meta.appendChild(
|
|
527
|
+
el('span', 'badge story-status-' + storyStatus, STORY_STATUS_LABELS[storyStatus] || storyStatus),
|
|
528
|
+
);
|
|
529
|
+
const dependencies = story.dependencies || [];
|
|
530
|
+
if (dependencies.length > 0) {
|
|
531
|
+
meta.appendChild(el('span', 'muted story-deps', 'depende de: ' + dependencies.join(', ')));
|
|
532
|
+
}
|
|
533
|
+
main.appendChild(meta);
|
|
534
|
+
|
|
455
535
|
item.appendChild(main);
|
|
456
536
|
const duration = metric(story.durationSeconds);
|
|
457
537
|
const side = itemSideText([
|
package/web/public/index.html
CHANGED
|
@@ -41,6 +41,17 @@
|
|
|
41
41
|
<div id="alerts-body"></div>
|
|
42
42
|
</section>
|
|
43
43
|
|
|
44
|
+
<div class="columns">
|
|
45
|
+
<section class="card">
|
|
46
|
+
<h2>Resumo da issue</h2>
|
|
47
|
+
<div id="issue-summary"></div>
|
|
48
|
+
</section>
|
|
49
|
+
<section class="card">
|
|
50
|
+
<h2>Repositório</h2>
|
|
51
|
+
<div id="repository"></div>
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
44
55
|
<section class="card" aria-live="polite">
|
|
45
56
|
<h2>Progresso</h2>
|
|
46
57
|
<div class="progress-row">
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/prompt-resolver.ts","../src/core/session-metrics.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * Resolve the absolute path to a directory shipped at the package root\n * (e.g. 'prompts', 'web/public'). Works from both source (src/core/) and\n * compiled (dist/) locations by walking up the directory tree until the\n * named folder is found. Returns null when it cannot be located.\n *\n * `startDir` is injectable for tests; it defaults to this module's directory.\n */\nexport function resolvePackageDir(name: string, startDir?: string): string | null {\n let dir = startDir ?? dirname(fileURLToPath(import.meta.url));\n\n for (let i = 0; i < 5; i++) {\n const candidate = join(dir, name);\n if (existsSync(candidate)) {\n return candidate;\n }\n dir = dirname(dir);\n }\n\n return null;\n}\n\nfunction getPromptsDir(): string {\n const dir = resolvePackageDir('prompts');\n if (dir === null) {\n throw new Error(\n 'Could not locate the prompts/ directory. Ensure the package is installed correctly.',\n );\n }\n return dir;\n}\n\n/**\n * Load a prompt template by name from the package's prompts/ directory.\n *\n * @param name - Prompt name without extension (e.g., 'execute', 'analyze')\n * @returns The raw template content with placeholders intact\n */\nexport async function loadPrompt(name: string): Promise<string> {\n const promptsDir = getPromptsDir();\n const filePath = join(promptsDir, `${name}.md`);\n\n try {\n return await readFile(filePath, 'utf-8');\n } catch {\n throw new Error(`Prompt template not found: ${filePath}`);\n }\n}\n\n/**\n * Replace placeholders in a prompt template with actual values.\n *\n * Placeholders use the format __KEY__ (e.g., __ISSUE_NUMBER__, __PRD_FILE__).\n */\nexport function applyPlaceholders(template: string, vars: Record<string, string>): string {\n let result = template;\n for (const [key, value] of Object.entries(vars)) {\n result = result.replaceAll(key, value);\n }\n return result;\n}\n","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 */\nconst runTotals = {\n all: {} as ClaudeUsage,\n byPhase: new Map<string, ClaudeUsage>(),\n};\n\nfunction recordRunUsage(phase: string, usage: ClaudeUsage): void {\n runTotals.all = sumUsage(runTotals.all, usage);\n runTotals.byPhase.set(phase, sumUsage(runTotals.byPhase.get(phase), usage));\n}\n\n/** Everything this process has spent so far, across all phases. */\nexport function getRunUsageTotals(): ClaudeUsage {\n return { ...runTotals.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 { ...(runTotals.byPhase.get(phase) ?? {}) };\n}\n\n/** Test seam: the counters are module state and must be cleared between runs. */\nexport function resetRunUsageTotals(): void {\n runTotals.all = {};\n runTotals.byPhase.clear();\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"],"mappings":";;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAUvB,SAAS,kBAAkB,MAAc,UAAkC;AAChF,MAAI,MAAM,YAAY,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE5D,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,UAAM,YAAY,KAAK,KAAK,IAAI;AAChC,QAAI,WAAW,SAAS,GAAG;AACzB,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,GAAG;AAAA,EACnB;AAEA,SAAO;AACT;AAEA,SAAS,gBAAwB;AAC/B,QAAM,MAAM,kBAAkB,SAAS;AACvC,MAAI,QAAQ,MAAM;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAQA,eAAsB,WAAW,MAA+B;AAC9D,QAAM,aAAa,cAAc;AACjC,QAAM,WAAW,KAAK,YAAY,GAAG,IAAI,KAAK;AAE9C,MAAI;AACF,WAAO,MAAM,SAAS,UAAU,OAAO;AAAA,EACzC,QAAQ;AACN,UAAM,IAAI,MAAM,8BAA8B,QAAQ,EAAE;AAAA,EAC1D;AACF;AAOO,SAAS,kBAAkB,UAAkB,MAAsC;AACxF,MAAI,SAAS;AACb,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,aAAS,OAAO,WAAW,KAAK,KAAK;AAAA,EACvC;AACA,SAAO;AACT;;;ACjDO,SAAS,oBAAoB,aAA6B;AAC/D,SAAO,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,IAAI,IAAI,eAAe,GAAI,CAAC;AAClE;AAeA,IAAM,YAAY;AAAA,EAChB,KAAK,CAAC;AAAA,EACN,SAAS,oBAAI,IAAyB;AACxC;AAEA,SAAS,eAAe,OAAe,OAA0B;AAC/D,YAAU,MAAM,SAAS,UAAU,KAAK,KAAK;AAC7C,YAAU,QAAQ,IAAI,OAAO,SAAS,UAAU,QAAQ,IAAI,KAAK,GAAG,KAAK,CAAC;AAC5E;AAGO,SAAS,oBAAiC;AAC/C,SAAO,EAAE,GAAG,UAAU,IAAI;AAC5B;AAGO,SAAS,oBAAoB,OAA4B;AAC9D,SAAO,EAAE,GAAI,UAAU,QAAQ,IAAI,KAAK,KAAK,CAAC,EAAG;AACnD;AAuBO,SAAS,oBACd,OACA,OACA,aACM;AACN,MAAI,SAAS,QAAQ,CAAC,aAAa,KAAK,EAAG;AAE3C,iBAAe,OAAO,KAAK;AAE3B,sBAAoB,EAAE,QAAQ;AAAA,IAC5B,MAAM;AAAA,IACN,IAAI,OAAO;AAAA,IACX,OAAO;AAAA,IACP;AAAA,IACA,GAAG;AAAA;AAAA;AAAA,IAGH,iBAAiB,gBAAgB,SAAY,SAAY,oBAAoB,WAAW;AAAA,EAC1F,CAAC;AACH;AAGO,IAAM,gBAAgB;AAYtB,SAAS,wBACd,WACA,OACA,iBACM;AACN,MAAI,SAAS,QAAQ,CAAC,aAAa,KAAK,EAAG;AAE3C,iBAAe,eAAe,KAAK;AAEnC,sBAAoB,EAAE,QAAQ;AAAA,IAC5B,MAAM;AAAA,IACN,IAAI,OAAO;AAAA,IACX,OAAO;AAAA,IACP,OAAO;AAAA,IACP;AAAA,IACA,GAAG;AAAA;AAAA;AAAA,IAGH;AAAA,EACF,CAAC;AACH;AAWO,SAAS,oBACd,SACA,OACA,iBACM;AACN,MAAI,CAAC,aAAa,KAAK,KAAK,oBAAoB,OAAW;AAE3D,sBAAoB,EAAE,QAAQ;AAAA,IAC5B,MAAM;AAAA,IACN,IAAI,OAAO;AAAA,IACX,OAAO;AAAA,IACP;AAAA,IACA,GAAI,SAAS,CAAC;AAAA,IACd;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/dist/execute-BNBA77WJ.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
runExecute
|
|
4
|
-
} from "./chunk-BMRBQ23Q.js";
|
|
5
|
-
import "./chunk-HDV3YDTG.js";
|
|
6
|
-
import "./chunk-QLVAEDVD.js";
|
|
7
|
-
import "./chunk-I7FTA5FI.js";
|
|
8
|
-
import "./chunk-5NLLEWJD.js";
|
|
9
|
-
import "./chunk-5Q2TMW7K.js";
|
|
10
|
-
export {
|
|
11
|
-
runExecute
|
|
12
|
-
};
|
|
13
|
-
//# sourceMappingURL=execute-BNBA77WJ.js.map
|
package/dist/init-IF72GR7O.js
DELETED
package/dist/plan-FITZZXZQ.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
runPlan
|
|
4
|
-
} from "./chunk-5IRBHUYO.js";
|
|
5
|
-
import "./chunk-D56VWHSM.js";
|
|
6
|
-
import "./chunk-CD7BMWXW.js";
|
|
7
|
-
import "./chunk-HDV3YDTG.js";
|
|
8
|
-
import "./chunk-DF7SUHT5.js";
|
|
9
|
-
import "./chunk-I7FTA5FI.js";
|
|
10
|
-
import "./chunk-5NLLEWJD.js";
|
|
11
|
-
import "./chunk-5Q2TMW7K.js";
|
|
12
|
-
export {
|
|
13
|
-
runPlan
|
|
14
|
-
};
|
|
15
|
-
//# sourceMappingURL=plan-FITZZXZQ.js.map
|