issue-flow 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -97
- package/dist/agent-4DHRKZ2Z.js +32 -0
- package/dist/{analyze-Q4ONIWRZ.js → analyze-UEJ2R3H5.js} +42 -41
- package/dist/analyze-UEJ2R3H5.js.map +1 -0
- package/dist/{apply-EEFPY2W4.js → apply-QDYNQMUX.js} +22 -24
- package/dist/apply-QDYNQMUX.js.map +1 -0
- package/dist/artifacts-AMJQJFFV.js +82 -0
- package/dist/artifacts-AMJQJFFV.js.map +1 -0
- package/dist/{bench-WWWXLTFH.js → bench-RNRXVEFK.js} +42 -42
- package/dist/{chunk-RYOUZ6EK.js → chunk-23VHVVKW.js} +3 -3
- package/dist/chunk-2EYLOS5I.js +99 -0
- package/dist/chunk-2EYLOS5I.js.map +1 -0
- package/dist/{chunk-C5TL7T4V.js → chunk-2YM2VH6R.js} +2 -2
- package/dist/{chunk-XUEMATVY.js → chunk-37O2FMAN.js} +28 -25
- package/dist/chunk-37O2FMAN.js.map +1 -0
- package/dist/chunk-44RF3VFG.js +717 -0
- package/dist/chunk-44RF3VFG.js.map +1 -0
- package/dist/chunk-6AOZCWN2.js +21 -0
- package/dist/chunk-6AOZCWN2.js.map +1 -0
- package/dist/chunk-7MJGYPHF.js +1571 -0
- package/dist/chunk-7MJGYPHF.js.map +1 -0
- package/dist/{chunk-SA6HXL4Y.js → chunk-7WBUEBD5.js} +9 -9
- package/dist/chunk-7WBUEBD5.js.map +1 -0
- package/dist/chunk-7ZZUSMBJ.js +35 -0
- package/dist/chunk-7ZZUSMBJ.js.map +1 -0
- package/dist/{chunk-QJ44VG5G.js → chunk-ADH6RJNX.js} +3 -3
- package/dist/{chunk-34OCSKOB.js → chunk-AKL5UPR6.js} +159 -114
- package/dist/chunk-AKL5UPR6.js.map +1 -0
- package/dist/{chunk-7F7LGMVO.js → chunk-BTBJ5UOO.js} +12 -12
- package/dist/{chunk-7F7LGMVO.js.map → chunk-BTBJ5UOO.js.map} +1 -1
- package/dist/{chunk-HOAHMERN.js → chunk-CLZ6RRZL.js} +21 -21
- package/dist/chunk-DBLXLKGU.js +1201 -0
- package/dist/chunk-DBLXLKGU.js.map +1 -0
- package/dist/{chunk-DUS5ZNKL.js → chunk-DIMDAYZS.js} +7 -6
- package/dist/chunk-DZ3DZ3EN.js +278 -0
- package/dist/chunk-DZ3DZ3EN.js.map +1 -0
- package/dist/{chunk-CRGIQFUQ.js → chunk-E2H3JV2E.js} +3 -1
- package/dist/chunk-E2H3JV2E.js.map +1 -0
- package/dist/{chunk-45WZ6GMI.js → chunk-EORDSKWZ.js} +17 -17
- package/dist/chunk-EORDSKWZ.js.map +1 -0
- package/dist/{chunk-LTH6XYZ3.js → chunk-EYKPWHT7.js} +40 -30
- package/dist/chunk-EYKPWHT7.js.map +1 -0
- package/dist/{chunk-OODXFCQV.js → chunk-FU2FQ4JE.js} +14 -16
- package/dist/{chunk-OODXFCQV.js.map → chunk-FU2FQ4JE.js.map} +1 -1
- package/dist/{chunk-V7QOIQYC.js → chunk-G442RDFX.js} +9 -23
- package/dist/{chunk-V7QOIQYC.js.map → chunk-G442RDFX.js.map} +1 -1
- package/dist/{chunk-QPGEFLMY.js → chunk-I64N6RL6.js} +7 -7
- package/dist/{chunk-KRAGAUKD.js → chunk-JBYXJOJJ.js} +201 -64
- package/dist/chunk-JBYXJOJJ.js.map +1 -0
- package/dist/{chunk-SOM4QA6C.js → chunk-JCR5C5A7.js} +93 -18
- package/dist/chunk-JCR5C5A7.js.map +1 -0
- package/dist/{chunk-RNFUVKC3.js → chunk-K5NM26NV.js} +30 -13
- package/dist/chunk-K5NM26NV.js.map +1 -0
- package/dist/{chunk-YVPZUN6X.js → chunk-KUA6KBBD.js} +335 -38
- package/dist/chunk-KUA6KBBD.js.map +1 -0
- package/dist/{chunk-KMCS76ZW.js → chunk-KUL64XUF.js} +44 -12
- package/dist/chunk-KUL64XUF.js.map +1 -0
- package/dist/{chunk-HAKDLR6N.js → chunk-MAJ6QDRL.js} +65 -78
- package/dist/chunk-MAJ6QDRL.js.map +1 -0
- package/dist/{chunk-DGRTJD5R.js → chunk-MML2G7IC.js} +7 -7
- package/dist/chunk-NP47HRX2.js +136 -0
- package/dist/chunk-NP47HRX2.js.map +1 -0
- package/dist/{chunk-3R27MJ5R.js → chunk-PFWHHYEO.js} +3 -3
- package/dist/{chunk-ECU7YO7S.js → chunk-PM5YWJYO.js} +22 -6
- package/dist/chunk-PM5YWJYO.js.map +1 -0
- package/dist/{chunk-E4I6MMHD.js → chunk-PXPTIXJF.js} +154 -101
- package/dist/chunk-PXPTIXJF.js.map +1 -0
- package/dist/{chunk-CWSQVHVV.js → chunk-QJHZ3HYJ.js} +248 -102
- package/dist/chunk-QJHZ3HYJ.js.map +1 -0
- package/dist/{chunk-LI35PIIS.js → chunk-RDPK7LYK.js} +20 -117
- package/dist/chunk-RDPK7LYK.js.map +1 -0
- package/dist/{chunk-PMLE7KAQ.js → chunk-RMIBLFG3.js} +30 -14
- package/dist/chunk-RMIBLFG3.js.map +1 -0
- package/dist/{chunk-62GVVZQO.js → chunk-T736DSZI.js} +32 -316
- package/dist/chunk-T736DSZI.js.map +1 -0
- package/dist/{chunk-TZYINKCC.js → chunk-UDUUUFQF.js} +57 -29
- package/dist/chunk-UDUUUFQF.js.map +1 -0
- package/dist/{chunk-NEWNXZ65.js → chunk-V7GETODQ.js} +36 -28
- package/dist/{chunk-NEWNXZ65.js.map → chunk-V7GETODQ.js.map} +1 -1
- package/dist/{chunk-PPZ3K4YM.js → chunk-VHQMMY2D.js} +2 -2
- package/dist/{chunk-ZVZMWNCG.js → chunk-VJ3PF6B7.js} +50 -16
- package/dist/chunk-VJ3PF6B7.js.map +1 -0
- package/dist/{chunk-L6HSKTQE.js → chunk-W7LZLJAF.js} +4 -4
- package/dist/{chunk-6K2LCFG6.js → chunk-WB4FA2EH.js} +70 -81
- package/dist/chunk-WB4FA2EH.js.map +1 -0
- package/dist/{chunk-LGHND47K.js → chunk-WQSGL5LX.js} +22 -16
- package/dist/chunk-WQSGL5LX.js.map +1 -0
- package/dist/{chunk-ICM463EO.js → chunk-XNK5UN2D.js} +5 -649
- package/dist/chunk-XNK5UN2D.js.map +1 -0
- package/dist/{chunk-7UMMFOL2.js → chunk-Y2KOWABB.js} +39 -24
- package/dist/chunk-Y2KOWABB.js.map +1 -0
- package/dist/{chunk-QF7VEL3M.js → chunk-YC24E66P.js} +568 -158
- package/dist/chunk-YC24E66P.js.map +1 -0
- package/dist/{chunk-EG2MGDP5.js → chunk-Z6RCKCVB.js} +52 -46
- package/dist/chunk-Z6RCKCVB.js.map +1 -0
- package/dist/{chunk-YUNBOUQH.js → chunk-ZPGNVBWY.js} +8 -8
- package/dist/cli.js +132 -62
- package/dist/cli.js.map +1 -1
- package/dist/compat-OJG76J4I.js +24 -0
- package/dist/{config-LEBZZCJP.js → config-6YTWPDBK.js} +20 -17
- package/dist/{contract-JH2WBIO6.js → contract-HWLFFL3F.js} +2 -2
- package/dist/{conventions-XOT6YW5L.js → conventions-5UPOTU42.js} +20 -16
- package/dist/{conventions-XOT6YW5L.js.map → conventions-5UPOTU42.js.map} +1 -1
- package/dist/db-QAZG4LQD.js +264 -0
- package/dist/db-QAZG4LQD.js.map +1 -0
- package/dist/diagnostics-YEFB42E5.js +25 -0
- package/dist/execute-QCT64D37.js +38 -0
- package/dist/{generate-JGW5JSKB.js → generate-QATMDKOK.js} +46 -43
- package/dist/generate-QATMDKOK.js.map +1 -0
- package/dist/{git-A2IP32PQ.js → git-XGOECXWG.js} +2 -3
- package/dist/history-NBPUZVGQ.js +101 -0
- package/dist/history-NBPUZVGQ.js.map +1 -0
- package/dist/init-4LB2YKWP.js +28 -0
- package/dist/{operations-O4FORFIH.js → operations-RUQP2Y2F.js} +78 -52
- package/dist/operations-RUQP2Y2F.js.map +1 -0
- package/dist/{permissions-G4XCAESF.js → permissions-QZPUUBSO.js} +5 -6
- package/dist/plan-2NMSHTMD.js +38 -0
- package/dist/{policy-U5NBFAO5.js → policy-RKKFF5A2.js} +17 -15
- package/dist/{policy-U5NBFAO5.js.map → policy-RKKFF5A2.js.map} +1 -1
- package/dist/pr-QKFO5HI3.js +42 -0
- package/dist/pr-review-H2BFD6F2.js +33 -0
- package/dist/prd-ET6XKYRS.js +37 -0
- package/dist/{ps-76L6ZAZL.js → ps-UIKSOUAQ.js} +26 -15
- package/dist/ps-UIKSOUAQ.js.map +1 -0
- package/dist/{recorder-2BAKPBRA.js → recorder-LOU3PTIE.js} +11 -7
- package/dist/registry-HV7M6MAH.js +17 -0
- package/dist/repository-RCRR6RDE.js +80 -0
- package/dist/resume-BWJZGBLZ.js +278 -0
- package/dist/resume-BWJZGBLZ.js.map +1 -0
- package/dist/review-2MTZLNSS.js +38 -0
- package/dist/routing-RM5ABHHD.js +35 -0
- package/dist/run-XMACQBRJ.js +61 -0
- package/dist/{session-publisher-KAJQRHNZ.js → session-publisher-Q5TNEZPX.js} +3 -3
- package/dist/session-publisher-Q5TNEZPX.js.map +1 -0
- package/dist/{state-manager-UPHO2EA2.js → state-manager-SRN4VJSX.js} +9 -4
- package/dist/state-manager-SRN4VJSX.js.map +1 -0
- package/dist/{usage-KTMYXAPF.js → usage-JOODVE5O.js} +69 -63
- package/dist/usage-JOODVE5O.js.map +1 -0
- package/dist/web-TLMWJ2BM.js +205 -0
- package/dist/web-TLMWJ2BM.js.map +1 -0
- package/package.json +22 -8
- package/prompts/analyze.md +16 -19
- package/prompts/execute.md +73 -144
- package/prompts/generate.md +23 -10
- package/prompts/plan.md +32 -60
- package/prompts/pr-review.md +58 -40
- package/prompts/pr.md +50 -16
- package/prompts/prd.md +26 -19
- package/prompts/review.md +41 -18
- package/web/public/app.js +111 -112
- package/dist/agent-HMZTXCRT.js +0 -30
- package/dist/analyze-Q4ONIWRZ.js.map +0 -1
- package/dist/apply-EEFPY2W4.js.map +0 -1
- package/dist/chunk-34OCSKOB.js.map +0 -1
- package/dist/chunk-45WZ6GMI.js.map +0 -1
- package/dist/chunk-62GVVZQO.js.map +0 -1
- package/dist/chunk-6K2LCFG6.js.map +0 -1
- package/dist/chunk-7UMMFOL2.js.map +0 -1
- package/dist/chunk-CRGIQFUQ.js.map +0 -1
- package/dist/chunk-CWSQVHVV.js.map +0 -1
- package/dist/chunk-E4I6MMHD.js.map +0 -1
- package/dist/chunk-ECU7YO7S.js.map +0 -1
- package/dist/chunk-EG2MGDP5.js.map +0 -1
- package/dist/chunk-HAKDLR6N.js.map +0 -1
- package/dist/chunk-ICM463EO.js.map +0 -1
- package/dist/chunk-KMCS76ZW.js.map +0 -1
- package/dist/chunk-KRAGAUKD.js.map +0 -1
- package/dist/chunk-LGHND47K.js.map +0 -1
- package/dist/chunk-LI35PIIS.js.map +0 -1
- package/dist/chunk-LTH6XYZ3.js.map +0 -1
- package/dist/chunk-PMLE7KAQ.js.map +0 -1
- package/dist/chunk-QF7VEL3M.js.map +0 -1
- package/dist/chunk-RNFUVKC3.js.map +0 -1
- package/dist/chunk-SA6HXL4Y.js.map +0 -1
- package/dist/chunk-SOM4QA6C.js.map +0 -1
- package/dist/chunk-TZYINKCC.js.map +0 -1
- package/dist/chunk-WCCZJL7W.js +0 -141
- package/dist/chunk-WCCZJL7W.js.map +0 -1
- package/dist/chunk-XEYH6ZZI.js +0 -289
- package/dist/chunk-XEYH6ZZI.js.map +0 -1
- package/dist/chunk-XUEMATVY.js.map +0 -1
- package/dist/chunk-XZUEFTUP.js +0 -243
- package/dist/chunk-XZUEFTUP.js.map +0 -1
- package/dist/chunk-YVPZUN6X.js.map +0 -1
- package/dist/chunk-ZVZMWNCG.js.map +0 -1
- package/dist/diagnostics-HTCWF7F4.js +0 -23
- package/dist/execute-LELOSR5T.js +0 -37
- package/dist/generate-JGW5JSKB.js.map +0 -1
- package/dist/init-TO6MUXO4.js +0 -26
- package/dist/operations-O4FORFIH.js.map +0 -1
- package/dist/plan-OXAT344Q.js +0 -34
- package/dist/pr-UZHNKKZC.js +0 -39
- package/dist/pr-review-Q4ACAWDM.js +0 -32
- package/dist/prd-6CYEMLDY.js +0 -33
- package/dist/ps-76L6ZAZL.js.map +0 -1
- package/dist/registry-XLHHYLTR.js +0 -15
- package/dist/resume-URCU6CIJ.js +0 -244
- package/dist/resume-URCU6CIJ.js.map +0 -1
- package/dist/review-LVZT2TMW.js +0 -35
- package/dist/routing-EK6RBZM4.js +0 -33
- package/dist/run-2RXJCJKT.js +0 -56
- package/dist/usage-KTMYXAPF.js.map +0 -1
- package/dist/web-B5UPHHR5.js +0 -172
- package/dist/web-B5UPHHR5.js.map +0 -1
- /package/dist/{agent-HMZTXCRT.js.map → agent-4DHRKZ2Z.js.map} +0 -0
- /package/dist/{bench-WWWXLTFH.js.map → bench-RNRXVEFK.js.map} +0 -0
- /package/dist/{chunk-RYOUZ6EK.js.map → chunk-23VHVVKW.js.map} +0 -0
- /package/dist/{chunk-C5TL7T4V.js.map → chunk-2YM2VH6R.js.map} +0 -0
- /package/dist/{chunk-QJ44VG5G.js.map → chunk-ADH6RJNX.js.map} +0 -0
- /package/dist/{chunk-HOAHMERN.js.map → chunk-CLZ6RRZL.js.map} +0 -0
- /package/dist/{chunk-DUS5ZNKL.js.map → chunk-DIMDAYZS.js.map} +0 -0
- /package/dist/{chunk-QPGEFLMY.js.map → chunk-I64N6RL6.js.map} +0 -0
- /package/dist/{chunk-DGRTJD5R.js.map → chunk-MML2G7IC.js.map} +0 -0
- /package/dist/{chunk-3R27MJ5R.js.map → chunk-PFWHHYEO.js.map} +0 -0
- /package/dist/{chunk-PPZ3K4YM.js.map → chunk-VHQMMY2D.js.map} +0 -0
- /package/dist/{chunk-L6HSKTQE.js.map → chunk-W7LZLJAF.js.map} +0 -0
- /package/dist/{chunk-YUNBOUQH.js.map → chunk-ZPGNVBWY.js.map} +0 -0
- /package/dist/{config-LEBZZCJP.js.map → compat-OJG76J4I.js.map} +0 -0
- /package/dist/{contract-JH2WBIO6.js.map → config-6YTWPDBK.js.map} +0 -0
- /package/dist/{diagnostics-HTCWF7F4.js.map → contract-HWLFFL3F.js.map} +0 -0
- /package/dist/{execute-LELOSR5T.js.map → diagnostics-YEFB42E5.js.map} +0 -0
- /package/dist/{git-A2IP32PQ.js.map → execute-QCT64D37.js.map} +0 -0
- /package/dist/{init-TO6MUXO4.js.map → git-XGOECXWG.js.map} +0 -0
- /package/dist/{permissions-G4XCAESF.js.map → init-4LB2YKWP.js.map} +0 -0
- /package/dist/{plan-OXAT344Q.js.map → permissions-QZPUUBSO.js.map} +0 -0
- /package/dist/{pr-UZHNKKZC.js.map → plan-2NMSHTMD.js.map} +0 -0
- /package/dist/{pr-review-Q4ACAWDM.js.map → pr-QKFO5HI3.js.map} +0 -0
- /package/dist/{prd-6CYEMLDY.js.map → pr-review-H2BFD6F2.js.map} +0 -0
- /package/dist/{recorder-2BAKPBRA.js.map → prd-ET6XKYRS.js.map} +0 -0
- /package/dist/{registry-XLHHYLTR.js.map → recorder-LOU3PTIE.js.map} +0 -0
- /package/dist/{review-LVZT2TMW.js.map → registry-HV7M6MAH.js.map} +0 -0
- /package/dist/{routing-EK6RBZM4.js.map → repository-RCRR6RDE.js.map} +0 -0
- /package/dist/{run-2RXJCJKT.js.map → review-2MTZLNSS.js.map} +0 -0
- /package/dist/{session-publisher-KAJQRHNZ.js.map → routing-RM5ABHHD.js.map} +0 -0
- /package/dist/{state-manager-UPHO2EA2.js.map → run-XMACQBRJ.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/verbose.ts","../src/ui/logger.ts","../src/config/sources.ts","../src/core/journal.ts","../src/storage/db/import.ts","../src/storage/resolve.ts","../src/storage/artifact-storage.ts"],"sourcesContent":["let _verbose = false;\n\nexport function setVerbose(enabled: boolean): void {\n _verbose = enabled;\n}\n\nexport function isVerbose(): boolean {\n return _verbose;\n}\n\nlet _onOutput: ((line: string) => void) | undefined;\n\n/**\n * Set a global output callback for routing verbose output (e.g., through listr2 task.output).\n * Pass undefined to clear and fall back to direct stderr writes.\n */\nexport function setOutputCallback(callback: ((line: string) => void) | undefined): void {\n _onOutput = callback;\n}\n\nexport function getOutputCallback(): ((line: string) => void) | undefined {\n return _onOutput;\n}\n\nlet _onStoryUpdate: ((stories: import('../types.js').UserStory[]) => void) | undefined;\n\n/**\n * Set a global callback for story progress updates during engine execution.\n * Called after each iteration with the latest user stories state.\n * Pass undefined to clear.\n */\nexport function setStoryUpdateCallback(\n callback: ((stories: import('../types.js').UserStory[]) => void) | undefined,\n): void {\n _onStoryUpdate = callback;\n}\n\nexport function getStoryUpdateCallback():\n | ((stories: import('../types.js').UserStory[]) => void)\n | undefined {\n return _onStoryUpdate;\n}\n\nlet _onStoryStage: ((storyId: string | undefined) => void) | undefined;\n\n/**\n * Set a global callback for the active story of the current `execute`\n * iteration. Called by `core/engine.ts` alongside every `iteration:start`\n * publication, with the same `storyId` (or `undefined` once every story\n * already passes) — the terminal renderer's only source for \"who is\n * executing right now\", never a second, independently computed heuristic.\n * Pass `undefined` to clear.\n */\nexport function setStoryStageCallback(\n callback: ((storyId: string | undefined) => void) | undefined,\n): void {\n _onStoryStage = callback;\n}\n\nexport function getStoryStageCallback(): ((storyId: string | undefined) => void) | undefined {\n return _onStoryStage;\n}\n\nexport interface ActivityNotice {\n tool: string;\n detail?: string;\n}\n\nlet _onActivity: ((notice: ActivityNotice) => void) | undefined;\n\n/**\n * Live tool activity for the clean terminal view. Published in every mode,\n * including non-verbose — the snapshot's `currentActivity` and the dashboard\n * both read the same event.\n */\nexport function setActivityCallback(\n callback: ((notice: ActivityNotice) => void) | undefined,\n): void {\n _onActivity = callback;\n}\n\nexport function getActivityCallback(): ((notice: ActivityNotice) => void) | undefined {\n return _onActivity;\n}\n\nlet _globalTimeout: number | undefined;\n\nexport function setGlobalTimeout(ms: number): void {\n _globalTimeout = ms;\n}\n\nexport function getGlobalTimeout(): number | undefined {\n return _globalTimeout;\n}\n\n/**\n * Silence tolerated before an agent is considered stuck, in milliseconds.\n *\n * Global for the same reason the timeout is: every phase invokes the CLI\n * through the same two functions, and threading it through each one of them\n * would be a parameter with a single value per process. `undefined` leaves the\n * default; `0` turns the watchdog off.\n */\nlet _inactivityTimeout: number | undefined;\n\nexport function setInactivityTimeout(ms: number): void {\n _inactivityTimeout = ms;\n}\n\nexport function getInactivityTimeout(): number | undefined {\n return _inactivityTimeout;\n}\n","import chalk from 'chalk';\nimport ora, { type Ora } from 'ora';\nimport { getSessionPublisher } from '../core/session-publisher.js';\nimport type { SessionLogLevel } from '../core/session-state.js';\nimport { isoNow } from '../core/state-manager.js';\nimport { getOutputCallback } from '../core/verbose.js';\nimport { writeDiagnostic } from '../storage/diagnostics.js';\n\n/**\n * Detect if unicode output is supported.\n */\nexport function useUnicode(): boolean {\n if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '') return false;\n if (!process.stdout.isTTY) return false;\n return true;\n}\n\n/**\n * Detect if color output is supported.\n */\nexport function useColor(): boolean {\n if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '') return false;\n if (!process.stdout.isTTY) return false;\n return true;\n}\n\nexport interface Icons {\n success: string;\n fail: string;\n pending: string;\n retry: string;\n warn: string;\n start: string;\n end: string;\n notReached: string;\n tool: string;\n connector: string;\n info: string;\n}\n\nexport function getIcons(): Icons {\n if (useUnicode()) {\n return {\n success: '\\u2713',\n fail: '\\u2717',\n pending: '\\u23F3',\n retry: '\\u21BB',\n warn: '\\u26A0',\n start: '\\u25B6',\n end: '\\u25A0',\n notReached: '\\u25CB',\n tool: '\\u25B8',\n connector: '\\u2502',\n info: '\\u00B7',\n };\n }\n return {\n success: '[OK]',\n fail: '[FAIL]',\n pending: '[...]',\n retry: '[RETRY]',\n warn: '[WARN]',\n start: '[START]',\n end: '[END]',\n notReached: '[ ]',\n tool: '>',\n connector: '|',\n info: '-',\n };\n}\n\n/**\n * Get the terminal width.\n */\nexport function getTermWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Emit a line of output. Routes through the global output callback when\n * running inside a listr2 task context, otherwise falls back to console.log.\n *\n * Also forwards the line to the session publisher with its structured level;\n * ANSI codes are stripped by the reducer, so terminal output stays untouched.\n */\nfunction emit(line: string, level: SessionLogLevel = 'info'): void {\n getSessionPublisher().publish({ type: 'log', at: isoNow(), level, message: line });\n writeDiagnostic({\n level: level === 'warn' ? 'warning' : level,\n message: line,\n });\n const cb = getOutputCallback();\n if (cb) {\n cb(line);\n } else {\n console.log(line);\n }\n}\n\nexport function printSuccess(message: string): void {\n const icons = getIcons();\n if (useColor()) {\n emit(chalk.green(`${icons.success} ${message}`));\n } else {\n emit(`${icons.success} ${message}`);\n }\n}\n\nexport function printError(message: string): void {\n const icons = getIcons();\n if (useColor()) {\n emit(chalk.red(`${icons.fail} ${message}`), 'error');\n } else {\n emit(`${icons.fail} ${message}`, 'error');\n }\n}\n\nexport function printWarning(message: string): void {\n const icons = getIcons();\n if (useColor()) {\n emit(chalk.yellow(`${icons.warn} ${message}`), 'warn');\n } else {\n emit(`${icons.warn} ${message}`, 'warn');\n }\n}\n\nexport function printRetry(message: string): void {\n const icons = getIcons();\n if (useColor()) {\n emit(chalk.yellow(`${icons.retry} ${message}`), 'warn');\n } else {\n emit(`${icons.retry} ${message}`, 'warn');\n }\n}\n\nexport function printInfo(message: string): void {\n const icons = getIcons();\n if (useColor()) {\n emit(chalk.blue(`${icons.info} ${message}`));\n } else {\n emit(`${icons.info} ${message}`);\n }\n}\n\n/**\n * Create an ora spinner with consistent styling.\n */\nexport function createSpinner(message: string): Ora {\n return ora({\n text: message,\n color: 'blue',\n spinner: 'dots',\n });\n}\n\n/**\n * Format a duration in seconds to a human-readable string.\n */\nexport function formatDuration(totalSeconds: number): string {\n const hours = Math.floor(totalSeconds / 3600);\n const mins = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n if (hours > 0) {\n return `${hours}h ${mins}m ${secs}s`;\n }\n if (mins > 0) {\n return `${mins}m ${secs}s`;\n }\n return `${secs}s`;\n}\n\n/**\n * Timer that calls a callback every second with the formatted elapsed time.\n */\nexport class ElapsedTimer {\n private startTime: number;\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private cleanup = () => this.stop();\n\n constructor(private onTick: (elapsed: string) => void) {\n this.startTime = Date.now();\n }\n\n start(): this {\n this.startTime = Date.now();\n this.intervalId = setInterval(() => {\n const seconds = Math.floor((Date.now() - this.startTime) / 1000);\n this.onTick(formatDuration(seconds));\n }, 1_000);\n // Prevent the timer from keeping the process alive\n if (this.intervalId && typeof this.intervalId === 'object' && 'unref' in this.intervalId) {\n this.intervalId.unref();\n }\n process.on('exit', this.cleanup);\n return this;\n }\n\n getElapsedSeconds(): number {\n return Math.floor((Date.now() - this.startTime) / 1000);\n }\n\n stop(): number {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n process.removeListener('exit', this.cleanup);\n return this.getElapsedSeconds();\n }\n}\n","import { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { getGlobalRoot } from '../storage/paths.js';\nimport { type GlobalConfig, globalConfigSchema } from '../storage/schemas.js';\nimport { printWarning } from '../ui/logger.js';\n\n/** Optional per-project configuration file, read from the project root. */\nexport const PROJECT_CONFIG_FILENAME = '.issue-flow.json';\n\n/**\n * Locate the project root without spawning `git`. Tests that mock `execa`\n * wholesale (execute-regression, executor) treat every spawn as the agent;\n * a `git rev-parse` here would steal that first call.\n */\nexport function findProjectRootFromCwd(start: string = process.cwd()): string | undefined {\n let dir = start;\n while (true) {\n if (existsSync(join(dir, PROJECT_CONFIG_FILENAME)) || existsSync(join(dir, '.git'))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\nexport async function readProjectConfigFile(\n projectRoot: string | undefined,\n warn: (message: string) => void,\n): Promise<Record<string, unknown> | null> {\n const root = projectRoot ?? findProjectRootFromCwd();\n if (root === undefined) {\n return null;\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/** 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","import { appendFile, mkdir, rename, stat, unlink } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport {\n createInitialSnapshot,\n MemoryPublisher,\n type MemoryPublisherOptions,\n reduceSessionEvent,\n type SessionEvent,\n type SessionPublisher,\n type SessionReducerOptions,\n type SessionSnapshot,\n} from './session-state.js';\n\n/**\n * Append-only journal of `SessionEvent`s.\n *\n * `session.json` is a *projection*: the reducer folds every event into one\n * snapshot and the events themselves are discarded. That is the right shape for\n * a dashboard and the wrong one for an audit — after a six-hour run, \"what\n * happened at 3am\" has no answer, because the retries, the failures and their\n * order were consumed to produce a single final state.\n *\n * This publisher writes the events instead of the state: one JSON line each, in\n * order, with a monotonic `seq`. It sits **beside** `FilePublisher`, never in\n * its place. The snapshot stays the projection; the journal is the history, and\n * replaying it through `reduceSessionEvent` reproduces the snapshot — which is\n * the property `replayJournal()` and its test exist to keep true.\n *\n * The invariant of `session-state.ts` holds here unchanged: `publish()` is\n * synchronous, never throws, and every I/O failure is swallowed after a single\n * warning. Monitoring never affects the pipeline.\n */\n\n/** One line of `events.jsonl`. */\nexport interface JournalEntry {\n /** 1-based, monotonic within a file *and* across a rotation. */\n seq: number;\n event: SessionEvent;\n}\n\n/**\n * Event history is durable by default. Rotation is opt-in because retaining a\n * single previous file silently discarded older audit events.\n */\nexport const DEFAULT_JOURNAL_MAX_BYTES = 0;\n\nexport interface JournalPublisherOptions extends MemoryPublisherOptions {\n /**\n * Rotate once the file would grow past this. `0` (the default) disables\n * rotation; an explicit retention policy must own any deletion.\n */\n maxFileBytes?: number;\n}\n\n/**\n * Publisher that appends every event to `events.jsonl`.\n *\n * Writes are serialized on a promise chain — the same discipline as\n * `FilePublisher` — so lines never interleave and the order on disk is the\n * order of `publish()`. Nothing is throttled: dropping or coalescing events is\n * exactly what the snapshot already does, and the point of the journal is that\n * it does not.\n */\nexport class JournalPublisher extends MemoryPublisher {\n private readonly filePath: string;\n private readonly rotatedPath: string;\n private readonly maxFileBytes: number;\n private seq = 0;\n private writeChain: Promise<void> = Promise.resolve();\n private closed = false;\n /** Bytes in the current file. `null` until the first write measures it. */\n private bytes: number | null = null;\n\n constructor(filePath: string, rotatedPath: string, options: JournalPublisherOptions = {}) {\n super(options);\n this.filePath = filePath;\n this.rotatedPath = rotatedPath;\n this.maxFileBytes = options.maxFileBytes ?? DEFAULT_JOURNAL_MAX_BYTES;\n }\n\n protected override afterPublish(event: SessionEvent): void {\n if (this.closed) return;\n this.seq++;\n const line = `${JSON.stringify({ seq: this.seq, event } satisfies JournalEntry)}\\n`;\n this.enqueue(line);\n }\n\n private enqueue(line: string): void {\n this.writeChain = this.writeChain.then(async () => {\n try {\n await this.appendLine(line);\n } catch (err) {\n this.warnOnce(err);\n }\n });\n }\n\n private async appendLine(line: string): Promise<void> {\n const size = Buffer.byteLength(line);\n\n if (this.bytes === null) {\n await mkdir(dirname(this.filePath), { recursive: true });\n this.bytes = await currentSize(this.filePath);\n }\n\n // Rotate *before* the write that would cross the ceiling, so a line is\n // never split across two files: an entry that cannot be parsed whole is\n // worse than a file slightly under its limit.\n if (this.maxFileBytes > 0 && this.bytes > 0 && this.bytes + size > this.maxFileBytes) {\n await this.rotate();\n }\n\n await appendFile(this.filePath, line, 'utf-8');\n this.bytes += size;\n }\n\n /**\n * Keep exactly one previous generation only when a caller explicitly asks\n * for rotation. The default never enters this path, so history is never\n * discarded without an explicit policy.\n */\n private async rotate(): Promise<void> {\n try {\n await unlink(this.rotatedPath);\n } catch {\n // No previous generation. Nothing to remove.\n }\n await rename(this.filePath, this.rotatedPath);\n this.bytes = 0;\n }\n\n override async flush(): Promise<void> {\n await this.writeChain;\n }\n\n override async close(): Promise<void> {\n await this.flush();\n this.closed = true;\n }\n}\n\n/** Size of `path` in bytes, or `0` when it does not exist yet. */\nasync function currentSize(path: string): Promise<number> {\n try {\n return (await stat(path)).size;\n } catch {\n return 0;\n }\n}\n\n/**\n * Parse the lines of a journal, skipping what cannot be read.\n *\n * A journal is appended to by a live process and read by another, so the last\n * line may well be a partial write. Tolerating it — and any line a newer\n * release wrote in a shape this one does not know — is what makes the file\n * usable at all; the alternative is a reader that throws on a healthy run.\n */\nexport function parseJournal(content: string): JournalEntry[] {\n const entries: JournalEntry[] = [];\n\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n\n if (!isEntry(parsed)) continue;\n entries.push(parsed);\n }\n\n return entries;\n}\n\nfunction isEntry(value: unknown): value is JournalEntry {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as { seq?: unknown; event?: unknown };\n if (typeof candidate.seq !== 'number') return false;\n const event = candidate.event;\n return (\n typeof event === 'object' && event !== null && typeof (event as SessionEvent).type === 'string'\n );\n}\n\n/**\n * Rebuild the snapshot from a journal.\n *\n * This is the reproducibility contract of the file: the same reducer that\n * produced `session.json` live, run over the events in the order they were\n * written, must land on the same state. A journal that cannot do that is a log,\n * not a journal.\n */\nexport function replayJournal(\n content: string,\n options: SessionReducerOptions = {},\n): SessionSnapshot {\n let snapshot = createInitialSnapshot();\n for (const entry of parseJournal(content)) {\n snapshot = reduceSessionEvent(snapshot, entry.event, options);\n }\n return snapshot;\n}\n\n/**\n * Fan one event stream out to several publishers.\n *\n * The journal sits **beside** the snapshot writer, so something has to hold\n * both. It is the composite rather than the journal that does it, because the\n * relationship is symmetric: neither surface owns the other, and dropping\n * either one has to leave the remaining one untouched.\n *\n * `snapshot()` and `version()` answer from the first member, which is the\n * primary surface. A member that fails has already swallowed its own error —\n * that is the contract of every publisher here — so the loop needs no guard of\n * its own beyond `publish()` never throwing.\n */\nexport class MultiPublisher implements SessionPublisher {\n private readonly members: readonly SessionPublisher[];\n private readonly primary: SessionPublisher;\n\n constructor(members: readonly SessionPublisher[]) {\n if (members.length === 0) {\n throw new Error('MultiPublisher needs at least one member');\n }\n this.members = members;\n this.primary = members[0] as SessionPublisher;\n }\n\n publish(event: SessionEvent): void {\n for (const member of this.members) {\n member.publish(event);\n }\n }\n\n snapshot(): SessionSnapshot {\n return this.primary.snapshot();\n }\n\n version(): number {\n return this.primary.version();\n }\n\n async flush(): Promise<void> {\n await Promise.all(this.members.map((member) => member.flush()));\n }\n\n async close(): Promise<void> {\n await Promise.all(this.members.map((member) => member.close()));\n }\n}\n","import { createHash } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile, rename } from 'node:fs/promises';\nimport { basename, join, relative } from 'node:path';\nimport { parseJournal } from '../../core/journal.js';\nimport { taskPlanSchema } from '../../schemas.js';\nimport { METADATA_FILENAME } from '../compat.js';\nimport {\n EVENTS_FILENAME,\n ISSUES_DIR_NAME,\n PROVIDERS_HEALTH_FILENAME,\n QUEUES_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n VERIFY_FILENAME,\n} from '../paths.js';\nimport { projectMetadataSchema, providersHealthSchema } from '../schemas.js';\nimport { openDatabase } from './driver.js';\nimport {\n getDatabasePath,\n type OpenIssueFlowDatabaseOptions,\n openIssueFlowDatabase,\n} from './index.js';\nimport { CURRENT_SCHEMA_VERSION } from './migrations.js';\nimport { writePlanRows } from './repository.js';\n\n/** A source file that was imported into one or more relational tables. */\ninterface Artifact {\n path: string;\n relativePath: string;\n sha256: string;\n kind: 'metadata' | 'tasks' | 'health' | 'queue' | 'events' | 'verify';\n value: unknown;\n issueId?: string;\n}\n\nexport interface ImportProjectOptions extends OpenIssueFlowDatabaseOptions {\n projectId: string;\n projectDir: string;\n projectRoot: string;\n remoteUrl: string | null;\n /** Number of pre-migration backups to retain. Defaults to five. */\n backupRetention?: number;\n /** Explicit database history retention. Omitted values retain every row. */\n retention?: {\n executions?: number;\n events?: number;\n snapshots?: number;\n };\n /** Import the potentially large legacy JSONL journal. Disabled by default. */\n withEvents?: boolean;\n onWarning?: (message: string) => void;\n}\n\nexport interface ImportProjectResult {\n imported: number;\n skipped: number;\n tableCounts: Record<string, number>;\n failed: boolean;\n}\n\nconst EMPTY_COUNTS: Record<string, number> = {\n projects: 0,\n issues: 0,\n pipelines: 0,\n stories: 0,\n executions: 0,\n events: 0,\n queues: 0,\n provider_health: 0,\n verifications: 0,\n pull_requests: 0,\n user_story_numbering: 0,\n};\n\nfunction digest(content: string): string {\n return createHash('sha256').update(content).digest('hex');\n}\n\nfunction stableId(prefix: string, value: string): string {\n return `${prefix}-${digest(value).slice(0, 24)}`;\n}\n\nasync function fileIfPresent(path: string): Promise<string | null> {\n try {\n return await readFile(path, 'utf-8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw error;\n }\n}\n\nasync function collectArtifacts(projectDir: string, withEvents: boolean): Promise<Artifact[]> {\n const artifacts: Artifact[] = [];\n const addJson = async (path: string, kind: Artifact['kind'], issueId?: string): Promise<void> => {\n const content = await fileIfPresent(path);\n if (content === null) return;\n artifacts.push({\n path,\n relativePath: relative(projectDir, path),\n sha256: digest(content),\n kind,\n issueId,\n value: JSON.parse(content),\n });\n };\n\n await addJson(join(projectDir, METADATA_FILENAME), 'metadata');\n await addJson(join(projectDir, PROVIDERS_HEALTH_FILENAME), 'health');\n\n const issuesDir = join(projectDir, ISSUES_DIR_NAME);\n let issueEntries: string[] = [];\n try {\n issueEntries = await readdir(issuesDir);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n for (const issueId of issueEntries) {\n const issueDir = join(issuesDir, issueId);\n await addJson(join(issueDir, 'tasks.json'), 'tasks', issueId);\n await addJson(join(issueDir, VERIFY_FILENAME), 'verify', issueId);\n if (withEvents) {\n for (const name of [ROTATED_EVENTS_FILENAME, EVENTS_FILENAME]) {\n const path = join(issueDir, name);\n const content = await fileIfPresent(path);\n if (content !== null) {\n artifacts.push({\n path,\n relativePath: relative(projectDir, path),\n sha256: digest(content),\n kind: 'events',\n issueId,\n value: parseJournal(content),\n });\n }\n }\n }\n }\n\n const queuesDir = join(projectDir, QUEUES_DIR_NAME);\n let queueEntries: string[] = [];\n try {\n queueEntries = await readdir(queuesDir);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n for (const queueId of queueEntries) {\n await addJson(join(queuesDir, queueId, 'execution-plan.json'), 'queue');\n }\n return artifacts;\n}\n\nfunction increment(counts: Record<string, number>, table: string): void {\n counts[table] = (counts[table] ?? 0) + 1;\n}\n\nfunction insertIssue(\n database: Awaited<ReturnType<typeof openIssueFlowDatabase>>,\n projectId: string,\n id: string,\n status: string,\n title: string | null,\n branchName: string | null,\n timestamp: string,\n counts: Record<string, number>,\n): void {\n database\n .prepare(\n `INSERT INTO issues (project_id, id, title, status, branch_name, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id, id) DO UPDATE SET\n title = excluded.title, status = excluded.status, branch_name = excluded.branch_name,\n updated_at = excluded.updated_at`,\n )\n .run(projectId, id, title, status, branchName, timestamp, timestamp);\n increment(counts, 'issues');\n}\n\nfunction importArtifact(\n database: Awaited<ReturnType<typeof openIssueFlowDatabase>>,\n artifact: Artifact,\n projectId: string,\n fallback: { root: string; remoteUrl: string | null; timestamp: string },\n counts: Record<string, number>,\n): void {\n const value = artifact.value as Record<string, unknown>;\n if (artifact.kind === 'metadata') {\n const metadata = projectMetadataSchema.parse(value);\n database\n .prepare(\n `INSERT INTO projects (id, root, remote_url, created_at, updated_at) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET root = excluded.root, remote_url = excluded.remote_url,\n updated_at = excluded.updated_at`,\n )\n .run(projectId, metadata.root, metadata.remoteUrl, metadata.createdAt, metadata.updatedAt);\n increment(counts, 'projects');\n if (metadata.userStoryNumbering !== undefined) {\n const decision = metadata.userStoryNumbering;\n database\n .prepare(\n `INSERT INTO user_story_numbering (project_id, next_number, source, issue_id, decided_at, detail)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(project_id) DO UPDATE SET next_number = excluded.next_number,\n source = excluded.source, issue_id = excluded.issue_id, decided_at = excluded.decided_at,\n detail = excluded.detail`,\n )\n .run(\n projectId,\n decision.nextNumber,\n decision.source,\n decision.issueNumber,\n decision.decidedAt,\n decision.detail ?? null,\n );\n increment(counts, 'user_story_numbering');\n }\n return;\n }\n if (artifact.kind === 'tasks') {\n // Some pre-plan legacy trees use `tasks.json` as a tiny placeholder. It\n // is not structured pipeline state yet, so leave it available for the\n // legacy reader instead of failing the project-wide SQLite import.\n const parsed = taskPlanSchema.safeParse(value);\n if (!parsed.success) return;\n const plan = parsed.data;\n const issueId = artifact.issueId ?? String(plan.issueNumber);\n writePlanRows(\n database,\n {\n tasksPath: artifact.path,\n projectId,\n issueId,\n projectRoot: fallback.root,\n },\n plan,\n );\n increment(counts, 'issues');\n increment(counts, 'pipelines');\n counts.stories += plan.userStories.length;\n for (const execution of plan.executions ?? []) {\n const cost = execution.cost.status === 'unknown' ? null : execution.cost.amount;\n database\n .prepare(\n `INSERT INTO executions\n (id, project_id, issue_id, status, started_at, finished_at, duration_ms, cost_status, cost_amount, payload_json)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET status = excluded.status, finished_at = excluded.finished_at,\n duration_ms = excluded.duration_ms, cost_status = excluded.cost_status,\n cost_amount = excluded.cost_amount, payload_json = excluded.payload_json`,\n )\n .run(\n execution.id,\n projectId,\n issueId,\n execution.status,\n execution.startedAt,\n execution.finishedAt,\n execution.durationMs,\n execution.cost.status,\n cost,\n JSON.stringify(execution),\n );\n increment(counts, 'executions');\n }\n if (plan.pullRequest !== undefined) {\n const id = stableId('pr', `${projectId}:${issueId}:${plan.pullRequest.number}`);\n database\n .prepare(\n `INSERT INTO pull_requests (id, project_id, issue_id, number, url, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET url = excluded.url, status = excluded.status`,\n )\n .run(\n id,\n projectId,\n issueId,\n plan.pullRequest.number,\n plan.pullRequest.url,\n 'created',\n plan.pullRequest.createdAt,\n );\n increment(counts, 'pull_requests');\n }\n return;\n }\n if (artifact.kind === 'health') {\n const health = providersHealthSchema.parse(value);\n for (const [provider, record] of Object.entries(health.providers)) {\n database\n .prepare(\n `INSERT INTO provider_health (project_id, provider, payload_json, updated_at) VALUES (?, ?, ?, ?)\n ON CONFLICT(project_id, provider) DO UPDATE SET payload_json = excluded.payload_json,\n updated_at = excluded.updated_at`,\n )\n .run(projectId, provider, JSON.stringify(record), fallback.timestamp);\n increment(counts, 'provider_health');\n }\n return;\n }\n if (artifact.kind === 'events') {\n const issueId = artifact.issueId ?? 'unknown';\n const runId = `legacy:${projectId}:${issueId}`;\n insertIssue(database, projectId, issueId, 'imported', null, null, fallback.timestamp, counts);\n database\n .prepare(\n `INSERT INTO runs (id, project_id, issue_id, status, started_at, finished_at, session_id)\n VALUES (?, ?, ?, 'imported', ?, ?, ?)\n ON CONFLICT(id) DO NOTHING`,\n )\n .run(runId, projectId, issueId, fallback.timestamp, fallback.timestamp, runId);\n let nextSequence =\n (database\n .prepare('SELECT COALESCE(MAX(sequence), 0) AS sequence FROM events WHERE run_id = ?')\n .get<{ sequence: number }>(runId)?.sequence ?? 0) + 1;\n for (const [index, entry] of (artifact.value as ReturnType<typeof parseJournal>).entries()) {\n const id = stableId('event', `${artifact.relativePath}:${index}`);\n const event = entry.event as { at?: string; type: string };\n database\n .prepare(\n `INSERT INTO events\n (id, project_id, run_id, occurred_at, kind, payload_json, session_id, sequence)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET occurred_at = excluded.occurred_at, kind = excluded.kind,\n payload_json = excluded.payload_json`,\n )\n .run(\n id,\n projectId,\n runId,\n event.at ?? fallback.timestamp,\n event.type,\n JSON.stringify(entry),\n runId,\n nextSequence++,\n );\n increment(counts, 'events');\n }\n return;\n }\n if (artifact.kind === 'verify') {\n insertIssue(\n database,\n projectId,\n artifact.issueId ?? 'unknown',\n 'imported',\n null,\n null,\n fallback.timestamp,\n counts,\n );\n const id = stableId('verify', artifact.relativePath);\n database\n .prepare(\n `INSERT INTO verifications (id, project_id, issue_id, status, created_at, payload_json)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET status = excluded.status, payload_json = excluded.payload_json`,\n )\n .run(\n id,\n projectId,\n artifact.issueId ?? null,\n String(value.verdict ?? 'unknown'),\n fallback.timestamp,\n JSON.stringify(value),\n );\n increment(counts, 'verifications');\n return;\n }\n\n const queue = value as {\n id: string;\n status: string;\n createdAt: string;\n updatedAt: string;\n issues: Array<{ id: string; position: number; status: string }>;\n pullRequest?: { number: number; url: string; createdAt: string };\n };\n database\n .prepare(\n `INSERT INTO queues (id, project_id, status, payload_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET status = excluded.status, payload_json = excluded.payload_json,\n updated_at = excluded.updated_at`,\n )\n .run(\n queue.id,\n projectId,\n queue.status,\n JSON.stringify(queue),\n queue.createdAt,\n queue.updatedAt,\n );\n increment(counts, 'queues');\n for (const entry of queue.issues) {\n insertIssue(database, projectId, entry.id, entry.status, null, null, queue.updatedAt, counts);\n database\n .prepare(\n `INSERT INTO queue_issues (queue_id, project_id, issue_id, position, status) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(queue_id, project_id, issue_id) DO UPDATE SET position = excluded.position,\n status = excluded.status`,\n )\n .run(queue.id, projectId, entry.id, entry.position, entry.status);\n }\n if (queue.pullRequest !== undefined) {\n const id = stableId('pr', `${projectId}:queue:${queue.id}:${queue.pullRequest.number}`);\n database\n .prepare(\n `INSERT INTO pull_requests (id, project_id, number, url, status, created_at) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET url = excluded.url, status = excluded.status`,\n )\n .run(\n id,\n projectId,\n queue.pullRequest.number,\n queue.pullRequest.url,\n 'created',\n queue.pullRequest.createdAt,\n );\n increment(counts, 'pull_requests');\n }\n}\n\nasync function retainBackups(directory: string, retention: number): Promise<void> {\n let names: string[] = [];\n try {\n names = (await readdir(directory)).filter(\n (name) => name.startsWith('issue-flow-') && name.endsWith('.db'),\n );\n } catch {\n return;\n }\n for (const name of names.sort().slice(0, Math.max(0, names.length - retention))) {\n // A retention cleanup is deliberately best-effort; losing an old backup\n // must never make a migration fail.\n await import('node:fs/promises').then(({ unlink }) =>\n unlink(join(directory, name)).catch(() => undefined),\n );\n }\n}\n\n/** Trim only explicitly limited project history; zero means retain all rows. */\nfunction applyRetention(\n database: Awaited<ReturnType<typeof openIssueFlowDatabase>>,\n projectId: string,\n retention: ImportProjectOptions['retention'],\n): void {\n const trim = (table: 'executions' | 'events' | 'snapshots', limit: number | undefined) => {\n if (limit === undefined || limit === 0) return;\n const timestamp =\n table === 'executions' ? 'started_at' : table === 'events' ? 'occurred_at' : 'updated_at';\n database\n .prepare(\n `DELETE FROM ${table} WHERE id IN (\n SELECT id FROM ${table} WHERE project_id = ?\n ORDER BY ${timestamp} DESC, rowid DESC LIMIT -1 OFFSET ?\n )`,\n )\n .run(projectId, limit);\n };\n trim('executions', retention?.executions);\n trim('events', retention?.events);\n trim('snapshots', retention?.snapshots);\n}\n\nexport const DEFAULT_BACKUP_RETENTION = 5;\n\nasync function prepareDatabase(options: ImportProjectOptions): Promise<void> {\n const path = getDatabasePath(options);\n if (!existsSync(path)) return;\n const raw = await openDatabase(path, options);\n try {\n const integrity = raw.integrityCheck();\n if (integrity !== 'ok') {\n raw.close();\n const quarantine = `${path}.corrupt-${Date.now()}`;\n await rename(path, quarantine);\n options.onWarning?.(\n `SQLite integrity check failed; isolated ${path} as ${quarantine} and will re-import preserved JSON artifacts.`,\n );\n return;\n }\n const version = Number(\n raw.prepare('PRAGMA user_version').get<{ user_version: number }>()?.user_version ?? 0,\n );\n // A database upgrade is destructive enough to deserve a recovery point\n // even when there are no JSON artifacts to import. An empty project may\n // still contain the only copy of its SQLite state.\n const backupDirectory = join(getDatabasePath(options).replace(/[/\\\\][^/\\\\]+$/, ''), 'backups');\n if (version < CURRENT_SCHEMA_VERSION) {\n const backup = join(backupDirectory, `issue-flow-${Date.now()}.db`);\n raw.backup(backup);\n }\n await retainBackups(backupDirectory, options.backupRetention ?? DEFAULT_BACKUP_RETENTION);\n } finally {\n try {\n raw.close();\n } catch {\n /* closed after quarantine */\n }\n }\n}\n\n/**\n * Import the structured JSON state already present in one resolved project tree.\n * Sources are read first and never written. A complete project is then applied\n * in one SQLite transaction, so a repeat after interruption is safe.\n */\nexport async function importProjectArtifacts(\n options: ImportProjectOptions,\n): Promise<ImportProjectResult> {\n const counts = { ...EMPTY_COUNTS };\n try {\n await prepareDatabase(options);\n const database = await openIssueFlowDatabase(options);\n try {\n const adopted = database\n .prepare('SELECT project_id FROM project_imports WHERE project_id = ?')\n .get<{ project_id: string }>(options.projectId);\n const artifacts = (\n await collectArtifacts(options.projectDir, options.withEvents === true)\n ).filter((artifact) => {\n if (adopted === undefined) return true;\n if (options.withEvents === true) return artifact.kind === 'events';\n // tasks.json is the portable interchange boundary. A Skill may update\n // it between CLI processes; the stored projection hash below makes an\n // unchanged CLI materialization free while importing real external drift.\n return artifact.kind === 'tasks';\n });\n let imported = 0;\n let skipped = 0;\n database.transaction(() => {\n database\n .prepare(\n `INSERT INTO projects (id, root, remote_url, created_at, updated_at) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET root = excluded.root, remote_url = excluded.remote_url,\n updated_at = excluded.updated_at`,\n )\n .run(\n options.projectId,\n options.projectRoot,\n options.remoteUrl,\n new Date().toISOString(),\n new Date().toISOString(),\n );\n increment(counts, 'projects');\n for (const artifact of artifacts) {\n const previous = database\n .prepare('SELECT sha256 FROM migrated_artifacts WHERE source_path = ?')\n .get<{ sha256: string }>(artifact.path);\n // Reprocess an otherwise unchanged legacy tasks file exactly once\n // when it predates the relational plan columns, so upgrading does\n // not require touching the source artifact.\n const requiresPlanProjection =\n artifact.kind === 'tasks' &&\n taskPlanSchema.safeParse(artifact.value).success &&\n database\n .prepare('SELECT project FROM pipelines WHERE project_id = ? AND issue_id = ?')\n .get<{ project: string | null }>(options.projectId, artifact.issueId ?? '')\n ?.project === null;\n if (previous?.sha256 === artifact.sha256 && !requiresPlanProjection) {\n skipped++;\n continue;\n }\n importArtifact(\n database,\n artifact,\n options.projectId,\n {\n root: options.projectRoot,\n remoteUrl: options.remoteUrl,\n timestamp: new Date().toISOString(),\n },\n counts,\n );\n database\n .prepare(\n `INSERT INTO migrated_artifacts (source_path, sha256, migrated_at, table_counts_json)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(source_path) DO UPDATE SET sha256 = excluded.sha256,\n migrated_at = excluded.migrated_at, table_counts_json = excluded.table_counts_json`,\n )\n .run(artifact.path, artifact.sha256, new Date().toISOString(), JSON.stringify(counts));\n imported++;\n }\n applyRetention(database, options.projectId, options.retention);\n database\n .prepare(\n `INSERT INTO project_imports (project_id, completed_at) VALUES (?, ?)\n ON CONFLICT(project_id) DO NOTHING`,\n )\n .run(options.projectId, new Date().toISOString());\n });\n return { imported, skipped, tableCounts: counts, failed: false };\n } finally {\n database.close();\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // These are database-opening compatibility failures, not failed imports.\n // Leave the database in place so the actionable driver/schema error is not\n // turned into a destructive-looking recovery event.\n if (\n message.includes('newer than this Issue Flow supports') ||\n message.includes('SQLite storage requires Node.js')\n ) {\n throw error;\n }\n const path = getDatabasePath(options);\n if (existsSync(path)) {\n const failed = `${path}.failed-${Date.now()}`;\n try {\n await rename(path, failed);\n options.onWarning?.(\n `SQLite import failed; preserved the database for diagnosis as ${basename(failed)}. Continuing with the intact JSON storage.`,\n );\n } catch {\n // The original import error remains the useful diagnostic.\n }\n }\n options.onWarning?.(`SQLite import skipped: ${message}`);\n return { imported: 0, skipped: 0, tableCounts: counts, failed: true };\n }\n}\n","import { stat } from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport {\n loadGlobalConfig,\n PROJECT_CONFIG_FILENAME,\n readProjectConfigFile,\n} from '../config/sources.js';\nimport { bindTelemetry } from '../telemetry/recorder.js';\nimport { printInfo } from '../ui/logger.js';\nimport { getProjectRoot } from '../utils/git.js';\nimport {\n directoryExists,\n ensureWorkspaceStorageIgnored,\n selectArtifactStorage,\n WORKSPACE_STORAGE_DIR,\n} from './artifact-storage.js';\nimport { type MigrationResult, migrateLegacyStorage, resolveStorageMode } from './compat.js';\nimport { importProjectArtifacts } from './db/import.js';\nimport {\n getDatabasePath,\n registerProjectDatabaseOptions,\n resetProjectDatabaseOptions,\n} from './db/index.js';\nimport {\n registerPlanRepository,\n registerQueueRepository,\n registerStorageProjections,\n resetPlanRepositories,\n type StoredRetentionPolicy,\n} from './db/repository.js';\nimport {\n type GetGlobalRootOptions,\n GLOBAL_ROOT_ENV,\n getGlobalRoot,\n getIssuePathsAt,\n getQueuePathsAt,\n ISSUES_DIR_NAME,\n type IssuePaths,\n PROVIDERS_HEALTH_FILENAME,\n type QueuePaths,\n RUN_LOCK_FILENAME,\n} from './paths.js';\nimport { storageConfigInputSchema } from './schemas.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. When a repository\n * has explicitly opted into workspace storage by creating\n * `.issue-flow/issues/`, resolution only maintains a scoped nested `.gitignore`\n * so operational state cannot be staged accidentally.\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 /** Diagnostics are separate from structured command output. */\n notice?: (message: string) => void;\n}\n\n/** Everything the process needs to know about a project, resolved once. */\ninterface ProjectResolution {\n projectId: string;\n legacyDir: string;\n projectDir: string;\n storageMode: 'global' | 'workspace';\n databaseOptions: GetGlobalRootOptions;\n /** JSON remains active after a failed import or an explicit compatibility setting. */\n driver: 'sqlite' | 'json';\n retention?: StoredRetentionPolicy;\n}\n\ninterface StorageResolutionConfig {\n driver: 'sqlite' | 'json';\n backupRetention?: number;\n retention?: StoredRetentionPolicy & { backups?: number };\n}\n\n/** Resolve only the storage knobs; invalid project input safely uses defaults. */\nasync function loadStorageResolutionConfig(\n projectRoot: string,\n options: ResolveIssuePathsOptions,\n): Promise<StorageResolutionConfig> {\n const global = await loadGlobalConfig({ env: options.env, warn: options.notice ?? printInfo });\n const project = await readProjectConfigFile(projectRoot, options.notice ?? printInfo);\n const parsedProject = storageConfigInputSchema.safeParse(project?.storage);\n if (!parsedProject.success && project?.storage !== undefined) {\n (options.notice ?? printInfo)(\n `Ignoring \"storage\" key of ${PROJECT_CONFIG_FILENAME}: ${parsedProject.error.issues[0]?.message ?? 'invalid value'}.`,\n );\n }\n return {\n driver: parsedProject.success\n ? (parsedProject.data.driver ?? global.storage?.driver ?? 'sqlite')\n : (global.storage?.driver ?? 'sqlite'),\n backupRetention:\n (parsedProject.success ? parsedProject.data.backupRetention : undefined) ??\n global.storage?.backupRetention,\n retention:\n (parsedProject.success ? parsedProject.data.retention : undefined) ??\n global.storage?.retention,\n };\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 active 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 // The path-to-repository registry has the same process lifetime as this\n // resolver cache. Clearing one without the other would leak a test's\n // temporary database into the next resolution.\n resetPlanRepositories();\n resetProjectDatabaseOptions();\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\nfunction databaseOptionsFor(\n projectRoot: string,\n storageMode: 'global' | 'workspace',\n options: ResolveIssuePathsOptions,\n): GetGlobalRootOptions {\n if (storageMode === 'global') return options.env === undefined ? {} : { env: options.env };\n return {\n env: {\n ...(options.env ?? process.env),\n [GLOBAL_ROOT_ENV]: join(projectRoot, WORKSPACE_STORAGE_DIR),\n },\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, notice = printInfo): 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 notice(`Migrated ${count} file${count === 1 ? '' : 's'} from ${result.legacyDir} to ${target}`);\n notice(\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: ResolveIssuePathsOptions,\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 const storage = await loadStorageResolutionConfig(projectRoot, options);\n const workspaceIssuesDir = join(projectRoot, WORKSPACE_STORAGE_DIR, ISSUES_DIR_NAME);\n const selected = selectArtifactStorage(\n projectRoot,\n getGlobalRoot(options),\n status.projectId,\n await directoryExists(workspaceIssuesDir),\n );\n const { storageMode, projectDir } = selected;\n const databaseOptions = databaseOptionsFor(projectRoot, storageMode, options);\n registerProjectDatabaseOptions(status.projectId, databaseOptions);\n\n if (storageMode === 'workspace') {\n await ensureWorkspaceStorageIgnored(projectRoot);\n } else if (status.mode === 'needs-migration') {\n announceMigration(\n await migrateLegacyStorage(projectRoot, options),\n options.notice ?? printInfo,\n );\n }\n\n const imported =\n storage.driver === 'sqlite' && !(await pathExists(join(projectDir, RUN_LOCK_FILENAME)))\n ? await importProjectArtifacts({\n ...databaseOptions,\n projectId: status.projectId,\n projectDir,\n projectRoot,\n remoteUrl: status.remoteUrl,\n ...((storage.retention?.backups ?? storage.backupRetention) === undefined\n ? {}\n : { backupRetention: storage.retention?.backups ?? storage.backupRetention }),\n ...(storage.retention === undefined ? {} : { retention: storage.retention }),\n onWarning: options.notice ?? printInfo,\n })\n : null;\n if (imported !== null && imported.imported > 0) {\n const counts = Object.entries(imported.tableCounts)\n .filter(([, count]) => count > 0)\n .map(([table, count]) => `${table}: ${count}`)\n .join(', ');\n (options.notice ?? printInfo)(\n `Imported ${imported.imported} structured artifact${imported.imported === 1 ? '' : 's'} from ${projectDir} into ${getDatabasePath(databaseOptions)} (${counts || 'no rows'}). No source artifacts were removed.`,\n );\n }\n\n return {\n projectId: status.projectId,\n legacyDir: status.legacyDir,\n projectDir,\n storageMode,\n databaseOptions,\n // An import failure is a successful *resolution* through the JSON\n // compatibility path. Do not subsequently register a SQLite repository,\n // or telemetry would create a fresh empty database and hide the fallback.\n driver: imported?.failed === true ? 'json' : storage.driver,\n retention: storage.retention,\n };\n}\n\n/** Cached half of {@link resolveIssuePaths}: one git call per project, per process. */\nfunction getProjectResolution(\n projectRoot: string,\n options: ResolveIssuePathsOptions,\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 active store. */\nexport interface ProjectStoragePaths {\n /** Deterministic id derived from the repository's remote (or its path). */\n projectId: string;\n /** Active structured-state driver after migration/recovery resolution. */\n storageDriver: 'sqlite' | 'json';\n /** Whether the explicit workspace override or the global fallback is active. */\n storageMode: 'global' | 'workspace';\n /** Database location selected with the artifact store. */\n databaseOptions: GetGlobalRootOptions;\n /** Global `<root>/projects/<projectId>` or opted-in `<workspace>/.issue-flow`. */\n projectDir: string;\n /** `<projectDir>/issues` — the parent of every issue directory. */\n issuesDir: string;\n /**\n * `<projectDir>/run.lock` — who is running in this project right now.\n *\n * Project-level rather than per issue on purpose: two runs in one repository\n * share a working tree and a branch, so \"a different issue\" is not a\n * different lock.\n */\n runLockFile: string;\n /** `<projectDir>/providers.json` — persisted health/cooldown per agent provider. */\n providersHealthFile: 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 = project.projectDir;\n const providersHealthFile = join(projectDir, PROVIDERS_HEALTH_FILENAME);\n if (project.driver === 'sqlite') {\n registerStorageProjections({\n context: {\n tasksPath: '',\n projectId: project.projectId,\n issueId: '',\n projectRoot,\n databaseOptions: project.databaseOptions,\n retention: project.retention,\n },\n providersHealthFile,\n });\n }\n\n return {\n projectId: project.projectId,\n storageDriver: project.driver,\n storageMode: project.storageMode,\n databaseOptions: project.databaseOptions,\n projectDir,\n issuesDir: join(projectDir, ISSUES_DIR_NAME),\n runLockFile: join(projectDir, RUN_LOCK_FILENAME),\n providersHealthFile,\n };\n}\n\n/**\n * Resolve the paths of a multi-issue execution queue in the current project.\n *\n * Shares the project cache with {@link resolveIssuePaths}, so a run that\n * resolves both still costs a single git call. There is no legacy tree for\n * queues — the concept did not exist before — so no migration is attempted.\n */\nexport async function resolveQueuePaths(\n queueId: string | number,\n options: ResolveIssuePathsOptions = {},\n): Promise<QueuePaths> {\n const { projectRoot: projectRootOption, ...rootOptions } = options;\n const projectRoot = resolve(projectRootOption ?? (await getProjectRoot()));\n\n const project = await getProjectResolution(projectRoot, rootOptions);\n const paths = getQueuePathsAt(project.projectDir, queueId);\n if (project.driver === 'sqlite') {\n registerQueueRepository({\n planFile: paths.planFile,\n projectId: project.projectId,\n projectRoot,\n databaseOptions: project.databaseOptions,\n retention: project.retention,\n });\n }\n return paths;\n}\n\n/**\n * Resolve every path of `issueNumber` under the selected store, migrating the\n * legacy `<projectRoot>/issues/` tree only while global storage is active.\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 = getIssuePathsAt(project.projectDir, issueNumber);\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 (\n project.storageMode === 'global' &&\n !(await directoryExists(paths.issueDir)) &&\n (await directoryExists(legacyIssueDir))\n ) {\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 if (project.driver === 'sqlite') {\n const context = {\n tasksPath: paths.tasksFile,\n projectId: project.projectId,\n issueId: basename(paths.issueDir),\n projectRoot,\n databaseOptions: project.databaseOptions,\n retention: project.retention,\n };\n registerPlanRepository(context);\n registerStorageProjections({\n context,\n verifyFile: paths.verifyFile,\n lastBranchFile: paths.lastBranchFile,\n providersHealthFile: join(project.projectDir, PROVIDERS_HEALTH_FILENAME),\n });\n bindTelemetry({ tasksPath: paths.tasksFile });\n } else {\n // The legacy state manager remains authoritative in JSON mode. Leaving a\n // telemetry binding here would lazily bootstrap a SQLite projection.\n bindTelemetry(null);\n }\n return paths;\n}\n","import { readFile, stat, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport const WORKSPACE_STORAGE_DIR = '.issue-flow';\n\nexport const WORKSPACE_IGNORE_BLOCK = [\n '# Issue Flow operational storage (managed)',\n '/issues/',\n '/queues/',\n '/issue-flow.db',\n '/issue-flow.db-*',\n '/run.lock',\n '/providers.json',\n '/metadata.json',\n '/backups/',\n '/.gitignore',\n].join('\\n');\n\nexport async function directoryExists(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\n/** Select one complete artifact store; individual files never mix roots. */\nexport function selectArtifactStorage(\n projectRoot: string,\n globalRoot: string,\n projectId: string,\n workspaceSelected: boolean,\n): { storageMode: 'global' | 'workspace'; projectDir: string } {\n return workspaceSelected\n ? { storageMode: 'workspace', projectDir: join(projectRoot, WORKSPACE_STORAGE_DIR) }\n : { storageMode: 'global', projectDir: join(globalRoot, 'projects', projectId) };\n}\n\n/** Protect an explicitly selected workspace store without hiding prompt overrides. */\nexport async function ensureWorkspaceStorageIgnored(projectRoot: string): Promise<void> {\n const path = join(projectRoot, WORKSPACE_STORAGE_DIR, '.gitignore');\n let current = '';\n try {\n current = await readFile(path, 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n if (\n WORKSPACE_IGNORE_BLOCK.split('\\n')\n .slice(1)\n .every((line) => current.includes(line))\n )\n return;\n const prefix = current.length === 0 ? '' : `${current.trimEnd()}\\n\\n`;\n await writeFile(path, `${prefix}${WORKSPACE_IGNORE_BLOCK}\\n`, 'utf8');\n}\n"],"mappings":"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","names":["dirname","dirname","existsSync","readFile","rename","join","readFile","join","unlink","existsSync","rename","stat","basename","join","readFile","stat","join","stat","join","basename"]}
|
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
} from "./chunk-P3LRUNC6.js";
|
|
5
5
|
import {
|
|
6
6
|
writeFileAtomic
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-PFWHHYEO.js";
|
|
8
8
|
|
|
9
9
|
// src/core/session/publishers.ts
|
|
10
|
-
import { utimes } from "fs/promises";
|
|
10
|
+
import { utimes } from "node:fs/promises";
|
|
11
11
|
|
|
12
12
|
// src/core/session/events.ts
|
|
13
13
|
var DEFAULT_LOG_LIMIT = 200;
|
|
@@ -102,7 +102,7 @@ function applyGitEvent(snapshot, event) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// src/core/session/reducer-log.ts
|
|
105
|
-
import { stripVTControlCharacters } from "util";
|
|
105
|
+
import { stripVTControlCharacters } from "node:util";
|
|
106
106
|
function applyLogEvent(snapshot, event, options) {
|
|
107
107
|
switch (event.type) {
|
|
108
108
|
case "log": {
|
|
@@ -189,7 +189,7 @@ function applyMetricsEvent(snapshot, event) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// src/core/session/reducer-phase.ts
|
|
192
|
-
import { stripVTControlCharacters as stripVTControlCharacters2 } from "util";
|
|
192
|
+
import { stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
|
|
193
193
|
|
|
194
194
|
// src/core/session/reducer-stage.ts
|
|
195
195
|
function isTerminalStage(stage) {
|
|
@@ -487,7 +487,7 @@ function applyResilienceEvent(snapshot, event) {
|
|
|
487
487
|
}
|
|
488
488
|
|
|
489
489
|
// src/core/session/reducer-session.ts
|
|
490
|
-
import { stripVTControlCharacters as stripVTControlCharacters3 } from "util";
|
|
490
|
+
import { stripVTControlCharacters as stripVTControlCharacters3 } from "node:util";
|
|
491
491
|
function applySessionEvent(snapshot, event) {
|
|
492
492
|
switch (event.type) {
|
|
493
493
|
case "session:start": {
|
|
@@ -878,6 +878,7 @@ function getSessionPublisher() {
|
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
export {
|
|
881
|
+
DEFAULT_SESSION_HEARTBEAT_MS,
|
|
881
882
|
createInitialSnapshot,
|
|
882
883
|
reduceSessionEvent,
|
|
883
884
|
NullPublisher,
|
|
@@ -886,4 +887,4 @@ export {
|
|
|
886
887
|
setSessionPublisher,
|
|
887
888
|
getSessionPublisher
|
|
888
889
|
};
|
|
889
|
-
//# sourceMappingURL=chunk-
|
|
890
|
+
//# sourceMappingURL=chunk-DIMDAYZS.js.map
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
ensureProvidersRegistered,
|
|
4
|
+
getProvider,
|
|
5
|
+
getRegisteredSources
|
|
6
|
+
} from "./chunk-T736DSZI.js";
|
|
7
|
+
import {
|
|
8
|
+
loadIssuesConfig
|
|
9
|
+
} from "./chunk-YC24E66P.js";
|
|
10
|
+
import {
|
|
11
|
+
printInfo,
|
|
12
|
+
printWarning
|
|
13
|
+
} from "./chunk-DBLXLKGU.js";
|
|
14
|
+
import {
|
|
15
|
+
actionOf,
|
|
16
|
+
failureOf
|
|
17
|
+
} from "./chunk-KUA6KBBD.js";
|
|
18
|
+
|
|
19
|
+
// src/issues/resolver.ts
|
|
20
|
+
import { createInterface } from "node:readline";
|
|
21
|
+
var IssueResolutionError = class extends Error {
|
|
22
|
+
exitCode;
|
|
23
|
+
/**
|
|
24
|
+
* The verdict of the origin that failed, when one origin failed for a reason
|
|
25
|
+
* worth naming. It is what tells "this Issue does not exist" apart from
|
|
26
|
+
* "GitHub could not be reached", which are the same message today.
|
|
27
|
+
*/
|
|
28
|
+
failure;
|
|
29
|
+
/** What a human has to do, for a failure that waiting cannot fix. */
|
|
30
|
+
action;
|
|
31
|
+
constructor(message, exitCode = 1, details = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "IssueResolutionError";
|
|
34
|
+
this.exitCode = exitCode;
|
|
35
|
+
this.failure = details.failure;
|
|
36
|
+
this.action = details.action;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
40
|
+
var SOURCE_LABELS = {
|
|
41
|
+
github: "GitHub",
|
|
42
|
+
local: "local"
|
|
43
|
+
};
|
|
44
|
+
function sourceLabel(source) {
|
|
45
|
+
return SOURCE_LABELS[source] ?? source;
|
|
46
|
+
}
|
|
47
|
+
function promptLabel(source) {
|
|
48
|
+
const label = sourceLabel(source);
|
|
49
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
50
|
+
}
|
|
51
|
+
function errorMessage(err) {
|
|
52
|
+
return err instanceof Error ? err.message : String(err);
|
|
53
|
+
}
|
|
54
|
+
async function fetchCandidate(source, id, warn) {
|
|
55
|
+
let provider;
|
|
56
|
+
try {
|
|
57
|
+
provider = getProvider(source);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { source, issue: null, reason: errorMessage(err) };
|
|
60
|
+
}
|
|
61
|
+
let availability;
|
|
62
|
+
try {
|
|
63
|
+
availability = await provider.checkAvailability?.() ?? {
|
|
64
|
+
available: await provider.isAvailable()
|
|
65
|
+
};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { source, issue: null, reason: `provider unavailable (${errorMessage(err)})` };
|
|
68
|
+
}
|
|
69
|
+
if (!availability.available) {
|
|
70
|
+
const reason = availability.failure?.message ?? "provider unavailable";
|
|
71
|
+
return {
|
|
72
|
+
source,
|
|
73
|
+
issue: null,
|
|
74
|
+
reason,
|
|
75
|
+
...availability.failure === void 0 ? {} : { failure: availability.failure },
|
|
76
|
+
...availability.action === void 0 ? {} : { action: availability.action }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const issue = await provider.get(id);
|
|
81
|
+
return { source, issue, reason: issue === null ? "not found" : null };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const reason = errorMessage(err);
|
|
84
|
+
warn(`Could not read issue '${id}' from ${sourceLabel(source)}: ${reason}`);
|
|
85
|
+
const failure = failureOf(err);
|
|
86
|
+
const action = actionOf(err);
|
|
87
|
+
return {
|
|
88
|
+
source,
|
|
89
|
+
issue: null,
|
|
90
|
+
reason,
|
|
91
|
+
...failure === null ? {} : { failure },
|
|
92
|
+
...action === null ? {} : { action }
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function shortHash(hash) {
|
|
97
|
+
const hex = hash.startsWith("sha256:") ? hash.slice("sha256:".length) : hash;
|
|
98
|
+
return hex.slice(0, 12);
|
|
99
|
+
}
|
|
100
|
+
function describeCandidate(source, issue) {
|
|
101
|
+
const lines = issue.body.length === 0 ? 0 : issue.body.split("\n").length;
|
|
102
|
+
return ` ${sourceLabel(source).padEnd(6)} title: "${issue.title}" | body: ${lines} line(s), ${issue.body.length} char(s) | updated: ${issue.updatedAt} | hash: ${shortHash(issue.contentHash)}`;
|
|
103
|
+
}
|
|
104
|
+
function listSources(candidates) {
|
|
105
|
+
const labels = candidates.map((candidate) => sourceLabel(candidate.source));
|
|
106
|
+
if (labels.length <= 1) {
|
|
107
|
+
return labels.join("");
|
|
108
|
+
}
|
|
109
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
110
|
+
}
|
|
111
|
+
function createLineReader(rl, output) {
|
|
112
|
+
const queued = [];
|
|
113
|
+
let pending = null;
|
|
114
|
+
let closed = false;
|
|
115
|
+
rl.on("line", (line) => {
|
|
116
|
+
if (pending !== null) {
|
|
117
|
+
const resolve = pending;
|
|
118
|
+
pending = null;
|
|
119
|
+
resolve(line);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
queued.push(line);
|
|
123
|
+
});
|
|
124
|
+
rl.on("close", () => {
|
|
125
|
+
closed = true;
|
|
126
|
+
if (pending !== null) {
|
|
127
|
+
const resolve = pending;
|
|
128
|
+
pending = null;
|
|
129
|
+
resolve(null);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return (query) => new Promise((resolve) => {
|
|
133
|
+
const buffered = queued.shift();
|
|
134
|
+
if (buffered !== void 0) {
|
|
135
|
+
resolve(buffered);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (closed) {
|
|
139
|
+
resolve(null);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
output.write(query);
|
|
143
|
+
pending = resolve;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async function promptChoice(sources, stdin, stdout, warn) {
|
|
147
|
+
const options = sources.map((source, index) => `[${index + 1}] ${promptLabel(source)}`);
|
|
148
|
+
const cancelOption = sources.length + 1;
|
|
149
|
+
const query = `Which version should be used? ${options.join(" ")} [${cancelOption}] Cancel: `;
|
|
150
|
+
const numbers = Array.from({ length: cancelOption }, (_, index) => String(index + 1));
|
|
151
|
+
const accepted = `${numbers.slice(0, -1).join(", ")} or ${numbers[numbers.length - 1]}`;
|
|
152
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
153
|
+
const ask = createLineReader(rl, stdout);
|
|
154
|
+
try {
|
|
155
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
|
|
156
|
+
const answer = await ask(query);
|
|
157
|
+
if (answer === null) {
|
|
158
|
+
return "cancel";
|
|
159
|
+
}
|
|
160
|
+
const choice = answer.trim();
|
|
161
|
+
const picked = sources[Number(choice) - 1];
|
|
162
|
+
if (choice.length > 0 && picked !== void 0) {
|
|
163
|
+
return picked;
|
|
164
|
+
}
|
|
165
|
+
if (choice === String(cancelOption)) {
|
|
166
|
+
return "cancel";
|
|
167
|
+
}
|
|
168
|
+
warn(`Invalid choice: '${choice}'. Enter ${accepted}.`);
|
|
169
|
+
}
|
|
170
|
+
return "cancel";
|
|
171
|
+
} finally {
|
|
172
|
+
rl.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function isInteractiveByDefault() {
|
|
176
|
+
const ci = process.env.CI;
|
|
177
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
178
|
+
return Boolean(process.stdin.isTTY) && !inCi;
|
|
179
|
+
}
|
|
180
|
+
function buildResolved(issue, source, local, github, divergent) {
|
|
181
|
+
return { issue, source, local, github, divergent };
|
|
182
|
+
}
|
|
183
|
+
function policyTarget(policy) {
|
|
184
|
+
if (policy === "prefer-local") {
|
|
185
|
+
return "local";
|
|
186
|
+
}
|
|
187
|
+
if (policy === "prefer-github") {
|
|
188
|
+
return "github";
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
async function resolveIssue(id, opts = {}) {
|
|
193
|
+
const info = opts.info ?? printInfo;
|
|
194
|
+
const warn = opts.warn ?? printWarning;
|
|
195
|
+
const config = opts.config ?? await loadIssuesConfig({ warn });
|
|
196
|
+
ensureProvidersRegistered();
|
|
197
|
+
const sources = opts.sources ?? getRegisteredSources();
|
|
198
|
+
const candidates = await Promise.all(
|
|
199
|
+
sources.map((source) => fetchCandidate(source, id, warn))
|
|
200
|
+
);
|
|
201
|
+
const issueOf = (source) => candidates.find((candidate) => candidate.source === source)?.issue ?? null;
|
|
202
|
+
const local = issueOf("local");
|
|
203
|
+
const github = issueOf("github");
|
|
204
|
+
const found = candidates.filter(
|
|
205
|
+
(candidate) => candidate.issue !== null
|
|
206
|
+
);
|
|
207
|
+
if (found.length === 0) {
|
|
208
|
+
const details = candidates.map((candidate) => `${sourceLabel(candidate.source)}: ${candidate.reason ?? "not found"}`).join("; ");
|
|
209
|
+
const where = details.length > 0 ? ` (${details})` : "";
|
|
210
|
+
const blocking = candidates.find((candidate) => candidate.action !== void 0);
|
|
211
|
+
if (blocking !== void 0) {
|
|
212
|
+
throw new IssueResolutionError(
|
|
213
|
+
`Cannot read issue '${id}' from ${sourceLabel(blocking.source)}: ${blocking.reason}`,
|
|
214
|
+
1,
|
|
215
|
+
{
|
|
216
|
+
...blocking.failure === void 0 ? {} : { failure: blocking.failure },
|
|
217
|
+
...blocking.action === void 0 ? {} : { action: blocking.action }
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
throw new IssueResolutionError(`Issue '${id}' not found in any registered origin${where}`);
|
|
222
|
+
}
|
|
223
|
+
const build = (candidate, divergent) => buildResolved(candidate.issue, candidate.source, local, github, divergent);
|
|
224
|
+
const firstFound = found[0];
|
|
225
|
+
if (found.length === 1) {
|
|
226
|
+
return build(firstFound, false);
|
|
227
|
+
}
|
|
228
|
+
const preferred = found.find((candidate) => candidate.source === config.preferredProvider) ?? firstFound;
|
|
229
|
+
const distinct = new Set(found.map((candidate) => candidate.issue.contentHash));
|
|
230
|
+
if (distinct.size === 1) {
|
|
231
|
+
info(
|
|
232
|
+
`Issue '${id}' has identical content in ${listSources(found)}; using ${sourceLabel(preferred.source)}.`
|
|
233
|
+
);
|
|
234
|
+
return build(preferred, false);
|
|
235
|
+
}
|
|
236
|
+
info(`Issue '${id}' differs between origins:`);
|
|
237
|
+
for (const candidate of found) {
|
|
238
|
+
info(describeCandidate(candidate.source, candidate.issue));
|
|
239
|
+
}
|
|
240
|
+
const target = policyTarget(config.conflictPolicy);
|
|
241
|
+
if (target !== null) {
|
|
242
|
+
const chosen = found.find((candidate) => candidate.source === target);
|
|
243
|
+
if (chosen !== void 0) {
|
|
244
|
+
info(`Conflict policy '${config.conflictPolicy}': using the ${sourceLabel(target)} version.`);
|
|
245
|
+
return build(chosen, true);
|
|
246
|
+
}
|
|
247
|
+
warn(
|
|
248
|
+
`Conflict policy '${config.conflictPolicy}' does not apply to Issue '${id}' (${sourceLabel(target)} has no version of it); using ${sourceLabel(preferred.source)}.`
|
|
249
|
+
);
|
|
250
|
+
return build(preferred, true);
|
|
251
|
+
}
|
|
252
|
+
const interactive = opts.interactive ?? isInteractiveByDefault();
|
|
253
|
+
if (!interactive) {
|
|
254
|
+
warn(
|
|
255
|
+
`Divergent Issue '${id}' and conflict policy 'ask' in a non-interactive environment; using the preferred provider (${sourceLabel(preferred.source)}).`
|
|
256
|
+
);
|
|
257
|
+
return build(preferred, true);
|
|
258
|
+
}
|
|
259
|
+
const choice = await promptChoice(
|
|
260
|
+
found.map((candidate) => candidate.source),
|
|
261
|
+
opts.stdin ?? process.stdin,
|
|
262
|
+
opts.stdout ?? process.stdout,
|
|
263
|
+
warn
|
|
264
|
+
);
|
|
265
|
+
if (choice === "cancel") {
|
|
266
|
+
throw new IssueResolutionError(
|
|
267
|
+
`Cancelled: Issue '${id}' diverges between ${listSources(found)}.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const picked = found.find((candidate) => candidate.source === choice);
|
|
271
|
+
return build(picked, true);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
IssueResolutionError,
|
|
276
|
+
resolveIssue
|
|
277
|
+
};
|
|
278
|
+
//# sourceMappingURL=chunk-DZ3DZ3EN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/issues/resolver.ts"],"sourcesContent":["import { createInterface, type Interface as ReadlineInterface } from 'node:readline';\nimport { loadIssuesConfig } from '../config.js';\nimport { actionOf, type ClassifiedFailure, failureOf } from '../resilience/errors.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport { ensureProvidersRegistered } from './bootstrap.js';\nimport type { ProviderAvailability } from './provider.js';\nimport { getProvider, getRegisteredSources } from './registry.js';\nimport type { Issue, IssueSource, IssuesConfig, ResolvedIssue } from './types.js';\n\n/**\n * Failure to settle on an Issue. Carries the exit code the CLI should return,\n * so callers propagate it instead of inventing their own.\n */\nexport class IssueResolutionError extends Error {\n readonly exitCode: number;\n /**\n * The verdict of the origin that failed, when one origin failed for a reason\n * worth naming. It is what tells \"this Issue does not exist\" apart from\n * \"GitHub could not be reached\", which are the same message today.\n */\n readonly failure: ClassifiedFailure | undefined;\n /** What a human has to do, for a failure that waiting cannot fix. */\n readonly action: string | undefined;\n\n constructor(\n message: string,\n exitCode = 1,\n details: { failure?: ClassifiedFailure; action?: string } = {},\n ) {\n super(message);\n this.name = 'IssueResolutionError';\n this.exitCode = exitCode;\n this.failure = details.failure;\n this.action = details.action;\n }\n}\n\nexport interface ResolveIssueOptions {\n /** Already-resolved provider configuration. Defaults to loadIssuesConfig(). */\n config?: IssuesConfig;\n /** Origins to query. Defaults to every registered source. */\n sources?: IssueSource[];\n /**\n * Whether a prompt may be shown. Defaults to a real TTY outside CI, which is\n * what keeps `ask` from ever blocking a headless run.\n */\n interactive?: boolean;\n /** Input stream for the conflict prompt. Defaults to process.stdin. */\n stdin?: NodeJS.ReadableStream;\n /** Output stream for the conflict prompt. Defaults to process.stdout. */\n stdout?: NodeJS.WritableStream;\n /** Informational sink. Defaults to printInfo. */\n info?: (message: string) => void;\n /** Warning sink. Defaults to printWarning. */\n warn?: (message: string) => void;\n}\n\n/** How many invalid answers are tolerated before the prompt gives up. */\nconst MAX_PROMPT_ATTEMPTS = 3;\n\n/**\n * Labels shown to users for the built-in origins. An origin registered from\n * outside this package is displayed under its own name, so a new provider needs\n * no entry here.\n */\nconst SOURCE_LABELS: Record<string, string> = {\n github: 'GitHub',\n local: 'local',\n};\n\nfunction sourceLabel(source: IssueSource): string {\n return SOURCE_LABELS[source] ?? source;\n}\n\n/** Same label, capitalized for the numbered prompt ('local' -> 'Local'). */\nfunction promptLabel(source: IssueSource): string {\n const label = sourceLabel(source);\n return label.charAt(0).toUpperCase() + label.slice(1);\n}\n\ninterface Candidate {\n source: IssueSource;\n issue: Issue | null;\n /** Why the origin produced nothing, used only to explain a total miss. */\n reason: string | null;\n /** The classified verdict behind `reason`, when the origin could give one. */\n failure?: ClassifiedFailure;\n /** What a human has to do about it (`gh auth login`), when that applies. */\n action?: string;\n}\n\n/** A candidate that actually produced an Issue. */\ninterface FoundCandidate extends Candidate {\n issue: Issue;\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Read one origin without letting it break the others.\n *\n * An unavailable provider or a failed read degrades to \"no candidate\" plus a\n * reason: as long as another origin has the Issue the pipeline keeps going, and\n * when nothing is found the reasons are surfaced in the final error so a broken\n * environment is never reported as a missing Issue.\n */\nasync function fetchCandidate(\n source: IssueSource,\n id: string,\n warn: (message: string) => void,\n): Promise<Candidate> {\n let provider: ReturnType<typeof getProvider>;\n try {\n provider = getProvider(source);\n } catch (err) {\n return { source, issue: null, reason: errorMessage(err) };\n }\n\n let availability: ProviderAvailability;\n try {\n availability = (await provider.checkAvailability?.()) ?? {\n available: await provider.isAvailable(),\n };\n } catch (err) {\n return { source, issue: null, reason: `provider unavailable (${errorMessage(err)})` };\n }\n if (!availability.available) {\n // The classified reason replaces the flat \"provider unavailable\": a\n // credential that expired and a network that blipped were indistinguishable\n // before, and only one of them is worth telling a human about.\n const reason = availability.failure?.message ?? 'provider unavailable';\n return {\n source,\n issue: null,\n reason,\n ...(availability.failure === undefined ? {} : { failure: availability.failure }),\n ...(availability.action === undefined ? {} : { action: availability.action }),\n };\n }\n\n try {\n const issue = await provider.get(id);\n return { source, issue, reason: issue === null ? 'not found' : null };\n } catch (err) {\n const reason = errorMessage(err);\n // A real failure (network, auth, corrupted metadata) must be visible even\n // when the other origin answers.\n warn(`Could not read issue '${id}' from ${sourceLabel(source)}: ${reason}`);\n const failure = failureOf(err);\n const action = actionOf(err);\n return {\n source,\n issue: null,\n reason,\n ...(failure === null ? {} : { failure }),\n ...(action === null ? {} : { action }),\n };\n }\n}\n\nfunction shortHash(hash: string): string {\n const hex = hash.startsWith('sha256:') ? hash.slice('sha256:'.length) : hash;\n return hex.slice(0, 12);\n}\n\n/** One line per origin describing what differs, without dumping every body. */\nfunction describeCandidate(source: IssueSource, issue: Issue): string {\n const lines = issue.body.length === 0 ? 0 : issue.body.split('\\n').length;\n return (\n ` ${sourceLabel(source).padEnd(6)} title: \"${issue.title}\" | body: ${lines} line(s), ` +\n `${issue.body.length} char(s) | updated: ${issue.updatedAt} | hash: ${shortHash(issue.contentHash)}`\n );\n}\n\n/** 'local and GitHub', 'local, GitHub and memory' — origins in query order. */\nfunction listSources(candidates: FoundCandidate[]): string {\n const labels = candidates.map((candidate) => sourceLabel(candidate.source));\n if (labels.length <= 1) {\n return labels.join('');\n }\n return `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`;\n}\n\n/**\n * Turn a readline interface into an awaitable line reader.\n *\n * Lines are queued instead of read on demand: `rl.question` drops whatever\n * arrives between two questions, which silently loses a piped answer typed\n * ahead of the prompt. Resolves `null` once the input is exhausted, so EOF ends\n * the prompt instead of hanging the process.\n */\nfunction createLineReader(\n rl: ReadlineInterface,\n output: NodeJS.WritableStream,\n): (query: string) => Promise<string | null> {\n const queued: string[] = [];\n let pending: ((line: string | null) => void) | null = null;\n let closed = false;\n\n rl.on('line', (line: string) => {\n if (pending !== null) {\n const resolve = pending;\n pending = null;\n resolve(line);\n return;\n }\n queued.push(line);\n });\n rl.on('close', () => {\n closed = true;\n if (pending !== null) {\n const resolve = pending;\n pending = null;\n resolve(null);\n }\n });\n\n return (query: string) =>\n new Promise<string | null>((resolve) => {\n const buffered = queued.shift();\n if (buffered !== undefined) {\n resolve(buffered);\n return;\n }\n if (closed) {\n resolve(null);\n return;\n }\n output.write(query);\n pending = resolve;\n });\n}\n\n/**\n * Interactive choice between the divergent versions.\n *\n * The options are numbered from the origins that actually answered, so a third\n * provider simply shows up as `[3] Memory` without this function knowing which\n * origins exist. Cancel is always the last option.\n */\nasync function promptChoice(\n sources: IssueSource[],\n stdin: NodeJS.ReadableStream,\n stdout: NodeJS.WritableStream,\n warn: (message: string) => void,\n): Promise<IssueSource | 'cancel'> {\n const options = sources.map((source, index) => `[${index + 1}] ${promptLabel(source)}`);\n const cancelOption = sources.length + 1;\n const query = `Which version should be used? ${options.join(' ')} [${cancelOption}] Cancel: `;\n const numbers = Array.from({ length: cancelOption }, (_, index) => String(index + 1));\n const accepted = `${numbers.slice(0, -1).join(', ')} or ${numbers[numbers.length - 1]}`;\n\n const rl = createInterface({ input: stdin, output: stdout });\n const ask = createLineReader(rl, stdout);\n try {\n for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {\n const answer = await ask(query);\n if (answer === null) {\n return 'cancel';\n }\n const choice = answer.trim();\n const picked = sources[Number(choice) - 1];\n if (choice.length > 0 && picked !== undefined) {\n return picked;\n }\n if (choice === String(cancelOption)) {\n return 'cancel';\n }\n warn(`Invalid choice: '${choice}'. Enter ${accepted}.`);\n }\n return 'cancel';\n } finally {\n rl.close();\n }\n}\n\nfunction isInteractiveByDefault(): boolean {\n const ci = process.env.CI;\n const inCi = ci !== undefined && ci !== '' && ci !== '0' && ci.toLowerCase() !== 'false';\n return Boolean(process.stdin.isTTY) && !inCi;\n}\n\nfunction buildResolved(\n issue: Issue,\n source: IssueSource,\n local: Issue | null,\n github: Issue | null,\n divergent: boolean,\n): ResolvedIssue {\n return { issue, source, local, github, divergent };\n}\n\n/** Origin `conflictPolicy` points at, or `null` for 'ask'. */\nfunction policyTarget(policy: IssuesConfig['conflictPolicy']): IssueSource | null {\n if (policy === 'prefer-local') {\n return 'local';\n }\n if (policy === 'prefer-github') {\n return 'github';\n }\n return null;\n}\n\n/**\n * Single entry point every command uses to decide which Issue the pipeline\n * works on.\n *\n * The logic is written against the origins that answered, never against a fixed\n * list of sources: registering a new provider is enough for it to take part in\n * the resolution, be reported in a divergence and appear in the prompt.\n *\n * Scenarios:\n * - only one origin has it -> that one, no questions asked;\n * - several, same contentHash -> equivalence is reported and the preferred\n * origin wins, without a prompt;\n * - several, different content -> the divergence is reported and\n * `conflictPolicy` decides (`ask` prompts on a TTY, falls back to\n * `preferredProvider` with a warning anywhere else);\n * - none -> IssueResolutionError, which carries the CLI exit code.\n */\nexport async function resolveIssue(\n id: string,\n opts: ResolveIssueOptions = {},\n): Promise<ResolvedIssue> {\n const info = opts.info ?? printInfo;\n const warn = opts.warn ?? printWarning;\n const config = opts.config ?? (await loadIssuesConfig({ warn }));\n\n ensureProvidersRegistered();\n const sources = opts.sources ?? getRegisteredSources();\n\n // Each origin is an independent I/O call (network for GitHub, disk for\n // local); querying them concurrently instead of one-by-one keeps the total\n // latency at the slowest origin instead of the sum of all of them, without\n // changing which candidate wins (order is preserved by Promise.all).\n const candidates: Candidate[] = await Promise.all(\n sources.map((source) => fetchCandidate(source, id, warn)),\n );\n\n const issueOf = (source: IssueSource): Issue | null =>\n candidates.find((candidate) => candidate.source === source)?.issue ?? null;\n const local = issueOf('local');\n const github = issueOf('github');\n\n const found = candidates.filter(\n (candidate): candidate is FoundCandidate => candidate.issue !== null,\n );\n\n if (found.length === 0) {\n const details = candidates\n .map((candidate) => `${sourceLabel(candidate.source)}: ${candidate.reason ?? 'not found'}`)\n .join('; ');\n const where = details.length > 0 ? ` (${details})` : '';\n\n // An origin that needs a human is the answer, not a footnote: reporting\n // \"Issue not found\" for an expired credential sends the user looking for\n // the wrong problem, and no amount of retrying would have helped.\n const blocking = candidates.find((candidate) => candidate.action !== undefined);\n if (blocking !== undefined) {\n throw new IssueResolutionError(\n `Cannot read issue '${id}' from ${sourceLabel(blocking.source)}: ${blocking.reason}`,\n 1,\n {\n ...(blocking.failure === undefined ? {} : { failure: blocking.failure }),\n ...(blocking.action === undefined ? {} : { action: blocking.action }),\n },\n );\n }\n\n throw new IssueResolutionError(`Issue '${id}' not found in any registered origin${where}`);\n }\n\n const build = (candidate: FoundCandidate, divergent: boolean): ResolvedIssue =>\n buildResolved(candidate.issue, candidate.source, local, github, divergent);\n\n const firstFound = found[0] as FoundCandidate;\n if (found.length === 1) {\n return build(firstFound, false);\n }\n\n // Several origins have it. The preferred one wins whenever it is among them;\n // otherwise the first origin queried does, so an unrelated preference never\n // leaves the pipeline without an Issue.\n const preferred =\n found.find((candidate) => candidate.source === config.preferredProvider) ?? firstFound;\n\n const distinct = new Set(found.map((candidate) => candidate.issue.contentHash));\n if (distinct.size === 1) {\n info(\n `Issue '${id}' has identical content in ${listSources(found)}; using ${sourceLabel(preferred.source)}.`,\n );\n return build(preferred, false);\n }\n\n info(`Issue '${id}' differs between origins:`);\n for (const candidate of found) {\n info(describeCandidate(candidate.source, candidate.issue));\n }\n\n const target = policyTarget(config.conflictPolicy);\n if (target !== null) {\n const chosen = found.find((candidate) => candidate.source === target);\n if (chosen !== undefined) {\n info(`Conflict policy '${config.conflictPolicy}': using the ${sourceLabel(target)} version.`);\n return build(chosen, true);\n }\n // The policy names an origin that has no version of this Issue: say so\n // instead of silently picking for the user under its name.\n warn(\n `Conflict policy '${config.conflictPolicy}' does not apply to Issue '${id}' ` +\n `(${sourceLabel(target)} has no version of it); using ${sourceLabel(preferred.source)}.`,\n );\n return build(preferred, true);\n }\n\n const interactive = opts.interactive ?? isInteractiveByDefault();\n if (!interactive) {\n warn(\n `Divergent Issue '${id}' and conflict policy 'ask' in a non-interactive environment; ` +\n `using the preferred provider (${sourceLabel(preferred.source)}).`,\n );\n return build(preferred, true);\n }\n\n const choice = await promptChoice(\n found.map((candidate) => candidate.source),\n opts.stdin ?? process.stdin,\n opts.stdout ?? process.stdout,\n warn,\n );\n if (choice === 'cancel') {\n throw new IssueResolutionError(\n `Cancelled: Issue '${id}' diverges between ${listSources(found)}.`,\n );\n }\n const picked = found.find((candidate) => candidate.source === choice) as FoundCandidate;\n return build(picked, true);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -53,6 +53,8 @@ function keyOf(record, key) {
|
|
|
53
53
|
return record.agent.model.resolved ?? record.agent.model.requested ?? "unknown";
|
|
54
54
|
case "purpose":
|
|
55
55
|
return record.purpose;
|
|
56
|
+
case "trigger":
|
|
57
|
+
return record.trigger;
|
|
56
58
|
case "status":
|
|
57
59
|
return record.status;
|
|
58
60
|
default: {
|
|
@@ -80,4 +82,4 @@ export {
|
|
|
80
82
|
summarize,
|
|
81
83
|
groupBy
|
|
82
84
|
};
|
|
83
|
-
//# sourceMappingURL=chunk-
|
|
85
|
+
//# sourceMappingURL=chunk-E2H3JV2E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/telemetry/aggregate.ts"],"sourcesContent":["import type { TaskPlan } from '../types.js';\nimport type { ExecutionRecord, ExecutionStatus, ExecutionSummary } from './types.js';\n\nfunction emptySummary(discarded = 0): ExecutionSummary {\n return {\n count: 0,\n discarded,\n byStatus: {},\n usage: {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheCreationTokens: 0,\n reasoningTokens: 0,\n },\n totalCost: { reported: 0, estimated: 0, unknownExecutions: 0 },\n };\n}\n\nfunction add(into: ExecutionSummary, record: ExecutionRecord): void {\n into.count += 1;\n into.byStatus[record.status] = (into.byStatus[record.status] ?? 0) + 1;\n if (record.usage !== null) {\n into.usage.inputTokens += record.usage.inputTokens ?? 0;\n into.usage.outputTokens += record.usage.outputTokens ?? 0;\n into.usage.cacheReadTokens += record.usage.cacheReadTokens ?? 0;\n into.usage.cacheCreationTokens += record.usage.cacheCreationTokens ?? 0;\n into.usage.reasoningTokens += record.usage.reasoningTokens ?? 0;\n }\n if (record.cost.status === 'reported') {\n into.totalCost.reported += record.cost.amount;\n } else if (record.cost.status === 'estimated') {\n into.totalCost.estimated += record.cost.amount;\n } else {\n into.totalCost.unknownExecutions += 1;\n }\n}\n\nfunction isPlan(value: TaskPlan | readonly ExecutionRecord[]): value is TaskPlan {\n return !Array.isArray(value) && 'userStories' in value;\n}\n\nexport function summarize(\n input: TaskPlan | readonly ExecutionRecord[],\n discarded = 0,\n): ExecutionSummary {\n const records = isPlan(input) ? (input.executions ?? []) : input;\n const summary = emptySummary(discarded);\n for (const record of records) add(summary, record);\n return summary;\n}\n\nexport type GroupKey = 'harness' | 'provider' | 'model' | 'purpose' | 'trigger' | 'status';\n\nfunction keyOf(record: ExecutionRecord, key: GroupKey): string {\n switch (key) {\n case 'harness':\n return record.agent.harness;\n case 'provider':\n return record.agent.provider ?? 'unknown';\n case 'model':\n return record.agent.model.resolved ?? record.agent.model.requested ?? 'unknown';\n case 'purpose':\n return record.purpose;\n case 'trigger':\n return record.trigger;\n case 'status':\n return record.status;\n default: {\n const _exhaustive: never = key;\n return _exhaustive;\n }\n }\n}\n\nexport function groupBy(\n records: readonly ExecutionRecord[],\n key: GroupKey,\n): Map<string, ExecutionSummary> {\n const groups = new Map<string, ExecutionRecord[]>();\n for (const record of records) {\n const name = keyOf(record, key);\n const bucket = groups.get(name);\n if (bucket === undefined) groups.set(name, [record]);\n else bucket.push(record);\n }\n const result = new Map<string, ExecutionSummary>();\n for (const [name, bucket] of groups) {\n result.set(name, summarize(bucket));\n }\n return result;\n}\n\nexport type { ExecutionStatus };\n"],"mappings":";;;AAGA,SAAS,aAAa,YAAY,GAAqB;AACrD,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA,UAAU,CAAC;AAAA,IACX,OAAO;AAAA,MACL,aAAa;AAAA,MACb,cAAc;AAAA,MACd,iBAAiB;AAAA,MACjB,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,IACnB;AAAA,IACA,WAAW,EAAE,UAAU,GAAG,WAAW,GAAG,mBAAmB,EAAE;AAAA,EAC/D;AACF;AAEA,SAAS,IAAI,MAAwB,QAA+B;AAClE,OAAK,SAAS;AACd,OAAK,SAAS,OAAO,MAAM,KAAK,KAAK,SAAS,OAAO,MAAM,KAAK,KAAK;AACrE,MAAI,OAAO,UAAU,MAAM;AACzB,SAAK,MAAM,eAAe,OAAO,MAAM,eAAe;AACtD,SAAK,MAAM,gBAAgB,OAAO,MAAM,gBAAgB;AACxD,SAAK,MAAM,mBAAmB,OAAO,MAAM,mBAAmB;AAC9D,SAAK,MAAM,uBAAuB,OAAO,MAAM,uBAAuB;AACtE,SAAK,MAAM,mBAAmB,OAAO,MAAM,mBAAmB;AAAA,EAChE;AACA,MAAI,OAAO,KAAK,WAAW,YAAY;AACrC,SAAK,UAAU,YAAY,OAAO,KAAK;AAAA,EACzC,WAAW,OAAO,KAAK,WAAW,aAAa;AAC7C,SAAK,UAAU,aAAa,OAAO,KAAK;AAAA,EAC1C,OAAO;AACL,SAAK,UAAU,qBAAqB;AAAA,EACtC;AACF;AAEA,SAAS,OAAO,OAAiE;AAC/E,SAAO,CAAC,MAAM,QAAQ,KAAK,KAAK,iBAAiB;AACnD;AAEO,SAAS,UACd,OACA,YAAY,GACM;AAClB,QAAM,UAAU,OAAO,KAAK,IAAK,MAAM,cAAc,CAAC,IAAK;AAC3D,QAAM,UAAU,aAAa,SAAS;AACtC,aAAW,UAAU,QAAS,KAAI,SAAS,MAAM;AACjD,SAAO;AACT;AAIA,SAAS,MAAM,QAAyB,KAAuB;AAC7D,UAAQ,KAAK;AAAA,IACX,KAAK;AACH,aAAO,OAAO,MAAM;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,MAAM,YAAY;AAAA,IAClC,KAAK;AACH,aAAO,OAAO,MAAM,MAAM,YAAY,OAAO,MAAM,MAAM,aAAa;AAAA,IACxE,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,SAAS;AACP,YAAM,cAAqB;AAC3B,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEO,SAAS,QACd,SACA,KAC+B;AAC/B,QAAM,SAAS,oBAAI,IAA+B;AAClD,aAAW,UAAU,SAAS;AAC5B,UAAM,OAAO,MAAM,QAAQ,GAAG;AAC9B,UAAM,SAAS,OAAO,IAAI,IAAI;AAC9B,QAAI,WAAW,OAAW,QAAO,IAAI,MAAM,CAAC,MAAM,CAAC;AAAA,QAC9C,QAAO,KAAK,MAAM;AAAA,EACzB;AACA,QAAM,SAAS,oBAAI,IAA8B;AACjD,aAAW,CAAC,MAAM,MAAM,KAAK,QAAQ;AACnC,WAAO,IAAI,MAAM,UAAU,MAAM,CAAC;AAAA,EACpC;AACA,SAAO;AACT;","names":[]}
|