issue-flow 0.7.6 → 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/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/web/lock.ts","../src/web/server.ts"],"sourcesContent":["import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { isoNow } from '../core/state-manager.js';\nimport { type GetGlobalRootOptions, getGlobalRoot } from '../storage/paths.js';\nimport { type WebLock, webLockSchema } from '../storage/schemas.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport { startWebServer, type WebServerHandle, type WebServerOptions } from './server.js';\n\n/**\n * Single-instance guard for the web monitoring server (`~/.issue-flow/web.lock`).\n *\n * A machine runs at most one monitoring server. Two entry points build on top\n * of the same lock:\n *\n * - `ensureSingleWebServer` binds (or reuses) a server **in the calling\n * process**. It is the low-level primitive, used by the `web serve` command\n * (the standalone process that ends up owning the lock) and by this\n * module's own tests. Nothing else should call it directly.\n * - `ensureWebMonitor` (US-002) is what `run`/`execute` call: it reuses an\n * active instance exactly like `ensureSingleWebServer`, but when none\n * exists it spawns `web serve` as a **detached** child instead of binding\n * locally, so the server outlives the pipeline process. Falls back to the\n * legacy in-process `startWebServer` (US-006) when the global storage tree\n * itself is unreachable.\n */\n\n/** Lock file name, sibling of `config.json` at the global storage root. */\nexport const WEB_LOCK_FILENAME = 'web.lock';\n\n/** How long a health probe against a candidate instance waits before giving up. */\nconst HEALTH_PROBE_TIMEOUT_MS = 1000;\n\n/**\n * A process that loses the atomic lock creation retries this many times,\n * waiting for the winner to finish starting up, before giving up.\n */\nconst LOCK_CLAIM_RETRIES = 5;\nconst LOCK_CLAIM_RETRY_DELAY_MS = 100;\n\nexport function getWebLockFile(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), WEB_LOCK_FILENAME);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** `process.kill(pid, 0)` throws ESRCH when the pid is gone, EPERM when it is\n * alive but owned by another user — only ESRCH means \"not alive\". */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** 0.0.0.0 is not a connectable address; probe loopback instead, same mapping\n * `startWebServer` uses to build its own display URL. */\nfunction probeUrl(host: string, port: number): string {\n const probeHost = host === '0.0.0.0' ? '127.0.0.1' : host;\n return `http://${probeHost}:${port}/api/health`;\n}\n\nasync function probeHealth(host: string, port: number): Promise<boolean> {\n try {\n const res = await fetch(probeUrl(host, port), {\n signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/** Malformed content (partial write, older/newer incompatible shape) reads as\n * \"no lock\" — the same degrade-to-absent behavior as `loadGlobalConfig()`. */\nexport async function readWebLock(lockFile: string): Promise<WebLock | null> {\n let raw: string;\n try {\n raw = await readFile(lockFile, 'utf-8');\n } catch {\n return null;\n }\n try {\n const result = webLockSchema.safeParse(JSON.parse(raw));\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nexport async function removeWebLock(lockFile: string): Promise<void> {\n try {\n await rm(lockFile, { force: true });\n } catch {\n /* best-effort cleanup */\n }\n}\n\n/**\n * A lock is only trustworthy when both signals agree: the owning process is\n * alive AND it answers `/api/health`. A dead pid is the obvious stale case;\n * a live pid that does not answer covers a process that still exists but\n * crashed before binding, was killed -9 leaving a zombie-adjacent state, or\n * whose port was reclaimed by something else entirely. Either way the lock is\n * removed so the next attempt starts clean instead of looping on it forever.\n */\nexport async function detectActiveInstance(lockFile: string): Promise<WebLock | null> {\n const lock = await readWebLock(lockFile);\n if (lock === null) return null;\n\n if (isProcessAlive(lock.pid) && (await probeHealth(lock.host, lock.port))) {\n return lock;\n }\n\n await removeWebLock(lockFile);\n return null;\n}\n\n/**\n * Atomic exclusive create: `wx` fails with EEXIST when another process won\n * the race, which is what lets two concurrent invocations agree on a single\n * winner without a separate lock-of-the-lock.\n */\nasync function claimWebLock(lockFile: string, lock: WebLock): Promise<boolean> {\n try {\n await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: 'utf-8', flag: 'wx' });\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;\n throw err;\n }\n}\n\n/** Handle returned when reusing an already-running instance instead of\n * binding a new one — there is no local `Server` to hold, and closing it\n * must not tear down a server another process owns. */\nfunction reusedHandle(lock: WebLock): WebServerHandle {\n const displayHost = lock.host === '0.0.0.0' ? 'localhost' : lock.host;\n return {\n host: lock.host,\n port: lock.port,\n url: `http://${displayHost}:${lock.port}`,\n close: async () => {},\n };\n}\n\n/**\n * Start the web monitoring server, first checking whether a live instance\n * already owns `web.lock` and reusing it instead of binding a second one.\n *\n * The lock is claimed only *after* a successful bind, never before: claiming\n * it with the port the server is about to (but hasn't yet) listen on would\n * make that lock briefly answer no health probe at all, and a concurrent\n * invocation would misread that gap as \"stale\" and delete a perfectly good\n * lock out from under its owner. Binding first also means two invocations\n * racing for the same fixed port never both reach the claim step — the OS\n * itself lets only one `listen()` succeed. The `wx` claim then exists for the\n * remaining race: two invocations that *both* manage to bind (only possible\n * with an ephemeral port) still agree on exactly one winner. The loser closes\n * the server it just opened and defers to whichever lock exists.\n *\n * Returns null exactly when `startWebServer` would: nothing here may affect\n * the pipeline, so a lock we cannot resolve after every retry degrades to \"no\n * server\" rather than throwing.\n */\nexport async function ensureSingleWebServer(\n options: WebServerOptions,\n lockOptions: GetGlobalRootOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n const lockFile = getWebLockFile(lockOptions);\n\n for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const handle = await startWebServer(options);\n if (handle === null) {\n // Could not bind. Most likely another instance's listen() won a race\n // on the same fixed port between our detection above and this call —\n // its lock should be visible shortly. Anything else (port taken by an\n // unrelated process, invalid host, ...) fails the same way on the next\n // retry too, and the loop's own limit stops it from spinning forever.\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n let claimed: boolean;\n try {\n claimed = await claimWebLock(lockFile, {\n pid: process.pid,\n port: handle.port,\n host: handle.host,\n startedAt: isoNow(),\n });\n } catch (err) {\n await handle.close();\n throw err;\n }\n\n if (!claimed) {\n // We bound successfully (an ephemeral port let both of us through),\n // but another invocation's lock got written first. We are not the\n // canonical instance: close what we just opened and defer to it.\n await handle.close();\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n await removeWebLock(lockFile);\n };\n return handle;\n }\n\n warn('Could not determine the web monitor lock ownership. Continuing without the web server.');\n return null;\n}\n\n/** Bounded wait for a freshly spawned `web serve` child to claim the lock. */\nconst DETACHED_CLAIM_TIMEOUT_MS = 5000;\nconst DETACHED_CLAIM_POLL_MS = 150;\n\nexport interface EnsureWebMonitorOptions extends GetGlobalRootOptions {\n /** Injectable for tests. Defaults to `node:child_process`'s `spawn`. */\n spawn?: (\n command: string,\n args: string[],\n options: { detached: true; stdio: 'ignore' },\n ) => ChildProcess;\n /** Node executable to spawn. Defaults to `process.execPath`. */\n execPath?: string;\n /** CLI entry script re-invoked as `<entry> web serve ...`. Defaults to `process.argv[1]`. */\n entryScript?: string;\n /** How long to wait for the spawned instance's lock to become healthy. */\n claimTimeoutMs?: number;\n claimPollIntervalMs?: number;\n}\n\n/**\n * Entry point for `run`/`execute`: get a web monitor without binding one in\n * this process (US-002).\n *\n * 1. Reuse a live, healthy instance exactly like `ensureSingleWebServer`.\n * 2. Otherwise, spawn `<node> <cli> web serve --port … --host … [--refresh …]`\n * detached (`{ detached: true, stdio: 'ignore' }`) and `unref()`ed, so it\n * survives this process exiting and never pipes its own stdio back here.\n * 3. Poll the lock file (bounded) until the spawned instance claims it, then\n * return a *reused* handle for it — this process never owns that `Server`,\n * the same shape `ensureSingleWebServer`'s reuse path already returns.\n *\n * Two failure modes degrade gracefully rather than affecting the pipeline:\n * the global storage tree being unreachable (no resolvable home directory)\n * falls back to `startWebServer` bound right here, exactly the pre-US-001\n * behavior (US-006); a spawn or claim failure with the storage tree otherwise\n * healthy just returns null, same contract `startWebServer` itself has.\n */\nexport async function ensureWebMonitor(\n options: WebServerOptions,\n monitorOptions: EnsureWebMonitorOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n\n let lockFile: string;\n try {\n lockFile = getWebLockFile(monitorOptions);\n } catch (err) {\n warn(\n `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`,\n );\n return startWebServer(options);\n }\n\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const execPath = monitorOptions.execPath ?? process.execPath;\n const entryScript = monitorOptions.entryScript ?? process.argv[1];\n if (entryScript === undefined) {\n warn('Web monitor: could not determine the CLI entry point to spawn a detached server.');\n return null;\n }\n\n const args = [\n entryScript,\n 'web',\n 'serve',\n '--port',\n String(options.port),\n '--host',\n options.host,\n ];\n if (options.refreshSeconds !== undefined) {\n args.push('--refresh', String(options.refreshSeconds));\n }\n\n const spawnFn = monitorOptions.spawn ?? nodeSpawn;\n try {\n const child = spawnFn(execPath, args, { detached: true, stdio: 'ignore' });\n child.unref();\n } catch (err) {\n warn(\n `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`,\n );\n return null;\n }\n\n const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;\n const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const claimed = await detectActiveInstance(lockFile);\n if (claimed !== null) {\n info(`Web monitor running at ${chalk.bold.cyan(reusedHandle(claimed).url)}`);\n return reusedHandle(claimed);\n }\n await sleep(pollMs);\n }\n\n warn(\n 'Web monitor: the detached server did not start in time. Continuing without the web server.',\n );\n return null;\n}\n","import { createHash } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { resolvePackageDir } from '../core/prompt-resolver.js';\nimport {\n NullPublisher,\n type SessionPublisher,\n type SessionSnapshot,\n} from '../core/session-state.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport type { SessionDirectoryHandle } from './session-directory.js';\n\n/**\n * HTTP server for the web monitoring mode. Plain node:http — no new runtime\n * dependencies. Serves the publisher's in-memory snapshot (it never re-reads\n * issues/N/session.json) and the static UI assets.\n *\n * Resilience contract: nothing here may ever affect the pipeline. Listen\n * failures (EADDRINUSE included) log a warning and the execution continues\n * without a server; the server is unref()'d so it cannot keep the process\n * alive; request handling is wrapped so a handler error answers 500 instead\n * of crashing the process.\n */\n\nconst require = createRequire(import.meta.url);\n\nfunction readPackageVersion(): string {\n try {\n return (require('../../package.json') as { version: string }).version;\n } catch {\n return '0.0.0';\n }\n}\n\nexport interface WebServerOptions {\n /**\n * Legacy single-session mode (pre-US-003, still used by the US-006\n * fallback): serves this publisher's in-memory snapshot directly, with no\n * directory scan and no lock file involved. Mutually exclusive with\n * `sessions` — when both are given, `sessions` wins.\n */\n publisher?: SessionPublisher;\n /**\n * Multi-session mode: sessions discovered by polling the global storage\n * tree (`web/session-directory.ts`). This is what every current entry\n * point (`web serve`) passes; `publisher` only exists for the legacy path.\n */\n sessions?: SessionDirectoryHandle;\n port: number;\n host: string;\n /** Suggested UI polling interval, exposed via /api/health. */\n refreshSeconds?: number;\n /** Package version reported by /api/health. Default: read from package.json. */\n version?: string;\n /** Directory holding index.html/app.css/app.js. Default: auto-resolved. */\n publicDir?: string;\n /** Info logger. Default: printInfo. */\n info?: (message: string) => void;\n /** Warning logger. Default: printWarning. */\n warn?: (message: string) => void;\n /**\n * Whether to `unref()` the underlying socket so it never keeps the process\n * alive on its own. Default `true`, matching the historical contract for a\n * server bound inline in the pipeline process. The standalone `web serve`\n * process (the detached server itself) passes `false`: it has nothing else\n * to do, so staying alive for as long as the server is bound *is* the job.\n */\n unref?: boolean;\n}\n\nexport interface WebServerHandle {\n /**\n * Absent when this handle represents an existing instance reused through\n * `ensureSingleWebServer` (`web/lock.ts`) instead of one bound locally.\n */\n server?: Server;\n /** Host the server is bound to. */\n host: string;\n /** Actual bound port (relevant when options.port is 0). */\n port: number;\n /** Human-facing access URL. */\n url: string;\n /** Close the server and release signal handlers. Idempotent, never rejects. */\n close(): Promise<void>;\n}\n\ninterface StaticAsset {\n body: string;\n contentType: string;\n}\n\nconst JSON_TYPE = 'application/json; charset=utf-8';\n\n/**\n * Uniform view over \"whatever sessions this server knows about\", so the route\n * handlers below never care whether they are backed by a single in-memory\n * publisher (legacy mode) or by the global directory scan (US-003/US-004).\n */\ninterface SessionSource {\n /** Every session currently considered active, in no particular order. */\n list(): SessionSnapshot[];\n /** A specific session by id, or undefined when it is not currently active. */\n get(sessionId: string): SessionSnapshot | undefined;\n}\n\nfunction publisherSessionSource(publisher: SessionPublisher): SessionSource {\n return {\n list: () => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === null ? [] : [snapshot];\n },\n get: (sessionId) => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === sessionId ? snapshot : undefined;\n },\n };\n}\n\nfunction directorySessionSource(handle: SessionDirectoryHandle): SessionSource {\n return {\n list: () => handle.sessions().map((entry) => entry.snapshot),\n get: (sessionId) => handle.getSession(sessionId)?.snapshot,\n };\n}\n\nfunction resolveSessionSource(options: WebServerOptions): SessionSource {\n if (options.sessions) return directorySessionSource(options.sessions);\n return publisherSessionSource(options.publisher ?? new NullPublisher());\n}\n\nconst STATIC_ROUTES: Record<string, { file: string; contentType: string }> = {\n '/': { file: 'index.html', contentType: 'text/html; charset=utf-8' },\n '/app.css': { file: 'app.css', contentType: 'text/css; charset=utf-8' },\n '/app.js': { file: 'app.js', contentType: 'text/javascript; charset=utf-8' },\n};\n\n/** Assets are read once at startup; missing files simply 404. */\nasync function loadStaticAssets(publicDir: string | null): Promise<Map<string, StaticAsset>> {\n const assets = new Map<string, StaticAsset>();\n if (publicDir === null) return assets;\n for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {\n try {\n const body = await readFile(join(publicDir, file), 'utf-8');\n assets.set(route, { body, contentType });\n } catch {\n // Asset not present (e.g. UI not built yet) — route answers 404.\n }\n }\n return assets;\n}\n\n/** Headers applied to every response, including 304s and errors. */\nfunction baseHeaders(res: ServerResponse): void {\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('Referrer-Policy', 'no-referrer');\n res.setHeader('X-Robots-Tag', 'noindex, nofollow');\n}\n\nfunction respond(res: ServerResponse, status: number, contentType: string, body: string): void {\n res.statusCode = status;\n res.setHeader('Content-Type', contentType);\n res.end(body);\n}\n\nfunction respondJson(res: ServerResponse, status: number, payload: unknown): void {\n respond(res, status, JSON_TYPE, JSON.stringify(payload));\n}\n\n/**\n * Start the monitoring HTTP server. Returns null when the server could not\n * listen (port in use, invalid host, ...) — the pipeline continues without\n * monitoring, it is never brought down by the server.\n */\nexport async function startWebServer(options: WebServerOptions): Promise<WebServerHandle | null> {\n const info = options.info ?? printInfo;\n const warn = options.warn ?? printWarning;\n const version = options.version ?? readPackageVersion();\n const startedAtMs = Date.now();\n\n // The UI ships at the package root as web/public/ (sibling of prompts/),\n // resolved the same way from src/ and from the published dist/ layout.\n const assets = await loadStaticAssets(\n options.publicDir ?? resolvePackageDir(join('web', 'public')),\n );\n\n const source = resolveSessionSource(options);\n\n // JSON serialization memoized per session id: an unchanged poll answers 304\n // with an empty body. Content-hashed rather than counter-based, unlike the\n // pre-multi-session version — a directory-backed session has no in-process\n // publisher to hand out a monotonic version(), and a hash works uniformly\n // for both sources.\n const statusCache = new Map<string, { body: string; etag: string }>();\n const statusPayload = (snapshot: SessionSnapshot): { body: string; etag: string } => {\n const key = snapshot.sessionId ?? '';\n const body = JSON.stringify(snapshot);\n const cached = statusCache.get(key);\n if (cached !== undefined && cached.body === body) return cached;\n const etag = `\"${createHash('sha1').update(body).digest('hex')}\"`;\n const entry = { body, etag };\n statusCache.set(key, entry);\n return entry;\n };\n\n const handleRequest = (req: IncomingMessage, res: ServerResponse): void => {\n baseHeaders(res);\n\n const requestUrl = new URL(req.url ?? '/', 'http://localhost');\n const path = requestUrl.pathname;\n\n // POST /api/control/* is reserved for future write operations (pause,\n // retry, ...); intentionally NOT registered — the v1 surface is\n // read-only (snapshot.readOnly is true and capabilities is empty).\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n respondJson(res, 404, { error: 'Not found' });\n return;\n }\n\n if (path === '/api/status' || path === '/status.json') {\n const sessionId = requestUrl.searchParams.get('session');\n let snapshot: SessionSnapshot | undefined;\n if (sessionId !== null) {\n snapshot = source.get(sessionId);\n if (snapshot === undefined) {\n respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });\n return;\n }\n } else {\n // Back-compat shorthand (US-005): with exactly one active session,\n // GET /api/status with no query string still answers it directly, the\n // same behavior a single-session run always had. Zero or several\n // active sessions is genuinely ambiguous without an id and answers an\n // explicit error instead of guessing.\n const active = source.list();\n if (active.length === 0) {\n respondJson(res, 404, {\n error: 'No active session. Pass ?session=<id> — see GET /api/sessions.',\n });\n return;\n }\n if (active.length > 1) {\n respondJson(res, 409, {\n error: 'Multiple active sessions; specify ?session=<id>.',\n sessions: active.map((entry) => entry.sessionId),\n });\n return;\n }\n snapshot = active[0];\n }\n\n const { body, etag } = statusPayload(snapshot);\n res.setHeader('ETag', etag);\n if (req.headers['if-none-match'] === etag) {\n res.statusCode = 304;\n res.end();\n return;\n }\n respond(res, 200, JSON_TYPE, body);\n return;\n }\n\n if (path === '/api/sessions') {\n respondJson(\n res,\n 200,\n source.list().map((snapshot) => ({\n sessionId: snapshot.sessionId,\n issueNumber: snapshot.issue.number,\n status: snapshot.status,\n startedAt: snapshot.startedAt,\n updatedAt: snapshot.updatedAt,\n statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? '')}`,\n })),\n );\n return;\n }\n\n if (path === '/api/health') {\n respondJson(res, 200, {\n ok: true,\n uptime: Math.round((Date.now() - startedAtMs) / 1000),\n version,\n refreshSeconds: options.refreshSeconds ?? 5,\n });\n return;\n }\n\n const asset = assets.get(path);\n if (asset) {\n respond(res, 200, asset.contentType, asset.body);\n return;\n }\n\n respondJson(res, 404, { error: 'Not found' });\n };\n\n const server = createServer((req, res) => {\n try {\n handleRequest(req, res);\n } catch (err) {\n try {\n respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });\n } catch {\n // Response already destroyed — nothing to do, never crash the process.\n }\n }\n });\n\n const listening = await new Promise<boolean>((resolve) => {\n const onError = (err: NodeJS.ErrnoException): void => {\n if (err.code === 'EADDRINUSE') {\n warn(\n `Web monitor port ${options.port} is already in use. Continuing without the web server.`,\n );\n } else {\n warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);\n }\n resolve(false);\n };\n server.once('error', onError);\n server.listen(options.port, options.host, () => {\n server.removeListener('error', onError);\n resolve(true);\n });\n });\n\n if (!listening) return null;\n\n // Never keep the process alive on its own, unless explicitly told not to\n // unref: the pipeline ending must end the process. The one exception is the\n // detached `web serve` process (options.unref === false) — it has no other\n // work, so staying alive for as long as the server is bound is the point.\n if (options.unref !== false) {\n server.unref();\n }\n // Post-listen errors must never become uncaught exceptions.\n server.on('error', (err) => {\n warn(`Web monitor server error: ${err.message}`);\n });\n\n const address = server.address() as AddressInfo;\n const port = address.port;\n const displayHost = options.host === '0.0.0.0' ? 'localhost' : options.host;\n const url = `http://${displayHost}:${port}`;\n\n if (options.host === '0.0.0.0') {\n warn('Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.');\n }\n info(`Web monitor running at ${chalk.bold.cyan(url)}`);\n\n let closed = false;\n let closePromise: Promise<void> | null = null;\n\n const doClose = (): Promise<void> =>\n new Promise<void>((resolve) => {\n process.removeListener('SIGINT', onSignal);\n process.removeListener('SIGTERM', onSignal);\n server.close(() => resolve());\n // Drop idle keep-alive connections so close() never hangs.\n server.closeAllConnections();\n });\n\n const close = (): Promise<void> => {\n if (closed) return closePromise ?? Promise.resolve();\n closed = true;\n closePromise = doClose();\n return closePromise;\n };\n\n // `handle.close` is built here, but ensureSingleWebServer/ensureWebMonitor\n // and the `web serve` command each wrap it afterwards (to also remove the\n // lock file, stop the session-directory poller, ...). onSignal below calls\n // `handle.close()` — the *current* value of the property — rather than the\n // `close` closed over here, so a `kill -TERM <pid>` (including `issue-flow\n // web stop`) always runs the fully wrapped shutdown, not just this raw\n // socket close. Capturing `close` directly here was a real bug: the wrapped\n // behavior only ever ran for an *explicit* `handle.close()` call, leaving\n // the lock file (and the poller) behind on every signal-driven shutdown.\n const handle: WebServerHandle = { server, host: options.host, port, url, close };\n\n // Explicit close on SIGINT/SIGTERM, then re-raise so the default\n // termination behavior still applies.\n const onSignal = (signal: NodeJS.Signals): void => {\n void handle.close().finally(() => {\n process.kill(process.pid, signal);\n });\n };\n process.once('SIGINT', onSignal);\n process.once('SIGTERM', onSignal);\n\n return handle;\n}\n"],"mappings":"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","names":["readFile","join","chalk","require","join","readFile","chalk"]}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isoNow,
|
|
11
11
|
parseUsage,
|
|
12
12
|
useColor
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-WO6ERABE.js";
|
|
14
14
|
|
|
15
15
|
// src/core/headless.ts
|
|
16
16
|
import { createInterface } from "readline";
|
|
@@ -279,4 +279,4 @@ async function runHeadless(options) {
|
|
|
279
279
|
export {
|
|
280
280
|
runHeadless
|
|
281
281
|
};
|
|
282
|
-
//# sourceMappingURL=chunk-
|
|
282
|
+
//# sourceMappingURL=chunk-KHTLN54U.js.map
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getSessionPublisher,
|
|
8
8
|
isoNow,
|
|
9
9
|
printRetry
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WO6ERABE.js";
|
|
11
11
|
|
|
12
12
|
// src/core/phase-runner.ts
|
|
13
13
|
import { readFile } from "fs/promises";
|
|
@@ -55,4 +55,4 @@ export {
|
|
|
55
55
|
runPhaseWithRetry,
|
|
56
56
|
readFileWithGrace
|
|
57
57
|
};
|
|
58
|
-
//# sourceMappingURL=chunk-
|
|
58
|
+
//# sourceMappingURL=chunk-OPIDR2DC.js.map
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
printWarning,
|
|
8
8
|
webConfigSchema,
|
|
9
9
|
writeFileAtomic
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WO6ERABE.js";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { readFile as readFile2 } from "fs/promises";
|
|
@@ -262,6 +262,12 @@ var globalConfigSchema = z.object({
|
|
|
262
262
|
retry: globalRetryConfigSchema,
|
|
263
263
|
commit: globalCommitConfigSchema
|
|
264
264
|
}).partial();
|
|
265
|
+
var webLockSchema = z.object({
|
|
266
|
+
pid: z.number().int().positive(),
|
|
267
|
+
port: z.number().int().positive(),
|
|
268
|
+
host: z.string().min(1),
|
|
269
|
+
startedAt: z.string().min(1)
|
|
270
|
+
});
|
|
265
271
|
|
|
266
272
|
// src/storage/compat.ts
|
|
267
273
|
var LEGACY_ISSUES_DIR_NAME = "issues";
|
|
@@ -767,6 +773,10 @@ export {
|
|
|
767
773
|
normalizeRemoteUrl,
|
|
768
774
|
stripRemoteUrlCredentials,
|
|
769
775
|
getCommitsSince,
|
|
776
|
+
PROJECTS_DIR_NAME,
|
|
777
|
+
ISSUES_DIR_NAME,
|
|
778
|
+
getGlobalRoot,
|
|
779
|
+
webLockSchema,
|
|
770
780
|
resolveProjectPaths,
|
|
771
781
|
resolveIssuePaths,
|
|
772
782
|
DEFAULTS,
|
|
@@ -785,4 +795,4 @@ export {
|
|
|
785
795
|
loadIssuesConfig,
|
|
786
796
|
loadPrReviewConfig
|
|
787
797
|
};
|
|
788
|
-
//# sourceMappingURL=chunk-
|
|
798
|
+
//# sourceMappingURL=chunk-RUTDK6ZF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/storage/paths.ts","../src/utils/shell.ts","../src/utils/git.ts","../src/storage/resolve.ts","../src/storage/compat.ts","../src/storage/schemas.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { platform } from 'node:os';\nimport { join } from 'node:path';\nimport type { IssuesConfig } from './issues/types.js';\nimport {\n issuesConfigSchema,\n type PrReviewConfig,\n prReviewConfigSchema,\n type WebConfig,\n webConfigSchema,\n} from './schemas.js';\nimport { getGlobalRoot } from './storage/paths.js';\nimport { resolveIssuePaths } from './storage/resolve.js';\nimport { type GlobalConfig, globalConfigSchema } from './storage/schemas.js';\nimport type { EngineConfig, ResolvedPaths } from './types.js';\nimport { printWarning } from './ui/logger.js';\nimport { getProjectRoot } from './utils/git.js';\nimport { run } from './utils/shell.js';\n\n/**\n * Default configuration values — matching the Bash script exactly.\n */\nexport const DEFAULTS = {\n retryLimit: 10,\n retryForever: false,\n backoffBaseSeconds: 30,\n backoffMaxSeconds: 900,\n} as const;\n\n/**\n * Create a EngineConfig with defaults merged with provided options.\n */\nexport function createConfig(options: Partial<EngineConfig>): EngineConfig {\n return {\n issueNumber: options.issueNumber,\n maxIterations: options.maxIterations,\n retryLimit: options.retryLimit ?? DEFAULTS.retryLimit,\n retryForever: options.retryForever ?? DEFAULTS.retryForever,\n backoffBaseSeconds: options.backoffBaseSeconds ?? DEFAULTS.backoffBaseSeconds,\n backoffMaxSeconds: options.backoffMaxSeconds ?? DEFAULTS.backoffMaxSeconds,\n };\n}\n\n/**\n * Resolve file paths based on issue number and project root.\n *\n * With --issue N, every artifact comes from the global storage layer via\n * `resolveIssuePaths()`, which also migrates the legacy `<projectRoot>/issues/`\n * tree on first read:\n * prdFile = ~/.issue-flow/projects/{id}/issues/{N}/tasks.json\n * progressFile = ~/.issue-flow/projects/{id}/issues/{N}/progress.txt\n *\n * Standalone:\n * prdFile = {projectRoot}/prd.json\n * progressFile = {projectRoot}/progress.txt\n *\n * Beware of the asymmetric mapping in the issue branch: `ResolvedPaths.prdFile`\n * is the engine's *task plan*, so it maps to `IssuePaths.tasksFile`\n * (`tasks.json`) and **not** to `IssuePaths.prdFile` (`prd.md`, the human-facing\n * document produced by the `prd` phase). The name predates the split and is kept\n * because standalone mode really does read a `prd.json`.\n *\n * `projectRoot` stays on the result either way: `core/engine.ts` uses it as the\n * cwd of its git operations, which the global storage does not replace.\n */\nexport async function resolvePaths(\n config: EngineConfig,\n scriptDir?: string,\n): Promise<ResolvedPaths> {\n const projectRoot = await getProjectRoot();\n\n if (config.issueNumber) {\n // projectRoot is forwarded so the resolver does not shell out to\n // `git rev-parse --show-toplevel` a second time for the answer we just got.\n const issuePaths = await resolveIssuePaths(config.issueNumber, { projectRoot });\n return {\n prdFile: issuePaths.tasksFile,\n progressFile: issuePaths.progressFile,\n archiveDir: issuePaths.archiveDir,\n lastBranchFile: issuePaths.lastBranchFile,\n projectRoot,\n };\n }\n\n // Standalone mode — use scriptDir if available, otherwise projectRoot\n const base = scriptDir ?? projectRoot;\n return {\n prdFile: join(base, 'prd.json'),\n progressFile: join(base, 'progress.txt'),\n archiveDir: join(base, 'archive'),\n lastBranchFile: join(base, '.last-branch'),\n projectRoot,\n };\n}\n\n/**\n * Return a platform-appropriate install hint for a given package.\n */\nexport function getInstallHint(pkg: string): string {\n const os = platform();\n\n if (os === 'darwin') {\n return `brew install ${pkg}`;\n }\n if (os === 'linux') {\n return `apt install ${pkg} (or your distro's package manager)`;\n }\n if (os === 'win32') {\n return `winget install ${pkg} (or choco install ${pkg})`;\n }\n\n return `install ${pkg} using your system package manager`;\n}\n\n/**\n * Validate that required external dependencies are available.\n * Returns an array of error messages (empty if all deps are found).\n */\nexport async function validateDependencies(): Promise<string[]> {\n const errors: string[] = [];\n\n // Check git\n const gitResult = await run('git', ['--version']);\n if (gitResult.exitCode !== 0) {\n errors.push(` - git (install with: ${getInstallHint('git')})`);\n }\n\n // Check claude\n const claudeResult = await run('claude', ['--version']);\n if (claudeResult.exitCode !== 0) {\n errors.push(' - claude (install with: npm install -g @anthropic-ai/claude-code)');\n }\n\n // Note: jq is NOT required — the TypeScript CLI handles JSON natively\n\n return errors;\n}\n\n// ── Per-project configuration file ──────────────────────────────────────────\n\n/** Optional per-project configuration file, read from the project root. */\nexport const PROJECT_CONFIG_FILENAME = '.issue-flow.json';\n\n/** Historical alias kept for the web monitoring call sites. */\nexport const WEB_CONFIG_FILENAME = PROJECT_CONFIG_FILENAME;\n\n/**\n * Read and parse .issue-flow.json from the project root.\n *\n * Never throws: an absent file, unreadable path, invalid JSON or a non-object\n * root all degrade to `null` (with a warning for the malformed cases), so every\n * consumer falls back to its own defaults.\n */\nasync function readProjectConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Record<string, unknown> | null> {\n let root = projectRoot;\n if (root === undefined) {\n try {\n root = await getProjectRoot();\n } catch {\n return null;\n }\n }\n\n const filePath = join(root, PROJECT_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch {\n // The file is entirely optional — absence is the common case.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: invalid JSON.`);\n return null;\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: expected a JSON object.`);\n return null;\n }\n\n return parsed as Record<string, unknown>;\n}\n\n// ── Global configuration file ───────────────────────────────────────────────\n\n/** Machine-wide configuration file, read from the global storage root. */\nexport const GLOBAL_CONFIG_FILENAME = 'config.json';\n\nexport interface LoadGlobalConfigOptions {\n /** Environment source, forwarded to getGlobalRoot(). Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory holding config.json. Defaults to getGlobalRoot(). */\n globalRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\n/**\n * Read and parse `~/.issue-flow/config.json`, the preferences a user sets once\n * for every project.\n *\n * Never throws, exactly like readProjectConfigFile(): an absent file, an\n * unreadable path, invalid JSON, a non-object root or an invalid key all\n * degrade to \"no global preference\" so the caller falls back to the layers\n * below. Absence is silent — it is the common case; every other failure warns,\n * because a preference the user did write is being dropped.\n *\n * Validation happens key by key on purpose: a typo in `retry` must not cost the\n * user their `web` settings. Unknown keys are dropped without a warning, which\n * is what keeps a file written by a newer release readable here.\n */\nexport async function loadGlobalConfig(\n options: LoadGlobalConfigOptions = {},\n): Promise<GlobalConfig> {\n const warn = options.warn ?? printWarning;\n\n let root: string;\n try {\n root = options.globalRoot ?? getGlobalRoot({ env: options.env ?? process.env });\n } catch (err: unknown) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n return {};\n }\n\n const filePath = join(root, GLOBAL_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: invalid JSON.`);\n return {};\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: expected a JSON object.`);\n return {};\n }\n\n const config: GlobalConfig = {};\n for (const [key, value] of Object.entries(parsed)) {\n const result = globalConfigSchema.safeParse({ [key]: value });\n if (!result.success) {\n warn(\n `Ignoring \"${key}\" key of ${GLOBAL_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n continue;\n }\n // An unknown key parses successfully into an empty object and disappears\n // here, which is the retro-compatible behaviour we want.\n Object.assign(config, result.data);\n }\n\n return config;\n}\n\n/**\n * The configuration sources of the engine, ordered from the lowest to the\n * highest precedence:\n *\n * defaults < config.json (global) < .issue-flow.json (project) < env < CLI\n */\nexport interface ConfigLayers<T extends object> {\n /** Hard-coded fallbacks, e.g. DEFAULTS or the values baked into a schema. */\n defaults?: Partial<T>;\n /** ~/.issue-flow/config.json, via loadGlobalConfig(). */\n global?: Partial<T>;\n /** The matching key of .issue-flow.json in the project root. */\n project?: Partial<T>;\n /** ISSUE_FLOW_* environment variables. */\n env?: Partial<T>;\n /** CLI flags. */\n cli?: Partial<T>;\n}\n\n/**\n * Merge configuration layers following the documented precedence.\n *\n * Pure and shallow: a layer only participates with the keys it actually\n * carries, so an absent key never erases the layer below it. `undefined` counts\n * as absent — that is what lets a layer be built by assigning only the values\n * that were really provided.\n *\n * Because the merge is shallow, nested objects (`web`, `retry`) are replaced\n * whole rather than merged field by field; callers that need per-field\n * precedence inside a nested key must flatten it into its own merge.\n *\n * Caveat for the `project` layer: it must be the *raw* set of keys the user\n * wrote, not the output of a schema that materializes defaults. In zod 4 a\n * `.default()` survives `.partial()`, so parsing the project file with\n * `webConfigSchema.partial()` yields every default and would make the project\n * layer swallow the global one.\n */\nexport function mergeConfigLayers<T extends object>(layers: ConfigLayers<T>): Partial<T> {\n const merged: Record<string, unknown> = {};\n\n for (const layer of [layers.defaults, layers.global, layers.project, layers.env, layers.cli]) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n }\n\n return merged as Partial<T>;\n}\n\n// ── Web monitoring configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts. Highest-precedence\n * source consumed by loadWebConfig().\n */\nlet webCliOverrides: Partial<WebConfig> = {};\n\nexport function setWebCliOverrides(overrides: Partial<WebConfig>): void {\n webCliOverrides = overrides;\n}\n\nexport interface LoadWebConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setWebCliOverrides(). */\n cli?: Partial<WebConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nconst FALSY_ENV_VALUES = new Set(['', '0', 'false', 'no', 'off']);\n\nfunction parseBooleanEnv(value: string): boolean {\n return !FALSY_ENV_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction readNumberEnv(\n env: NodeJS.ProcessEnv,\n name: string,\n warn: (message: string) => void,\n): number | undefined {\n const raw = env[name];\n if (raw === undefined) {\n return undefined;\n }\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`Ignoring ${name}=\"${raw}\": not a number.`);\n return undefined;\n }\n return parsed;\n}\n\nfunction readWebConfigEnv(\n env: NodeJS.ProcessEnv,\n warn: (message: string) => void,\n): Partial<WebConfig> {\n const layer: Partial<WebConfig> = {};\n if (env.ISSUE_FLOW_WEB !== undefined) {\n layer.enabled = parseBooleanEnv(env.ISSUE_FLOW_WEB);\n }\n const port = readNumberEnv(env, 'ISSUE_FLOW_WEB_PORT', warn);\n if (port !== undefined) {\n layer.port = port;\n }\n if (env.ISSUE_FLOW_WEB_HOST !== undefined) {\n layer.host = env.ISSUE_FLOW_WEB_HOST;\n }\n const refresh = readNumberEnv(env, 'ISSUE_FLOW_WEB_REFRESH', warn);\n if (refresh !== undefined) {\n layer.refreshSeconds = refresh;\n }\n const logLimit = readNumberEnv(env, 'ISSUE_FLOW_WEB_LOG_LIMIT', warn);\n if (logLimit !== undefined) {\n layer.logLimit = logLimit;\n }\n return layer;\n}\n\nasync function readWebConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<WebConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const web = file?.web;\n if (web === undefined) {\n return {};\n }\n\n const result = webConfigSchema.partial().safeParse(web);\n if (!result.success) {\n warn(\n `Ignoring \"web\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the web monitoring configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning.\n */\nexport async function loadWebConfig(options: LoadWebConfigOptions = {}): Promise<WebConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? webCliOverrides;\n const env = options.env ?? process.env;\n\n const fileLayer = await readWebConfigFile(options.projectRoot, warn);\n const envLayer = readWebConfigEnv(env, warn);\n const merged = { ...fileLayer, ...envLayer, ...cli };\n\n const result = webConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid web monitoring configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return webConfigSchema.parse({});\n}\n\n// ── Issue provider configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts (--local, --github,\n * --prefer-local, --prefer-github, --ask). Highest-precedence source consumed\n * by loadIssuesConfig().\n */\nlet issuesCliOverrides: Partial<IssuesConfig> = {};\n\nexport function setIssuesCliOverrides(overrides: Partial<IssuesConfig>): void {\n issuesCliOverrides = overrides;\n}\n\nexport interface LoadIssuesConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setIssuesCliOverrides(). */\n cli?: Partial<IssuesConfig>;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nasync function readIssuesConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<IssuesConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const issues = file?.issues;\n if (issues === undefined) {\n return {};\n }\n\n const result = issuesConfigSchema.partial().safeParse(issues);\n if (!result.success) {\n warn(\n `Ignoring \"issues\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the Issue provider configuration with the documented precedence:\n * CLI flag > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning, and the defaults reproduce the GitHub-only behaviour.\n */\nexport async function loadIssuesConfig(\n options: LoadIssuesConfigOptions = {},\n): Promise<IssuesConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? issuesCliOverrides;\n\n const fileLayer = await readIssuesConfigFile(options.projectRoot, warn);\n const merged = { ...fileLayer, ...cli };\n\n const result = issuesConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid Issue provider configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return issuesConfigSchema.parse({});\n}\n\n// ── PR review configuration ─────────────────────────────────────────────────\n\nexport interface LoadPrReviewConfigOptions {\n /** Highest-precedence layer, for a future `--publisher` flag. */\n cli?: Partial<PrReviewConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nfunction readPrReviewConfigEnv(env: NodeJS.ProcessEnv): Partial<PrReviewConfig> {\n const publisher = env.ISSUE_FLOW_PR_REVIEW_PUBLISHER;\n // An unknown value is not dropped here: it goes through the schema below, so\n // the user gets the same warning a bad config file would produce.\n return publisher === undefined ? {} : { publisher: publisher as PrReviewConfig['publisher'] };\n}\n\nasync function readPrReviewConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<PrReviewConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const prReview = file?.prReview;\n if (prReview === undefined) {\n return {};\n }\n\n const result = prReviewConfigSchema.partial().safeParse(prReview);\n if (!result.success) {\n warn(\n `Ignoring \"prReview\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the `pr-review` configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: an absent, malformed or unknown value degrades to the default\n * publisher with a warning, so a typo in .issue-flow.json costs a warning\n * rather than the review.\n */\nexport async function loadPrReviewConfig(\n options: LoadPrReviewConfigOptions = {},\n): Promise<PrReviewConfig> {\n const warn = options.warn ?? printWarning;\n const env = options.env ?? process.env;\n\n const fileLayer = await readPrReviewConfigFile(options.projectRoot, warn);\n const envLayer = readPrReviewConfigEnv(env);\n const merged = { ...fileLayer, ...envLayer, ...(options.cli ?? {}) };\n\n const result = prReviewConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid PR review configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return prReviewConfigSchema.parse({});\n}\n","import { createHash } from 'node:crypto';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\n\n/** Directory created under the user's home when no override is provided. */\nexport const GLOBAL_DIR_NAME = '.issue-flow';\n\n/** Environment variable that relocates the whole global storage tree. */\nexport const GLOBAL_ROOT_ENV = 'ISSUE_FLOW_HOME';\n\n/** Directory under the global root holding one folder per project. */\nexport const PROJECTS_DIR_NAME = 'projects';\n\n/** Directory under a project holding one folder per issue. */\nexport const ISSUES_DIR_NAME = 'issues';\n\nexport interface GetGlobalRootOptions {\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve the root directory of the global storage tree (`~/.issue-flow`).\n *\n * Every path under the global storage MUST be derived from this function: it is\n * the single seam where `ISSUE_FLOW_HOME` takes effect, which is what lets\n * tests, CI and sandboxes point the whole tree at a temporary directory instead\n * of touching the real `$HOME`. A call site that joins `homedir()` by hand\n * silently opts out of that isolation.\n *\n * Pure and synchronous: it never creates directories nor touches the\n * filesystem — callers decide when (and whether) the directory should exist.\n */\nexport function getGlobalRoot(options: GetGlobalRootOptions = {}): string {\n const env = options.env ?? process.env;\n\n const override = env[GLOBAL_ROOT_ENV]?.trim();\n if (override) {\n // A relative override is resolved against the CWD so that every consumer\n // gets an absolute path, exactly as in the homedir() branch below.\n return resolve(override);\n }\n\n // The only failure mode: an environment where the home directory cannot be\n // determined (some containers and CI images). The override is the escape\n // hatch, so the error names it.\n let home: string;\n try {\n home = homedir();\n } catch {\n home = '';\n }\n if (home.trim() === '') {\n throw new Error(\n `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`,\n );\n }\n\n return join(home, GLOBAL_DIR_NAME);\n}\n\n/** Maximum length of the human-readable half of a project id. */\nconst SLUG_MAX_LENGTH = 32;\n\n/** Number of hex characters kept from the sha256 of the seed. */\nconst HASH_LENGTH = 12;\n\n/** Used when the repository name has no character that survives sanitization. */\nconst FALLBACK_SLUG = 'project';\n\n/**\n * Reduce an arbitrary repository name to a path-safe slug: lowercase, only\n * `[a-z0-9-]`, runs of separators collapsed, truncated to {@link SLUG_MAX_LENGTH}.\n */\nfunction slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, SLUG_MAX_LENGTH)\n .replace(/-+$/g, '');\n\n return slug === '' ? FALLBACK_SLUG : slug;\n}\n\n/**\n * Derive a stable identifier for a project, in the form `<slug>-<hash12>`.\n *\n * The seed is canonical rather than local, so the same repository yields the\n * same id on any machine: `remote:<normalizedRemote>` whenever an `origin`\n * remote exists, falling back to `path:<absoluteProjectRoot>` when it does not.\n * That is what makes moving or renaming the local folder harmless for a cloned\n * repository — the history stays attached to the remote identity.\n *\n * The `remote:` / `path:` prefix is part of the hashed seed, so a project\n * identified by path can never collide with one identified by remote even if\n * the two strings were to coincide. The slug is only cosmetic (it makes the\n * directory recognizable); the hash carries the identity.\n *\n * Known limitation of the path fallback: for a repository with no `origin`\n * remote, the absolute path *is* the identity. Moving or renaming that folder\n * produces a different id, and the previous history is left behind under the\n * old one. Configuring a remote before adopting the global storage avoids it.\n */\nexport async function getProjectId(projectRoot: string): Promise<string> {\n const remote = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n return projectIdFromRemote(remote, projectRoot);\n}\n\n/**\n * Pure half of {@link getProjectId}: derive the id once the normalized remote\n * (or `null`) is already known.\n *\n * Exported so a caller that already resolved the remote for another reason\n * (e.g. {@link resolveStorageMode} in `compat.ts`, which also persists it into\n * `metadata.json`) can compute the id without shelling out to git a second\n * time.\n */\nexport function projectIdFromRemote(remote: string | null, projectRoot: string): string {\n let seed: string;\n let name: string;\n if (remote) {\n // `host/org/repo` -> the repository name is the last path segment.\n seed = `remote:${remote}`;\n name = remote.split('/').pop() ?? '';\n } else {\n const absolute = resolve(projectRoot);\n seed = `path:${absolute}`;\n name = basename(absolute);\n }\n\n const hash = createHash('sha256').update(seed).digest('hex').slice(0, HASH_LENGTH);\n\n return `${slugify(name)}-${hash}`;\n}\n\n/**\n * Absolute directory of a project inside the global storage tree.\n *\n * `projectId` is expected to come from {@link getProjectId}, which already\n * guarantees a single safe path segment.\n */\nexport function getProjectDir(projectId: string, options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);\n}\n\n/**\n * Every artifact of one issue, resolved from a single place.\n *\n * Note that `prdFile` is the requirements document (`prd.md`) and `tasksFile`\n * is the task plan (`tasks.json`) — unlike `ResolvedPaths.prdFile` in\n * `types.ts`, which historically points at `tasks.json`.\n */\nexport interface IssuePaths {\n issueDir: string;\n issueFile: string;\n metadataFile: string;\n prdFile: string;\n tasksFile: string;\n progressFile: string;\n analysisFile: string;\n sessionFile: string;\n lastBranchFile: string;\n archiveDir: string;\n prReviewDir: string;\n}\n\n/**\n * Issue identifiers become path segments, so anything that could escape the\n * issues directory is rejected before it reaches `join` — the same discipline\n * as `normalizeId()` in `issues/providers/local.ts`.\n *\n * Non-numeric identifiers (`auth-refactor`, `pr-184`) are first-class here:\n * the local provider already supports them, so the global storage must too.\n */\nfunction normalizeIssueNumber(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n\n if (normalized.length === 0) {\n throw new Error('Issue identifier cannot be empty');\n }\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n\n return normalized;\n}\n\n/**\n * Resolve every path of a single issue under the global storage.\n *\n * This is the only place allowed to know the layout of an issue directory:\n * call sites ask for the artifact they need instead of joining names by hand,\n * so renaming or adding an artifact stays a one-file change.\n *\n * Pure and synchronous: nothing here creates directories or touches the\n * filesystem.\n */\nexport function getIssuePaths(\n projectId: string,\n issueNumber: string | number,\n options: GetGlobalRootOptions = {},\n): IssuePaths {\n const issueDir = join(\n getProjectDir(projectId, options),\n ISSUES_DIR_NAME,\n normalizeIssueNumber(issueNumber),\n );\n\n return {\n issueDir,\n issueFile: join(issueDir, 'issue.md'),\n metadataFile: join(issueDir, 'metadata.json'),\n prdFile: join(issueDir, 'prd.md'),\n tasksFile: join(issueDir, 'tasks.json'),\n progressFile: join(issueDir, 'progress.txt'),\n analysisFile: join(issueDir, 'analysis.md'),\n sessionFile: join(issueDir, 'session.json'),\n lastBranchFile: join(issueDir, '.last-branch'),\n archiveDir: join(issueDir, 'archive'),\n prReviewDir: join(issueDir, 'pr-review'),\n };\n}\n","import { type Options as ExecaOptions, execa } from 'execa';\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Execute a command with arguments and capture its output.\n * Uses execa which calls execFile internally (no shell injection risk).\n * Does not throw on non-zero exit codes.\n */\nexport async function run(\n command: string,\n args: string[] = [],\n options?: ExecaOptions,\n): Promise<ExecResult> {\n const result = await execa(command, args, {\n reject: false,\n ...options,\n });\n\n return {\n stdout: result.stdout?.toString() ?? '',\n stderr: result.stderr?.toString() ?? '',\n exitCode: result.exitCode ?? 1,\n };\n}\n","import { type ExecResult, run } from './shell.js';\n\n/**\n * Get the root directory of the current git repository.\n * Throws if not inside a git repository.\n */\nexport async function getProjectRoot(): Promise<string> {\n const result = await run('git', ['rev-parse', '--show-toplevel']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Not inside a git repository. Please run issue-flow from within a git project.',\n );\n }\n\n return result.stdout.trim();\n}\n\n/**\n * Get the current git branch name.\n * Returns an empty string if in detached HEAD state.\n */\nexport async function getCurrentBranch(): Promise<string> {\n const result = await run('git', ['branch', '--show-current']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Failed to detect git branch. Ensure git is installed and you are inside a repository.',\n );\n }\n\n return result.stdout.trim();\n}\n\nexport interface CommitInfo {\n hash: string;\n subject: string;\n}\n\n/**\n * Get the base branch of the repository: the remote HEAD (origin/HEAD) when\n * available, otherwise the first of main/master that exists locally.\n * Never throws; defaults to 'main'.\n */\nexport async function getBaseBranch(): Promise<string> {\n const remoteHead = await run('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);\n if (remoteHead.exitCode === 0) {\n const name = remoteHead.stdout.trim().replace(/^origin\\//, '');\n if (name) return name;\n }\n\n for (const candidate of ['main', 'master']) {\n const check = await run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${candidate}`]);\n if (check.exitCode === 0) return candidate;\n }\n\n return 'main';\n}\n\n/**\n * Get the URL of the `origin` remote.\n * Never throws: a missing remote, a non-zero exit code, an empty stdout or a\n * git binary that cannot even be spawned all resolve to `null` — the same\n * discipline as {@link getBaseBranch}, so callers can treat \"no remote\" as an\n * ordinary state instead of an error path.\n *\n * `cwd` selects which repository is queried; omitting it falls back to\n * `process.cwd()`, which is almost never what a caller resolving a specific\n * `projectRoot` wants — pass it explicitly whenever one is available.\n */\nexport async function getRemoteUrl(cwd?: string): Promise<string | null> {\n let result: ExecResult;\n try {\n result = await run('git', ['remote', 'get-url', 'origin'], { cwd });\n } catch {\n return null;\n }\n\n if (result.exitCode !== 0) return null;\n\n const url = result.stdout.trim();\n return url === '' ? null : url;\n}\n\n/**\n * Get the abbreviated hash of the current HEAD commit.\n * Never throws, same discipline as {@link getRemoteUrl}: a repository with no\n * commits yet, a non-zero exit code, an empty stdout or a git binary that\n * cannot be spawned all resolve to `null`, so callers can treat \"no HEAD\" as\n * an ordinary state instead of an error path.\n *\n * `cwd` selects which repository is queried; omitting it falls back to\n * `process.cwd()`.\n */\nexport async function getHeadCommit(cwd?: string): Promise<string | null> {\n let result: ExecResult;\n try {\n result = await run('git', ['rev-parse', '--short', 'HEAD'], { cwd });\n } catch {\n return null;\n }\n\n if (result.exitCode !== 0) return null;\n\n const hash = result.stdout.trim();\n return hash === '' ? null : hash;\n}\n\n/**\n * Reduce a git remote URL to a canonical `host/path` identity, so that every\n * way of addressing the same repository collapses to the same string:\n *\n * ```\n * https://github.com/org/repo.git -> github.com/org/repo\n * git@github.com:org/repo.git -> github.com/org/repo\n * ssh://git@github.com:22/org/repo.git -> github.com/org/repo\n * https://user:token@github.com/org/repo -> github.com/org/repo\n * https://github.com/Org/Repo/ -> github.com/org/repo\n * ```\n *\n * Pure function: it never shells out, so it is testable without a git\n * repository. Returns `null` when no host and path can be extracted.\n *\n * Known limitation: both host and path are lowercased. Hostnames are\n * case-insensitive by definition, but repository paths are case-sensitive on\n * some self-hosted servers — so `host/org/Repo` and `host/org/repo` normalize\n * to the same identity even if that server considers them distinct. This is a\n * deliberate trade: matching the same repository across clones (where users\n * routinely type the wrong case) matters more than telling apart two repos\n * whose paths differ only by case.\n */\nexport function normalizeRemoteUrl(url: string | null | undefined): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (trimmed === '') return null;\n\n let authority: string;\n let path: string;\n\n const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.exec(trimmed);\n if (scheme) {\n const rest = trimmed.slice(scheme[0].length);\n const slash = rest.indexOf('/');\n if (slash === -1) return null;\n authority = rest.slice(0, slash);\n path = rest.slice(slash + 1);\n } else {\n // scp-like syntax: [user@]host:path — everything after the first colon is\n // the path, which is how git itself reads it (no port is possible here).\n const colon = trimmed.indexOf(':');\n if (colon === -1) return null;\n authority = trimmed.slice(0, colon);\n path = trimmed.slice(colon + 1);\n }\n\n // Drop embedded credentials (user, user:token) and the ssh user.\n const at = authority.lastIndexOf('@');\n if (at !== -1) authority = authority.slice(at + 1);\n\n const host = authority.replace(/:\\d+$/, '').toLowerCase();\n if (host === '') return null;\n\n const normalizedPath = path\n .replace(/[?#].*$/, '')\n .replace(/\\/+/g, '/')\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\.git$/i, '')\n .replace(/\\/+$/, '')\n .toLowerCase();\n if (normalizedPath === '') return null;\n\n return `${host}/${normalizedPath}`;\n}\n\n/**\n * Remove Basic-Auth-style credentials (`user:token@`, `user@`) embedded in an\n * `http(s)` remote URL, without touching anything else about it — case, port,\n * the `.git` suffix, query string and trailing slashes are all preserved.\n *\n * Deliberately scoped to `http`/`https`: that is the only place a git remote\n * can carry a real secret (a PAT is routinely embedded as\n * `https://x-access-token:TOKEN@host/...` by CI and automation), and the URL\n * stays a valid, clonable remote once the userinfo is gone. SSH has no\n * equivalent syntax — both `ssh://user@host/path` and the scp-like shorthand\n * `user@host:path` require that user segment to connect at all (it is almost\n * always the fixed, non-secret `git` service account), so this function\n * leaves any other scheme, the scp-like form and local paths untouched.\n *\n * Unlike {@link normalizeRemoteUrl}, which collapses a URL to a lossy\n * `host/path` identity, this keeps the URL \"as configured\" — it exists for\n * surfaces (like the web monitor's `repository.remoteUrl`) that want to show\n * the real remote, minus whatever secret an HTTPS token commonly embeds in\n * it.\n *\n * Pure function, never throws. Returns `null` for a nullish or empty input.\n */\nexport function stripRemoteUrlCredentials(url: string | null | undefined): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (trimmed === '') return null;\n\n const httpScheme = /^https?:\\/\\//i.exec(trimmed);\n if (!httpScheme) return trimmed;\n\n const rest = trimmed.slice(httpScheme[0].length);\n const slash = rest.indexOf('/');\n const authority = slash === -1 ? rest : rest.slice(0, slash);\n const remainder = slash === -1 ? '' : rest.slice(slash);\n\n const at = authority.lastIndexOf('@');\n if (at === -1) return trimmed;\n return `${httpScheme[0]}${authority.slice(at + 1)}${remainder}`;\n}\n\n/**\n * List commits on HEAD that are not on the given base branch (base..HEAD),\n * most recent first. Never throws; returns [] when the range cannot be\n * resolved (e.g. unknown base branch or shallow clone).\n */\nexport async function getCommitsSince(base: string): Promise<CommitInfo[]> {\n const result = await run('git', ['log', '--pretty=format:%h%x09%s', `${base}..HEAD`]);\n if (result.exitCode !== 0) return [];\n\n return result.stdout\n .split('\\n')\n .filter((line) => line.trim() !== '')\n .map((line) => {\n const tab = line.indexOf('\\t');\n return tab === -1\n ? { hash: line.trim(), subject: '' }\n : { hash: line.slice(0, tab), subject: line.slice(tab + 1) };\n });\n}\n","import { stat } from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport { printInfo } from '../ui/logger.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport { type MigrationResult, migrateLegacyStorage, resolveStorageMode } from './compat.js';\nimport {\n type GetGlobalRootOptions,\n getGlobalRoot,\n getIssuePaths,\n getProjectDir,\n ISSUES_DIR_NAME,\n type IssuePaths,\n} from './paths.js';\n\n/**\n * The single entry point every pipeline command uses to learn where an issue's\n * artifacts live.\n *\n * `paths.ts` and `compat.ts` stay pure and explicit — they take a `projectId` or\n * a `projectRoot` and never decide anything on their own. This module is the\n * opposite end: it knows the current repository, resolves the storage mode,\n * triggers the legacy migration when one is due, and caches all of that for the\n * lifetime of the process, so a call site is reduced to one line.\n *\n * It deliberately does **not** create the issue directory. Callers that write\n * keep doing their own `mkdir(..., { recursive: true })`, which preserves the\n * rule in `storage/CLAUDE.md` that path resolution never touches the\n * filesystem's shape.\n */\n\nexport interface ResolveIssuePathsOptions extends GetGlobalRootOptions {\n /**\n * Repository root. Defaults to {@link getProjectRoot}, i.e. the git toplevel\n * of the current working directory — which is what makes the result identical\n * from the repo root and from any subdirectory.\n */\n projectRoot?: string;\n}\n\n/** Everything the process needs to know about a project, resolved once. */\ninterface ProjectResolution {\n projectId: string;\n legacyDir: string;\n}\n\n/**\n * Cached project resolutions, keyed by `<globalRoot>::<projectRoot>`.\n *\n * `projectRoot` is the identity of the project, but the global root takes part\n * in the key as well: `ISSUE_FLOW_HOME` decides *where* the resolution lands,\n * so two different roots must never share an entry (that would silently hand a\n * test the previous test's tree).\n *\n * The cached value is the in-flight promise rather than its result, so two\n * commands resolving concurrently share a single git call instead of racing\n * into two migrations.\n */\nconst projectCache = new Map<string, Promise<ProjectResolution>>();\n\n/**\n * Issues whose global directory has already been checked in this process, so\n * the per-issue fallback below stats each issue at most once.\n */\nconst checkedIssues = new Set<string>();\n\n/**\n * Drop every cached resolution.\n *\n * Exported for tests: the cache is keyed by project and global root, so a suite\n * that moves `ISSUE_FLOW_HOME` between cases must reset it in `beforeEach`.\n */\nexport function resetStorageResolutionCache(): void {\n projectCache.clear();\n checkedIssues.clear();\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Tell the user that their artifacts moved, and that nothing was taken away.\n *\n * A migration that copied nothing stays silent: `migrateLegacyStorage` is\n * called speculatively (once per project, then once per issue seen for the\n * first time), and every one of those runs after the first copies zero files.\n * Printing on those would turn a one-time notice into noise on every command.\n *\n * The \"one notice per process\" guarantee falls out of the US-001 cache: the\n * project-level migration runs once, and the per-issue fallback only reaches a\n * second migration when an issue really is still legacy-only — in which case a\n * notice is the correct output, not a duplicate.\n */\nfunction announceMigration(result: MigrationResult): void {\n if (result.copied.length === 0) return;\n\n const count = result.copied.length;\n const target = join(result.globalDir, ISSUES_DIR_NAME);\n\n printInfo(\n `Migrated ${count} file${count === 1 ? '' : 's'} from ${result.legacyDir} to ${target}`,\n );\n printInfo(\n `The legacy directory was not modified or removed — ${result.legacyDir} is kept as-is, read-only, for compatibility.`,\n );\n}\n\n/**\n * Resolve the project id, migrating the legacy tree when it is the only thing\n * that exists yet.\n *\n * A migration failure is not swallowed: `migrateLegacyStorage` already names the\n * issue and file that failed and states that the legacy directory was left\n * untouched, which is exactly what the user needs to fix it and retry.\n */\nasync function resolveProject(\n projectRoot: string,\n options: GetGlobalRootOptions,\n): Promise<ProjectResolution> {\n // resolveStorageMode returns the project id *and* the remote in a single git\n // call — calling getProjectId() afterwards would shell out to\n // `git remote get-url origin` a second time for the same answer.\n const status = await resolveStorageMode(projectRoot, options);\n\n if (status.mode === 'needs-migration') {\n announceMigration(await migrateLegacyStorage(projectRoot, options));\n }\n\n return {\n projectId: status.projectId,\n legacyDir: status.legacyDir,\n };\n}\n\n/** Cached half of {@link resolveIssuePaths}: one git call per project, per process. */\nfunction getProjectResolution(\n projectRoot: string,\n options: GetGlobalRootOptions,\n): Promise<ProjectResolution> {\n const key = `${getGlobalRoot(options)}::${projectRoot}`;\n\n const cached = projectCache.get(key);\n if (cached) return cached;\n\n // A rejected resolution is evicted so the next command retries instead of\n // inheriting a permanently failed promise.\n const pending = resolveProject(projectRoot, options).catch((err) => {\n projectCache.delete(key);\n throw err;\n });\n\n projectCache.set(key, pending);\n return pending;\n}\n\n/** Project-level directories of the current repository inside the global tree. */\nexport interface ProjectStoragePaths {\n /** Deterministic id derived from the repository's remote (or its path). */\n projectId: string;\n /** `<globalRoot>/projects/<projectId>`. */\n projectDir: string;\n /** `<projectDir>/issues` — the parent of every issue directory. */\n issuesDir: string;\n}\n\n/**\n * Resolve the project-level directories, without naming an issue.\n *\n * The issue-agnostic half of {@link resolveIssuePaths}, for the two questions\n * that are about the project rather than about one issue: \"can I write here?\"\n * (`LocalFileIssueProvider.isAvailable`) and \"which identifiers are taken?\"\n * (`highestLocalNumber`, which walks `issuesDir`). It shares the same cache, so\n * asking here and then resolving an issue still costs a single git call.\n *\n * Like the rest of this module it creates nothing — a caller that needs the\n * directory to exist does its own `mkdir(..., { recursive: true })`.\n */\nexport async function resolveProjectPaths(\n options: ResolveIssuePathsOptions = {},\n): Promise<ProjectStoragePaths> {\n const { projectRoot: projectRootOption, ...rootOptions } = options;\n const projectRoot = resolve(projectRootOption ?? (await getProjectRoot()));\n\n const project = await getProjectResolution(projectRoot, rootOptions);\n const projectDir = getProjectDir(project.projectId, rootOptions);\n\n return {\n projectId: project.projectId,\n projectDir,\n issuesDir: join(projectDir, ISSUES_DIR_NAME),\n };\n}\n\n/**\n * Resolve every path of `issueNumber` under the global storage, migrating the\n * legacy `<projectRoot>/issues/` tree first when needed.\n *\n * The migration is triggered in two situations:\n *\n * 1. project level — the global project directory does not exist at all and the\n * legacy one does (`mode === 'needs-migration'`);\n * 2. issue level — the project directory exists (so the mode is already\n * `global`) but *this* issue is only present in the legacy tree. That\n * happens whenever a project was migrated before an older issue was ever\n * read, or when a collaborator on a previous version added an issue to the\n * legacy directory afterwards. Re-running the migration is cheap and safe:\n * it skips every destination that already exists.\n */\nexport async function resolveIssuePaths(\n issueNumber: string | number,\n options: ResolveIssuePathsOptions = {},\n): Promise<IssuePaths> {\n const { projectRoot: projectRootOption, ...rootOptions } = options;\n const projectRoot = resolve(projectRootOption ?? (await getProjectRoot()));\n\n const project = await getProjectResolution(projectRoot, rootOptions);\n const paths = getIssuePaths(project.projectId, issueNumber, rootOptions);\n\n if (!checkedIssues.has(paths.issueDir)) {\n // `getIssuePaths` already validated and normalized the identifier, so the\n // last segment of issueDir is the safe form to look for under the legacy\n // tree.\n const legacyIssueDir = join(project.legacyDir, basename(paths.issueDir));\n\n if (!(await directoryExists(paths.issueDir)) && (await directoryExists(legacyIssueDir))) {\n announceMigration(await migrateLegacyStorage(projectRoot, rootOptions));\n }\n\n // Marked only once the check completed: a failed migration must be retried\n // by the next command, not remembered as done.\n checkedIssues.add(paths.issueDir);\n }\n\n return paths;\n}\n","import type { Dirent } from 'node:fs';\nimport { copyFile, mkdir, readdir, readFile, stat } from 'node:fs/promises';\nimport { join, resolve, sep } from 'node:path';\nimport { isoNow } from '../core/state-manager.js';\nimport { writeFileAtomic } from '../utils/fs.js';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\nimport {\n type GetGlobalRootOptions,\n getProjectDir,\n ISSUES_DIR_NAME,\n projectIdFromRemote,\n} from './paths.js';\nimport { type ProjectMetadata, projectMetadataSchema, STORAGE_SCHEMA_VERSION } from './schemas.js';\n\n/**\n * Coexistence between the legacy per-project layout (`<projectRoot>/issues/`)\n * and the global storage tree (`~/.issue-flow/projects/<project-id>/`).\n *\n * The single rule of this module: **the legacy directory is read-only.** Nothing\n * here removes or rewrites a byte under `<projectRoot>/issues/` — not even\n * behind an opt-in flag — so adopting the global storage can never cost a user\n * their history. Migration is a copy, and a copy that refuses to overwrite.\n */\n\n/** Directory holding the legacy per-project artifacts. */\nexport const LEGACY_ISSUES_DIR_NAME = 'issues';\n\n/** File holding the per-project metadata inside the global storage. */\nexport const METADATA_FILENAME = 'metadata.json';\n\n/**\n * - `global`: the global directory is the source of truth (it already exists,\n * or neither layout does and a fresh project starts there).\n * - `needs-migration`: only the legacy directory exists, so its content has not\n * been copied over yet.\n */\nexport type StorageMode = 'global' | 'needs-migration';\n\nexport interface StorageStatus {\n mode: StorageMode;\n /** Deterministic id, derived from {@link projectIdFromRemote}. */\n projectId: string;\n /** `<globalRoot>/projects/<projectId>`. */\n globalDir: string;\n /** `<projectRoot>/issues`. */\n legacyDir: string;\n /** Whether `globalDir` currently exists on disk. */\n globalExists: boolean;\n /** Whether `legacyDir` currently exists on disk. */\n legacyExists: boolean;\n /**\n * Normalized `origin` remote of `projectRoot`, or `null` without one.\n * Resolved once here so `migrateLegacyStorage` can reuse it instead of\n * shelling out to git a second time for `metadata.json`.\n */\n remoteUrl: string | null;\n}\n\nexport interface MigrationResult {\n projectId: string;\n globalDir: string;\n legacyDir: string;\n /** Mode observed *before* the migration ran. */\n previousMode: StorageMode;\n /** Paths copied, relative to `legacyDir`. */\n copied: string[];\n /** Paths left alone because the destination already had a file. */\n skipped: string[];\n /** Absolute path of the metadata file written by this run. */\n metadataFile: string;\n /** Metadata as persisted. */\n metadata: ProjectMetadata;\n}\n\nexport interface MigrateLegacyStorageOptions extends GetGlobalRootOptions {\n /** Timestamp source, injectable so tests can assert createdAt/updatedAt. */\n now?: () => string;\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Describe where a project's artifacts live, without touching anything.\n *\n * Read-only by construction: it stats the two candidate directories and reports\n * what it found. It never creates a directory, so a caller can ask the question\n * (a status command, a warning banner) without committing the user to the new\n * layout.\n *\n * An existing global directory always wins: once artifacts are there, that is\n * the source of truth, and a legacy directory left behind is simply preserved\n * rather than re-read or re-migrated.\n */\nexport async function resolveStorageMode(\n projectRoot: string,\n options: GetGlobalRootOptions = {},\n): Promise<StorageStatus> {\n const remoteUrl = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n const projectId = projectIdFromRemote(remoteUrl, projectRoot);\n const globalDir = getProjectDir(projectId, options);\n const legacyDir = join(resolve(projectRoot), LEGACY_ISSUES_DIR_NAME);\n\n const [globalExists, legacyExists] = await Promise.all([\n directoryExists(globalDir),\n directoryExists(legacyDir),\n ]);\n\n return {\n // Only one state calls for work: legacy content that has nowhere to be read\n // from yet. Everything else — including \"neither exists\" — is already the\n // global layout.\n mode: legacyExists && !globalExists ? 'needs-migration' : 'global',\n projectId,\n globalDir,\n legacyDir,\n globalExists,\n legacyExists,\n remoteUrl,\n };\n}\n\n/**\n * Wrap an IO failure with the issue and file it happened on, so a partial\n * migration is diagnosable instead of a bare `EACCES`.\n */\nfunction migrationFailure(issue: string, relativePath: string, err: unknown): Error {\n const reason = err instanceof Error ? err.message : String(err);\n\n return new Error(\n `Failed to migrate '${relativePath}' (issue '${issue}'): ${reason}. ` +\n 'The legacy directory was left untouched — fix the cause and run the migration again.',\n );\n}\n\n/** First path segment of a relative path — the issue a file belongs to. */\nfunction issueOf(relativePath: string): string {\n return relativePath.split(sep)[0] || '.';\n}\n\n/**\n * Copy one directory level, recursing into subdirectories.\n *\n * Symlinks and other non-regular entries are skipped on purpose: following them\n * could copy content from outside the legacy directory into the global tree.\n */\nasync function copyTree(\n sourceRoot: string,\n targetRoot: string,\n relativeDir: string,\n result: { copied: string[]; skipped: string[] },\n): Promise<void> {\n const source = join(sourceRoot, relativeDir);\n const target = join(targetRoot, relativeDir);\n\n let entries: Dirent[];\n try {\n entries = await readdir(source, { withFileTypes: true });\n } catch (err) {\n throw migrationFailure(issueOf(relativeDir), relativeDir || '.', err);\n }\n\n try {\n // recursive: true also makes re-running a no-op on an existing directory.\n await mkdir(target, { recursive: true });\n } catch (err) {\n throw migrationFailure(issueOf(relativeDir), relativeDir || '.', err);\n }\n\n for (const entry of entries) {\n const relativePath = join(relativeDir, entry.name);\n\n if (entry.isDirectory()) {\n await copyTree(sourceRoot, targetRoot, relativePath, result);\n continue;\n }\n if (!entry.isFile()) {\n continue;\n }\n\n const to = join(targetRoot, relativePath);\n try {\n // Idempotency and safety in one check: a destination that already exists\n // is never overwritten, so a second run copies nothing and a hand-edited\n // artifact in the global tree survives.\n if (await pathExists(to)) {\n result.skipped.push(relativePath);\n continue;\n }\n await copyFile(join(sourceRoot, relativePath), to);\n result.copied.push(relativePath);\n } catch (err) {\n throw migrationFailure(issueOf(relativePath), relativePath, err);\n }\n }\n}\n\n/**\n * Read `metadata.json`, tolerating every failure.\n *\n * A missing, corrupt or schema-invalid file must not block a migration: the\n * caller simply writes a fresh one. The only thing worth recovering is\n * `createdAt`/`lastAttemptAt` from a valid previous file.\n */\nasync function readExistingMetadata(file: string): Promise<ProjectMetadata | null> {\n let raw: string;\n try {\n raw = await readFile(file, 'utf-8');\n } catch {\n return null;\n }\n\n try {\n const parsed = projectMetadataSchema.safeParse(JSON.parse(raw));\n return parsed.success ? parsed.data : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Copy `<projectRoot>/issues/` into the project's global directory and stamp\n * its `metadata.json`.\n *\n * Non-destructive and idempotent, by design rather than by convention:\n *\n * - the source is only ever read — there is no removal option in this release;\n * - a destination file that already exists is skipped, never overwritten, so a\n * second run reports everything as skipped and changes nothing but\n * `updatedAt`;\n * - a failure mid-way leaves the legacy directory intact and names the issue\n * and file that failed, so the run can be repeated after the fix and will\n * resume by skipping what already made it across.\n *\n * Called on a project with no legacy directory it is still useful: it creates\n * the project directory and its metadata.\n */\nexport async function migrateLegacyStorage(\n projectRoot: string,\n options: MigrateLegacyStorageOptions = {},\n): Promise<MigrationResult> {\n const status = await resolveStorageMode(projectRoot, { env: options.env });\n const result = { copied: [] as string[], skipped: [] as string[] };\n\n try {\n await mkdir(status.globalDir, { recursive: true });\n } catch (err) {\n throw migrationFailure('.', status.globalDir, err);\n }\n\n if (status.legacyExists) {\n await copyTree(status.legacyDir, join(status.globalDir, ISSUES_DIR_NAME), '', result);\n }\n\n const now = (options.now ?? isoNow)();\n const metadataFile = join(status.globalDir, METADATA_FILENAME);\n const existing = await readExistingMetadata(metadataFile);\n\n const metadata: ProjectMetadata = {\n schemaVersion: STORAGE_SCHEMA_VERSION,\n projectId: status.projectId,\n root: resolve(projectRoot),\n remoteUrl: status.remoteUrl,\n // The project was first seen whenever it was first written, not now.\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n lastAttemptAt: existing?.lastAttemptAt ?? null,\n };\n\n await writeFileAtomic(metadataFile, `${JSON.stringify(metadata, null, 2)}\\n`);\n\n return {\n projectId: status.projectId,\n globalDir: status.globalDir,\n legacyDir: status.legacyDir,\n previousMode: status.mode,\n ...result,\n metadataFile,\n metadata,\n };\n}\n","import { z } from 'zod';\nimport { webConfigSchema } from '../schemas.js';\n\n/**\n * Zod schemas for the files written under the global storage tree\n * (`~/.issue-flow`): the per-project `metadata.json` and the global\n * `config.json`.\n *\n * They live here rather than in `src/schemas.ts` so that file stays focused on\n * the pipeline domain (task plans, Issue metadata, session snapshots) and the\n * storage layer keeps owning its own formats.\n */\n\n/** Version stamped on files written by this release of the storage layer. */\nexport const STORAGE_SCHEMA_VERSION = 1;\n\n/**\n * `~/.issue-flow/projects/<project-id>/metadata.json`.\n *\n * Deliberately **not** `.strict()`: a newer release may add fields (dashboard\n * history, counters) and an older one must still be able to read the file\n * instead of rejecting it. Unknown keys are dropped on parse, never fatal.\n *\n * `root` is the last known local checkout — informative only. Identity lives in\n * `projectId` (see `getProjectId()` in `paths.ts`), so moving the folder of a\n * project that has a remote updates `root` without changing the id.\n */\nexport const projectMetadataSchema = z.object({\n schemaVersion: z.number().int().positive(),\n projectId: z.string().min(1),\n root: z.string().min(1),\n /** Normalized remote (`host/org/repo`), or null when the project has none. */\n remoteUrl: z.string().nullable(),\n createdAt: z.string().min(1),\n updatedAt: z.string().min(1),\n /** Null until the project is used by a pipeline run. */\n lastAttemptAt: z.string().nullable(),\n});\n\n/**\n * Global counterpart of the `web` key of `.issue-flow.json`, restricted to the\n * settings that make sense machine-wide: `enabled` and `includeLogs` stay a\n * per-project decision.\n *\n * The validation of each field is reused from `webConfigSchema` through\n * `.unwrap()`, which strips the `.default()` and keeps the constraints. Picking\n * and calling `.partial()` would not be enough: in zod 4 an optional field\n * still materializes its default when the key is absent\n * (`z.object({ port: z.number().default(3737) }).partial().parse({})` yields\n * `{ port: 3737 }`), and a materialized default in this layer would override\n * the project layer during the merge.\n */\nconst webShape = webConfigSchema.shape;\nexport const globalWebConfigSchema = z\n .object({\n port: webShape.port.unwrap(),\n host: webShape.host.unwrap(),\n refreshSeconds: webShape.refreshSeconds.unwrap(),\n logLimit: webShape.logLimit.unwrap(),\n })\n .partial();\n\n/** Global retry preferences, mirroring `DEFAULTS` in `config.ts`. */\nexport const globalRetryConfigSchema = z\n .object({\n retryLimit: z.number().int().nonnegative(),\n retryForever: z.boolean(),\n backoffBaseSeconds: z.number().nonnegative(),\n backoffMaxSeconds: z.number().nonnegative(),\n })\n .partial();\n\n/** Global commit preferences. Consumed by later issues, not by this release. */\nexport const globalCommitConfigSchema = z\n .object({\n signoff: z.boolean(),\n conventional: z.boolean(),\n })\n .partial();\n\n/**\n * `~/.issue-flow/config.json`.\n *\n * **Every key is optional and no key carries a `.default()`.** This is a design\n * constraint, not an omission: the global file is an intermediate precedence\n * layer (CLI > env > `.issue-flow.json` > `config.json` > defaults), so a\n * default materialized here would be indistinguishable from a value the user\n * actually wrote and would silently override the project layer during the\n * merge. Same reason `readWebConfigFile()` parses with\n * `webConfigSchema.partial()`. Defaults belong to the final layer only.\n */\nexport const globalConfigSchema = z\n .object({\n schemaVersion: z.number().int().positive(),\n /** Override for the directory holding `projects/`. */\n storageDir: z.string().min(1),\n web: globalWebConfigSchema,\n retry: globalRetryConfigSchema,\n commit: globalCommitConfigSchema,\n })\n .partial();\n\nexport type ProjectMetadata = z.infer<typeof projectMetadataSchema>;\nexport type GlobalWebConfig = z.infer<typeof globalWebConfigSchema>;\nexport type GlobalRetryConfig = z.infer<typeof globalRetryConfigSchema>;\nexport type GlobalCommitConfig = z.infer<typeof globalCommitConfigSchema>;\nexport type GlobalConfig = z.infer<typeof globalConfigSchema>;\n"],"mappings":"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","names":["readFile","join","stat","basename","join","resolve","join","resolve","join","resolve","directoryExists","stat","join","resolve","basename","join","readFile"]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/storage/paths.ts","../src/utils/shell.ts","../src/utils/git.ts","../src/storage/resolve.ts","../src/storage/compat.ts","../src/storage/schemas.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { platform } from 'node:os';\nimport { join } from 'node:path';\nimport type { IssuesConfig } from './issues/types.js';\nimport {\n issuesConfigSchema,\n type PrReviewConfig,\n prReviewConfigSchema,\n type WebConfig,\n webConfigSchema,\n} from './schemas.js';\nimport { getGlobalRoot } from './storage/paths.js';\nimport { resolveIssuePaths } from './storage/resolve.js';\nimport { type GlobalConfig, globalConfigSchema } from './storage/schemas.js';\nimport type { EngineConfig, ResolvedPaths } from './types.js';\nimport { printWarning } from './ui/logger.js';\nimport { getProjectRoot } from './utils/git.js';\nimport { run } from './utils/shell.js';\n\n/**\n * Default configuration values — matching the Bash script exactly.\n */\nexport const DEFAULTS = {\n retryLimit: 10,\n retryForever: false,\n backoffBaseSeconds: 30,\n backoffMaxSeconds: 900,\n} as const;\n\n/**\n * Create a EngineConfig with defaults merged with provided options.\n */\nexport function createConfig(options: Partial<EngineConfig>): EngineConfig {\n return {\n issueNumber: options.issueNumber,\n maxIterations: options.maxIterations,\n retryLimit: options.retryLimit ?? DEFAULTS.retryLimit,\n retryForever: options.retryForever ?? DEFAULTS.retryForever,\n backoffBaseSeconds: options.backoffBaseSeconds ?? DEFAULTS.backoffBaseSeconds,\n backoffMaxSeconds: options.backoffMaxSeconds ?? DEFAULTS.backoffMaxSeconds,\n };\n}\n\n/**\n * Resolve file paths based on issue number and project root.\n *\n * With --issue N, every artifact comes from the global storage layer via\n * `resolveIssuePaths()`, which also migrates the legacy `<projectRoot>/issues/`\n * tree on first read:\n * prdFile = ~/.issue-flow/projects/{id}/issues/{N}/tasks.json\n * progressFile = ~/.issue-flow/projects/{id}/issues/{N}/progress.txt\n *\n * Standalone:\n * prdFile = {projectRoot}/prd.json\n * progressFile = {projectRoot}/progress.txt\n *\n * Beware of the asymmetric mapping in the issue branch: `ResolvedPaths.prdFile`\n * is the engine's *task plan*, so it maps to `IssuePaths.tasksFile`\n * (`tasks.json`) and **not** to `IssuePaths.prdFile` (`prd.md`, the human-facing\n * document produced by the `prd` phase). The name predates the split and is kept\n * because standalone mode really does read a `prd.json`.\n *\n * `projectRoot` stays on the result either way: `core/engine.ts` uses it as the\n * cwd of its git operations, which the global storage does not replace.\n */\nexport async function resolvePaths(\n config: EngineConfig,\n scriptDir?: string,\n): Promise<ResolvedPaths> {\n const projectRoot = await getProjectRoot();\n\n if (config.issueNumber) {\n // projectRoot is forwarded so the resolver does not shell out to\n // `git rev-parse --show-toplevel` a second time for the answer we just got.\n const issuePaths = await resolveIssuePaths(config.issueNumber, { projectRoot });\n return {\n prdFile: issuePaths.tasksFile,\n progressFile: issuePaths.progressFile,\n archiveDir: issuePaths.archiveDir,\n lastBranchFile: issuePaths.lastBranchFile,\n projectRoot,\n };\n }\n\n // Standalone mode — use scriptDir if available, otherwise projectRoot\n const base = scriptDir ?? projectRoot;\n return {\n prdFile: join(base, 'prd.json'),\n progressFile: join(base, 'progress.txt'),\n archiveDir: join(base, 'archive'),\n lastBranchFile: join(base, '.last-branch'),\n projectRoot,\n };\n}\n\n/**\n * Return a platform-appropriate install hint for a given package.\n */\nexport function getInstallHint(pkg: string): string {\n const os = platform();\n\n if (os === 'darwin') {\n return `brew install ${pkg}`;\n }\n if (os === 'linux') {\n return `apt install ${pkg} (or your distro's package manager)`;\n }\n if (os === 'win32') {\n return `winget install ${pkg} (or choco install ${pkg})`;\n }\n\n return `install ${pkg} using your system package manager`;\n}\n\n/**\n * Validate that required external dependencies are available.\n * Returns an array of error messages (empty if all deps are found).\n */\nexport async function validateDependencies(): Promise<string[]> {\n const errors: string[] = [];\n\n // Check git\n const gitResult = await run('git', ['--version']);\n if (gitResult.exitCode !== 0) {\n errors.push(` - git (install with: ${getInstallHint('git')})`);\n }\n\n // Check claude\n const claudeResult = await run('claude', ['--version']);\n if (claudeResult.exitCode !== 0) {\n errors.push(' - claude (install with: npm install -g @anthropic-ai/claude-code)');\n }\n\n // Note: jq is NOT required — the TypeScript CLI handles JSON natively\n\n return errors;\n}\n\n// ── Per-project configuration file ──────────────────────────────────────────\n\n/** Optional per-project configuration file, read from the project root. */\nexport const PROJECT_CONFIG_FILENAME = '.issue-flow.json';\n\n/** Historical alias kept for the web monitoring call sites. */\nexport const WEB_CONFIG_FILENAME = PROJECT_CONFIG_FILENAME;\n\n/**\n * Read and parse .issue-flow.json from the project root.\n *\n * Never throws: an absent file, unreadable path, invalid JSON or a non-object\n * root all degrade to `null` (with a warning for the malformed cases), so every\n * consumer falls back to its own defaults.\n */\nasync function readProjectConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Record<string, unknown> | null> {\n let root = projectRoot;\n if (root === undefined) {\n try {\n root = await getProjectRoot();\n } catch {\n return null;\n }\n }\n\n const filePath = join(root, PROJECT_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch {\n // The file is entirely optional — absence is the common case.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: invalid JSON.`);\n return null;\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${PROJECT_CONFIG_FILENAME}: expected a JSON object.`);\n return null;\n }\n\n return parsed as Record<string, unknown>;\n}\n\n// ── Global configuration file ───────────────────────────────────────────────\n\n/** Machine-wide configuration file, read from the global storage root. */\nexport const GLOBAL_CONFIG_FILENAME = 'config.json';\n\nexport interface LoadGlobalConfigOptions {\n /** Environment source, forwarded to getGlobalRoot(). Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory holding config.json. Defaults to getGlobalRoot(). */\n globalRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\n/**\n * Read and parse `~/.issue-flow/config.json`, the preferences a user sets once\n * for every project.\n *\n * Never throws, exactly like readProjectConfigFile(): an absent file, an\n * unreadable path, invalid JSON, a non-object root or an invalid key all\n * degrade to \"no global preference\" so the caller falls back to the layers\n * below. Absence is silent — it is the common case; every other failure warns,\n * because a preference the user did write is being dropped.\n *\n * Validation happens key by key on purpose: a typo in `retry` must not cost the\n * user their `web` settings. Unknown keys are dropped without a warning, which\n * is what keeps a file written by a newer release readable here.\n */\nexport async function loadGlobalConfig(\n options: LoadGlobalConfigOptions = {},\n): Promise<GlobalConfig> {\n const warn = options.warn ?? printWarning;\n\n let root: string;\n try {\n root = options.globalRoot ?? getGlobalRoot({ env: options.env ?? process.env });\n } catch (err: unknown) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n return {};\n }\n\n const filePath = join(root, GLOBAL_CONFIG_FILENAME);\n let raw: string;\n try {\n raw = await readFile(filePath, 'utf-8');\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: ${(err as Error).message}`);\n }\n return {};\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: invalid JSON.`);\n return {};\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n warn(`Ignoring ${GLOBAL_CONFIG_FILENAME}: expected a JSON object.`);\n return {};\n }\n\n const config: GlobalConfig = {};\n for (const [key, value] of Object.entries(parsed)) {\n const result = globalConfigSchema.safeParse({ [key]: value });\n if (!result.success) {\n warn(\n `Ignoring \"${key}\" key of ${GLOBAL_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n continue;\n }\n // An unknown key parses successfully into an empty object and disappears\n // here, which is the retro-compatible behaviour we want.\n Object.assign(config, result.data);\n }\n\n return config;\n}\n\n/**\n * The configuration sources of the engine, ordered from the lowest to the\n * highest precedence:\n *\n * defaults < config.json (global) < .issue-flow.json (project) < env < CLI\n */\nexport interface ConfigLayers<T extends object> {\n /** Hard-coded fallbacks, e.g. DEFAULTS or the values baked into a schema. */\n defaults?: Partial<T>;\n /** ~/.issue-flow/config.json, via loadGlobalConfig(). */\n global?: Partial<T>;\n /** The matching key of .issue-flow.json in the project root. */\n project?: Partial<T>;\n /** ISSUE_FLOW_* environment variables. */\n env?: Partial<T>;\n /** CLI flags. */\n cli?: Partial<T>;\n}\n\n/**\n * Merge configuration layers following the documented precedence.\n *\n * Pure and shallow: a layer only participates with the keys it actually\n * carries, so an absent key never erases the layer below it. `undefined` counts\n * as absent — that is what lets a layer be built by assigning only the values\n * that were really provided.\n *\n * Because the merge is shallow, nested objects (`web`, `retry`) are replaced\n * whole rather than merged field by field; callers that need per-field\n * precedence inside a nested key must flatten it into its own merge.\n *\n * Caveat for the `project` layer: it must be the *raw* set of keys the user\n * wrote, not the output of a schema that materializes defaults. In zod 4 a\n * `.default()` survives `.partial()`, so parsing the project file with\n * `webConfigSchema.partial()` yields every default and would make the project\n * layer swallow the global one.\n */\nexport function mergeConfigLayers<T extends object>(layers: ConfigLayers<T>): Partial<T> {\n const merged: Record<string, unknown> = {};\n\n for (const layer of [layers.defaults, layers.global, layers.project, layers.env, layers.cli]) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (value !== undefined) {\n merged[key] = value;\n }\n }\n }\n\n return merged as Partial<T>;\n}\n\n// ── Web monitoring configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts. Highest-precedence\n * source consumed by loadWebConfig().\n */\nlet webCliOverrides: Partial<WebConfig> = {};\n\nexport function setWebCliOverrides(overrides: Partial<WebConfig>): void {\n webCliOverrides = overrides;\n}\n\nexport interface LoadWebConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setWebCliOverrides(). */\n cli?: Partial<WebConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nconst FALSY_ENV_VALUES = new Set(['', '0', 'false', 'no', 'off']);\n\nfunction parseBooleanEnv(value: string): boolean {\n return !FALSY_ENV_VALUES.has(value.trim().toLowerCase());\n}\n\nfunction readNumberEnv(\n env: NodeJS.ProcessEnv,\n name: string,\n warn: (message: string) => void,\n): number | undefined {\n const raw = env[name];\n if (raw === undefined) {\n return undefined;\n }\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) {\n warn(`Ignoring ${name}=\"${raw}\": not a number.`);\n return undefined;\n }\n return parsed;\n}\n\nfunction readWebConfigEnv(\n env: NodeJS.ProcessEnv,\n warn: (message: string) => void,\n): Partial<WebConfig> {\n const layer: Partial<WebConfig> = {};\n if (env.ISSUE_FLOW_WEB !== undefined) {\n layer.enabled = parseBooleanEnv(env.ISSUE_FLOW_WEB);\n }\n const port = readNumberEnv(env, 'ISSUE_FLOW_WEB_PORT', warn);\n if (port !== undefined) {\n layer.port = port;\n }\n if (env.ISSUE_FLOW_WEB_HOST !== undefined) {\n layer.host = env.ISSUE_FLOW_WEB_HOST;\n }\n const refresh = readNumberEnv(env, 'ISSUE_FLOW_WEB_REFRESH', warn);\n if (refresh !== undefined) {\n layer.refreshSeconds = refresh;\n }\n const logLimit = readNumberEnv(env, 'ISSUE_FLOW_WEB_LOG_LIMIT', warn);\n if (logLimit !== undefined) {\n layer.logLimit = logLimit;\n }\n return layer;\n}\n\nasync function readWebConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<WebConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const web = file?.web;\n if (web === undefined) {\n return {};\n }\n\n const result = webConfigSchema.partial().safeParse(web);\n if (!result.success) {\n warn(\n `Ignoring \"web\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the web monitoring configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning.\n */\nexport async function loadWebConfig(options: LoadWebConfigOptions = {}): Promise<WebConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? webCliOverrides;\n const env = options.env ?? process.env;\n\n const fileLayer = await readWebConfigFile(options.projectRoot, warn);\n const envLayer = readWebConfigEnv(env, warn);\n const merged = { ...fileLayer, ...envLayer, ...cli };\n\n const result = webConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid web monitoring configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return webConfigSchema.parse({});\n}\n\n// ── Issue provider configuration ────────────────────────────────────────────\n\n/**\n * CLI overrides captured by the preAction hook in cli.ts (--local, --github,\n * --prefer-local, --prefer-github, --ask). Highest-precedence source consumed\n * by loadIssuesConfig().\n */\nlet issuesCliOverrides: Partial<IssuesConfig> = {};\n\nexport function setIssuesCliOverrides(overrides: Partial<IssuesConfig>): void {\n issuesCliOverrides = overrides;\n}\n\nexport interface LoadIssuesConfigOptions {\n /** CLI flag overrides. Defaults to the values set via setIssuesCliOverrides(). */\n cli?: Partial<IssuesConfig>;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nasync function readIssuesConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<IssuesConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const issues = file?.issues;\n if (issues === undefined) {\n return {};\n }\n\n const result = issuesConfigSchema.partial().safeParse(issues);\n if (!result.success) {\n warn(\n `Ignoring \"issues\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the Issue provider configuration with the documented precedence:\n * CLI flag > .issue-flow.json > defaults.\n *\n * Never throws: missing or invalid sources degrade to the defaults with a\n * warning, and the defaults reproduce the GitHub-only behaviour.\n */\nexport async function loadIssuesConfig(\n options: LoadIssuesConfigOptions = {},\n): Promise<IssuesConfig> {\n const warn = options.warn ?? printWarning;\n const cli = options.cli ?? issuesCliOverrides;\n\n const fileLayer = await readIssuesConfigFile(options.projectRoot, warn);\n const merged = { ...fileLayer, ...cli };\n\n const result = issuesConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid Issue provider configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return issuesConfigSchema.parse({});\n}\n\n// ── PR review configuration ─────────────────────────────────────────────────\n\nexport interface LoadPrReviewConfigOptions {\n /** Highest-precedence layer, for a future `--publisher` flag. */\n cli?: Partial<PrReviewConfig>;\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n /** Directory containing .issue-flow.json. Defaults to the git project root. */\n projectRoot?: string;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\nfunction readPrReviewConfigEnv(env: NodeJS.ProcessEnv): Partial<PrReviewConfig> {\n const publisher = env.ISSUE_FLOW_PR_REVIEW_PUBLISHER;\n // An unknown value is not dropped here: it goes through the schema below, so\n // the user gets the same warning a bad config file would produce.\n return publisher === undefined ? {} : { publisher: publisher as PrReviewConfig['publisher'] };\n}\n\nasync function readPrReviewConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Partial<PrReviewConfig>> {\n const file = await readProjectConfigFile(projectRoot, warn);\n const prReview = file?.prReview;\n if (prReview === undefined) {\n return {};\n }\n\n const result = prReviewConfigSchema.partial().safeParse(prReview);\n if (!result.success) {\n warn(\n `Ignoring \"prReview\" key of ${PROJECT_CONFIG_FILENAME}: ${result.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n return {};\n }\n return result.data;\n}\n\n/**\n * Resolve the `pr-review` configuration with the documented precedence:\n * CLI flag > environment variable > .issue-flow.json > defaults.\n *\n * Never throws: an absent, malformed or unknown value degrades to the default\n * publisher with a warning, so a typo in .issue-flow.json costs a warning\n * rather than the review.\n */\nexport async function loadPrReviewConfig(\n options: LoadPrReviewConfigOptions = {},\n): Promise<PrReviewConfig> {\n const warn = options.warn ?? printWarning;\n const env = options.env ?? process.env;\n\n const fileLayer = await readPrReviewConfigFile(options.projectRoot, warn);\n const envLayer = readPrReviewConfigEnv(env);\n const merged = { ...fileLayer, ...envLayer, ...(options.cli ?? {}) };\n\n const result = prReviewConfigSchema.safeParse(merged);\n if (result.success) {\n return result.data;\n }\n warn(\n `Invalid PR review configuration (${result.error.issues[0]?.message ?? 'invalid value'}); using defaults.`,\n );\n return prReviewConfigSchema.parse({});\n}\n","import { createHash } from 'node:crypto';\nimport { homedir } from 'node:os';\nimport { basename, join, resolve } from 'node:path';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\n\n/** Directory created under the user's home when no override is provided. */\nexport const GLOBAL_DIR_NAME = '.issue-flow';\n\n/** Environment variable that relocates the whole global storage tree. */\nexport const GLOBAL_ROOT_ENV = 'ISSUE_FLOW_HOME';\n\n/** Directory under the global root holding one folder per project. */\nexport const PROJECTS_DIR_NAME = 'projects';\n\n/** Directory under a project holding one folder per issue. */\nexport const ISSUES_DIR_NAME = 'issues';\n\nexport interface GetGlobalRootOptions {\n /** Environment source. Defaults to process.env. */\n env?: NodeJS.ProcessEnv;\n}\n\n/**\n * Resolve the root directory of the global storage tree (`~/.issue-flow`).\n *\n * Every path under the global storage MUST be derived from this function: it is\n * the single seam where `ISSUE_FLOW_HOME` takes effect, which is what lets\n * tests, CI and sandboxes point the whole tree at a temporary directory instead\n * of touching the real `$HOME`. A call site that joins `homedir()` by hand\n * silently opts out of that isolation.\n *\n * Pure and synchronous: it never creates directories nor touches the\n * filesystem — callers decide when (and whether) the directory should exist.\n */\nexport function getGlobalRoot(options: GetGlobalRootOptions = {}): string {\n const env = options.env ?? process.env;\n\n const override = env[GLOBAL_ROOT_ENV]?.trim();\n if (override) {\n // A relative override is resolved against the CWD so that every consumer\n // gets an absolute path, exactly as in the homedir() branch below.\n return resolve(override);\n }\n\n // The only failure mode: an environment where the home directory cannot be\n // determined (some containers and CI images). The override is the escape\n // hatch, so the error names it.\n let home: string;\n try {\n home = homedir();\n } catch {\n home = '';\n }\n if (home.trim() === '') {\n throw new Error(\n `Unable to resolve the home directory for the global storage. Set ${GLOBAL_ROOT_ENV} to an explicit path.`,\n );\n }\n\n return join(home, GLOBAL_DIR_NAME);\n}\n\n/** Maximum length of the human-readable half of a project id. */\nconst SLUG_MAX_LENGTH = 32;\n\n/** Number of hex characters kept from the sha256 of the seed. */\nconst HASH_LENGTH = 12;\n\n/** Used when the repository name has no character that survives sanitization. */\nconst FALLBACK_SLUG = 'project';\n\n/**\n * Reduce an arbitrary repository name to a path-safe slug: lowercase, only\n * `[a-z0-9-]`, runs of separators collapsed, truncated to {@link SLUG_MAX_LENGTH}.\n */\nfunction slugify(name: string): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, SLUG_MAX_LENGTH)\n .replace(/-+$/g, '');\n\n return slug === '' ? FALLBACK_SLUG : slug;\n}\n\n/**\n * Derive a stable identifier for a project, in the form `<slug>-<hash12>`.\n *\n * The seed is canonical rather than local, so the same repository yields the\n * same id on any machine: `remote:<normalizedRemote>` whenever an `origin`\n * remote exists, falling back to `path:<absoluteProjectRoot>` when it does not.\n * That is what makes moving or renaming the local folder harmless for a cloned\n * repository — the history stays attached to the remote identity.\n *\n * The `remote:` / `path:` prefix is part of the hashed seed, so a project\n * identified by path can never collide with one identified by remote even if\n * the two strings were to coincide. The slug is only cosmetic (it makes the\n * directory recognizable); the hash carries the identity.\n *\n * Known limitation of the path fallback: for a repository with no `origin`\n * remote, the absolute path *is* the identity. Moving or renaming that folder\n * produces a different id, and the previous history is left behind under the\n * old one. Configuring a remote before adopting the global storage avoids it.\n */\nexport async function getProjectId(projectRoot: string): Promise<string> {\n const remote = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n return projectIdFromRemote(remote, projectRoot);\n}\n\n/**\n * Pure half of {@link getProjectId}: derive the id once the normalized remote\n * (or `null`) is already known.\n *\n * Exported so a caller that already resolved the remote for another reason\n * (e.g. {@link resolveStorageMode} in `compat.ts`, which also persists it into\n * `metadata.json`) can compute the id without shelling out to git a second\n * time.\n */\nexport function projectIdFromRemote(remote: string | null, projectRoot: string): string {\n let seed: string;\n let name: string;\n if (remote) {\n // `host/org/repo` -> the repository name is the last path segment.\n seed = `remote:${remote}`;\n name = remote.split('/').pop() ?? '';\n } else {\n const absolute = resolve(projectRoot);\n seed = `path:${absolute}`;\n name = basename(absolute);\n }\n\n const hash = createHash('sha256').update(seed).digest('hex').slice(0, HASH_LENGTH);\n\n return `${slugify(name)}-${hash}`;\n}\n\n/**\n * Absolute directory of a project inside the global storage tree.\n *\n * `projectId` is expected to come from {@link getProjectId}, which already\n * guarantees a single safe path segment.\n */\nexport function getProjectDir(projectId: string, options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), PROJECTS_DIR_NAME, projectId);\n}\n\n/**\n * Every artifact of one issue, resolved from a single place.\n *\n * Note that `prdFile` is the requirements document (`prd.md`) and `tasksFile`\n * is the task plan (`tasks.json`) — unlike `ResolvedPaths.prdFile` in\n * `types.ts`, which historically points at `tasks.json`.\n */\nexport interface IssuePaths {\n issueDir: string;\n issueFile: string;\n metadataFile: string;\n prdFile: string;\n tasksFile: string;\n progressFile: string;\n analysisFile: string;\n sessionFile: string;\n lastBranchFile: string;\n archiveDir: string;\n prReviewDir: string;\n}\n\n/**\n * Issue identifiers become path segments, so anything that could escape the\n * issues directory is rejected before it reaches `join` — the same discipline\n * as `normalizeId()` in `issues/providers/local.ts`.\n *\n * Non-numeric identifiers (`auth-refactor`, `pr-184`) are first-class here:\n * the local provider already supports them, so the global storage must too.\n */\nfunction normalizeIssueNumber(issueNumber: string | number): string {\n const normalized = String(issueNumber).trim().replace(/^#/, '');\n\n if (normalized.length === 0) {\n throw new Error('Issue identifier cannot be empty');\n }\n if (/[/\\\\]/.test(normalized) || normalized === '.' || normalized === '..') {\n throw new Error(`Invalid issue identifier: '${issueNumber}'`);\n }\n\n return normalized;\n}\n\n/**\n * Resolve every path of a single issue under the global storage.\n *\n * This is the only place allowed to know the layout of an issue directory:\n * call sites ask for the artifact they need instead of joining names by hand,\n * so renaming or adding an artifact stays a one-file change.\n *\n * Pure and synchronous: nothing here creates directories or touches the\n * filesystem.\n */\nexport function getIssuePaths(\n projectId: string,\n issueNumber: string | number,\n options: GetGlobalRootOptions = {},\n): IssuePaths {\n const issueDir = join(\n getProjectDir(projectId, options),\n ISSUES_DIR_NAME,\n normalizeIssueNumber(issueNumber),\n );\n\n return {\n issueDir,\n issueFile: join(issueDir, 'issue.md'),\n metadataFile: join(issueDir, 'metadata.json'),\n prdFile: join(issueDir, 'prd.md'),\n tasksFile: join(issueDir, 'tasks.json'),\n progressFile: join(issueDir, 'progress.txt'),\n analysisFile: join(issueDir, 'analysis.md'),\n sessionFile: join(issueDir, 'session.json'),\n lastBranchFile: join(issueDir, '.last-branch'),\n archiveDir: join(issueDir, 'archive'),\n prReviewDir: join(issueDir, 'pr-review'),\n };\n}\n","import { type Options as ExecaOptions, execa } from 'execa';\n\nexport interface ExecResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Execute a command with arguments and capture its output.\n * Uses execa which calls execFile internally (no shell injection risk).\n * Does not throw on non-zero exit codes.\n */\nexport async function run(\n command: string,\n args: string[] = [],\n options?: ExecaOptions,\n): Promise<ExecResult> {\n const result = await execa(command, args, {\n reject: false,\n ...options,\n });\n\n return {\n stdout: result.stdout?.toString() ?? '',\n stderr: result.stderr?.toString() ?? '',\n exitCode: result.exitCode ?? 1,\n };\n}\n","import { type ExecResult, run } from './shell.js';\n\n/**\n * Get the root directory of the current git repository.\n * Throws if not inside a git repository.\n */\nexport async function getProjectRoot(): Promise<string> {\n const result = await run('git', ['rev-parse', '--show-toplevel']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Not inside a git repository. Please run issue-flow from within a git project.',\n );\n }\n\n return result.stdout.trim();\n}\n\n/**\n * Get the current git branch name.\n * Returns an empty string if in detached HEAD state.\n */\nexport async function getCurrentBranch(): Promise<string> {\n const result = await run('git', ['branch', '--show-current']);\n\n if (result.exitCode !== 0) {\n throw new Error(\n 'Failed to detect git branch. Ensure git is installed and you are inside a repository.',\n );\n }\n\n return result.stdout.trim();\n}\n\nexport interface CommitInfo {\n hash: string;\n subject: string;\n}\n\n/**\n * Get the base branch of the repository: the remote HEAD (origin/HEAD) when\n * available, otherwise the first of main/master that exists locally.\n * Never throws; defaults to 'main'.\n */\nexport async function getBaseBranch(): Promise<string> {\n const remoteHead = await run('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);\n if (remoteHead.exitCode === 0) {\n const name = remoteHead.stdout.trim().replace(/^origin\\//, '');\n if (name) return name;\n }\n\n for (const candidate of ['main', 'master']) {\n const check = await run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${candidate}`]);\n if (check.exitCode === 0) return candidate;\n }\n\n return 'main';\n}\n\n/**\n * Get the URL of the `origin` remote.\n * Never throws: a missing remote, a non-zero exit code, an empty stdout or a\n * git binary that cannot even be spawned all resolve to `null` — the same\n * discipline as {@link getBaseBranch}, so callers can treat \"no remote\" as an\n * ordinary state instead of an error path.\n *\n * `cwd` selects which repository is queried; omitting it falls back to\n * `process.cwd()`, which is almost never what a caller resolving a specific\n * `projectRoot` wants — pass it explicitly whenever one is available.\n */\nexport async function getRemoteUrl(cwd?: string): Promise<string | null> {\n let result: ExecResult;\n try {\n result = await run('git', ['remote', 'get-url', 'origin'], { cwd });\n } catch {\n return null;\n }\n\n if (result.exitCode !== 0) return null;\n\n const url = result.stdout.trim();\n return url === '' ? null : url;\n}\n\n/**\n * Get the abbreviated hash of the current HEAD commit.\n * Never throws, same discipline as {@link getRemoteUrl}: a repository with no\n * commits yet, a non-zero exit code, an empty stdout or a git binary that\n * cannot be spawned all resolve to `null`, so callers can treat \"no HEAD\" as\n * an ordinary state instead of an error path.\n *\n * `cwd` selects which repository is queried; omitting it falls back to\n * `process.cwd()`.\n */\nexport async function getHeadCommit(cwd?: string): Promise<string | null> {\n let result: ExecResult;\n try {\n result = await run('git', ['rev-parse', '--short', 'HEAD'], { cwd });\n } catch {\n return null;\n }\n\n if (result.exitCode !== 0) return null;\n\n const hash = result.stdout.trim();\n return hash === '' ? null : hash;\n}\n\n/**\n * Reduce a git remote URL to a canonical `host/path` identity, so that every\n * way of addressing the same repository collapses to the same string:\n *\n * ```\n * https://github.com/org/repo.git -> github.com/org/repo\n * git@github.com:org/repo.git -> github.com/org/repo\n * ssh://git@github.com:22/org/repo.git -> github.com/org/repo\n * https://user:token@github.com/org/repo -> github.com/org/repo\n * https://github.com/Org/Repo/ -> github.com/org/repo\n * ```\n *\n * Pure function: it never shells out, so it is testable without a git\n * repository. Returns `null` when no host and path can be extracted.\n *\n * Known limitation: both host and path are lowercased. Hostnames are\n * case-insensitive by definition, but repository paths are case-sensitive on\n * some self-hosted servers — so `host/org/Repo` and `host/org/repo` normalize\n * to the same identity even if that server considers them distinct. This is a\n * deliberate trade: matching the same repository across clones (where users\n * routinely type the wrong case) matters more than telling apart two repos\n * whose paths differ only by case.\n */\nexport function normalizeRemoteUrl(url: string | null | undefined): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (trimmed === '') return null;\n\n let authority: string;\n let path: string;\n\n const scheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.exec(trimmed);\n if (scheme) {\n const rest = trimmed.slice(scheme[0].length);\n const slash = rest.indexOf('/');\n if (slash === -1) return null;\n authority = rest.slice(0, slash);\n path = rest.slice(slash + 1);\n } else {\n // scp-like syntax: [user@]host:path — everything after the first colon is\n // the path, which is how git itself reads it (no port is possible here).\n const colon = trimmed.indexOf(':');\n if (colon === -1) return null;\n authority = trimmed.slice(0, colon);\n path = trimmed.slice(colon + 1);\n }\n\n // Drop embedded credentials (user, user:token) and the ssh user.\n const at = authority.lastIndexOf('@');\n if (at !== -1) authority = authority.slice(at + 1);\n\n const host = authority.replace(/:\\d+$/, '').toLowerCase();\n if (host === '') return null;\n\n const normalizedPath = path\n .replace(/[?#].*$/, '')\n .replace(/\\/+/g, '/')\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\.git$/i, '')\n .replace(/\\/+$/, '')\n .toLowerCase();\n if (normalizedPath === '') return null;\n\n return `${host}/${normalizedPath}`;\n}\n\n/**\n * Remove Basic-Auth-style credentials (`user:token@`, `user@`) embedded in an\n * `http(s)` remote URL, without touching anything else about it — case, port,\n * the `.git` suffix, query string and trailing slashes are all preserved.\n *\n * Deliberately scoped to `http`/`https`: that is the only place a git remote\n * can carry a real secret (a PAT is routinely embedded as\n * `https://x-access-token:TOKEN@host/...` by CI and automation), and the URL\n * stays a valid, clonable remote once the userinfo is gone. SSH has no\n * equivalent syntax — both `ssh://user@host/path` and the scp-like shorthand\n * `user@host:path` require that user segment to connect at all (it is almost\n * always the fixed, non-secret `git` service account), so this function\n * leaves any other scheme, the scp-like form and local paths untouched.\n *\n * Unlike {@link normalizeRemoteUrl}, which collapses a URL to a lossy\n * `host/path` identity, this keeps the URL \"as configured\" — it exists for\n * surfaces (like the web monitor's `repository.remoteUrl`) that want to show\n * the real remote, minus whatever secret an HTTPS token commonly embeds in\n * it.\n *\n * Pure function, never throws. Returns `null` for a nullish or empty input.\n */\nexport function stripRemoteUrlCredentials(url: string | null | undefined): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (trimmed === '') return null;\n\n const httpScheme = /^https?:\\/\\//i.exec(trimmed);\n if (!httpScheme) return trimmed;\n\n const rest = trimmed.slice(httpScheme[0].length);\n const slash = rest.indexOf('/');\n const authority = slash === -1 ? rest : rest.slice(0, slash);\n const remainder = slash === -1 ? '' : rest.slice(slash);\n\n const at = authority.lastIndexOf('@');\n if (at === -1) return trimmed;\n return `${httpScheme[0]}${authority.slice(at + 1)}${remainder}`;\n}\n\n/**\n * List commits on HEAD that are not on the given base branch (base..HEAD),\n * most recent first. Never throws; returns [] when the range cannot be\n * resolved (e.g. unknown base branch or shallow clone).\n */\nexport async function getCommitsSince(base: string): Promise<CommitInfo[]> {\n const result = await run('git', ['log', '--pretty=format:%h%x09%s', `${base}..HEAD`]);\n if (result.exitCode !== 0) return [];\n\n return result.stdout\n .split('\\n')\n .filter((line) => line.trim() !== '')\n .map((line) => {\n const tab = line.indexOf('\\t');\n return tab === -1\n ? { hash: line.trim(), subject: '' }\n : { hash: line.slice(0, tab), subject: line.slice(tab + 1) };\n });\n}\n","import { stat } from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport { printInfo } from '../ui/logger.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport { type MigrationResult, migrateLegacyStorage, resolveStorageMode } from './compat.js';\nimport {\n type GetGlobalRootOptions,\n getGlobalRoot,\n getIssuePaths,\n getProjectDir,\n ISSUES_DIR_NAME,\n type IssuePaths,\n} from './paths.js';\n\n/**\n * The single entry point every pipeline command uses to learn where an issue's\n * artifacts live.\n *\n * `paths.ts` and `compat.ts` stay pure and explicit — they take a `projectId` or\n * a `projectRoot` and never decide anything on their own. This module is the\n * opposite end: it knows the current repository, resolves the storage mode,\n * triggers the legacy migration when one is due, and caches all of that for the\n * lifetime of the process, so a call site is reduced to one line.\n *\n * It deliberately does **not** create the issue directory. Callers that write\n * keep doing their own `mkdir(..., { recursive: true })`, which preserves the\n * rule in `storage/CLAUDE.md` that path resolution never touches the\n * filesystem's shape.\n */\n\nexport interface ResolveIssuePathsOptions extends GetGlobalRootOptions {\n /**\n * Repository root. Defaults to {@link getProjectRoot}, i.e. the git toplevel\n * of the current working directory — which is what makes the result identical\n * from the repo root and from any subdirectory.\n */\n projectRoot?: string;\n}\n\n/** Everything the process needs to know about a project, resolved once. */\ninterface ProjectResolution {\n projectId: string;\n legacyDir: string;\n}\n\n/**\n * Cached project resolutions, keyed by `<globalRoot>::<projectRoot>`.\n *\n * `projectRoot` is the identity of the project, but the global root takes part\n * in the key as well: `ISSUE_FLOW_HOME` decides *where* the resolution lands,\n * so two different roots must never share an entry (that would silently hand a\n * test the previous test's tree).\n *\n * The cached value is the in-flight promise rather than its result, so two\n * commands resolving concurrently share a single git call instead of racing\n * into two migrations.\n */\nconst projectCache = new Map<string, Promise<ProjectResolution>>();\n\n/**\n * Issues whose global directory has already been checked in this process, so\n * the per-issue fallback below stats each issue at most once.\n */\nconst checkedIssues = new Set<string>();\n\n/**\n * Drop every cached resolution.\n *\n * Exported for tests: the cache is keyed by project and global root, so a suite\n * that moves `ISSUE_FLOW_HOME` between cases must reset it in `beforeEach`.\n */\nexport function resetStorageResolutionCache(): void {\n projectCache.clear();\n checkedIssues.clear();\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Tell the user that their artifacts moved, and that nothing was taken away.\n *\n * A migration that copied nothing stays silent: `migrateLegacyStorage` is\n * called speculatively (once per project, then once per issue seen for the\n * first time), and every one of those runs after the first copies zero files.\n * Printing on those would turn a one-time notice into noise on every command.\n *\n * The \"one notice per process\" guarantee falls out of the US-001 cache: the\n * project-level migration runs once, and the per-issue fallback only reaches a\n * second migration when an issue really is still legacy-only — in which case a\n * notice is the correct output, not a duplicate.\n */\nfunction announceMigration(result: MigrationResult): void {\n if (result.copied.length === 0) return;\n\n const count = result.copied.length;\n const target = join(result.globalDir, ISSUES_DIR_NAME);\n\n printInfo(\n `Migrated ${count} file${count === 1 ? '' : 's'} from ${result.legacyDir} to ${target}`,\n );\n printInfo(\n `The legacy directory was not modified or removed — ${result.legacyDir} is kept as-is, read-only, for compatibility.`,\n );\n}\n\n/**\n * Resolve the project id, migrating the legacy tree when it is the only thing\n * that exists yet.\n *\n * A migration failure is not swallowed: `migrateLegacyStorage` already names the\n * issue and file that failed and states that the legacy directory was left\n * untouched, which is exactly what the user needs to fix it and retry.\n */\nasync function resolveProject(\n projectRoot: string,\n options: GetGlobalRootOptions,\n): Promise<ProjectResolution> {\n // resolveStorageMode returns the project id *and* the remote in a single git\n // call — calling getProjectId() afterwards would shell out to\n // `git remote get-url origin` a second time for the same answer.\n const status = await resolveStorageMode(projectRoot, options);\n\n if (status.mode === 'needs-migration') {\n announceMigration(await migrateLegacyStorage(projectRoot, options));\n }\n\n return {\n projectId: status.projectId,\n legacyDir: status.legacyDir,\n };\n}\n\n/** Cached half of {@link resolveIssuePaths}: one git call per project, per process. */\nfunction getProjectResolution(\n projectRoot: string,\n options: GetGlobalRootOptions,\n): Promise<ProjectResolution> {\n const key = `${getGlobalRoot(options)}::${projectRoot}`;\n\n const cached = projectCache.get(key);\n if (cached) return cached;\n\n // A rejected resolution is evicted so the next command retries instead of\n // inheriting a permanently failed promise.\n const pending = resolveProject(projectRoot, options).catch((err) => {\n projectCache.delete(key);\n throw err;\n });\n\n projectCache.set(key, pending);\n return pending;\n}\n\n/** Project-level directories of the current repository inside the global tree. */\nexport interface ProjectStoragePaths {\n /** Deterministic id derived from the repository's remote (or its path). */\n projectId: string;\n /** `<globalRoot>/projects/<projectId>`. */\n projectDir: string;\n /** `<projectDir>/issues` — the parent of every issue directory. */\n issuesDir: string;\n}\n\n/**\n * Resolve the project-level directories, without naming an issue.\n *\n * The issue-agnostic half of {@link resolveIssuePaths}, for the two questions\n * that are about the project rather than about one issue: \"can I write here?\"\n * (`LocalFileIssueProvider.isAvailable`) and \"which identifiers are taken?\"\n * (`highestLocalNumber`, which walks `issuesDir`). It shares the same cache, so\n * asking here and then resolving an issue still costs a single git call.\n *\n * Like the rest of this module it creates nothing — a caller that needs the\n * directory to exist does its own `mkdir(..., { recursive: true })`.\n */\nexport async function resolveProjectPaths(\n options: ResolveIssuePathsOptions = {},\n): Promise<ProjectStoragePaths> {\n const { projectRoot: projectRootOption, ...rootOptions } = options;\n const projectRoot = resolve(projectRootOption ?? (await getProjectRoot()));\n\n const project = await getProjectResolution(projectRoot, rootOptions);\n const projectDir = getProjectDir(project.projectId, rootOptions);\n\n return {\n projectId: project.projectId,\n projectDir,\n issuesDir: join(projectDir, ISSUES_DIR_NAME),\n };\n}\n\n/**\n * Resolve every path of `issueNumber` under the global storage, migrating the\n * legacy `<projectRoot>/issues/` tree first when needed.\n *\n * The migration is triggered in two situations:\n *\n * 1. project level — the global project directory does not exist at all and the\n * legacy one does (`mode === 'needs-migration'`);\n * 2. issue level — the project directory exists (so the mode is already\n * `global`) but *this* issue is only present in the legacy tree. That\n * happens whenever a project was migrated before an older issue was ever\n * read, or when a collaborator on a previous version added an issue to the\n * legacy directory afterwards. Re-running the migration is cheap and safe:\n * it skips every destination that already exists.\n */\nexport async function resolveIssuePaths(\n issueNumber: string | number,\n options: ResolveIssuePathsOptions = {},\n): Promise<IssuePaths> {\n const { projectRoot: projectRootOption, ...rootOptions } = options;\n const projectRoot = resolve(projectRootOption ?? (await getProjectRoot()));\n\n const project = await getProjectResolution(projectRoot, rootOptions);\n const paths = getIssuePaths(project.projectId, issueNumber, rootOptions);\n\n if (!checkedIssues.has(paths.issueDir)) {\n // `getIssuePaths` already validated and normalized the identifier, so the\n // last segment of issueDir is the safe form to look for under the legacy\n // tree.\n const legacyIssueDir = join(project.legacyDir, basename(paths.issueDir));\n\n if (!(await directoryExists(paths.issueDir)) && (await directoryExists(legacyIssueDir))) {\n announceMigration(await migrateLegacyStorage(projectRoot, rootOptions));\n }\n\n // Marked only once the check completed: a failed migration must be retried\n // by the next command, not remembered as done.\n checkedIssues.add(paths.issueDir);\n }\n\n return paths;\n}\n","import type { Dirent } from 'node:fs';\nimport { copyFile, mkdir, readdir, readFile, stat } from 'node:fs/promises';\nimport { join, resolve, sep } from 'node:path';\nimport { isoNow } from '../core/state-manager.js';\nimport { writeFileAtomic } from '../utils/fs.js';\nimport { getRemoteUrl, normalizeRemoteUrl } from '../utils/git.js';\nimport {\n type GetGlobalRootOptions,\n getProjectDir,\n ISSUES_DIR_NAME,\n projectIdFromRemote,\n} from './paths.js';\nimport { type ProjectMetadata, projectMetadataSchema, STORAGE_SCHEMA_VERSION } from './schemas.js';\n\n/**\n * Coexistence between the legacy per-project layout (`<projectRoot>/issues/`)\n * and the global storage tree (`~/.issue-flow/projects/<project-id>/`).\n *\n * The single rule of this module: **the legacy directory is read-only.** Nothing\n * here removes or rewrites a byte under `<projectRoot>/issues/` — not even\n * behind an opt-in flag — so adopting the global storage can never cost a user\n * their history. Migration is a copy, and a copy that refuses to overwrite.\n */\n\n/** Directory holding the legacy per-project artifacts. */\nexport const LEGACY_ISSUES_DIR_NAME = 'issues';\n\n/** File holding the per-project metadata inside the global storage. */\nexport const METADATA_FILENAME = 'metadata.json';\n\n/**\n * - `global`: the global directory is the source of truth (it already exists,\n * or neither layout does and a fresh project starts there).\n * - `needs-migration`: only the legacy directory exists, so its content has not\n * been copied over yet.\n */\nexport type StorageMode = 'global' | 'needs-migration';\n\nexport interface StorageStatus {\n mode: StorageMode;\n /** Deterministic id, derived from {@link projectIdFromRemote}. */\n projectId: string;\n /** `<globalRoot>/projects/<projectId>`. */\n globalDir: string;\n /** `<projectRoot>/issues`. */\n legacyDir: string;\n /** Whether `globalDir` currently exists on disk. */\n globalExists: boolean;\n /** Whether `legacyDir` currently exists on disk. */\n legacyExists: boolean;\n /**\n * Normalized `origin` remote of `projectRoot`, or `null` without one.\n * Resolved once here so `migrateLegacyStorage` can reuse it instead of\n * shelling out to git a second time for `metadata.json`.\n */\n remoteUrl: string | null;\n}\n\nexport interface MigrationResult {\n projectId: string;\n globalDir: string;\n legacyDir: string;\n /** Mode observed *before* the migration ran. */\n previousMode: StorageMode;\n /** Paths copied, relative to `legacyDir`. */\n copied: string[];\n /** Paths left alone because the destination already had a file. */\n skipped: string[];\n /** Absolute path of the metadata file written by this run. */\n metadataFile: string;\n /** Metadata as persisted. */\n metadata: ProjectMetadata;\n}\n\nexport interface MigrateLegacyStorageOptions extends GetGlobalRootOptions {\n /** Timestamp source, injectable so tests can assert createdAt/updatedAt. */\n now?: () => string;\n}\n\nasync function directoryExists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Describe where a project's artifacts live, without touching anything.\n *\n * Read-only by construction: it stats the two candidate directories and reports\n * what it found. It never creates a directory, so a caller can ask the question\n * (a status command, a warning banner) without committing the user to the new\n * layout.\n *\n * An existing global directory always wins: once artifacts are there, that is\n * the source of truth, and a legacy directory left behind is simply preserved\n * rather than re-read or re-migrated.\n */\nexport async function resolveStorageMode(\n projectRoot: string,\n options: GetGlobalRootOptions = {},\n): Promise<StorageStatus> {\n const remoteUrl = normalizeRemoteUrl(await getRemoteUrl(projectRoot));\n const projectId = projectIdFromRemote(remoteUrl, projectRoot);\n const globalDir = getProjectDir(projectId, options);\n const legacyDir = join(resolve(projectRoot), LEGACY_ISSUES_DIR_NAME);\n\n const [globalExists, legacyExists] = await Promise.all([\n directoryExists(globalDir),\n directoryExists(legacyDir),\n ]);\n\n return {\n // Only one state calls for work: legacy content that has nowhere to be read\n // from yet. Everything else — including \"neither exists\" — is already the\n // global layout.\n mode: legacyExists && !globalExists ? 'needs-migration' : 'global',\n projectId,\n globalDir,\n legacyDir,\n globalExists,\n legacyExists,\n remoteUrl,\n };\n}\n\n/**\n * Wrap an IO failure with the issue and file it happened on, so a partial\n * migration is diagnosable instead of a bare `EACCES`.\n */\nfunction migrationFailure(issue: string, relativePath: string, err: unknown): Error {\n const reason = err instanceof Error ? err.message : String(err);\n\n return new Error(\n `Failed to migrate '${relativePath}' (issue '${issue}'): ${reason}. ` +\n 'The legacy directory was left untouched — fix the cause and run the migration again.',\n );\n}\n\n/** First path segment of a relative path — the issue a file belongs to. */\nfunction issueOf(relativePath: string): string {\n return relativePath.split(sep)[0] || '.';\n}\n\n/**\n * Copy one directory level, recursing into subdirectories.\n *\n * Symlinks and other non-regular entries are skipped on purpose: following them\n * could copy content from outside the legacy directory into the global tree.\n */\nasync function copyTree(\n sourceRoot: string,\n targetRoot: string,\n relativeDir: string,\n result: { copied: string[]; skipped: string[] },\n): Promise<void> {\n const source = join(sourceRoot, relativeDir);\n const target = join(targetRoot, relativeDir);\n\n let entries: Dirent[];\n try {\n entries = await readdir(source, { withFileTypes: true });\n } catch (err) {\n throw migrationFailure(issueOf(relativeDir), relativeDir || '.', err);\n }\n\n try {\n // recursive: true also makes re-running a no-op on an existing directory.\n await mkdir(target, { recursive: true });\n } catch (err) {\n throw migrationFailure(issueOf(relativeDir), relativeDir || '.', err);\n }\n\n for (const entry of entries) {\n const relativePath = join(relativeDir, entry.name);\n\n if (entry.isDirectory()) {\n await copyTree(sourceRoot, targetRoot, relativePath, result);\n continue;\n }\n if (!entry.isFile()) {\n continue;\n }\n\n const to = join(targetRoot, relativePath);\n try {\n // Idempotency and safety in one check: a destination that already exists\n // is never overwritten, so a second run copies nothing and a hand-edited\n // artifact in the global tree survives.\n if (await pathExists(to)) {\n result.skipped.push(relativePath);\n continue;\n }\n await copyFile(join(sourceRoot, relativePath), to);\n result.copied.push(relativePath);\n } catch (err) {\n throw migrationFailure(issueOf(relativePath), relativePath, err);\n }\n }\n}\n\n/**\n * Read `metadata.json`, tolerating every failure.\n *\n * A missing, corrupt or schema-invalid file must not block a migration: the\n * caller simply writes a fresh one. The only thing worth recovering is\n * `createdAt`/`lastAttemptAt` from a valid previous file.\n */\nasync function readExistingMetadata(file: string): Promise<ProjectMetadata | null> {\n let raw: string;\n try {\n raw = await readFile(file, 'utf-8');\n } catch {\n return null;\n }\n\n try {\n const parsed = projectMetadataSchema.safeParse(JSON.parse(raw));\n return parsed.success ? parsed.data : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Copy `<projectRoot>/issues/` into the project's global directory and stamp\n * its `metadata.json`.\n *\n * Non-destructive and idempotent, by design rather than by convention:\n *\n * - the source is only ever read — there is no removal option in this release;\n * - a destination file that already exists is skipped, never overwritten, so a\n * second run reports everything as skipped and changes nothing but\n * `updatedAt`;\n * - a failure mid-way leaves the legacy directory intact and names the issue\n * and file that failed, so the run can be repeated after the fix and will\n * resume by skipping what already made it across.\n *\n * Called on a project with no legacy directory it is still useful: it creates\n * the project directory and its metadata.\n */\nexport async function migrateLegacyStorage(\n projectRoot: string,\n options: MigrateLegacyStorageOptions = {},\n): Promise<MigrationResult> {\n const status = await resolveStorageMode(projectRoot, { env: options.env });\n const result = { copied: [] as string[], skipped: [] as string[] };\n\n try {\n await mkdir(status.globalDir, { recursive: true });\n } catch (err) {\n throw migrationFailure('.', status.globalDir, err);\n }\n\n if (status.legacyExists) {\n await copyTree(status.legacyDir, join(status.globalDir, ISSUES_DIR_NAME), '', result);\n }\n\n const now = (options.now ?? isoNow)();\n const metadataFile = join(status.globalDir, METADATA_FILENAME);\n const existing = await readExistingMetadata(metadataFile);\n\n const metadata: ProjectMetadata = {\n schemaVersion: STORAGE_SCHEMA_VERSION,\n projectId: status.projectId,\n root: resolve(projectRoot),\n remoteUrl: status.remoteUrl,\n // The project was first seen whenever it was first written, not now.\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n lastAttemptAt: existing?.lastAttemptAt ?? null,\n };\n\n await writeFileAtomic(metadataFile, `${JSON.stringify(metadata, null, 2)}\\n`);\n\n return {\n projectId: status.projectId,\n globalDir: status.globalDir,\n legacyDir: status.legacyDir,\n previousMode: status.mode,\n ...result,\n metadataFile,\n metadata,\n };\n}\n","import { z } from 'zod';\nimport { webConfigSchema } from '../schemas.js';\n\n/**\n * Zod schemas for the files written under the global storage tree\n * (`~/.issue-flow`): the per-project `metadata.json` and the global\n * `config.json`.\n *\n * They live here rather than in `src/schemas.ts` so that file stays focused on\n * the pipeline domain (task plans, Issue metadata, session snapshots) and the\n * storage layer keeps owning its own formats.\n */\n\n/** Version stamped on files written by this release of the storage layer. */\nexport const STORAGE_SCHEMA_VERSION = 1;\n\n/**\n * `~/.issue-flow/projects/<project-id>/metadata.json`.\n *\n * Deliberately **not** `.strict()`: a newer release may add fields (dashboard\n * history, counters) and an older one must still be able to read the file\n * instead of rejecting it. Unknown keys are dropped on parse, never fatal.\n *\n * `root` is the last known local checkout — informative only. Identity lives in\n * `projectId` (see `getProjectId()` in `paths.ts`), so moving the folder of a\n * project that has a remote updates `root` without changing the id.\n */\nexport const projectMetadataSchema = z.object({\n schemaVersion: z.number().int().positive(),\n projectId: z.string().min(1),\n root: z.string().min(1),\n /** Normalized remote (`host/org/repo`), or null when the project has none. */\n remoteUrl: z.string().nullable(),\n createdAt: z.string().min(1),\n updatedAt: z.string().min(1),\n /** Null until the project is used by a pipeline run. */\n lastAttemptAt: z.string().nullable(),\n});\n\n/**\n * Global counterpart of the `web` key of `.issue-flow.json`, restricted to the\n * settings that make sense machine-wide: `enabled` and `includeLogs` stay a\n * per-project decision.\n *\n * The validation of each field is reused from `webConfigSchema` through\n * `.unwrap()`, which strips the `.default()` and keeps the constraints. Picking\n * and calling `.partial()` would not be enough: in zod 4 an optional field\n * still materializes its default when the key is absent\n * (`z.object({ port: z.number().default(3737) }).partial().parse({})` yields\n * `{ port: 3737 }`), and a materialized default in this layer would override\n * the project layer during the merge.\n */\nconst webShape = webConfigSchema.shape;\nexport const globalWebConfigSchema = z\n .object({\n port: webShape.port.unwrap(),\n host: webShape.host.unwrap(),\n refreshSeconds: webShape.refreshSeconds.unwrap(),\n logLimit: webShape.logLimit.unwrap(),\n })\n .partial();\n\n/** Global retry preferences, mirroring `DEFAULTS` in `config.ts`. */\nexport const globalRetryConfigSchema = z\n .object({\n retryLimit: z.number().int().nonnegative(),\n retryForever: z.boolean(),\n backoffBaseSeconds: z.number().nonnegative(),\n backoffMaxSeconds: z.number().nonnegative(),\n })\n .partial();\n\n/** Global commit preferences. Consumed by later issues, not by this release. */\nexport const globalCommitConfigSchema = z\n .object({\n signoff: z.boolean(),\n conventional: z.boolean(),\n })\n .partial();\n\n/**\n * `~/.issue-flow/config.json`.\n *\n * **Every key is optional and no key carries a `.default()`.** This is a design\n * constraint, not an omission: the global file is an intermediate precedence\n * layer (CLI > env > `.issue-flow.json` > `config.json` > defaults), so a\n * default materialized here would be indistinguishable from a value the user\n * actually wrote and would silently override the project layer during the\n * merge. Same reason `readWebConfigFile()` parses with\n * `webConfigSchema.partial()`. Defaults belong to the final layer only.\n */\nexport const globalConfigSchema = z\n .object({\n schemaVersion: z.number().int().positive(),\n /** Override for the directory holding `projects/`. */\n storageDir: z.string().min(1),\n web: globalWebConfigSchema,\n retry: globalRetryConfigSchema,\n commit: globalCommitConfigSchema,\n })\n .partial();\n\n/**\n * `~/.issue-flow/web.lock`.\n *\n * Marks the single web monitoring server active on this machine: `pid` and\n * `port` let a new invocation tell a live instance from a stale one before\n * attempting to bind its own (see `web/lock.ts`). Not `.strict()` for the same\n * reason as the schemas above — a newer release may add fields.\n */\nexport const webLockSchema = z.object({\n pid: z.number().int().positive(),\n port: z.number().int().positive(),\n host: z.string().min(1),\n startedAt: z.string().min(1),\n});\n\nexport type ProjectMetadata = z.infer<typeof projectMetadataSchema>;\nexport type GlobalWebConfig = z.infer<typeof globalWebConfigSchema>;\nexport type GlobalRetryConfig = z.infer<typeof globalRetryConfigSchema>;\nexport type GlobalCommitConfig = z.infer<typeof globalCommitConfigSchema>;\nexport type GlobalConfig = z.infer<typeof globalConfigSchema>;\nexport type WebLock = z.infer<typeof webLockSchema>;\n"],"mappings":"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","names":["readFile","join","stat","basename","join","resolve","join","resolve","join","resolve","directoryExists","stat","join","resolve","basename","join","readFile"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/prompt-resolver.ts
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { readFile } from "fs/promises";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
function resolvePackageDir(name, startDir) {
|
|
9
|
+
let dir = startDir ?? dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
for (let i = 0; i < 5; i++) {
|
|
11
|
+
const candidate = join(dir, name);
|
|
12
|
+
if (existsSync(candidate)) {
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
dir = dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function getPromptsDir() {
|
|
20
|
+
const dir = resolvePackageDir("prompts");
|
|
21
|
+
if (dir === null) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Could not locate the prompts/ directory. Ensure the package is installed correctly."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
async function loadPrompt(name) {
|
|
29
|
+
const promptsDir = getPromptsDir();
|
|
30
|
+
const filePath = join(promptsDir, `${name}.md`);
|
|
31
|
+
try {
|
|
32
|
+
return await readFile(filePath, "utf-8");
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`Prompt template not found: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function applyPlaceholders(template, vars) {
|
|
38
|
+
let result = template;
|
|
39
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
40
|
+
result = result.replaceAll(key, value);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
resolvePackageDir,
|
|
47
|
+
loadPrompt,
|
|
48
|
+
applyPlaceholders
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=chunk-SLHZPB3C.js.map
|