issue-flow 0.14.0 → 0.15.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/dist/{agent-OX3KPHE4.js → agent-AT64RXTA.js} +11 -11
- package/dist/{analyze-WIND2SEB.js → analyze-O2PYDIFK.js} +18 -18
- package/dist/{apply-JNOI2LR4.js → apply-SSU6QCD4.js} +11 -11
- package/dist/{bench-B4VZON7E.js → bench-ODFPAGW6.js} +16 -16
- package/dist/{chunk-QD4VOILZ.js → chunk-2ZAM45PZ.js} +9 -9
- package/dist/{chunk-PBTA3QQX.js → chunk-4RXYFWP7.js} +3 -3
- package/dist/{chunk-CGKLE6WR.js → chunk-5ZKBIXET.js} +12 -12
- package/dist/{chunk-WB2X37MO.js → chunk-6E2PEBFT.js} +7 -7
- package/dist/{chunk-UJ3L2MS7.js → chunk-6FXOU2KR.js} +6 -6
- package/dist/{chunk-RVO7L4FX.js → chunk-6SRE4KD7.js} +4 -4
- package/dist/{chunk-KMRFRXIT.js → chunk-A35HE4V7.js} +3 -3
- package/dist/{chunk-6BENHR2I.js → chunk-ARWNYXFL.js} +12 -12
- package/dist/{chunk-3WFZETIM.js → chunk-AYYMW33T.js} +2 -2
- package/dist/{chunk-LFEQIZLB.js → chunk-BMHGRYL6.js} +396 -64
- package/dist/chunk-BMHGRYL6.js.map +1 -0
- package/dist/{chunk-PZTWU277.js → chunk-F4OK7KEP.js} +7 -7
- package/dist/{chunk-SZMHCEYV.js → chunk-FK4IDY6P.js} +9 -9
- package/dist/{chunk-BEZ5BA5U.js → chunk-GSZCQUCY.js} +2 -2
- package/dist/{chunk-X44FKNKP.js → chunk-HGSYD57B.js} +34 -30
- package/dist/chunk-HGSYD57B.js.map +1 -0
- package/dist/{chunk-2IQ6M4ZE.js → chunk-KAHQ6F5C.js} +11 -11
- package/dist/{chunk-F6TOYSYY.js → chunk-MEFY4B7T.js} +9 -9
- package/dist/{chunk-BRCNQXYQ.js → chunk-MGCOQU6N.js} +4 -4
- package/dist/{chunk-KT7JKOXB.js → chunk-MROS3SKL.js} +3 -3
- package/dist/{chunk-TYRNTSPJ.js → chunk-NVZMBXTE.js} +3 -3
- package/dist/{chunk-PHW2YV6N.js → chunk-PCN7ZQND.js} +5 -5
- package/dist/{chunk-76MMPJ53.js → chunk-QRCGW2ZO.js} +5 -5
- package/dist/{chunk-OR43364P.js → chunk-RPXDBX6R.js} +3 -3
- package/dist/{chunk-JN6D3MY6.js → chunk-RWEXK55D.js} +2 -2
- package/dist/{chunk-67YIU2VB.js → chunk-TBDP6QPJ.js} +4 -4
- package/dist/{chunk-G5EK2XRN.js → chunk-UNQYMR3H.js} +2 -2
- package/dist/{chunk-QK7WUO7A.js → chunk-VR34TXHF.js} +6 -6
- package/dist/{chunk-QJ5UZAH5.js → chunk-WM3Y7Q4A.js} +10 -10
- package/dist/{chunk-LBGHI4S2.js → chunk-XDKGA6UX.js} +4 -2
- package/dist/{chunk-LBGHI4S2.js.map → chunk-XDKGA6UX.js.map} +1 -1
- package/dist/{chunk-REO3PXXO.js → chunk-XG2NYDRA.js} +3 -3
- package/dist/{chunk-LEW4QM74.js → chunk-XLVGPXNL.js} +2 -2
- package/dist/{chunk-J3SOTUVI.js → chunk-XQHFC62U.js} +2 -2
- package/dist/{chunk-QZVKRDJY.js → chunk-XRLRKTQA.js} +10 -10
- package/dist/{chunk-B72RQZBV.js → chunk-ZFIXEYUJ.js} +21 -18
- package/dist/chunk-ZFIXEYUJ.js.map +1 -0
- package/dist/{chunk-V7MO5EDS.js → chunk-ZPIVCM7Q.js} +3 -3
- package/dist/{chunk-OOVG2KBY.js → chunk-ZXPASFHG.js} +2 -2
- package/dist/cli.js +56 -65
- package/dist/cli.js.map +1 -1
- package/dist/{config-HMFMCY2M.js → config-UMP6DBJB.js} +10 -10
- package/dist/{conventions-NJF2OPVQ.js → conventions-DKH7AE4T.js} +12 -12
- package/dist/{diagnostics-THH42ZMP.js → diagnostics-GOGCLTRS.js} +6 -6
- package/dist/execute-E3BRMDAQ.js +35 -0
- package/dist/{generate-FW5C6NP7.js → generate-AGBT7EHX.js} +18 -18
- package/dist/{git-W72QQ6GT.js → git-AC542GU2.js} +2 -2
- package/dist/init-LLBVJNPE.js +24 -0
- package/dist/{operations-BCFOBH42.js → operations-JQYK2EMT.js} +11 -11
- package/dist/{permissions-4AKNQZHK.js → permissions-6HL52K54.js} +3 -3
- package/dist/plan-HSLWDJEZ.js +34 -0
- package/dist/{policy-DT6Q6ZVM.js → policy-IMRXGG7X.js} +11 -11
- package/dist/pr-6TCZXG6K.js +39 -0
- package/dist/pr-review-6MWJO2NP.js +32 -0
- package/dist/prd-D6YDYAFX.js +33 -0
- package/dist/{ps-RFORTCUD.js → ps-KUUZ6TUL.js} +9 -9
- package/dist/{recorder-KCCPMKQG.js → recorder-BJ2KIAWT.js} +5 -5
- package/dist/{registry-GLPXOMFG.js → registry-SB7XTMGE.js} +5 -5
- package/dist/{resume-M5RLKP4I.js → resume-QNLQSCR5.js} +34 -34
- package/dist/review-3KJ4BH2V.js +35 -0
- package/dist/{routing-NNT5WKGX.js → routing-TN7GLMGU.js} +11 -11
- package/dist/run-5EHIFF2V.js +54 -0
- package/dist/{state-manager-GQH32C44.js → state-manager-ZEPWTCA6.js} +3 -3
- package/dist/{usage-V2CHE5PY.js → usage-QL563I7F.js} +9 -9
- package/dist/{web-EA3KGNFN.js → web-PPSMNTQ6.js} +18 -57
- package/dist/{web-EA3KGNFN.js.map → web-PPSMNTQ6.js.map} +1 -1
- package/package.json +1 -1
- package/web/AGENTS.md +4 -0
- package/web/public/app.js +29 -10
- package/dist/chunk-B72RQZBV.js.map +0 -1
- package/dist/chunk-LFEQIZLB.js.map +0 -1
- package/dist/chunk-X44FKNKP.js.map +0 -1
- package/dist/execute-KFNNZZHI.js +0 -35
- package/dist/init-HTN4V67Q.js +0 -24
- package/dist/plan-SIZLGA4V.js +0 -34
- package/dist/pr-OMWCGUOW.js +0 -39
- package/dist/pr-review-N24CZPKR.js +0 -32
- package/dist/prd-5OFTZAHJ.js +0 -33
- package/dist/review-YM4JAABY.js +0 -35
- package/dist/run-PZUKHPQK.js +0 -54
- /package/dist/{agent-OX3KPHE4.js.map → agent-AT64RXTA.js.map} +0 -0
- /package/dist/{analyze-WIND2SEB.js.map → analyze-O2PYDIFK.js.map} +0 -0
- /package/dist/{apply-JNOI2LR4.js.map → apply-SSU6QCD4.js.map} +0 -0
- /package/dist/{bench-B4VZON7E.js.map → bench-ODFPAGW6.js.map} +0 -0
- /package/dist/{chunk-QD4VOILZ.js.map → chunk-2ZAM45PZ.js.map} +0 -0
- /package/dist/{chunk-PBTA3QQX.js.map → chunk-4RXYFWP7.js.map} +0 -0
- /package/dist/{chunk-CGKLE6WR.js.map → chunk-5ZKBIXET.js.map} +0 -0
- /package/dist/{chunk-WB2X37MO.js.map → chunk-6E2PEBFT.js.map} +0 -0
- /package/dist/{chunk-UJ3L2MS7.js.map → chunk-6FXOU2KR.js.map} +0 -0
- /package/dist/{chunk-RVO7L4FX.js.map → chunk-6SRE4KD7.js.map} +0 -0
- /package/dist/{chunk-KMRFRXIT.js.map → chunk-A35HE4V7.js.map} +0 -0
- /package/dist/{chunk-6BENHR2I.js.map → chunk-ARWNYXFL.js.map} +0 -0
- /package/dist/{chunk-3WFZETIM.js.map → chunk-AYYMW33T.js.map} +0 -0
- /package/dist/{chunk-PZTWU277.js.map → chunk-F4OK7KEP.js.map} +0 -0
- /package/dist/{chunk-SZMHCEYV.js.map → chunk-FK4IDY6P.js.map} +0 -0
- /package/dist/{chunk-BEZ5BA5U.js.map → chunk-GSZCQUCY.js.map} +0 -0
- /package/dist/{chunk-2IQ6M4ZE.js.map → chunk-KAHQ6F5C.js.map} +0 -0
- /package/dist/{chunk-F6TOYSYY.js.map → chunk-MEFY4B7T.js.map} +0 -0
- /package/dist/{chunk-BRCNQXYQ.js.map → chunk-MGCOQU6N.js.map} +0 -0
- /package/dist/{chunk-KT7JKOXB.js.map → chunk-MROS3SKL.js.map} +0 -0
- /package/dist/{chunk-TYRNTSPJ.js.map → chunk-NVZMBXTE.js.map} +0 -0
- /package/dist/{chunk-PHW2YV6N.js.map → chunk-PCN7ZQND.js.map} +0 -0
- /package/dist/{chunk-76MMPJ53.js.map → chunk-QRCGW2ZO.js.map} +0 -0
- /package/dist/{chunk-OR43364P.js.map → chunk-RPXDBX6R.js.map} +0 -0
- /package/dist/{chunk-JN6D3MY6.js.map → chunk-RWEXK55D.js.map} +0 -0
- /package/dist/{chunk-67YIU2VB.js.map → chunk-TBDP6QPJ.js.map} +0 -0
- /package/dist/{chunk-G5EK2XRN.js.map → chunk-UNQYMR3H.js.map} +0 -0
- /package/dist/{chunk-QK7WUO7A.js.map → chunk-VR34TXHF.js.map} +0 -0
- /package/dist/{chunk-QJ5UZAH5.js.map → chunk-WM3Y7Q4A.js.map} +0 -0
- /package/dist/{chunk-REO3PXXO.js.map → chunk-XG2NYDRA.js.map} +0 -0
- /package/dist/{chunk-LEW4QM74.js.map → chunk-XLVGPXNL.js.map} +0 -0
- /package/dist/{chunk-J3SOTUVI.js.map → chunk-XQHFC62U.js.map} +0 -0
- /package/dist/{chunk-QZVKRDJY.js.map → chunk-XRLRKTQA.js.map} +0 -0
- /package/dist/{chunk-V7MO5EDS.js.map → chunk-ZPIVCM7Q.js.map} +0 -0
- /package/dist/{chunk-OOVG2KBY.js.map → chunk-ZXPASFHG.js.map} +0 -0
- /package/dist/{config-HMFMCY2M.js.map → config-UMP6DBJB.js.map} +0 -0
- /package/dist/{conventions-NJF2OPVQ.js.map → conventions-DKH7AE4T.js.map} +0 -0
- /package/dist/{diagnostics-THH42ZMP.js.map → diagnostics-GOGCLTRS.js.map} +0 -0
- /package/dist/{execute-KFNNZZHI.js.map → execute-E3BRMDAQ.js.map} +0 -0
- /package/dist/{generate-FW5C6NP7.js.map → generate-AGBT7EHX.js.map} +0 -0
- /package/dist/{git-W72QQ6GT.js.map → git-AC542GU2.js.map} +0 -0
- /package/dist/{init-HTN4V67Q.js.map → init-LLBVJNPE.js.map} +0 -0
- /package/dist/{operations-BCFOBH42.js.map → operations-JQYK2EMT.js.map} +0 -0
- /package/dist/{permissions-4AKNQZHK.js.map → permissions-6HL52K54.js.map} +0 -0
- /package/dist/{plan-SIZLGA4V.js.map → plan-HSLWDJEZ.js.map} +0 -0
- /package/dist/{policy-DT6Q6ZVM.js.map → policy-IMRXGG7X.js.map} +0 -0
- /package/dist/{pr-OMWCGUOW.js.map → pr-6TCZXG6K.js.map} +0 -0
- /package/dist/{pr-review-N24CZPKR.js.map → pr-review-6MWJO2NP.js.map} +0 -0
- /package/dist/{prd-5OFTZAHJ.js.map → prd-D6YDYAFX.js.map} +0 -0
- /package/dist/{ps-RFORTCUD.js.map → ps-KUUZ6TUL.js.map} +0 -0
- /package/dist/{recorder-KCCPMKQG.js.map → recorder-BJ2KIAWT.js.map} +0 -0
- /package/dist/{registry-GLPXOMFG.js.map → registry-SB7XTMGE.js.map} +0 -0
- /package/dist/{resume-M5RLKP4I.js.map → resume-QNLQSCR5.js.map} +0 -0
- /package/dist/{review-YM4JAABY.js.map → review-3KJ4BH2V.js.map} +0 -0
- /package/dist/{routing-NNT5WKGX.js.map → routing-TN7GLMGU.js.map} +0 -0
- /package/dist/{run-PZUKHPQK.js.map → run-5EHIFF2V.js.map} +0 -0
- /package/dist/{state-manager-GQH32C44.js.map → state-manager-ZEPWTCA6.js.map} +0 -0
- /package/dist/{usage-V2CHE5PY.js.map → usage-QL563I7F.js.map} +0 -0
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
readSessionFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RWEXK55D.js";
|
|
5
5
|
import {
|
|
6
6
|
ensureSingleWebServer,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import "./chunk-UJ3L2MS7.js";
|
|
12
|
-
import "./chunk-KMRFRXIT.js";
|
|
7
|
+
stopWebMonitor
|
|
8
|
+
} from "./chunk-BMHGRYL6.js";
|
|
9
|
+
import "./chunk-6FXOU2KR.js";
|
|
10
|
+
import "./chunk-A35HE4V7.js";
|
|
13
11
|
import {
|
|
14
12
|
parseJournal
|
|
15
13
|
} from "./chunk-3BWZO4YI.js";
|
|
16
14
|
import {
|
|
17
15
|
loadWebConfig
|
|
18
|
-
} from "./chunk-
|
|
19
|
-
import "./chunk-
|
|
20
|
-
import
|
|
21
|
-
printError,
|
|
22
|
-
printInfo,
|
|
23
|
-
printWarning
|
|
24
|
-
} from "./chunk-TYRNTSPJ.js";
|
|
16
|
+
} from "./chunk-WM3Y7Q4A.js";
|
|
17
|
+
import "./chunk-VR34TXHF.js";
|
|
18
|
+
import "./chunk-NVZMBXTE.js";
|
|
25
19
|
import "./chunk-6QYMSNVA.js";
|
|
26
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-AYYMW33T.js";
|
|
27
21
|
import {
|
|
28
22
|
EVENTS_FILENAME,
|
|
29
23
|
ISSUES_DIR_NAME,
|
|
@@ -32,12 +26,12 @@ import {
|
|
|
32
26
|
SESSION_FILENAME,
|
|
33
27
|
getGlobalRoot
|
|
34
28
|
} from "./chunk-HAKDLR6N.js";
|
|
35
|
-
import "./chunk-
|
|
36
|
-
import "./chunk-
|
|
29
|
+
import "./chunk-GSZCQUCY.js";
|
|
30
|
+
import "./chunk-TBDP6QPJ.js";
|
|
37
31
|
import "./chunk-P3LRUNC6.js";
|
|
38
|
-
import "./chunk-
|
|
39
|
-
import "./chunk-
|
|
40
|
-
import "./chunk-
|
|
32
|
+
import "./chunk-UNQYMR3H.js";
|
|
33
|
+
import "./chunk-XQHFC62U.js";
|
|
34
|
+
import "./chunk-XDKGA6UX.js";
|
|
41
35
|
import "./chunk-7Z4Y3VKM.js";
|
|
42
36
|
import "./chunk-XEYH6ZZI.js";
|
|
43
37
|
|
|
@@ -130,11 +124,6 @@ function watchSessionDirectory(options = {}) {
|
|
|
130
124
|
}
|
|
131
125
|
|
|
132
126
|
// src/commands/web.ts
|
|
133
|
-
var STOP_POLL_INTERVAL_MS = 100;
|
|
134
|
-
var STOP_TIMEOUT_MS = 5e3;
|
|
135
|
-
function sleep(ms) {
|
|
136
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
-
}
|
|
138
127
|
async function runWebServe(options) {
|
|
139
128
|
const cli = {};
|
|
140
129
|
if (options.port !== void 0) cli.port = options.port;
|
|
@@ -169,40 +158,12 @@ async function runWebServe(options) {
|
|
|
169
158
|
return 0;
|
|
170
159
|
}
|
|
171
160
|
async function runWebStop() {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
printInfo("No web monitor is currently running.");
|
|
176
|
-
return 0;
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
process.kill(lock.pid, "SIGTERM");
|
|
180
|
-
} catch (err) {
|
|
181
|
-
if (err.code === "ESRCH") {
|
|
182
|
-
printWarning(
|
|
183
|
-
"The web monitor lock referenced a process that is no longer running; removing it."
|
|
184
|
-
);
|
|
185
|
-
await removeWebLock(lockFile);
|
|
186
|
-
return 0;
|
|
187
|
-
}
|
|
188
|
-
printError(
|
|
189
|
-
`Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`
|
|
190
|
-
);
|
|
191
|
-
return 1;
|
|
192
|
-
}
|
|
193
|
-
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
194
|
-
while (Date.now() < deadline) {
|
|
195
|
-
if (await readWebLock(lockFile) === null) {
|
|
196
|
-
printInfo(`Web monitor (pid ${lock.pid}) stopped.`);
|
|
197
|
-
return 0;
|
|
198
|
-
}
|
|
199
|
-
await sleep(STOP_POLL_INTERVAL_MS);
|
|
200
|
-
}
|
|
201
|
-
printWarning("Sent the stop signal, but the web monitor did not confirm shutdown in time.");
|
|
202
|
-
return 0;
|
|
161
|
+
const webConfig = await loadWebConfig();
|
|
162
|
+
const result = await stopWebMonitor({ port: webConfig.port, host: webConfig.host });
|
|
163
|
+
return result === "failed" || result === "unowned" ? 1 : 0;
|
|
203
164
|
}
|
|
204
165
|
export {
|
|
205
166
|
runWebServe,
|
|
206
167
|
runWebStop
|
|
207
168
|
};
|
|
208
|
-
//# sourceMappingURL=web-
|
|
169
|
+
//# sourceMappingURL=web-PPSMNTQ6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type JournalEntry, parseJournal } from '../core/journal.js';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport {\n EVENTS_FILENAME,\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n SESSION_FILENAME,\n} from '../storage/paths.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` touches a live session every 10s without rewriting\n * its content. Three missed heartbeats plus ample scheduling/filesystem slack\n * avoid flapping while still removing an abruptly killed run promptly.\n */\nexport const DEFAULT_STALE_AFTER_MS = 90_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Journal entries for one active session, oldest generation first. */\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));\n }),\n );\n\n return perProject.flat();\n}\n\nasync function readJournalFile(filePath: string): Promise<JournalEntry[]> {\n try {\n return parseJournal(await readFile(filePath, 'utf-8'));\n } catch {\n return [];\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n events: async (sessionId: string) => {\n const session = sessions.get(sessionId);\n if (session === undefined) return undefined;\n const [rotated, current] = await Promise.all([\n readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),\n readJournalFile(join(session.issueDir, EVENTS_FILENAME)),\n ]);\n return [...rotated, ...current];\n },\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { printError, printInfo, printWarning } from '../ui/logger.js';\nimport { ensureSingleWebServer, getWebLockFile, readWebLock, removeWebLock } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nconst STOP_POLL_INTERVAL_MS = 100;\nconst STOP_TIMEOUT_MS = 5000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const lockFile = getWebLockFile();\n const lock = await readWebLock(lockFile);\n if (lock === null) {\n printInfo('No web monitor is currently running.');\n return 0;\n }\n\n try {\n process.kill(lock.pid, 'SIGTERM');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ESRCH') {\n printWarning(\n 'The web monitor lock referenced a process that is no longer running; removing it.',\n );\n await removeWebLock(lockFile);\n return 0;\n }\n printError(\n `Failed to stop the web monitor: ${err instanceof Error ? err.message : String(err)}`,\n );\n return 1;\n }\n\n const deadline = Date.now() + STOP_TIMEOUT_MS;\n while (Date.now() < deadline) {\n if ((await readWebLock(lockFile)) === null) {\n printInfo(`Web monitor (pid ${lock.pid}) stopped.`);\n return 0;\n }\n await sleep(STOP_POLL_INTERVAL_MS);\n }\n\n printWarning('Sent the stop signal, but the web monitor did not confirm shutdown in time.');\n return 0;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/web/session-directory.ts","../src/commands/web.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type JournalEntry, parseJournal } from '../core/journal.js';\nimport type { ValidatedSessionSnapshot } from '../schemas.js';\nimport {\n EVENTS_FILENAME,\n type GetGlobalRootOptions,\n getGlobalRoot,\n ISSUES_DIR_NAME,\n PROJECTS_DIR_NAME,\n ROTATED_EVENTS_FILENAME,\n SESSION_FILENAME,\n} from '../storage/paths.js';\nimport { readSessionFile } from '../storage/session-file.js';\n\n/**\n * Multi-session discovery for the web monitoring server (US-003).\n *\n * The single-instance server (`web/lock.ts`) runs decoupled from any one\n * pipeline invocation, so it cannot hold a `SessionPublisher` in memory for\n * \"the\" run being monitored — there may be zero, one or several running at\n * once, each in its own process. Instead this module polls every\n * `~/.issue-flow/projects/<project>/issues/<n>/session.json` on disk (the\n * same file `FilePublisher` already writes) and keeps an in-memory map of the\n * ones that are both well-formed and recently updated.\n *\n * Polling rather than `fs.watch` is a deliberate choice, not a placeholder:\n * `fs.watch`'s `recursive` option is only reliable on macOS and Windows (Linux\n * needs a manual per-directory watch tree, and the whole thing still degrades\n * to polling on network filesystems), while a `~/.issue-flow` tree is small\n * and local — a cheap poll of it is simpler and behaves identically on every\n * platform, which is what the US-003 acceptance criteria ask to validate.\n */\n\n/** How often the directory tree is rescanned, unless overridden. */\nexport const DEFAULT_POLL_INTERVAL_MS = 3000;\n\n/**\n * A session stops being reported once its file has gone this long without an\n * update. `FilePublisher` touches a live session every 10s without rewriting\n * its content. Three missed heartbeats plus ample scheduling/filesystem slack\n * avoid flapping while still removing an abruptly killed run promptly.\n */\nexport const DEFAULT_STALE_AFTER_MS = 90_000;\n\nexport interface ActiveSession {\n /** Directory the session.json was read from, one level up from the file. */\n issueDir: string;\n filePath: string;\n snapshot: ValidatedSessionSnapshot;\n /** mtime of the file at the last successful read, in epoch ms. */\n updatedAtMs: number;\n}\n\nexport interface SessionDirectoryOptions extends GetGlobalRootOptions {\n /** Poll interval, in ms. Default {@link DEFAULT_POLL_INTERVAL_MS}. */\n pollIntervalMs?: number;\n /** Staleness threshold, in ms. Default {@link DEFAULT_STALE_AFTER_MS}. */\n staleAfterMs?: number;\n /** Invoked (at most once) when a poll fails unexpectedly. Best-effort logging only. */\n onWarn?: (message: string) => void;\n}\n\nexport interface SessionDirectoryHandle {\n /** Snapshot of every session currently considered active. */\n sessions(): ActiveSession[];\n /** A single active session by id, or undefined. */\n getSession(sessionId: string): ActiveSession | undefined;\n /** Journal entries for one active session, oldest generation first. */\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n /** Force an immediate rescan instead of waiting for the next tick. Never throws. */\n refresh(): Promise<void>;\n /** Stop polling. Idempotent. */\n close(): void;\n}\n\nasync function listSubdirectories(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n // Symlinks are skipped on purpose, same discipline as the storage\n // migration walk: following one could pull session data from outside the\n // global tree into the listing.\n return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);\n } catch {\n // Missing/unreadable projects (or issues) directory reads as \"none yet\" —\n // this is routinely the case before the first --web run ever happens.\n return [];\n }\n}\n\n/** Every `session.json` path currently on disk, one per project/issue pair. */\nasync function discoverSessionFiles(root: string): Promise<string[]> {\n const projectsDir = join(root, PROJECTS_DIR_NAME);\n const projectIds = await listSubdirectories(projectsDir);\n\n const perProject = await Promise.all(\n projectIds.map(async (projectId) => {\n const issuesDir = join(projectsDir, projectId, ISSUES_DIR_NAME);\n const issueIds = await listSubdirectories(issuesDir);\n return issueIds.map((issueId) => join(issuesDir, issueId, SESSION_FILENAME));\n }),\n );\n\n return perProject.flat();\n}\n\nasync function readJournalFile(filePath: string): Promise<JournalEntry[]> {\n try {\n return parseJournal(await readFile(filePath, 'utf-8'));\n } catch {\n return [];\n }\n}\n\n/**\n * Start polling the global storage tree for active sessions.\n *\n * Returns a handle immediately; the first scan happens synchronously in the\n * background and `sessions()` answers `[]` until it completes, same as \"no\n * sessions found yet\" — there is nothing to await here that would not also\n * delay the server's very first bind.\n */\nexport function watchSessionDirectory(\n options: SessionDirectoryOptions = {},\n): SessionDirectoryHandle {\n const root = getGlobalRoot(options);\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;\n const warn = options.onWarn;\n\n // Keyed by sessionId, not by file path: a session's identity is the id\n // inside its content, and that is what /api/status?session=<id> looks up.\n let sessions = new Map<string, ActiveSession>();\n let warned = false;\n\n async function scan(): Promise<void> {\n let files: string[];\n try {\n files = await discoverSessionFiles(root);\n } catch (err) {\n if (!warned) {\n warned = true;\n warn?.(\n `issue-flow: web monitor could not scan the session directory (will keep retrying silently): ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n return;\n }\n\n const now = Date.now();\n const next = new Map<string, ActiveSession>();\n for (const filePath of files) {\n const result = await readSessionFile(filePath);\n if (result === null) continue;\n if (now - result.updatedAtMs > staleAfterMs) continue; // stopped updating: not active\n const sessionId = result.snapshot.sessionId as string;\n next.set(sessionId, {\n issueDir: join(filePath, '..'),\n filePath,\n snapshot: result.snapshot,\n updatedAtMs: result.updatedAtMs,\n });\n }\n sessions = next;\n }\n\n // The first scan runs immediately (not just after the first interval tick)\n // so a server started right after a session begins does not report \"no\n // sessions\" for a whole poll interval.\n void scan();\n const timer = setInterval(() => void scan(), pollIntervalMs);\n timer.unref();\n\n return {\n sessions: () => [...sessions.values()],\n getSession: (sessionId: string) => sessions.get(sessionId),\n events: async (sessionId: string) => {\n const session = sessions.get(sessionId);\n if (session === undefined) return undefined;\n const [rotated, current] = await Promise.all([\n readJournalFile(join(session.issueDir, ROTATED_EVENTS_FILENAME)),\n readJournalFile(join(session.issueDir, EVENTS_FILENAME)),\n ]);\n return [...rotated, ...current];\n },\n refresh: scan,\n close: () => clearInterval(timer),\n };\n}\n","import { loadWebConfig } from '../config.js';\nimport type { WebConfig } from '../schemas.js';\nimport { ensureSingleWebServer, stopWebMonitor } from '../web/lock.js';\nimport { watchSessionDirectory } from '../web/session-directory.js';\n\n/**\n * `issue-flow web serve` / `issue-flow web stop` (US-002).\n *\n * `serve` is the process `ensureWebMonitor()` (`web/lock.ts`) spawns detached\n * — it is not meant to be run interactively, though nothing stops a user from\n * doing so to watch every session on the machine without going through a\n * pipeline command first. It never exits on its own: the server stays bound\n * (see `unref: false` in `web/server.ts`) until `stop` sends it `SIGTERM`, at\n * which point `startWebServer`'s own signal handler closes it (removing the\n * lock) and re-raises the signal for the default termination behavior.\n */\n\nexport interface RunWebServeOptions {\n port?: number;\n host?: string;\n refresh?: number;\n}\n\n/**\n * Bind (or defer to) the single web monitor instance and keep the process\n * alive for as long as it stays bound. Logging is intentionally silent on the\n * happy path: this process is spawned with `stdio: 'ignore'`, so anything\n * printed here goes nowhere — the caller (`ensureWebMonitor`) is the one that\n * tells the user the server is up.\n */\nexport async function runWebServe(options: RunWebServeOptions): Promise<number> {\n // Built conditionally, not `{ port: options.port, ... }`: loadWebConfig()\n // spreads this object directly over the lower-precedence layers, and an\n // explicit `undefined` value would overwrite an env/.issue-flow.json\n // setting instead of falling through to it (only an *absent* key does).\n const cli: Partial<WebConfig> = {};\n if (options.port !== undefined) cli.port = options.port;\n if (options.host !== undefined) cli.host = options.host;\n if (options.refresh !== undefined) cli.refreshSeconds = options.refresh;\n const webConfig = await loadWebConfig({ cli });\n\n const sessions = watchSessionDirectory();\n\n const noop = (): void => {};\n const handle = await ensureSingleWebServer({\n sessions,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n unref: false,\n info: noop,\n warn: noop,\n });\n\n if (handle === null) {\n sessions.close();\n return 1;\n }\n\n if (handle.server === undefined) {\n // Another instance already won the race (or was already running by the\n // time this process got here): nothing to serve, so it exits right away\n // instead of idling as a redundant detached process.\n sessions.close();\n return 0;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n sessions.close();\n };\n\n return 0;\n}\n\n/** Stop the single running web monitor instance, if any. */\nexport async function runWebStop(): Promise<number> {\n const webConfig = await loadWebConfig();\n const result = await stopWebMonitor({ port: webConfig.port, host: webConfig.host });\n return result === 'failed' || result === 'unowned' ? 1 : 0;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/web/AGENTS.md
CHANGED
|
@@ -18,6 +18,10 @@ informado). Os dois significam "não informado" e nunca podem virar `0`,
|
|
|
18
18
|
qualquer coisa que não seja número finito para `null`. Prefira
|
|
19
19
|
`x !== null && x !== undefined` a `!x`: zero é um valor legítimo.
|
|
20
20
|
|
|
21
|
+
Toda resposta nova carrega `X-Issue-Flow-Instance`. O client guarda a primeira
|
|
22
|
+
identidade observada e chama `window.location.reload()` quando ela muda: isso é
|
|
23
|
+
o handoff de assets depois de `--restart-web`, não um estado de sessão.
|
|
24
|
+
|
|
21
25
|
Com **uma** sessão ativa o painel abre direto no detalhe (comportamento
|
|
22
26
|
histórico). Com **duas ou mais**, `renderDashboard()` lista um card por
|
|
23
27
|
execução; o clique define `state.selectedSessionId` e o poll passa a usar o
|
package/web/public/app.js
CHANGED
|
@@ -127,6 +127,9 @@
|
|
|
127
127
|
// guardar o nó levaria a uma referência morta.
|
|
128
128
|
selectedDetail: null,
|
|
129
129
|
configWritable: false,
|
|
130
|
+
// Identifica o processo que serviu os assets atuais. Uma troca significa
|
|
131
|
+
// que --restart-web colocou código novo no mesmo origin e exige reload.
|
|
132
|
+
serverInstanceId: null,
|
|
130
133
|
// Multi-sessão (#35): lista de /api/sessions e seleção explícita do usuário.
|
|
131
134
|
// selectedSessionId null = modo automático (1 sessão → detalhe; 2+ → dashboard).
|
|
132
135
|
sessions: [],
|
|
@@ -528,6 +531,20 @@
|
|
|
528
531
|
state.eventsUrl = null;
|
|
529
532
|
}
|
|
530
533
|
|
|
534
|
+
function serverInstanceChanged(response) {
|
|
535
|
+
const instanceId = response.headers.get('X-Issue-Flow-Instance');
|
|
536
|
+
if (!instanceId) return false; // servidor anterior à identidade de instância
|
|
537
|
+
if (state.serverInstanceId === null) {
|
|
538
|
+
state.serverInstanceId = instanceId;
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
if (state.serverInstanceId !== instanceId) {
|
|
542
|
+
window.location.reload();
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
531
548
|
function requestPoll() {
|
|
532
549
|
clearTimer();
|
|
533
550
|
if (state.polling) {
|
|
@@ -560,6 +577,7 @@
|
|
|
560
577
|
state.pollAgain = false;
|
|
561
578
|
try {
|
|
562
579
|
const sessionsRes = await fetch('api/sessions', { cache: 'no-store' });
|
|
580
|
+
if (serverInstanceChanged(sessionsRes)) return;
|
|
563
581
|
if (!sessionsRes.ok) throw new Error('HTTP ' + sessionsRes.status);
|
|
564
582
|
const sessions = await sessionsRes.json();
|
|
565
583
|
if (!Array.isArray(sessions)) throw new Error('sessions payload invalid');
|
|
@@ -1721,18 +1739,19 @@
|
|
|
1721
1739
|
// Default do seletor vem da configuração do servidor (/api/health);
|
|
1722
1740
|
// a escolha do usuário em localStorage tem precedência.
|
|
1723
1741
|
const stored = readStoredRefresh();
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
// Sem /api/health segue o default local (5s).
|
|
1742
|
+
try {
|
|
1743
|
+
const healthRes = await fetch('api/health', { cache: 'no-store' });
|
|
1744
|
+
serverInstanceChanged(healthRes);
|
|
1745
|
+
const health = await healthRes.json();
|
|
1746
|
+
state.configWritable = list(health.capabilities).includes('config:agent:write');
|
|
1747
|
+
const suggested = Number(health.refreshSeconds);
|
|
1748
|
+
if (stored === null && Number.isFinite(suggested) && suggested > 0) {
|
|
1749
|
+
state.refreshSeconds = suggested;
|
|
1734
1750
|
}
|
|
1751
|
+
} catch (err) {
|
|
1752
|
+
// Sem /api/health segue o default local (5s).
|
|
1735
1753
|
}
|
|
1754
|
+
if (stored !== null) state.refreshSeconds = stored;
|
|
1736
1755
|
buildRefreshSelect();
|
|
1737
1756
|
|
|
1738
1757
|
window.setInterval(renderTimers, 1000);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/execute.ts","../src/core/engine.ts","../src/ui/progress.ts","../src/ui/summary.ts","../src/ui/text.ts","../src/core/executor.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { createConfig, loadWebConfig, resolvePaths, validateDependencies } from '../config.js';\nimport { runEngine } from '../core/engine.js';\nimport { getSessionPublisher, setSessionPublisher } from '../core/session-publisher.js';\nimport { FilePublisher, NullPublisher } from '../core/session-state.js';\nimport { allStoriesPass, isoNow, loadTaskPlan, saveTaskPlan } from '../core/state-manager.js';\nimport { resolveIssuePaths } from '../storage/resolve.js';\nimport { printError } from '../ui/logger.js';\nimport { ensureWebMonitor } from '../web/lock.js';\n\nexport interface ExecuteOptions {\n issue?: string;\n maxIterations?: number;\n retryLimit?: number;\n retryForever?: boolean;\n /**\n * Conventional-commit scope for the stories of this issue (`issue-71`).\n * Only the multi-issue queue sets it — see `EngineConfig.commitScope`.\n */\n commitScope?: string;\n}\n\nexport async function runExecute(\n positionalMaxIter: number | undefined,\n options: ExecuteOptions,\n): Promise<number> {\n const errors = await validateDependencies();\n if (errors.length > 0) {\n printError('The following required tools are not installed:');\n for (const err of errors) {\n console.log(err);\n }\n return 1;\n }\n\n const maxIterations = options.maxIterations ?? positionalMaxIter;\n\n const config = createConfig({\n issueNumber: options.issue,\n maxIterations,\n retryLimit: options.retryLimit,\n retryForever: options.retryForever,\n commitScope: options.commitScope,\n });\n\n const paths = await resolvePaths(config);\n const webConfig = await loadWebConfig();\n const inheritedPublisher = getSessionPublisher();\n let standalonePublisher: FilePublisher | null = null;\n\n // `run` owns the publisher when it delegates to this command. A direct\n // `execute --issue N --web` has no owner, so install the same file-backed\n // surface here and tear down only what this invocation created.\n if (\n webConfig.enabled &&\n config.issueNumber !== undefined &&\n inheritedPublisher instanceof NullPublisher\n ) {\n const issuePaths = await resolveIssuePaths(config.issueNumber, {\n projectRoot: paths.projectRoot,\n });\n standalonePublisher = new FilePublisher(issuePaths.sessionFile, {\n logLimit: webConfig.logLimit,\n includeLogs: webConfig.includeLogs,\n });\n setSessionPublisher(standalonePublisher);\n const numericIssue = /^\\d+$/.test(config.issueNumber) ? Number(config.issueNumber) : null;\n standalonePublisher.publish({\n type: 'session:start',\n at: isoNow(),\n sessionId: randomUUID(),\n issueNumber: numericIssue,\n phases: ['execute'],\n });\n standalonePublisher.publish({ type: 'phase:start', at: isoNow(), phase: 'execute' });\n await ensureWebMonitor({\n publisher: standalonePublisher,\n port: webConfig.port,\n host: webConfig.host,\n refreshSeconds: webConfig.refreshSeconds,\n });\n }\n\n let exitCode: number | null = null;\n try {\n exitCode = await runEngine(config, paths);\n\n // Update pipeline state if all stories pass\n if (exitCode === 0 && config.issueNumber) {\n try {\n const plan = await loadTaskPlan(paths.prdFile);\n if (allStoriesPass(plan)) {\n plan.pipeline.executionCompleted = true;\n await saveTaskPlan(paths.prdFile, plan);\n }\n } catch {\n // Non-critical — engine already handled state\n }\n }\n\n return exitCode;\n } finally {\n if (standalonePublisher !== null) {\n const success = exitCode === 0;\n standalonePublisher.publish({\n type: 'phase:end',\n at: isoNow(),\n phase: 'execute',\n success,\n ...(success ? {} : { error: 'Execute phase failed' }),\n });\n standalonePublisher.publish({\n type: 'session:end',\n at: isoNow(),\n status: success ? 'completed' : 'failed',\n });\n await standalonePublisher.close();\n setSessionPublisher(undefined);\n }\n }\n}\n","import { existsSync } from 'node:fs';\nimport { cp, mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { resolveAgentFor } from '../agents/resolve.js';\nimport { getActiveResilienceConfig, loadGlobalConfig } from '../config.js';\nimport { archiveFolderName, type ChangeType, commitMessage } from '../conventions/git/index.js';\nimport { resolvePolicyPlaceholders } from '../policy/placeholders.js';\nimport { classify } from '../resilience/errors.js';\nimport type { RetryPolicy } from '../resilience/policy.js';\nimport { resolvePolicy } from '../resilience/policy.js';\nimport { fixedBackoffPolicy, withRetry } from '../resilience/retry.js';\nimport type { ClaudeResult, EngineConfig, ResolvedPaths, TaskPlan, UserStory } from '../types.js';\nimport { printError, printInfo, printRetry, printSuccess, printWarning } from '../ui/logger.js';\nimport { printIterationHeader } from '../ui/progress.js';\nimport { printStartupHeader, printSummaryBox } from '../ui/summary.js';\nimport { committedStoryIds, getBaseBranch, isWorkingTreeClean } from '../utils/git.js';\nimport { applyAcceptanceToPlan, resolveIssueDir, runAcceptanceGate } from '../verify/gate.js';\nimport { executeClaude } from './executor.js';\nimport { divideUsage } from './metrics.js';\nimport { applyPlaceholders, loadPrompt } from './prompt-resolver.js';\nimport {\n EXECUTE_PHASE,\n elapsedSecondsSince,\n getPhaseUsageTotals,\n publishIterationMetrics,\n publishStoryMetrics,\n} from './session-metrics.js';\nimport { getSessionPublisher } from './session-publisher.js';\nimport { getShutdownSignal } from './shutdown.js';\nimport {\n allStoriesPass,\n applyStoryMetrics,\n clearLastError,\n hasPendingCorrection,\n initializeState,\n isoNow,\n loadTaskPlan,\n markIssueCompleted,\n markIssueInProgress,\n saveTaskPlan,\n selectActiveStory,\n setLastError,\n trimErrorMessage,\n} from './state-manager.js';\nimport {\n getInactivityTimeout,\n getOutputCallback,\n getStoryStageCallback,\n getStoryUpdateCallback,\n isVerbose,\n} from './verbose.js';\n\n/**\n * Emit a message through the output callback if available, otherwise console.log.\n * Used for bare console.log calls in engine functions so they route through listr2.\n */\nfunction emitLog(message: string): void {\n const cb = getOutputCallback();\n if (cb) {\n // Skip empty lines in listr2 context — they don't render meaningfully\n if (message) {\n cb(message);\n }\n } else {\n console.log(message);\n }\n}\n\n/** What one pass of the execute loop produces, retry or not. */\ninterface ExecuteAttempt {\n result: ClaudeResult;\n /** When the attempt started, for `clearLastError`. */\n startedAt: string;\n /** How long the attempt took, for the metrics split across stories. */\n seconds: number;\n /** The stories as they were before the agent ran, for attribution. */\n storiesBefore: UserStory[];\n}\n\n/**\n * The execute loop's retry budget, unchanged in effect from the hand-rolled\n * loop it replaces: `retryLimit` counts *retries*, so the attempt budget is one\n * more than it, and `retryForever` lifts the count while `backoffMaxSeconds`\n * still caps the wait.\n */\nfunction executeRetryPolicy(config: EngineConfig): RetryPolicy {\n return fixedBackoffPolicy(\n config.retryLimit + 1,\n config.backoffBaseSeconds,\n config.backoffMaxSeconds,\n { retryForever: config.retryForever },\n );\n}\n\n/**\n * Ids of the stories that flipped from `passes: false` to `passes: true`\n * between the two readings of the plan taken around one execute iteration.\n *\n * Stories already passing before the iteration, and stories that only exist in\n * the newer plan, are never attributed: nothing says this iteration paid for\n * them.\n */\nfunction newlyCompletedStoryIds(before: UserStory[], after: UserStory[]): string[] {\n const wasPassing = new Map(before.map((story) => [story.id, story.passes]));\n return after\n .filter((story) => story.passes && wasPassing.get(story.id) === false)\n .map((story) => story.id);\n}\n\nexport interface CommitPlaceholderOptions {\n issueNumber?: string | number | null;\n signoff?: boolean;\n type?: ChangeType;\n}\n\nfunction numericIssue(value: string | number | null | undefined): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n const parsed = typeof value === 'number' ? value : Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Commit message formats handed to the execute prompt.\n *\n * Built by `commitMessage()` so Conventional Commits is the default even when\n * the repository declared nothing. A scope is the issue id of a queue, which\n * is what makes `git log` on the shared branch readable per issue.\n */\nexport function commitPlaceholders(\n scope?: string,\n convention?: string | null,\n options?: CommitPlaceholderOptions,\n): Record<string, string> {\n const declared = convention !== undefined && convention !== null && convention !== '';\n const type = options?.type ?? 'feat';\n const issueNumber = numericIssue(options?.issueNumber);\n const resolvedScope = scope === undefined || scope === '' ? undefined : scope;\n\n if (declared && options?.type === undefined) {\n const suffix = resolvedScope === undefined ? '' : `(${resolvedScope})`;\n return {\n __COMMIT_MESSAGE__: commitMessage({\n type,\n scope: resolvedScope,\n subject: '[Story ID] - [Story Title]',\n issueNumber,\n signoff: options?.signoff,\n }).replace(`${type}${suffix}:`, `<type>${suffix}:`),\n __FIX_COMMIT_MESSAGE__: commitMessage({\n type: 'fix',\n scope: resolvedScope,\n subject: 'address review findings',\n issueNumber,\n signoff: options?.signoff,\n }),\n };\n }\n\n return {\n __COMMIT_MESSAGE__: commitMessage({\n type,\n scope: resolvedScope,\n subject: '[Story ID] - [Story Title]',\n issueNumber,\n signoff: options?.signoff,\n }),\n __FIX_COMMIT_MESSAGE__: commitMessage({\n type: 'fix',\n scope: resolvedScope,\n subject: 'address review findings',\n issueNumber,\n signoff: options?.signoff,\n }),\n };\n}\n\n/**\n * The watchdog budget of one iteration, from the process-wide setting.\n *\n * Absent means the default; `0` turns the watchdog off. Reading it here rather\n * than threading it through the engine's config keeps it a single value per\n * process, which is what it is.\n */\nfunction inactivityOptions(): { inactivityTimeoutMs?: number } {\n const configured = getInactivityTimeout();\n return configured === undefined ? {} : { inactivityTimeoutMs: configured };\n}\n\n/**\n * Mark as passing every pending story whose commit is already on the branch.\n *\n * Two conditions, and both are required:\n *\n * - the story's id appears in a commit subject of this branch, which is what\n * the execute prompt writes (`<type>(scope): US-001 - Title`);\n * - the working tree is **clean**, so nothing is half-applied. A dirty tree\n * means work in flight, and adopting a story on that basis would call\n * finished something that is not.\n *\n * Returns the ids it adopted. Never throws: a git that cannot answer, or a plan\n * that cannot be written, leaves the loop doing exactly what it did before.\n */\nasync function adoptCommittedStories(tasksPath: string): Promise<string[]> {\n try {\n const plan = await loadTaskPlan(tasksPath);\n const pending = plan.userStories.filter((story) => !story.passes);\n if (pending.length === 0) return [];\n\n if (!(await isWorkingTreeClean())) return [];\n\n const committed = await committedStoryIds(await getBaseBranch());\n const adopted = pending.filter((story) => committed.has(story.id)).map((story) => story.id);\n if (adopted.length === 0) return [];\n\n await saveTaskPlan(tasksPath, {\n ...plan,\n userStories: plan.userStories.map((story) =>\n adopted.includes(story.id) ? { ...story, passes: true } : story,\n ),\n });\n return adopted;\n } catch {\n return [];\n }\n}\n\n/**\n * Initialize the progress file if it doesn't exist.\n */\nasync function ensureProgressFile(progressFile: string): Promise<void> {\n if (!existsSync(progressFile)) {\n const content = `# Issue Flow Progress Log\\nStarted: ${new Date().toString()}\\n---\\n`;\n await writeFile(progressFile, content, 'utf-8');\n }\n}\n\n/**\n * Archive previous run artifacts if the branch has changed.\n */\nasync function archiveIfBranchChanged(plan: TaskPlan, paths: ResolvedPaths): Promise<void> {\n const { lastBranchFile, archiveDir, prdFile, progressFile } = paths;\n\n if (!existsSync(lastBranchFile)) {\n return;\n }\n\n const currentBranch = plan.branchName ?? '';\n let lastBranch = '';\n\n try {\n lastBranch = (await readFile(lastBranchFile, 'utf-8')).trim();\n } catch {\n return;\n }\n\n if (currentBranch && lastBranch && currentBranch !== lastBranch) {\n const dateStr = new Date().toISOString().split('T')[0];\n const folderName = archiveFolderName(lastBranch);\n const archiveFolder = join(archiveDir, `${dateStr}-${folderName}`);\n\n printInfo(`Archiving previous run: ${lastBranch}`);\n await mkdir(archiveFolder, { recursive: true });\n\n if (existsSync(prdFile)) {\n await cp(prdFile, join(archiveFolder, 'tasks.json'));\n }\n if (existsSync(progressFile)) {\n await cp(progressFile, join(archiveFolder, 'progress.txt'));\n }\n\n printInfo(` Archived to: ${archiveFolder}`);\n\n // Reset progress file for new run\n await writeFile(\n progressFile,\n `# Issue Flow Progress Log\\nStarted: ${new Date().toString()}\\n---\\n`,\n 'utf-8',\n );\n }\n}\n\n/**\n * Write the current branch to the last-branch tracking file.\n */\nasync function trackBranch(plan: TaskPlan, lastBranchFile: string): Promise<void> {\n const branch = plan.branchName ?? '';\n if (branch) {\n await writeFile(lastBranchFile, `${branch}\\n`, 'utf-8');\n }\n}\n\n/**\n * Run the issue-flow engine loop.\n *\n * This replicates the full execution flow:\n * 1. Load and initialize task plan state\n * 2. Check for early exit (already complete)\n * 3. Archive previous run if branch changed\n * 4. Resolve prompt\n * 5. Main loop: iterate, execute Claude, handle results\n * 6. Print summary\n */\nexport async function runEngine(config: EngineConfig, paths: ResolvedPaths): Promise<number> {\n // Load task plan\n if (!existsSync(paths.prdFile)) {\n printError(`PRD file not found at ${paths.prdFile}`);\n if (config.issueNumber) {\n emitLog(`Have you run the resolve-issue skill for issue #${config.issueNumber} first?`);\n }\n return 1;\n }\n\n let plan = await loadTaskPlan(paths.prdFile);\n plan = initializeState(plan);\n await saveTaskPlan(paths.prdFile, plan);\n\n // Check if already completed. A pending correction (a failed review whose\n // findings haven't been addressed yet) always overrides this, even though\n // every userStories[].passes is already true — otherwise a correction\n // cycle's re-run of execute would exit here without ever looking at what\n // the review found.\n if (plan.issueStatus === 'completed' && allStoriesPass(plan) && !hasPendingCorrection(plan)) {\n emitLog(`Issue already marked complete in ${paths.prdFile}`);\n return 0;\n }\n\n // Warn if marked complete but stories still pending\n if (plan.issueStatus === 'completed' && !allStoriesPass(plan)) {\n printWarning(\n 'Issue marked completed but some stories are still pending. Resetting to in_progress.',\n );\n plan = markIssueInProgress(plan);\n plan = setLastError(\n plan,\n 'invalid_completion_state',\n 'tasks.json claimed the issue was completed before every story had passes=true.',\n );\n await saveTaskPlan(paths.prdFile, plan);\n }\n\n // Check if all stories already pass\n if (allStoriesPass(plan) && !hasPendingCorrection(plan)) {\n emitLog('All user stories already pass. Marking issue as completed.');\n plan = markIssueCompleted(plan);\n await saveTaskPlan(paths.prdFile, plan);\n return finishWithAcceptance(config, paths, plan);\n }\n\n // Archive previous run if branch changed\n await archiveIfBranchChanged(plan, paths);\n\n // Track current branch\n await trackBranch(plan, paths.lastBranchFile);\n\n // Initialize progress file\n await ensureProgressFile(paths.progressFile);\n\n // Load prompt template. The root is handed over rather than rediscovered:\n // it was resolved once by `resolvePaths()`, and a second `git rev-parse` here\n // would be a subprocess per run for an answer already in hand.\n const promptTemplate = await loadPrompt('execute', { projectRoot: paths.projectRoot });\n\n // The repository's own conventions, resolved once for the whole loop: they\n // cannot change mid-run, and every iteration renders the same projection.\n const policy = await resolvePolicyPlaceholders({ root: paths.projectRoot });\n const signoff = (await loadGlobalConfig()).commit?.signoff === true;\n\n const executeAgent = await resolveAgentFor('execute');\n printStartupHeader(config, plan, {\n provider: executeAgent.provider,\n model: executeAgent.model,\n ...(executeAgent.codex.reasoningEffort ? { detail: executeAgent.codex.reasoningEffort } : {}),\n });\n\n const startTime = Date.now();\n let i = 0;\n let totalRetryCount = 0;\n\n // Main loop\n while (true) {\n // Check iteration limit\n if (config.maxIterations !== undefined && i >= config.maxIterations) {\n break;\n }\n\n i++;\n\n // Safety net for the crash between a story's commit and the agent writing\n // `passes: true` (scenario I). The commit is the durable fact; the plan is\n // written after it, so a process that died in between leaves work that is\n // done and a plan that says it is not — and the next iteration redoes it on\n // top of a commit that already exists.\n //\n // This never *replaces* the agent's `passes`, which stays the primary\n // source: it only closes the window the agent cannot close itself.\n const adopted = await adoptCommittedStories(paths.prdFile);\n if (adopted.length > 0) {\n printInfo(\n `Adopting ${adopted.join(', ')}: already committed on this branch with a clean tree.`,\n );\n plan = await loadTaskPlan(paths.prdFile);\n if (plan.userStories.every((story) => story.passes) && plan.lastReviewFindings === null) {\n // Everything the loop was going to do is already done.\n break;\n }\n }\n\n // One iteration is one attempt of the Claude CLI, retried in place by the\n // project's single retry executor (`resilience/retry.ts`). A retry costs no\n // iteration budget, exactly as the `i--` it replaces cost none. Everything a\n // retried attempt has to redo — re-reading the plan, republishing\n // `iteration:start`, re-rendering the prompt — lives inside `attempt`,\n // because the loop this used to be written as re-ran all of it too.\n const attempted = await withRetry<ExecuteAttempt>(\n async () => {\n // Re-read plan to get latest state\n plan = await loadTaskPlan(paths.prdFile);\n // Publish the plan before naming its active story. This covers direct\n // `issue-flow execute` runs and resumes that never pass through the\n // plan runner, while retaining the empty-plan no-op contract.\n if (plan.userStories.length > 0) {\n getSessionPublisher().publish({\n type: 'stories:update',\n at: isoNow(),\n stories: plan.userStories,\n });\n }\n // Baseline for story attribution: whatever was still pending before the\n // agent ran is what this iteration can claim credit for.\n const storiesBefore = plan.userStories;\n\n // Highest-priority story with passes: false — the same rule\n // prompts/execute.md gives the agent. Computed once, here, and shared by\n // the published event and the verbose terminal header, so every surface\n // agrees on who is active instead of each deriving its own heuristic.\n const activeStoryId = selectActiveStory(plan.userStories)?.id;\n getSessionPublisher().publish({\n type: 'iteration:start',\n at: isoNow(),\n iteration: i,\n storyId: activeStoryId,\n });\n getStoryStageCallback()?.(activeStoryId);\n\n if (isVerbose()) {\n printIterationHeader(i, config.maxIterations, plan.userStories, activeStoryId);\n }\n\n // Apply placeholders to prompt\n const prompt = applyPlaceholders(promptTemplate, {\n __PRD_FILE__: paths.prdFile,\n __PROGRESS_FILE__: paths.progressFile,\n ...policy,\n // The convention comes off the projection above rather than from a second\n // discovery: one resolution per run is the whole point of the cache.\n ...commitPlaceholders(config.commitScope, policy.__COMMIT_CONVENTION__, {\n issueNumber: plan.issueNumber,\n signoff,\n }),\n __STORIES_PER_ITERATION__: String(config.storiesPerIteration ?? 1),\n });\n\n const startedAt = isoNow();\n plan = markIssueInProgress(plan, startedAt);\n await saveTaskPlan(paths.prdFile, plan);\n\n // Execute Claude\n const startedAtMs = Date.now();\n const result = await executeClaude(prompt, {\n ...inactivityOptions(),\n iteration: i,\n correctionCycle: plan.correctionCycle,\n ...(activeStoryId === undefined ? {} : { storyIds: [activeStoryId] }),\n });\n const seconds = elapsedSecondsSince(startedAtMs);\n\n if (result.exitCode !== 0) {\n // A failed iteration still burned tokens: report them on the execute\n // phase, with no attribution to any story.\n publishIterationMetrics(i, result.cost, seconds);\n }\n\n return { result, startedAt, seconds, storiesBefore };\n },\n {\n policy: executeRetryPolicy(config),\n // A `Ctrl+C` during a fifteen-minute backoff must stop in that instant,\n // not fifteen minutes later. `abortableDelay` resolves `false` on the\n // abort, which `withRetry` reports as `aborted` rather than a failure.\n signal: getShutdownSignal(),\n // The exit code and the output are all the evidence the CLI leaves\n // behind, which is exactly what `isTransientFailure()` used to be given\n // here — `classify()` is the same verdict plus the kind.\n evaluate: ({ result }) =>\n result.exitCode === 0\n ? null\n : classify({ source: 'agent', exitCode: result.exitCode, stdout: result.output }),\n onAttempt: async ({ value, attempt, failure, willRetry, delayMs }) => {\n if (failure === null) return;\n\n const errorMessage = trimErrorMessage(value.result.output);\n\n if (!failure.retryable) {\n plan = await loadTaskPlan(paths.prdFile);\n plan = setLastError(plan, 'fatal_claude_failure', errorMessage);\n if (resolvePolicy(failure.kind, getActiveResilienceConfig()).onExhausted === 'block') {\n plan = {\n ...plan,\n runState: {\n currentPhase: 'execute',\n attempt,\n owner: plan.runState?.owner ?? null,\n status: 'blocked',\n blockedReason: errorMessage,\n lastHeartbeatAt: isoNow(),\n },\n };\n }\n await saveTaskPlan(paths.prdFile, plan);\n return;\n }\n\n // Recorded even on the attempt that spends the last of the budget:\n // the run ends on a transient failure, and the plan has to say so.\n totalRetryCount++;\n plan = await loadTaskPlan(paths.prdFile);\n plan = setLastError(plan, 'transient_claude_failure', errorMessage);\n await saveTaskPlan(paths.prdFile, plan);\n if (!willRetry) return;\n\n const delaySeconds = delayMs / 1000;\n getSessionPublisher().publish({\n type: 'retry',\n at: isoNow(),\n attempt,\n delaySeconds,\n reason: 'transient_claude_failure',\n kind: failure.kind,\n });\n\n if (isVerbose()) {\n emitLog('');\n }\n printRetry(\n `Transient Claude failure on iteration ${i} (attempt ${attempt}). Retrying in ${delaySeconds}s.`,\n );\n },\n },\n );\n\n const {\n result,\n seconds: iterationSeconds,\n storiesBefore: storiesBeforeIteration,\n } = attempted.value;\n const iterationStartedAt = attempted.value.startedAt;\n\n if (result.exitCode !== 0) {\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n plan = await loadTaskPlan(paths.prdFile);\n printSummaryBox(\n 'failed',\n i,\n totalRetryCount,\n elapsed,\n plan,\n attempted.exhausted\n ? `Exceeded retry limit (${config.retryLimit}) on transient errors`\n : `Claude CLI failed with exit code ${result.exitCode}`,\n getPhaseUsageTotals(EXECUTE_PHASE),\n );\n return result.exitCode;\n }\n\n plan = await loadTaskPlan(paths.prdFile);\n plan = clearLastError(plan, iterationStartedAt);\n await saveTaskPlan(paths.prdFile, plan);\n\n // Notify story progress listeners (e.g., listr2 subtasks)\n const storyUpdateCb = getStoryUpdateCallback();\n if (storyUpdateCb) {\n storyUpdateCb(plan.userStories);\n }\n getSessionPublisher().publish({\n type: 'stories:update',\n at: isoNow(),\n stories: plan.userStories,\n });\n\n // Story attribution — an approximation by construction: the CLI reports a\n // single usage for the whole iteration, so the stories that flipped to\n // passing in it split tokens, cost and duration evenly. With none, nothing\n // is attributed and the whole cost stays on the execute phase. Published\n // after stories:update, which rebuilds the stories array.\n const completedStoryIds = newlyCompletedStoryIds(storiesBeforeIteration, plan.userStories);\n if (completedStoryIds.length > 0) {\n const storyShare = divideUsage(result.cost, completedStoryIds.length);\n const storySeconds = Math.round(iterationSeconds / completedStoryIds.length);\n for (const storyId of completedStoryIds) {\n publishStoryMetrics(storyId, storyShare, storySeconds);\n }\n\n // The same shares also land on tasks.json, so the numbers outlive the\n // session and are readable with web monitoring off. Persisting them is\n // observational: a write failure must never change the iteration's\n // outcome, so the plan in memory only advances when the write succeeded.\n const planWithMetrics = applyStoryMetrics(plan, completedStoryIds, storyShare, storySeconds);\n try {\n await saveTaskPlan(paths.prdFile, planWithMetrics);\n plan = planWithMetrics;\n } catch {\n // Metrics are a nice-to-have; the iteration succeeded regardless.\n }\n }\n\n getSessionPublisher().publish({ type: 'iteration:end', at: isoNow(), iteration: i });\n publishIterationMetrics(i, result.cost, iterationSeconds);\n\n // Check for completion signal\n if (result.output.includes('<promise>COMPLETE</promise>')) {\n plan = await loadTaskPlan(paths.prdFile);\n if (allStoriesPass(plan) && !hasPendingCorrection(plan)) {\n plan = markIssueCompleted(plan);\n await saveTaskPlan(paths.prdFile, plan);\n\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n printSummaryBox(\n 'success',\n i,\n totalRetryCount,\n elapsed,\n plan,\n undefined,\n getPhaseUsageTotals(EXECUTE_PHASE),\n );\n return finishWithAcceptance(config, paths, plan);\n }\n\n plan = setLastError(\n plan,\n 'invalid_completion_signal',\n hasPendingCorrection(plan)\n ? 'Claude returned <promise>COMPLETE</promise> while lastReviewFindings was still set.'\n : 'Claude returned <promise>COMPLETE</promise> before every story had passes=true.',\n );\n await saveTaskPlan(paths.prdFile, plan);\n\n emitLog('');\n printWarning(\n hasPendingCorrection(plan)\n ? 'Claude returned a completion signal, but lastReviewFindings is still set. Ignoring completion and continuing.'\n : 'Claude returned a completion signal, but tasks.json still has pending stories. Ignoring completion and continuing.',\n );\n }\n\n if (isVerbose()) {\n printSuccess(`Iteration ${i} complete. Continuing...`);\n }\n }\n\n // Reached max iterations\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n plan = await loadTaskPlan(paths.prdFile);\n printSummaryBox(\n 'incomplete',\n config.maxIterations ?? i,\n totalRetryCount,\n elapsed,\n plan,\n 'Reached max iterations without completing all tasks.',\n getPhaseUsageTotals(EXECUTE_PHASE),\n );\n return 1;\n}\n\nasync function finishWithAcceptance(\n config: EngineConfig,\n paths: ResolvedPaths,\n plan: TaskPlan,\n): Promise<number> {\n const issueDir = resolveIssueDir(config, paths);\n const outcome = await runAcceptanceGate({\n issueDir,\n cwd: paths.projectRoot,\n addDirs: config.issueNumber === undefined ? undefined : [issueDir],\n skipReviewer: true,\n });\n const applied = applyAcceptanceToPlan(plan, outcome);\n if (applied.failed) {\n await saveTaskPlan(paths.prdFile, applied.plan);\n return 1;\n }\n return 0;\n}\n","import chalk from 'chalk';\nimport { getOutputCallback } from '../core/verbose.js';\nimport type { UserStory } from '../types.js';\nimport { getIcons, useColor, useUnicode } from './logger.js';\n\n/**\n * Print a visual progress bar showing passed/total stories.\n */\nexport function printProgressBar(passed: number, total: number): string {\n const barWidth = 20;\n let pct = 0;\n let filled = 0;\n\n if (total > 0) {\n pct = Math.floor((passed * 100) / total);\n filled = Math.floor((passed * barWidth) / total);\n }\n\n const empty = barWidth - filled;\n const fillChar = useUnicode() ? '\\u2588' : '#';\n const emptyChar = useUnicode() ? '\\u2591' : '-';\n\n const bar = fillChar.repeat(filled) + emptyChar.repeat(empty);\n const text = `${passed}/${total} (${pct}%)`;\n\n if (useColor()) {\n return `${chalk.green(bar)} ${text}`;\n }\n return `${bar} ${text}`;\n}\n\n/**\n * Print the iteration header showing story statuses and progress bar.\n *\n * When a global output callback is set (e.g., inside a listr2 task),\n * output is routed through it instead of console.log.\n *\n * `activeStoryId`, when provided, identifies the story the engine is\n * actually about to work on this iteration (the same id published on\n * `iteration:start` \\u2014 see `core/state-manager.ts`'s `selectActiveStory`),\n * so the header agrees with the session snapshot instead of guessing.\n * Omitted, it falls back to the previous heuristic (\"first non-passing story\n * in array order\"), which existing callers that cannot supply the real id\n * keep relying on.\n */\nexport function printIterationHeader(\n iteration: number,\n maxIter: number | undefined,\n stories: UserStory[],\n activeStoryId?: string,\n): void {\n const icons = getIcons();\n const colored = useColor();\n const cb = getOutputCallback();\n\n const iterLabel = maxIter ? `Iteration ${iteration} of ${maxIter}` : `Iteration ${iteration}`;\n\n const total = stories.length;\n const passed = stories.filter((s) => s.passes).length;\n\n const lines: string[] = [];\n\n if (colored) {\n lines.push(chalk.blue(`\\u2501\\u2501\\u2501 ${icons.start} ${iterLabel} \\u2501\\u2501\\u2501`));\n } else {\n lines.push(`--- ${icons.start} ${iterLabel} ---`);\n }\n\n // Display each story status\n let foundFirstPending = false;\n for (const story of stories) {\n let icon: string;\n let colorFn: (s: string) => string;\n const isActive =\n activeStoryId !== undefined\n ? story.id === activeStoryId\n : !story.passes && !foundFirstPending;\n\n if (story.passes) {\n icon = icons.success;\n colorFn = colored ? chalk.green : (s: string) => s;\n } else if (isActive) {\n icon = icons.pending;\n colorFn = colored ? chalk.yellow : (s: string) => s;\n foundFirstPending = true;\n } else {\n // Not yet reached\n icon = icons.notReached;\n colorFn = colored ? chalk.gray : (s: string) => s;\n }\n\n lines.push(colorFn(` ${icon} ${story.id}: ${story.title}`));\n }\n\n lines.push(` ${printProgressBar(passed, total)}`);\n\n if (cb) {\n // Send as single output to listr2 task context\n cb(lines.join('\\n'));\n } else {\n console.log('');\n for (const line of lines) {\n console.log(line);\n }\n console.log('');\n }\n}\n","import chalk from 'chalk';\nimport { type ClaudeUsage, formatTokens } from '../core/metrics.js';\nimport { groupBy, summarize } from '../telemetry/aggregate.js';\nimport type { EngineConfig, PrReviewRecommendation, TaskPlan } from '../types.js';\nimport {\n formatDuration,\n getIcons,\n getTermWidth,\n printSuccess,\n printWarning,\n useColor,\n useUnicode,\n} from './logger.js';\n\n/**\n * Truncate or pad a string to fit within a given width.\n */\nfunction fitLine(text: string, width: number): string {\n if (text.length > width) {\n return text.substring(0, width);\n }\n return text.padEnd(width);\n}\n\n/**\n * Print a box with border characters around content lines.\n * Lines equal to \"---\" render as separator rows.\n */\nexport function printBox(lines: string[]): void {\n const colored = useColor();\n const unicode = useUnicode();\n const termWidth = getTermWidth();\n\n // Find max content width\n let maxContentWidth = 0;\n for (const line of lines) {\n if (line !== '---' && line.length > maxContentWidth) {\n maxContentWidth = line.length;\n }\n }\n\n // Cap to terminal width (border + padding = 4 chars)\n const available = termWidth - 4;\n if (maxContentWidth > available) {\n maxContentWidth = available;\n }\n if (maxContentWidth < 20) {\n maxContentWidth = 20;\n }\n\n // Box drawing characters\n const tl = unicode ? '\\u256D' : '+';\n const tr = unicode ? '\\u256E' : '+';\n const bl = unicode ? '\\u2570' : '+';\n const br = unicode ? '\\u256F' : '+';\n const h = unicode ? '\\u2500' : '-';\n const v = unicode ? '\\u2502' : '|';\n const sepL = unicode ? '\\u251C' : '+';\n const sepR = unicode ? '\\u2524' : '+';\n\n const hrule = h.repeat(maxContentWidth + 2);\n\n const blue = colored ? chalk.blue : (s: string) => s;\n const _reset = (s: string) => s;\n\n // Top border\n console.log(blue(`${tl}${hrule}${tr}`));\n\n // Content lines\n for (const line of lines) {\n if (line === '---') {\n console.log(blue(`${sepL}${hrule}${sepR}`));\n } else {\n const fitted = fitLine(line, maxContentWidth);\n console.log(`${blue(v)} ${fitted} ${blue(v)}`);\n }\n }\n\n // Bottom border\n console.log(blue(`${bl}${hrule}${br}`));\n}\n\n/**\n * Print the startup header box showing engine configuration.\n */\nexport interface StartupAgentInfo {\n provider: string;\n model?: string | null;\n detail?: string;\n}\n\nexport function printStartupHeader(\n config: EngineConfig,\n plan: TaskPlan,\n agent?: StartupAgentInfo,\n): void {\n const icons = getIcons();\n\n const storiesTotal = plan.userStories.length;\n const storiesPassing = plan.userStories.filter((s) => s.passes).length;\n const branchName = plan.branchName ?? 'N/A';\n\n const issueLabel = config.issueNumber ? `Issue #${config.issueNumber}` : 'Standalone mode';\n\n const maxIterLabel =\n config.maxIterations !== undefined ? String(config.maxIterations) : 'unlimited';\n\n const retryLabel = config.retryForever\n ? 'unlimited retries'\n : `${config.retryLimit} consecutive retries`;\n\n const lines = [\n `${icons.start} Issue Flow`,\n '---',\n `Issue: ${issueLabel}`,\n `Branch: ${branchName}`,\n `Stories: ${storiesPassing}/${storiesTotal} passing`,\n `Iterations: ${maxIterLabel}`,\n `Retries: ${retryLabel}`,\n ];\n const remaining = storiesTotal - storiesPassing;\n if (remaining > 0) {\n // p50 of execute on the #79 baseline (US-009/US-010 mean, single observation).\n const p50ExecuteSeconds = 518;\n lines.push(\n `Estimate: ~${formatDuration(remaining * p50ExecuteSeconds)} (${remaining} × p50 execute)`,\n );\n }\n if (agent) {\n const model = agent.model ? ` · ${agent.model}` : '';\n const detail = agent.detail ? ` (${agent.detail})` : '';\n lines.push(`Agent: ${agent.provider}${model}${detail}`);\n }\n printBox(lines);\n}\n\n/**\n * Print the final summary box.\n */\nexport function printSummaryBox(\n status: 'success' | 'incomplete' | 'failed',\n iterations: number,\n totalRetries: number,\n elapsedSeconds: number,\n plan: TaskPlan,\n extraInfo?: string,\n /**\n * Tokens and cost spent by this run, already resolved by the caller (the\n * process-owned counters in `core/session-metrics.ts`, never the session\n * snapshot — that one is empty whenever web monitoring is off). Omitted or\n * empty means \"the CLI reported nothing\", and the line is skipped entirely.\n */\n usage?: ClaudeUsage | null,\n usageByAgent?: Record<string, ClaudeUsage>,\n): void {\n const icons = getIcons();\n\n const storiesTotal = plan.userStories.length;\n const storiesPassing = plan.userStories.filter((s) => s.passes).length;\n const duration = formatDuration(elapsedSeconds);\n\n let statusIcon: string;\n let statusLabel: string;\n\n switch (status) {\n case 'success':\n statusIcon = icons.success;\n statusLabel = 'Completed';\n break;\n case 'incomplete':\n statusIcon = icons.warn;\n statusLabel = 'Incomplete';\n break;\n case 'failed':\n statusIcon = icons.fail;\n statusLabel = 'Failed';\n break;\n }\n\n const boxLines = [\n `${icons.end} Issue Flow Summary`,\n '---',\n `Status: ${statusIcon} ${statusLabel}`,\n `Stories: ${storiesPassing}/${storiesTotal} passing`,\n `Iterations: ${iterations}`,\n `Duration: ${duration}`,\n ];\n\n boxLines.push(...tokenBoxLines(usage, usageByAgent));\n boxLines.push(...executionCostLines(plan));\n\n boxLines.push(`Retries: ${totalRetries}`);\n\n if (extraInfo) {\n boxLines.push('---');\n boxLines.push(extraInfo);\n }\n\n console.log('');\n printBox(boxLines);\n}\n\n/**\n * What the optional `pr-review` phase produced, as far as the summary cares.\n * `null` in `recommendation` means the verdict could not be recovered from\n * disk — the exit code still told us whether changes were requested.\n */\nexport interface RunSummaryPrReview {\n requestedChanges: boolean;\n recommendation: PrReviewRecommendation | null;\n reportPath: string | null;\n}\n\n/**\n * Everything the final `run` summary prints, already resolved by the caller.\n */\nexport interface RunSummaryInfo {\n issueNumber: string;\n /** 'unknown' when the branch could not be detected. */\n branchName: string;\n /** Pipelines run with --no-branch open no Pull Request. */\n noBranch: boolean;\n storyCount: number;\n elapsedSeconds: number;\n /** 'unknown' when no Pull Request could be resolved. */\n prUrl: string;\n /** Only set when the pr-review phase actually ran. */\n prReview?: RunSummaryPrReview | null;\n /**\n * Tokens and cost of the whole pipeline, from the process-owned counters in\n * `core/session-metrics.ts`. Absent or empty omits the line.\n */\n usage?: ClaudeUsage | null;\n /** When more than one agent ran, the summary prints one Tokens line each. */\n usageByAgent?: Record<string, ClaudeUsage>;\n /** Acceptance-contract verdict. Absent when the contract never ran. */\n verification?: { verdict: 'passed' | 'failed' | 'unverified'; level?: string | null } | null;\n}\n\nfunction executionCostLines(plan: TaskPlan, prefix = 'Cost: '): string[] {\n const records = plan.executions ?? [];\n if (records.length === 0) return [];\n const lines: string[] = [];\n const groups = groupBy(records, 'harness');\n let index = 0;\n const pad = ' '.repeat(prefix.length);\n for (const [harness, summary] of groups) {\n const parts: string[] = [];\n if (summary.totalCost.reported > 0) {\n parts.push(`$${summary.totalCost.reported.toFixed(4)} reported`);\n }\n if (summary.totalCost.estimated > 0) {\n parts.push(`$${summary.totalCost.estimated.toFixed(4)} estimated`);\n }\n if (summary.totalCost.unknownExecutions > 0) {\n parts.push(`${summary.totalCost.unknownExecutions} unknown`);\n }\n if (parts.length === 0) {\n const totals = summarize(records).totalCost;\n if (totals.reported === 0 && totals.estimated === 0) {\n parts.push('not reported');\n }\n }\n if (parts.length === 0) continue;\n lines.push(`${index === 0 ? prefix : pad}${harness} · ${parts.join(' · ')}`);\n index += 1;\n }\n return lines;\n}\n\nfunction tokenBoxLines(\n usage?: ClaudeUsage | null,\n usageByAgent?: Record<string, ClaudeUsage>,\n): string[] {\n const agents = usageByAgent ? Object.keys(usageByAgent) : [];\n if (agents.length > 1) {\n const lines: string[] = [];\n for (const [index, id] of agents.entries()) {\n const tokens = formatTokens(usageByAgent?.[id]);\n if (tokens === '') continue;\n lines.push(index === 0 ? `Tokens: ${id} · ${tokens}` : ` ${id} · ${tokens}`);\n }\n return lines;\n }\n const tokens = formatTokens(usage);\n return tokens === '' ? [] : [`Tokens: ${tokens}`];\n}\n\nfunction tokenSummaryLines(\n usage?: ClaudeUsage | null,\n usageByAgent?: Record<string, ClaudeUsage>,\n prefix = ' Tokens: ',\n): string[] {\n const agents = usageByAgent ? Object.keys(usageByAgent) : [];\n if (agents.length > 1) {\n const pad = ' '.repeat(prefix.length);\n const lines: string[] = [];\n for (const [index, id] of agents.entries()) {\n const tokens = formatTokens(usageByAgent?.[id]);\n if (tokens === '') continue;\n lines.push(`${index === 0 ? prefix : pad}${id} · ${tokens}`);\n }\n return lines;\n }\n const tokens = formatTokens(usage);\n return tokens === '' ? [] : [`${prefix}${tokens}`];\n}\n\n/**\n * Detail lines of the final `run` summary (everything below the headline).\n *\n * Kept separate from the printing so the shape is testable without capturing\n * stdout. Without `prReview` the lines are exactly the ones the pipeline has\n * always printed.\n */\nexport function buildRunSummaryLines(info: RunSummaryInfo): string[] {\n const lines = [\n ` Branch: ${info.branchName}${info.noBranch ? ' (current)' : ''}`,\n ` Stories: ${info.storyCount}`,\n ` Duration: ${formatDuration(info.elapsedSeconds)}`,\n ];\n\n lines.push(...tokenSummaryLines(info.usage, info.usageByAgent));\n\n if (!info.noBranch) {\n lines.push(` PR: ${info.prUrl}`);\n }\n\n if (info.verification != null) {\n const label =\n info.verification.verdict === 'passed'\n ? 'passed'\n : info.verification.verdict === 'failed'\n ? 'failed'\n : 'unverified';\n lines.push(` Contract: ${label}`);\n }\n\n const review = info.prReview;\n if (review) {\n // The exit code is the source of truth for the verdict, so a report whose\n // recommendation could not be read still reports REQUEST_CHANGES.\n const verdict =\n review.recommendation ?? (review.requestedChanges ? 'REQUEST_CHANGES' : 'unknown');\n lines.push(` Review: ${verdict}`);\n if (review.reportPath !== null) {\n lines.push(` Report: ${review.reportPath}`);\n }\n }\n\n return lines;\n}\n\n/** One issue of a multi-issue queue, as the consolidated summary reports it. */\nexport interface QueueIssueSummary {\n id: string;\n title: string;\n storyCount: number;\n elapsedSeconds: number;\n /** Tokens and cost of this issue alone — never of the whole queue. */\n usage?: ClaudeUsage | null;\n}\n\n/** Everything the consolidated summary of a queue prints. */\nexport interface QueueSummaryInfo {\n /** Identifier of the queue, i.e. of its primary issue. */\n queueId: string;\n /** Branch every issue shared, `null` when the queue ran with --no-branch. */\n branchName: string | null;\n issues: QueueIssueSummary[];\n /** Issues discovered but left out of the run. */\n excluded: { id: string; title: string }[];\n elapsedSeconds: number;\n /** The single consolidated Pull Request, `null` when none was opened. */\n prUrl: string | null;\n /** Tokens and cost of the whole queue. */\n usage?: ClaudeUsage | null;\n /** Verdict of the pr-review phase, when the queue ran it. */\n prReview?: RunSummaryPrReview | null;\n}\n\n/**\n * Detail lines of a queue's final summary.\n *\n * Per-issue lines carry their own duration and cost, which is the visible half\n * of the per-issue metric scoping: what one issue spent is never folded into\n * another's line.\n */\nexport function buildQueueSummaryLines(info: QueueSummaryInfo): string[] {\n const lines: string[] = [];\n\n lines.push(` Branch: ${info.branchName ?? 'current'}`);\n lines.push(` Issues: ${info.issues.length}`);\n\n for (const issue of info.issues) {\n const tokens = formatTokens(issue.usage);\n const title = issue.title === '' ? '' : ` ${issue.title}`;\n lines.push(\n ` #${issue.id}${title} — ${issue.storyCount} stories, ${formatDuration(issue.elapsedSeconds)}` +\n (tokens === '' ? '' : `, ${tokens}`),\n );\n }\n\n lines.push(` Duration: ${formatDuration(info.elapsedSeconds)}`);\n\n const tokens = formatTokens(info.usage);\n if (tokens !== '') {\n lines.push(` Tokens: ${tokens}`);\n }\n if (info.prUrl !== null) {\n lines.push(` PR: ${info.prUrl}`);\n }\n if (info.excluded.length > 0) {\n lines.push(` Skipped: ${info.excluded.map((entry) => `#${entry.id}`).join(', ')}`);\n }\n\n const review = info.prReview;\n if (review) {\n lines.push(\n ` Review: ${review.recommendation ?? (review.requestedChanges ? 'REQUEST_CHANGES' : 'unknown')}`,\n );\n if (review.reportPath !== null) {\n lines.push(` Report: ${review.reportPath}`);\n }\n }\n\n return lines;\n}\n\n/** Print the consolidated summary of a multi-issue queue. */\nexport function printQueueSummary(info: QueueSummaryInfo): void {\n console.log('');\n if (info.prReview?.requestedChanges === true) {\n printWarning(`Queue finished for issue #${info.queueId}, but the PR review requested changes.`);\n } else {\n printSuccess(`Queue complete for issue #${info.queueId} (${info.issues.length} issues)!`);\n }\n for (const line of buildQueueSummaryLines(info)) {\n console.log(line);\n }\n}\n\n/**\n * Print the final summary of a `run` (the flat listing, not the box used by\n * the execute engine).\n */\nexport function printRunSummary(info: RunSummaryInfo): void {\n console.log('');\n if (info.prReview?.requestedChanges) {\n printWarning(\n `Pipeline finished for issue #${info.issueNumber}, but the PR review requested changes.`,\n );\n } else if (info.verification?.verdict === 'unverified') {\n printWarning(\n `Pipeline finished for issue #${info.issueNumber}, but the acceptance contract is unverified.`,\n );\n } else if (info.verification?.verdict === 'failed') {\n printWarning(\n `Pipeline finished for issue #${info.issueNumber}, but the acceptance contract failed.`,\n );\n } else {\n printSuccess(`Pipeline complete for issue #${info.issueNumber}!`);\n }\n for (const line of buildRunSummaryLines(info)) {\n console.log(line);\n }\n}\n","/**\n * Strip the markdown markers that leak into a terminal that cannot render them.\n *\n * Used by the clean view and by failure excerpts. Verbose mode keeps the\n * original text — this is only for the default surface.\n */\nexport function stripMarkdown(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/`([^`]+)`/g, '$1')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '$1')\n .replace(/__([^_]+)__/g, '$1')\n .replace(/(?<!\\*)\\*([^*]+)\\*(?!\\*)/g, '$1')\n .replace(/^#{1,6}\\s+/gm, '')\n .replace(/^\\s*[-*+]\\s+/gm, '')\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')\n .replace(/[ \\t]{2,}/g, ' ')\n .trim();\n}\n\n/** Last non-empty lines of a failure report, already stripped of markdown. */\nexport function failureExcerpt(output: string, limit = 8): string[] {\n return output\n .split('\\n')\n .map((line) => stripMarkdown(line))\n .filter((line) => line.length > 0)\n .slice(-limit);\n}\n","import { invokeSelectedAgent } from '../agents/invoke.js';\nimport type { ClaudeResult } from '../types.js';\nimport { failureExcerpt } from '../ui/text.js';\nimport { getOutputCallback, isVerbose } from './verbose.js';\n\nexport interface ExecuteClaudeOptions {\n /**\n * Silence tolerated before the agent is considered stuck. `0` disables the\n * watchdog, which is the behaviour every release before it had.\n */\n inactivityTimeoutMs?: number;\n iteration?: number;\n correctionCycle?: number;\n storyIds?: string[];\n}\n\n/**\n * Execute the resolved agent with a prompt piped to stdin.\n *\n * The default (Claude, unconfigured) is still\n * `claude --dangerously-skip-permissions --print --output-format stream-json --verbose`\n * with the prompt on stdin and `timeout: 0`. Callers see `exitCode`, the\n * assistant text as `output`, and usage. On failure the raw diagnostics stay\n * unwrapped so `isTransientFailure()` can read them.\n */\nexport async function executeClaude(\n prompt: string,\n options: ExecuteClaudeOptions = {},\n): Promise<ClaudeResult> {\n let selected: Awaited<ReturnType<typeof invokeSelectedAgent>>;\n try {\n selected = await invokeSelectedAgent({\n prompt,\n phase: 'execute',\n timeout: 0,\n permission: 'autonomous',\n inactivityTimeoutMs: options.inactivityTimeoutMs,\n iteration: options.iteration,\n correctionCycle: options.correctionCycle,\n storyIds: options.storyIds,\n });\n } catch (err) {\n return {\n exitCode: 1,\n output: err instanceof Error ? err.message : String(err),\n cost: null,\n };\n }\n const { run } = selected;\n\n let output: string;\n let cost = run.usage;\n\n if (!run.success || run.exitCode !== 0) {\n output = run.rawOutput || run.error || '';\n cost = null;\n } else {\n output = run.result || run.rawOutput;\n }\n\n emitAgentOutput(output, !(run.success && run.exitCode === 0));\n\n return {\n exitCode: run.exitCode === 0 && run.success ? 0 : run.exitCode === 0 ? 1 : run.exitCode,\n output,\n cost,\n };\n}\n\n/**\n * Clean mode never dumps the agent report. Verbose breaks it line by line.\n * A failure always prints a short excerpt — the user should not need\n * `--verbose` to see why a story stopped.\n */\nfunction emitAgentOutput(output: string, failed: boolean): void {\n const onOutput = getOutputCallback();\n if (!onOutput) return;\n const trimmed = output.trim();\n if (!trimmed) return;\n\n if (isVerbose()) {\n for (const line of trimmed.split('\\n')) {\n if (line.trim()) onOutput(line);\n }\n return;\n }\n\n if (failed) {\n for (const line of failureExcerpt(trimmed)) {\n onOutput(line);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;;;ACA3B,SAAS,kBAAkB;AAC3B,SAAS,IAAI,OAAO,UAAU,iBAAiB;AAC/C,SAAS,YAAY;;;ACFrB,OAAO,WAAW;AAQX,SAAS,iBAAiB,QAAgB,OAAuB;AACtE,QAAM,WAAW;AACjB,MAAI,MAAM;AACV,MAAI,SAAS;AAEb,MAAI,QAAQ,GAAG;AACb,UAAM,KAAK,MAAO,SAAS,MAAO,KAAK;AACvC,aAAS,KAAK,MAAO,SAAS,WAAY,KAAK;AAAA,EACjD;AAEA,QAAM,QAAQ,WAAW;AACzB,QAAM,WAAW,WAAW,IAAI,WAAW;AAC3C,QAAM,YAAY,WAAW,IAAI,WAAW;AAE5C,QAAM,MAAM,SAAS,OAAO,MAAM,IAAI,UAAU,OAAO,KAAK;AAC5D,QAAM,OAAO,GAAG,MAAM,IAAI,KAAK,KAAK,GAAG;AAEvC,MAAI,SAAS,GAAG;AACd,WAAO,GAAG,MAAM,MAAM,GAAG,CAAC,IAAI,IAAI;AAAA,EACpC;AACA,SAAO,GAAG,GAAG,IAAI,IAAI;AACvB;AAgBO,SAAS,qBACd,WACA,SACA,SACA,eACM;AACN,QAAM,QAAQ,SAAS;AACvB,QAAM,UAAU,SAAS;AACzB,QAAM,KAAK,kBAAkB;AAE7B,QAAM,YAAY,UAAU,aAAa,SAAS,OAAO,OAAO,KAAK,aAAa,SAAS;AAE3F,QAAM,QAAQ,QAAQ;AACtB,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAE/C,QAAM,QAAkB,CAAC;AAEzB,MAAI,SAAS;AACX,UAAM,KAAK,MAAM,KAAK,sBAAsB,MAAM,KAAK,IAAI,SAAS,qBAAqB,CAAC;AAAA,EAC5F,OAAO;AACL,UAAM,KAAK,OAAO,MAAM,KAAK,IAAI,SAAS,MAAM;AAAA,EAClD;AAGA,MAAI,oBAAoB;AACxB,aAAW,SAAS,SAAS;AAC3B,QAAI;AACJ,QAAI;AACJ,UAAM,WACJ,kBAAkB,SACd,MAAM,OAAO,gBACb,CAAC,MAAM,UAAU,CAAC;AAExB,QAAI,MAAM,QAAQ;AAChB,aAAO,MAAM;AACb,gBAAU,UAAU,MAAM,QAAQ,CAAC,MAAc;AAAA,IACnD,WAAW,UAAU;AACnB,aAAO,MAAM;AACb,gBAAU,UAAU,MAAM,SAAS,CAAC,MAAc;AAClD,0BAAoB;AAAA,IACtB,OAAO;AAEL,aAAO,MAAM;AACb,gBAAU,UAAU,MAAM,OAAO,CAAC,MAAc;AAAA,IAClD;AAEA,UAAM,KAAK,QAAQ,KAAK,IAAI,IAAI,MAAM,EAAE,KAAK,MAAM,KAAK,EAAE,CAAC;AAAA,EAC7D;AAEA,QAAM,KAAK,KAAK,iBAAiB,QAAQ,KAAK,CAAC,EAAE;AAEjD,MAAI,IAAI;AAEN,OAAG,MAAM,KAAK,IAAI,CAAC;AAAA,EACrB,OAAO;AACL,YAAQ,IAAI,EAAE;AACd,eAAW,QAAQ,OAAO;AACxB,cAAQ,IAAI,IAAI;AAAA,IAClB;AACA,YAAQ,IAAI,EAAE;AAAA,EAChB;AACF;;;AC1GA,OAAOA,YAAW;AAiBlB,SAAS,QAAQ,MAAc,OAAuB;AACpD,MAAI,KAAK,SAAS,OAAO;AACvB,WAAO,KAAK,UAAU,GAAG,KAAK;AAAA,EAChC;AACA,SAAO,KAAK,OAAO,KAAK;AAC1B;AAMO,SAAS,SAAS,OAAuB;AAC9C,QAAM,UAAU,SAAS;AACzB,QAAM,UAAU,WAAW;AAC3B,QAAM,YAAY,aAAa;AAG/B,MAAI,kBAAkB;AACtB,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,SAAS,KAAK,SAAS,iBAAiB;AACnD,wBAAkB,KAAK;AAAA,IACzB;AAAA,EACF;AAGA,QAAM,YAAY,YAAY;AAC9B,MAAI,kBAAkB,WAAW;AAC/B,sBAAkB;AAAA,EACpB;AACA,MAAI,kBAAkB,IAAI;AACxB,sBAAkB;AAAA,EACpB;AAGA,QAAM,KAAK,UAAU,WAAW;AAChC,QAAM,KAAK,UAAU,WAAW;AAChC,QAAM,KAAK,UAAU,WAAW;AAChC,QAAM,KAAK,UAAU,WAAW;AAChC,QAAM,IAAI,UAAU,WAAW;AAC/B,QAAM,IAAI,UAAU,WAAW;AAC/B,QAAM,OAAO,UAAU,WAAW;AAClC,QAAM,OAAO,UAAU,WAAW;AAElC,QAAM,QAAQ,EAAE,OAAO,kBAAkB,CAAC;AAE1C,QAAM,OAAO,UAAUC,OAAM,OAAO,CAAC,MAAc;AACnD,QAAM,SAAS,CAAC,MAAc;AAG9B,UAAQ,IAAI,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,EAAE,CAAC;AAGtC,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,OAAO;AAClB,cAAQ,IAAI,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,EAAE,CAAC;AAAA,IAC5C,OAAO;AACL,YAAM,SAAS,QAAQ,MAAM,eAAe;AAC5C,cAAQ,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,MAAM,IAAI,KAAK,CAAC,CAAC,EAAE;AAAA,IAC/C;AAAA,EACF;AAGA,UAAQ,IAAI,KAAK,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,EAAE,CAAC;AACxC;AAWO,SAAS,mBACd,QACA,MACA,OACM;AACN,QAAM,QAAQ,SAAS;AAEvB,QAAM,eAAe,KAAK,YAAY;AACtC,QAAM,iBAAiB,KAAK,YAAY,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAChE,QAAM,aAAa,KAAK,cAAc;AAEtC,QAAM,aAAa,OAAO,cAAc,UAAU,OAAO,WAAW,KAAK;AAEzE,QAAM,eACJ,OAAO,kBAAkB,SAAY,OAAO,OAAO,aAAa,IAAI;AAEtE,QAAM,aAAa,OAAO,eACtB,sBACA,GAAG,OAAO,UAAU;AAExB,QAAM,QAAQ;AAAA,IACZ,GAAG,MAAM,KAAK;AAAA,IACd;AAAA,IACA,gBAAgB,UAAU;AAAA,IAC1B,gBAAgB,UAAU;AAAA,IAC1B,gBAAgB,cAAc,IAAI,YAAY;AAAA,IAC9C,gBAAgB,YAAY;AAAA,IAC5B,gBAAgB,UAAU;AAAA,EAC5B;AACA,QAAM,YAAY,eAAe;AACjC,MAAI,YAAY,GAAG;AAEjB,UAAM,oBAAoB;AAC1B,UAAM;AAAA,MACJ,iBAAiB,eAAe,YAAY,iBAAiB,CAAC,KAAK,SAAS;AAAA,IAC9E;AAAA,EACF;AACA,MAAI,OAAO;AACT,UAAM,QAAQ,MAAM,QAAQ,SAAM,MAAM,KAAK,KAAK;AAClD,UAAM,SAAS,MAAM,SAAS,KAAK,MAAM,MAAM,MAAM;AACrD,UAAM,KAAK,gBAAgB,MAAM,QAAQ,GAAG,KAAK,GAAG,MAAM,EAAE;AAAA,EAC9D;AACA,WAAS,KAAK;AAChB;AAKO,SAAS,gBACd,QACA,YACA,cACA,gBACA,MACA,WAOA,OACA,cACM;AACN,QAAM,QAAQ,SAAS;AAEvB,QAAM,eAAe,KAAK,YAAY;AACtC,QAAM,iBAAiB,KAAK,YAAY,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE;AAChE,QAAM,WAAW,eAAe,cAAc;AAE9C,MAAI;AACJ,MAAI;AAEJ,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,mBAAa,MAAM;AACnB,oBAAc;AACd;AAAA,IACF,KAAK;AACH,mBAAa,MAAM;AACnB,oBAAc;AACd;AAAA,IACF,KAAK;AACH,mBAAa,MAAM;AACnB,oBAAc;AACd;AAAA,EACJ;AAEA,QAAM,WAAW;AAAA,IACf,GAAG,MAAM,GAAG;AAAA,IACZ;AAAA,IACA,gBAAgB,UAAU,IAAI,WAAW;AAAA,IACzC,gBAAgB,cAAc,IAAI,YAAY;AAAA,IAC9C,gBAAgB,UAAU;AAAA,IAC1B,gBAAgB,QAAQ;AAAA,EAC1B;AAEA,WAAS,KAAK,GAAG,cAAc,OAAO,YAAY,CAAC;AACnD,WAAS,KAAK,GAAG,mBAAmB,IAAI,CAAC;AAEzC,WAAS,KAAK,gBAAgB,YAAY,EAAE;AAE5C,MAAI,WAAW;AACb,aAAS,KAAK,KAAK;AACnB,aAAS,KAAK,SAAS;AAAA,EACzB;AAEA,UAAQ,IAAI,EAAE;AACd,WAAS,QAAQ;AACnB;AAuCA,SAAS,mBAAmB,MAAgB,SAAS,iBAA2B;AAC9E,QAAM,UAAU,KAAK,cAAc,CAAC;AACpC,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,QAAM,QAAkB,CAAC;AACzB,QAAM,SAAS,QAAQ,SAAS,SAAS;AACzC,MAAI,QAAQ;AACZ,QAAM,MAAM,IAAI,OAAO,OAAO,MAAM;AACpC,aAAW,CAAC,SAAS,OAAO,KAAK,QAAQ;AACvC,UAAM,QAAkB,CAAC;AACzB,QAAI,QAAQ,UAAU,WAAW,GAAG;AAClC,YAAM,KAAK,IAAI,QAAQ,UAAU,SAAS,QAAQ,CAAC,CAAC,WAAW;AAAA,IACjE;AACA,QAAI,QAAQ,UAAU,YAAY,GAAG;AACnC,YAAM,KAAK,IAAI,QAAQ,UAAU,UAAU,QAAQ,CAAC,CAAC,YAAY;AAAA,IACnE;AACA,QAAI,QAAQ,UAAU,oBAAoB,GAAG;AAC3C,YAAM,KAAK,GAAG,QAAQ,UAAU,iBAAiB,UAAU;AAAA,IAC7D;AACA,QAAI,MAAM,WAAW,GAAG;AACtB,YAAM,SAAS,UAAU,OAAO,EAAE;AAClC,UAAI,OAAO,aAAa,KAAK,OAAO,cAAc,GAAG;AACnD,cAAM,KAAK,cAAc;AAAA,MAC3B;AAAA,IACF;AACA,QAAI,MAAM,WAAW,EAAG;AACxB,UAAM,KAAK,GAAG,UAAU,IAAI,SAAS,GAAG,GAAG,OAAO,SAAM,MAAM,KAAK,QAAK,CAAC,EAAE;AAC3E,aAAS;AAAA,EACX;AACA,SAAO;AACT;AAEA,SAAS,cACP,OACA,cACU;AACV,QAAM,SAAS,eAAe,OAAO,KAAK,YAAY,IAAI,CAAC;AAC3D,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,QAAkB,CAAC;AACzB,eAAW,CAAC,OAAO,EAAE,KAAK,OAAO,QAAQ,GAAG;AAC1C,YAAMC,UAAS,aAAa,eAAe,EAAE,CAAC;AAC9C,UAAIA,YAAW,GAAI;AACnB,YAAM,KAAK,UAAU,IAAI,gBAAgB,EAAE,SAAMA,OAAM,KAAK,gBAAgB,EAAE,SAAMA,OAAM,EAAE;AAAA,IAC9F;AACA,WAAO;AAAA,EACT;AACA,QAAM,SAAS,aAAa,KAAK;AACjC,SAAO,WAAW,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,EAAE;AACvD;AAEA,SAAS,kBACP,OACA,cACA,SAAS,gBACC;AACV,QAAM,SAAS,eAAe,OAAO,KAAK,YAAY,IAAI,CAAC;AAC3D,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,MAAM,IAAI,OAAO,OAAO,MAAM;AACpC,UAAM,QAAkB,CAAC;AACzB,eAAW,CAAC,OAAO,EAAE,KAAK,OAAO,QAAQ,GAAG;AAC1C,YAAMA,UAAS,aAAa,eAAe,EAAE,CAAC;AAC9C,UAAIA,YAAW,GAAI;AACnB,YAAM,KAAK,GAAG,UAAU,IAAI,SAAS,GAAG,GAAG,EAAE,SAAMA,OAAM,EAAE;AAAA,IAC7D;AACA,WAAO;AAAA,EACT;AACA,QAAM,SAAS,aAAa,KAAK;AACjC,SAAO,WAAW,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,MAAM,EAAE;AACnD;AASO,SAAS,qBAAqB,MAAgC;AACnE,QAAM,QAAQ;AAAA,IACZ,eAAe,KAAK,UAAU,GAAG,KAAK,WAAW,eAAe,EAAE;AAAA,IAClE,eAAe,KAAK,UAAU;AAAA,IAC9B,eAAe,eAAe,KAAK,cAAc,CAAC;AAAA,EACpD;AAEA,QAAM,KAAK,GAAG,kBAAkB,KAAK,OAAO,KAAK,YAAY,CAAC;AAE9D,MAAI,CAAC,KAAK,UAAU;AAClB,UAAM,KAAK,eAAe,KAAK,KAAK,EAAE;AAAA,EACxC;AAEA,MAAI,KAAK,gBAAgB,MAAM;AAC7B,UAAM,QACJ,KAAK,aAAa,YAAY,WAC1B,WACA,KAAK,aAAa,YAAY,WAC5B,WACA;AACR,UAAM,KAAK,eAAe,KAAK,EAAE;AAAA,EACnC;AAEA,QAAM,SAAS,KAAK;AACpB,MAAI,QAAQ;AAGV,UAAM,UACJ,OAAO,mBAAmB,OAAO,mBAAmB,oBAAoB;AAC1E,UAAM,KAAK,eAAe,OAAO,EAAE;AACnC,QAAI,OAAO,eAAe,MAAM;AAC9B,YAAM,KAAK,eAAe,OAAO,UAAU,EAAE;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO;AACT;AAqCO,SAAS,uBAAuB,MAAkC;AACvE,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,eAAe,KAAK,cAAc,SAAS,EAAE;AACxD,QAAM,KAAK,eAAe,KAAK,OAAO,MAAM,EAAE;AAE9C,aAAW,SAAS,KAAK,QAAQ;AAC/B,UAAMA,UAAS,aAAa,MAAM,KAAK;AACvC,UAAM,QAAQ,MAAM,UAAU,KAAK,KAAK,IAAI,MAAM,KAAK;AACvD,UAAM;AAAA,MACJ,QAAQ,MAAM,EAAE,GAAG,KAAK,WAAM,MAAM,UAAU,aAAa,eAAe,MAAM,cAAc,CAAC,MAC5FA,YAAW,KAAK,KAAK,KAAKA,OAAM;AAAA,IACrC;AAAA,EACF;AAEA,QAAM,KAAK,eAAe,eAAe,KAAK,cAAc,CAAC,EAAE;AAE/D,QAAM,SAAS,aAAa,KAAK,KAAK;AACtC,MAAI,WAAW,IAAI;AACjB,UAAM,KAAK,eAAe,MAAM,EAAE;AAAA,EACpC;AACA,MAAI,KAAK,UAAU,MAAM;AACvB,UAAM,KAAK,eAAe,KAAK,KAAK,EAAE;AAAA,EACxC;AACA,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,UAAM,KAAK,eAAe,KAAK,SAAS,IAAI,CAAC,UAAU,IAAI,MAAM,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EACrF;AAEA,QAAM,SAAS,KAAK;AACpB,MAAI,QAAQ;AACV,UAAM;AAAA,MACJ,eAAe,OAAO,mBAAmB,OAAO,mBAAmB,oBAAoB,UAAU;AAAA,IACnG;AACA,QAAI,OAAO,eAAe,MAAM;AAC9B,YAAM,KAAK,eAAe,OAAO,UAAU,EAAE;AAAA,IAC/C;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,kBAAkB,MAA8B;AAC9D,UAAQ,IAAI,EAAE;AACd,MAAI,KAAK,UAAU,qBAAqB,MAAM;AAC5C,iBAAa,6BAA6B,KAAK,OAAO,wCAAwC;AAAA,EAChG,OAAO;AACL,iBAAa,6BAA6B,KAAK,OAAO,KAAK,KAAK,OAAO,MAAM,WAAW;AAAA,EAC1F;AACA,aAAW,QAAQ,uBAAuB,IAAI,GAAG;AAC/C,YAAQ,IAAI,IAAI;AAAA,EAClB;AACF;AAMO,SAAS,gBAAgB,MAA4B;AAC1D,UAAQ,IAAI,EAAE;AACd,MAAI,KAAK,UAAU,kBAAkB;AACnC;AAAA,MACE,gCAAgC,KAAK,WAAW;AAAA,IAClD;AAAA,EACF,WAAW,KAAK,cAAc,YAAY,cAAc;AACtD;AAAA,MACE,gCAAgC,KAAK,WAAW;AAAA,IAClD;AAAA,EACF,WAAW,KAAK,cAAc,YAAY,UAAU;AAClD;AAAA,MACE,gCAAgC,KAAK,WAAW;AAAA,IAClD;AAAA,EACF,OAAO;AACL,iBAAa,gCAAgC,KAAK,WAAW,GAAG;AAAA,EAClE;AACA,aAAW,QAAQ,qBAAqB,IAAI,GAAG;AAC7C,YAAQ,IAAI,IAAI;AAAA,EAClB;AACF;;;AC5cO,SAAS,cAAc,MAAsB;AAClD,SAAO,KACJ,QAAQ,mBAAmB,GAAG,EAC9B,QAAQ,cAAc,IAAI,EAC1B,QAAQ,oBAAoB,IAAI,EAChC,QAAQ,gBAAgB,IAAI,EAC5B,QAAQ,6BAA6B,IAAI,EACzC,QAAQ,gBAAgB,EAAE,EAC1B,QAAQ,kBAAkB,EAAE,EAC5B,QAAQ,0BAA0B,IAAI,EACtC,QAAQ,cAAc,GAAG,EACzB,KAAK;AACV;AAGO,SAAS,eAAe,QAAgB,QAAQ,GAAa;AAClE,SAAO,OACJ,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,cAAc,IAAI,CAAC,EACjC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,EAChC,MAAM,CAAC,KAAK;AACjB;;;ACFA,eAAsB,cACpB,QACA,UAAgC,CAAC,GACV;AACvB,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,oBAAoB;AAAA,MACnC;AAAA,MACA,OAAO;AAAA,MACP,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,qBAAqB,QAAQ;AAAA,MAC7B,WAAW,QAAQ;AAAA,MACnB,iBAAiB,QAAQ;AAAA,MACzB,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MACvD,MAAM;AAAA,IACR;AAAA,EACF;AACA,QAAM,EAAE,IAAI,IAAI;AAEhB,MAAI;AACJ,MAAI,OAAO,IAAI;AAEf,MAAI,CAAC,IAAI,WAAW,IAAI,aAAa,GAAG;AACtC,aAAS,IAAI,aAAa,IAAI,SAAS;AACvC,WAAO;AAAA,EACT,OAAO;AACL,aAAS,IAAI,UAAU,IAAI;AAAA,EAC7B;AAEA,kBAAgB,QAAQ,EAAE,IAAI,WAAW,IAAI,aAAa,EAAE;AAE5D,SAAO;AAAA,IACL,UAAU,IAAI,aAAa,KAAK,IAAI,UAAU,IAAI,IAAI,aAAa,IAAI,IAAI,IAAI;AAAA,IAC/E;AAAA,IACA;AAAA,EACF;AACF;AAOA,SAAS,gBAAgB,QAAgB,QAAuB;AAC9D,QAAM,WAAW,kBAAkB;AACnC,MAAI,CAAC,SAAU;AACf,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,CAAC,QAAS;AAEd,MAAI,UAAU,GAAG;AACf,eAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,UAAI,KAAK,KAAK,EAAG,UAAS,IAAI;AAAA,IAChC;AACA;AAAA,EACF;AAEA,MAAI,QAAQ;AACV,eAAW,QAAQ,eAAe,OAAO,GAAG;AAC1C,eAAS,IAAI;AAAA,IACf;AAAA,EACF;AACF;;;AJpCA,SAAS,QAAQ,SAAuB;AACtC,QAAM,KAAK,kBAAkB;AAC7B,MAAI,IAAI;AAEN,QAAI,SAAS;AACX,SAAG,OAAO;AAAA,IACZ;AAAA,EACF,OAAO;AACL,YAAQ,IAAI,OAAO;AAAA,EACrB;AACF;AAmBA,SAAS,mBAAmB,QAAmC;AAC7D,SAAO;AAAA,IACL,OAAO,aAAa;AAAA,IACpB,OAAO;AAAA,IACP,OAAO;AAAA,IACP,EAAE,cAAc,OAAO,aAAa;AAAA,EACtC;AACF;AAUA,SAAS,uBAAuB,QAAqB,OAA8B;AACjF,QAAM,aAAa,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,MAAM,CAAC,CAAC;AAC1E,SAAO,MACJ,OAAO,CAAC,UAAU,MAAM,UAAU,WAAW,IAAI,MAAM,EAAE,MAAM,KAAK,EACpE,IAAI,CAAC,UAAU,MAAM,EAAE;AAC5B;AAQA,SAAS,aAAa,OAA+D;AACnF,MAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,GAAI,QAAO;AAClE,QAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAC/D,SAAO,OAAO,UAAU,MAAM,KAAK,SAAS,IAAI,SAAS;AAC3D;AASO,SAAS,mBACd,OACA,YACA,SACwB;AACxB,QAAM,WAAW,eAAe,UAAa,eAAe,QAAQ,eAAe;AACnF,QAAM,OAAO,SAAS,QAAQ;AAC9B,QAAM,cAAc,aAAa,SAAS,WAAW;AACrD,QAAM,gBAAgB,UAAU,UAAa,UAAU,KAAK,SAAY;AAExE,MAAI,YAAY,SAAS,SAAS,QAAW;AAC3C,UAAM,SAAS,kBAAkB,SAAY,KAAK,IAAI,aAAa;AACnE,WAAO;AAAA,MACL,oBAAoB,cAAc;AAAA,QAChC;AAAA,QACA,OAAO;AAAA,QACP,SAAS;AAAA,QACT;AAAA,QACA,SAAS,SAAS;AAAA,MACpB,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,MAAM,KAAK,SAAS,MAAM,GAAG;AAAA,MAClD,wBAAwB,cAAc;AAAA,QACpC,MAAM;AAAA,QACN,OAAO;AAAA,QACP,SAAS;AAAA,QACT;AAAA,QACA,SAAS,SAAS;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL,oBAAoB,cAAc;AAAA,MAChC;AAAA,MACA,OAAO;AAAA,MACP,SAAS;AAAA,MACT;AAAA,MACA,SAAS,SAAS;AAAA,IACpB,CAAC;AAAA,IACD,wBAAwB,cAAc;AAAA,MACpC,MAAM;AAAA,MACN,OAAO;AAAA,MACP,SAAS;AAAA,MACT;AAAA,MACA,SAAS,SAAS;AAAA,IACpB,CAAC;AAAA,EACH;AACF;AASA,SAAS,oBAAsD;AAC7D,QAAM,aAAa,qBAAqB;AACxC,SAAO,eAAe,SAAY,CAAC,IAAI,EAAE,qBAAqB,WAAW;AAC3E;AAgBA,eAAe,sBAAsB,WAAsC;AACzE,MAAI;AACF,UAAM,OAAO,MAAM,aAAa,SAAS;AACzC,UAAM,UAAU,KAAK,YAAY,OAAO,CAAC,UAAU,CAAC,MAAM,MAAM;AAChE,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAElC,QAAI,CAAE,MAAM,mBAAmB,EAAI,QAAO,CAAC;AAE3C,UAAM,YAAY,MAAM,kBAAkB,MAAM,cAAc,CAAC;AAC/D,UAAM,UAAU,QAAQ,OAAO,CAAC,UAAU,UAAU,IAAI,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,UAAU,MAAM,EAAE;AAC1F,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAElC,UAAM,aAAa,WAAW;AAAA,MAC5B,GAAG;AAAA,MACH,aAAa,KAAK,YAAY;AAAA,QAAI,CAAC,UACjC,QAAQ,SAAS,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,QAAQ,KAAK,IAAI;AAAA,MAC5D;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAKA,eAAe,mBAAmB,cAAqC;AACrE,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,UAAM,UAAU;AAAA,YAAuC,oBAAI,KAAK,GAAE,SAAS,CAAC;AAAA;AAAA;AAC5E,UAAM,UAAU,cAAc,SAAS,OAAO;AAAA,EAChD;AACF;AAKA,eAAe,uBAAuB,MAAgB,OAAqC;AACzF,QAAM,EAAE,gBAAgB,YAAY,SAAS,aAAa,IAAI;AAE9D,MAAI,CAAC,WAAW,cAAc,GAAG;AAC/B;AAAA,EACF;AAEA,QAAM,gBAAgB,KAAK,cAAc;AACzC,MAAI,aAAa;AAEjB,MAAI;AACF,kBAAc,MAAM,SAAS,gBAAgB,OAAO,GAAG,KAAK;AAAA,EAC9D,QAAQ;AACN;AAAA,EACF;AAEA,MAAI,iBAAiB,cAAc,kBAAkB,YAAY;AAC/D,UAAM,WAAU,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AACrD,UAAM,aAAa,kBAAkB,UAAU;AAC/C,UAAM,gBAAgB,KAAK,YAAY,GAAG,OAAO,IAAI,UAAU,EAAE;AAEjE,cAAU,2BAA2B,UAAU,EAAE;AACjD,UAAM,MAAM,eAAe,EAAE,WAAW,KAAK,CAAC;AAE9C,QAAI,WAAW,OAAO,GAAG;AACvB,YAAM,GAAG,SAAS,KAAK,eAAe,YAAY,CAAC;AAAA,IACrD;AACA,QAAI,WAAW,YAAY,GAAG;AAC5B,YAAM,GAAG,cAAc,KAAK,eAAe,cAAc,CAAC;AAAA,IAC5D;AAEA,cAAU,mBAAmB,aAAa,EAAE;AAG5C,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,YAAuC,oBAAI,KAAK,GAAE,SAAS,CAAC;AAAA;AAAA;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AACF;AAKA,eAAe,YAAY,MAAgB,gBAAuC;AAChF,QAAM,SAAS,KAAK,cAAc;AAClC,MAAI,QAAQ;AACV,UAAM,UAAU,gBAAgB,GAAG,MAAM;AAAA,GAAM,OAAO;AAAA,EACxD;AACF;AAaA,eAAsB,UAAU,QAAsB,OAAuC;AAE3F,MAAI,CAAC,WAAW,MAAM,OAAO,GAAG;AAC9B,eAAW,yBAAyB,MAAM,OAAO,EAAE;AACnD,QAAI,OAAO,aAAa;AACtB,cAAQ,mDAAmD,OAAO,WAAW,SAAS;AAAA,IACxF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAM,aAAa,MAAM,OAAO;AAC3C,SAAO,gBAAgB,IAAI;AAC3B,QAAM,aAAa,MAAM,SAAS,IAAI;AAOtC,MAAI,KAAK,gBAAgB,eAAe,eAAe,IAAI,KAAK,CAAC,qBAAqB,IAAI,GAAG;AAC3F,YAAQ,oCAAoC,MAAM,OAAO,EAAE;AAC3D,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,gBAAgB,eAAe,CAAC,eAAe,IAAI,GAAG;AAC7D;AAAA,MACE;AAAA,IACF;AACA,WAAO,oBAAoB,IAAI;AAC/B,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,aAAa,MAAM,SAAS,IAAI;AAAA,EACxC;AAGA,MAAI,eAAe,IAAI,KAAK,CAAC,qBAAqB,IAAI,GAAG;AACvD,YAAQ,4DAA4D;AACpE,WAAO,mBAAmB,IAAI;AAC9B,UAAM,aAAa,MAAM,SAAS,IAAI;AACtC,WAAO,qBAAqB,QAAQ,OAAO,IAAI;AAAA,EACjD;AAGA,QAAM,uBAAuB,MAAM,KAAK;AAGxC,QAAM,YAAY,MAAM,MAAM,cAAc;AAG5C,QAAM,mBAAmB,MAAM,YAAY;AAK3C,QAAM,iBAAiB,MAAM,WAAW,WAAW,EAAE,aAAa,MAAM,YAAY,CAAC;AAIrF,QAAM,SAAS,MAAM,0BAA0B,EAAE,MAAM,MAAM,YAAY,CAAC;AAC1E,QAAM,WAAW,MAAM,iBAAiB,GAAG,QAAQ,YAAY;AAE/D,QAAM,eAAe,MAAM,gBAAgB,SAAS;AACpD,qBAAmB,QAAQ,MAAM;AAAA,IAC/B,UAAU,aAAa;AAAA,IACvB,OAAO,aAAa;AAAA,IACpB,GAAI,aAAa,MAAM,kBAAkB,EAAE,QAAQ,aAAa,MAAM,gBAAgB,IAAI,CAAC;AAAA,EAC7F,CAAC;AAED,QAAM,YAAY,KAAK,IAAI;AAC3B,MAAI,IAAI;AACR,MAAI,kBAAkB;AAGtB,SAAO,MAAM;AAEX,QAAI,OAAO,kBAAkB,UAAa,KAAK,OAAO,eAAe;AACnE;AAAA,IACF;AAEA;AAUA,UAAM,UAAU,MAAM,sBAAsB,MAAM,OAAO;AACzD,QAAI,QAAQ,SAAS,GAAG;AACtB;AAAA,QACE,YAAY,QAAQ,KAAK,IAAI,CAAC;AAAA,MAChC;AACA,aAAO,MAAM,aAAa,MAAM,OAAO;AACvC,UAAI,KAAK,YAAY,MAAM,CAAC,UAAU,MAAM,MAAM,KAAK,KAAK,uBAAuB,MAAM;AAEvF;AAAA,MACF;AAAA,IACF;AAQA,UAAM,YAAY,MAAM;AAAA,MACtB,YAAY;AAEV,eAAO,MAAM,aAAa,MAAM,OAAO;AAIvC,YAAI,KAAK,YAAY,SAAS,GAAG;AAC/B,8BAAoB,EAAE,QAAQ;AAAA,YAC5B,MAAM;AAAA,YACN,IAAI,OAAO;AAAA,YACX,SAAS,KAAK;AAAA,UAChB,CAAC;AAAA,QACH;AAGA,cAAM,gBAAgB,KAAK;AAM3B,cAAM,gBAAgB,kBAAkB,KAAK,WAAW,GAAG;AAC3D,4BAAoB,EAAE,QAAQ;AAAA,UAC5B,MAAM;AAAA,UACN,IAAI,OAAO;AAAA,UACX,WAAW;AAAA,UACX,SAAS;AAAA,QACX,CAAC;AACD,8BAAsB,IAAI,aAAa;AAEvC,YAAI,UAAU,GAAG;AACf,+BAAqB,GAAG,OAAO,eAAe,KAAK,aAAa,aAAa;AAAA,QAC/E;AAGA,cAAM,SAAS,kBAAkB,gBAAgB;AAAA,UAC/C,cAAc,MAAM;AAAA,UACpB,mBAAmB,MAAM;AAAA,UACzB,GAAG;AAAA;AAAA;AAAA,UAGH,GAAG,mBAAmB,OAAO,aAAa,OAAO,uBAAuB;AAAA,YACtE,aAAa,KAAK;AAAA,YAClB;AAAA,UACF,CAAC;AAAA,UACD,2BAA2B,OAAO,OAAO,uBAAuB,CAAC;AAAA,QACnE,CAAC;AAED,cAAM,YAAY,OAAO;AACzB,eAAO,oBAAoB,MAAM,SAAS;AAC1C,cAAM,aAAa,MAAM,SAAS,IAAI;AAGtC,cAAM,cAAc,KAAK,IAAI;AAC7B,cAAMC,UAAS,MAAM,cAAc,QAAQ;AAAA,UACzC,GAAG,kBAAkB;AAAA,UACrB,WAAW;AAAA,UACX,iBAAiB,KAAK;AAAA,UACtB,GAAI,kBAAkB,SAAY,CAAC,IAAI,EAAE,UAAU,CAAC,aAAa,EAAE;AAAA,QACrE,CAAC;AACD,cAAM,UAAU,oBAAoB,WAAW;AAE/C,YAAIA,QAAO,aAAa,GAAG;AAGzB,kCAAwB,GAAGA,QAAO,MAAM,OAAO;AAAA,QACjD;AAEA,eAAO,EAAE,QAAAA,SAAQ,WAAW,SAAS,cAAc;AAAA,MACrD;AAAA,MACA;AAAA,QACE,QAAQ,mBAAmB,MAAM;AAAA;AAAA;AAAA;AAAA,QAIjC,QAAQ,kBAAkB;AAAA;AAAA;AAAA;AAAA,QAI1B,UAAU,CAAC,EAAE,QAAAA,QAAO,MAClBA,QAAO,aAAa,IAChB,OACA,SAAS,EAAE,QAAQ,SAAS,UAAUA,QAAO,UAAU,QAAQA,QAAO,OAAO,CAAC;AAAA,QACpF,WAAW,OAAO,EAAE,OAAO,SAAS,SAAS,WAAW,QAAQ,MAAM;AACpE,cAAI,YAAY,KAAM;AAEtB,gBAAM,eAAe,iBAAiB,MAAM,OAAO,MAAM;AAEzD,cAAI,CAAC,QAAQ,WAAW;AACtB,mBAAO,MAAM,aAAa,MAAM,OAAO;AACvC,mBAAO,aAAa,MAAM,wBAAwB,YAAY;AAC9D,gBAAI,cAAc,QAAQ,MAAM,0BAA0B,CAAC,EAAE,gBAAgB,SAAS;AACpF,qBAAO;AAAA,gBACL,GAAG;AAAA,gBACH,UAAU;AAAA,kBACR,cAAc;AAAA,kBACd;AAAA,kBACA,OAAO,KAAK,UAAU,SAAS;AAAA,kBAC/B,QAAQ;AAAA,kBACR,eAAe;AAAA,kBACf,iBAAiB,OAAO;AAAA,gBAC1B;AAAA,cACF;AAAA,YACF;AACA,kBAAM,aAAa,MAAM,SAAS,IAAI;AACtC;AAAA,UACF;AAIA;AACA,iBAAO,MAAM,aAAa,MAAM,OAAO;AACvC,iBAAO,aAAa,MAAM,4BAA4B,YAAY;AAClE,gBAAM,aAAa,MAAM,SAAS,IAAI;AACtC,cAAI,CAAC,UAAW;AAEhB,gBAAM,eAAe,UAAU;AAC/B,8BAAoB,EAAE,QAAQ;AAAA,YAC5B,MAAM;AAAA,YACN,IAAI,OAAO;AAAA,YACX;AAAA,YACA;AAAA,YACA,QAAQ;AAAA,YACR,MAAM,QAAQ;AAAA,UAChB,CAAC;AAED,cAAI,UAAU,GAAG;AACf,oBAAQ,EAAE;AAAA,UACZ;AACA;AAAA,YACE,yCAAyC,CAAC,aAAa,OAAO,kBAAkB,YAAY;AAAA,UAC9F;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM;AAAA,MACJ;AAAA,MACA,SAAS;AAAA,MACT,eAAe;AAAA,IACjB,IAAI,UAAU;AACd,UAAM,qBAAqB,UAAU,MAAM;AAE3C,QAAI,OAAO,aAAa,GAAG;AACzB,YAAMC,WAAU,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AAC1D,aAAO,MAAM,aAAa,MAAM,OAAO;AACvC;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACAA;AAAA,QACA;AAAA,QACA,UAAU,YACN,yBAAyB,OAAO,UAAU,0BAC1C,oCAAoC,OAAO,QAAQ;AAAA,QACvD,oBAAoB,aAAa;AAAA,MACnC;AACA,aAAO,OAAO;AAAA,IAChB;AAEA,WAAO,MAAM,aAAa,MAAM,OAAO;AACvC,WAAO,eAAe,MAAM,kBAAkB;AAC9C,UAAM,aAAa,MAAM,SAAS,IAAI;AAGtC,UAAM,gBAAgB,uBAAuB;AAC7C,QAAI,eAAe;AACjB,oBAAc,KAAK,WAAW;AAAA,IAChC;AACA,wBAAoB,EAAE,QAAQ;AAAA,MAC5B,MAAM;AAAA,MACN,IAAI,OAAO;AAAA,MACX,SAAS,KAAK;AAAA,IAChB,CAAC;AAOD,UAAM,oBAAoB,uBAAuB,wBAAwB,KAAK,WAAW;AACzF,QAAI,kBAAkB,SAAS,GAAG;AAChC,YAAM,aAAa,YAAY,OAAO,MAAM,kBAAkB,MAAM;AACpE,YAAM,eAAe,KAAK,MAAM,mBAAmB,kBAAkB,MAAM;AAC3E,iBAAW,WAAW,mBAAmB;AACvC,4BAAoB,SAAS,YAAY,YAAY;AAAA,MACvD;AAMA,YAAM,kBAAkB,kBAAkB,MAAM,mBAAmB,YAAY,YAAY;AAC3F,UAAI;AACF,cAAM,aAAa,MAAM,SAAS,eAAe;AACjD,eAAO;AAAA,MACT,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,wBAAoB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC;AACnF,4BAAwB,GAAG,OAAO,MAAM,gBAAgB;AAGxD,QAAI,OAAO,OAAO,SAAS,6BAA6B,GAAG;AACzD,aAAO,MAAM,aAAa,MAAM,OAAO;AACvC,UAAI,eAAe,IAAI,KAAK,CAAC,qBAAqB,IAAI,GAAG;AACvD,eAAO,mBAAmB,IAAI;AAC9B,cAAM,aAAa,MAAM,SAAS,IAAI;AAEtC,cAAMA,WAAU,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AAC1D;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACAA;AAAA,UACA;AAAA,UACA;AAAA,UACA,oBAAoB,aAAa;AAAA,QACnC;AACA,eAAO,qBAAqB,QAAQ,OAAO,IAAI;AAAA,MACjD;AAEA,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,qBAAqB,IAAI,IACrB,wFACA;AAAA,MACN;AACA,YAAM,aAAa,MAAM,SAAS,IAAI;AAEtC,cAAQ,EAAE;AACV;AAAA,QACE,qBAAqB,IAAI,IACrB,kHACA;AAAA,MACN;AAAA,IACF;AAEA,QAAI,UAAU,GAAG;AACf,mBAAa,aAAa,CAAC,0BAA0B;AAAA,IACvD;AAAA,EACF;AAGA,QAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AAC1D,SAAO,MAAM,aAAa,MAAM,OAAO;AACvC;AAAA,IACE;AAAA,IACA,OAAO,iBAAiB;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB,aAAa;AAAA,EACnC;AACA,SAAO;AACT;AAEA,eAAe,qBACb,QACA,OACA,MACiB;AACjB,QAAM,WAAW,gBAAgB,QAAQ,KAAK;AAC9C,QAAM,UAAU,MAAM,kBAAkB;AAAA,IACtC;AAAA,IACA,KAAK,MAAM;AAAA,IACX,SAAS,OAAO,gBAAgB,SAAY,SAAY,CAAC,QAAQ;AAAA,IACjE,cAAc;AAAA,EAChB,CAAC;AACD,QAAM,UAAU,sBAAsB,MAAM,OAAO;AACnD,MAAI,QAAQ,QAAQ;AAClB,UAAM,aAAa,MAAM,SAAS,QAAQ,IAAI;AAC9C,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;AD/pBA,eAAsB,WACpB,mBACA,SACiB;AACjB,QAAM,SAAS,MAAM,qBAAqB;AAC1C,MAAI,OAAO,SAAS,GAAG;AACrB,eAAW,iDAAiD;AAC5D,eAAW,OAAO,QAAQ;AACxB,cAAQ,IAAI,GAAG;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,QAAQ,iBAAiB;AAE/C,QAAM,SAAS,aAAa;AAAA,IAC1B,aAAa,QAAQ;AAAA,IACrB;AAAA,IACA,YAAY,QAAQ;AAAA,IACpB,cAAc,QAAQ;AAAA,IACtB,aAAa,QAAQ;AAAA,EACvB,CAAC;AAED,QAAM,QAAQ,MAAM,aAAa,MAAM;AACvC,QAAM,YAAY,MAAM,cAAc;AACtC,QAAM,qBAAqB,oBAAoB;AAC/C,MAAI,sBAA4C;AAKhD,MACE,UAAU,WACV,OAAO,gBAAgB,UACvB,8BAA8B,eAC9B;AACA,UAAM,aAAa,MAAM,kBAAkB,OAAO,aAAa;AAAA,MAC7D,aAAa,MAAM;AAAA,IACrB,CAAC;AACD,0BAAsB,IAAI,cAAc,WAAW,aAAa;AAAA,MAC9D,UAAU,UAAU;AAAA,MACpB,aAAa,UAAU;AAAA,IACzB,CAAC;AACD,wBAAoB,mBAAmB;AACvC,UAAMC,gBAAe,QAAQ,KAAK,OAAO,WAAW,IAAI,OAAO,OAAO,WAAW,IAAI;AACrF,wBAAoB,QAAQ;AAAA,MAC1B,MAAM;AAAA,MACN,IAAI,OAAO;AAAA,MACX,WAAW,WAAW;AAAA,MACtB,aAAaA;AAAA,MACb,QAAQ,CAAC,SAAS;AAAA,IACpB,CAAC;AACD,wBAAoB,QAAQ,EAAE,MAAM,eAAe,IAAI,OAAO,GAAG,OAAO,UAAU,CAAC;AACnF,UAAM,iBAAiB;AAAA,MACrB,WAAW;AAAA,MACX,MAAM,UAAU;AAAA,MAChB,MAAM,UAAU;AAAA,MAChB,gBAAgB,UAAU;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI,WAA0B;AAC9B,MAAI;AACF,eAAW,MAAM,UAAU,QAAQ,KAAK;AAGxC,QAAI,aAAa,KAAK,OAAO,aAAa;AACxC,UAAI;AACF,cAAM,OAAO,MAAM,aAAa,MAAM,OAAO;AAC7C,YAAI,eAAe,IAAI,GAAG;AACxB,eAAK,SAAS,qBAAqB;AACnC,gBAAM,aAAa,MAAM,SAAS,IAAI;AAAA,QACxC;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,WAAO;AAAA,EACT,UAAE;AACA,QAAI,wBAAwB,MAAM;AAChC,YAAM,UAAU,aAAa;AAC7B,0BAAoB,QAAQ;AAAA,QAC1B,MAAM;AAAA,QACN,IAAI,OAAO;AAAA,QACX,OAAO;AAAA,QACP;AAAA,QACA,GAAI,UAAU,CAAC,IAAI,EAAE,OAAO,uBAAuB;AAAA,MACrD,CAAC;AACD,0BAAoB,QAAQ;AAAA,QAC1B,MAAM;AAAA,QACN,IAAI,OAAO;AAAA,QACX,QAAQ,UAAU,cAAc;AAAA,MAClC,CAAC;AACD,YAAM,oBAAoB,MAAM;AAChC,0BAAoB,MAAS;AAAA,IAC/B;AAAA,EACF;AACF;","names":["chalk","chalk","tokens","result","elapsed","numericIssue"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/web/lock.ts","../src/web/server.ts"],"sourcesContent":["import { type ChildProcess, spawn as nodeSpawn } from 'node:child_process';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { isoNow } from '../core/state-manager.js';\n// The liveness probe is shared with `storage/lock.ts`, which generalised this\n// module's guard into run ownership: one definition of \"is that pid alive\",\n// with one place to get the EPERM case right.\nimport { isProcessAlive } from '../storage/lock.js';\nimport { type GetGlobalRootOptions, getGlobalRoot } from '../storage/paths.js';\nimport { type WebLock, webLockSchema } from '../storage/schemas.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport { startWebServer, type WebServerHandle, type WebServerOptions } from './server.js';\n\n/**\n * Single-instance guard for the web monitoring server (`~/.issue-flow/web.lock`).\n *\n * A machine runs at most one monitoring server. Two entry points build on top\n * of the same lock:\n *\n * - `ensureSingleWebServer` binds (or reuses) a server **in the calling\n * process**. It is the low-level primitive, used by the `web serve` command\n * (the standalone process that ends up owning the lock) and by this\n * module's own tests. Nothing else should call it directly.\n * - `ensureWebMonitor` (US-002) is what `run`/`execute` call: it reuses an\n * active instance exactly like `ensureSingleWebServer`, but when none\n * exists it spawns `web serve` as a **detached** child instead of binding\n * locally, so the server outlives the pipeline process. Falls back to the\n * legacy in-process `startWebServer` (US-006) when the global storage tree\n * itself is unreachable.\n */\n\n/** Lock file name, sibling of `config.json` at the global storage root. */\nexport const WEB_LOCK_FILENAME = 'web.lock';\n\n/** How long a health probe against a candidate instance waits before giving up. */\nconst HEALTH_PROBE_TIMEOUT_MS = 1000;\n\n/**\n * A process that loses the atomic lock creation retries this many times,\n * waiting for the winner to finish starting up, before giving up.\n */\nconst LOCK_CLAIM_RETRIES = 5;\nconst LOCK_CLAIM_RETRY_DELAY_MS = 100;\n\nexport function getWebLockFile(options: GetGlobalRootOptions = {}): string {\n return join(getGlobalRoot(options), WEB_LOCK_FILENAME);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** 0.0.0.0 is not a connectable address; probe loopback instead, same mapping\n * `startWebServer` uses to build its own display URL. */\nfunction probeUrl(host: string, port: number): string {\n const probeHost = host === '0.0.0.0' ? '127.0.0.1' : host;\n return `http://${probeHost}:${port}/api/health`;\n}\n\nasync function probeHealth(host: string, port: number): Promise<boolean> {\n try {\n const res = await fetch(probeUrl(host, port), {\n signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\n/** Malformed content (partial write, older/newer incompatible shape) reads as\n * \"no lock\" — the same degrade-to-absent behavior as `loadGlobalConfig()`. */\nexport async function readWebLock(lockFile: string): Promise<WebLock | null> {\n let raw: string;\n try {\n raw = await readFile(lockFile, 'utf-8');\n } catch {\n return null;\n }\n try {\n const result = webLockSchema.safeParse(JSON.parse(raw));\n return result.success ? result.data : null;\n } catch {\n return null;\n }\n}\n\nexport async function removeWebLock(lockFile: string): Promise<void> {\n try {\n await rm(lockFile, { force: true });\n } catch {\n /* best-effort cleanup */\n }\n}\n\n/**\n * A lock is only trustworthy when both signals agree: the owning process is\n * alive AND it answers `/api/health`. A dead pid is the obvious stale case;\n * a live pid that does not answer covers a process that still exists but\n * crashed before binding, was killed -9 leaving a zombie-adjacent state, or\n * whose port was reclaimed by something else entirely. Either way the lock is\n * removed so the next attempt starts clean instead of looping on it forever.\n */\nexport async function detectActiveInstance(lockFile: string): Promise<WebLock | null> {\n const lock = await readWebLock(lockFile);\n if (lock === null) return null;\n\n if (isProcessAlive(lock.pid) && (await probeHealth(lock.host, lock.port))) {\n return lock;\n }\n\n await removeWebLock(lockFile);\n return null;\n}\n\n/**\n * Atomic exclusive create: `wx` fails with EEXIST when another process won\n * the race, which is what lets two concurrent invocations agree on a single\n * winner without a separate lock-of-the-lock.\n */\nasync function claimWebLock(lockFile: string, lock: WebLock): Promise<boolean> {\n try {\n await writeFile(lockFile, JSON.stringify(lock, null, 2), { encoding: 'utf-8', flag: 'wx' });\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'EEXIST') return false;\n throw err;\n }\n}\n\n/** Handle returned when reusing an already-running instance instead of\n * binding a new one — there is no local `Server` to hold, and closing it\n * must not tear down a server another process owns. */\nfunction reusedHandle(lock: WebLock): WebServerHandle {\n const displayHost = lock.host === '0.0.0.0' ? 'localhost' : lock.host;\n return {\n host: lock.host,\n port: lock.port,\n url: `http://${displayHost}:${lock.port}`,\n close: async () => {},\n };\n}\n\n/**\n * Start the web monitoring server, first checking whether a live instance\n * already owns `web.lock` and reusing it instead of binding a second one.\n *\n * The lock is claimed only *after* a successful bind, never before: claiming\n * it with the port the server is about to (but hasn't yet) listen on would\n * make that lock briefly answer no health probe at all, and a concurrent\n * invocation would misread that gap as \"stale\" and delete a perfectly good\n * lock out from under its owner. Binding first also means two invocations\n * racing for the same fixed port never both reach the claim step — the OS\n * itself lets only one `listen()` succeed. The `wx` claim then exists for the\n * remaining race: two invocations that *both* manage to bind (only possible\n * with an ephemeral port) still agree on exactly one winner. The loser closes\n * the server it just opened and defers to whichever lock exists.\n *\n * Returns null exactly when `startWebServer` would: nothing here may affect\n * the pipeline, so a lock we cannot resolve after every retry degrades to \"no\n * server\" rather than throwing.\n */\nexport async function ensureSingleWebServer(\n options: WebServerOptions,\n lockOptions: GetGlobalRootOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n const lockFile = getWebLockFile(lockOptions);\n\n for (let attempt = 0; attempt <= LOCK_CLAIM_RETRIES; attempt++) {\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const handle = await startWebServer(options);\n if (handle === null) {\n // Could not bind. Most likely another instance's listen() won a race\n // on the same fixed port between our detection above and this call —\n // its lock should be visible shortly. Anything else (port taken by an\n // unrelated process, invalid host, ...) fails the same way on the next\n // retry too, and the loop's own limit stops it from spinning forever.\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n let claimed: boolean;\n try {\n claimed = await claimWebLock(lockFile, {\n pid: process.pid,\n port: handle.port,\n host: handle.host,\n startedAt: isoNow(),\n });\n } catch (err) {\n await handle.close();\n throw err;\n }\n\n if (!claimed) {\n // We bound successfully (an ephemeral port let both of us through),\n // but another invocation's lock got written first. We are not the\n // canonical instance: close what we just opened and defer to it.\n await handle.close();\n await sleep(LOCK_CLAIM_RETRY_DELAY_MS);\n continue;\n }\n\n const originalClose = handle.close;\n handle.close = async () => {\n await originalClose();\n await removeWebLock(lockFile);\n };\n return handle;\n }\n\n warn('Could not determine the web monitor lock ownership. Continuing without the web server.');\n return null;\n}\n\n/** Bounded wait for a freshly spawned `web serve` child to claim the lock. */\nconst DETACHED_CLAIM_TIMEOUT_MS = 5000;\nconst DETACHED_CLAIM_POLL_MS = 150;\n\nexport interface EnsureWebMonitorOptions extends GetGlobalRootOptions {\n /** Injectable for tests. Defaults to `node:child_process`'s `spawn`. */\n spawn?: (\n command: string,\n args: string[],\n options: { detached: true; stdio: 'ignore' },\n ) => ChildProcess;\n /** Node executable to spawn. Defaults to `process.execPath`. */\n execPath?: string;\n /** CLI entry script re-invoked as `<entry> web serve ...`. Defaults to `process.argv[1]`. */\n entryScript?: string;\n /** How long to wait for the spawned instance's lock to become healthy. */\n claimTimeoutMs?: number;\n claimPollIntervalMs?: number;\n}\n\n/**\n * Entry point for `run`/`execute`: get a web monitor without binding one in\n * this process (US-002).\n *\n * 1. Reuse a live, healthy instance exactly like `ensureSingleWebServer`.\n * 2. Otherwise, spawn `<node> <cli> web serve --port … --host … [--refresh …]`\n * detached (`{ detached: true, stdio: 'ignore' }`) and `unref()`ed, so it\n * survives this process exiting and never pipes its own stdio back here.\n * 3. Poll the lock file (bounded) until the spawned instance claims it, then\n * return a *reused* handle for it — this process never owns that `Server`,\n * the same shape `ensureSingleWebServer`'s reuse path already returns.\n *\n * Two failure modes degrade gracefully rather than affecting the pipeline:\n * the global storage tree being unreachable (no resolvable home directory)\n * falls back to `startWebServer` bound right here, exactly the pre-US-001\n * behavior (US-006); a spawn or claim failure with the storage tree otherwise\n * healthy just returns null, same contract `startWebServer` itself has.\n */\nexport async function ensureWebMonitor(\n options: WebServerOptions,\n monitorOptions: EnsureWebMonitorOptions = {},\n): Promise<WebServerHandle | null> {\n const warn = options.warn ?? printWarning;\n const info = options.info ?? printInfo;\n\n let lockFile: string;\n try {\n lockFile = getWebLockFile(monitorOptions);\n } catch (err) {\n warn(\n `Web monitor: global storage unavailable (${err instanceof Error ? err.message : String(err)}). Falling back to legacy in-process mode.`,\n );\n return startWebServer(options);\n }\n\n const active = await detectActiveInstance(lockFile);\n if (active !== null) {\n info(`Reusing existing web monitor at ${reusedHandle(active).url}`);\n return reusedHandle(active);\n }\n\n const execPath = monitorOptions.execPath ?? process.execPath;\n const entryScript = monitorOptions.entryScript ?? process.argv[1];\n if (entryScript === undefined) {\n warn('Web monitor: could not determine the CLI entry point to spawn a detached server.');\n return null;\n }\n\n const args = [\n entryScript,\n 'web',\n 'serve',\n '--port',\n String(options.port),\n '--host',\n options.host,\n ];\n if (options.refreshSeconds !== undefined) {\n args.push('--refresh', String(options.refreshSeconds));\n }\n\n const spawnFn = monitorOptions.spawn ?? nodeSpawn;\n try {\n const child = spawnFn(execPath, args, { detached: true, stdio: 'ignore' });\n child.unref();\n } catch (err) {\n warn(\n `Web monitor: failed to start the detached server (${err instanceof Error ? err.message : String(err)}). Continuing without the web server.`,\n );\n return null;\n }\n\n const timeoutMs = monitorOptions.claimTimeoutMs ?? DETACHED_CLAIM_TIMEOUT_MS;\n const pollMs = monitorOptions.claimPollIntervalMs ?? DETACHED_CLAIM_POLL_MS;\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const claimed = await detectActiveInstance(lockFile);\n if (claimed !== null) {\n info(`Web monitor running at ${chalk.bold.cyan(reusedHandle(claimed).url)}`);\n return reusedHandle(claimed);\n }\n await sleep(pollMs);\n }\n\n warn(\n 'Web monitor: the detached server did not start in time. Continuing without the web server.',\n );\n return null;\n}\n","import { createHash } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport { join } from 'node:path';\nimport chalk from 'chalk';\nimport { isAgentPhase, isAgentProviderId } from '../agents/types.js';\nimport { writeAgentPreference } from '../commands/agent.js';\nimport type { JournalEntry } from '../core/journal.js';\nimport { resolvePackageDir } from '../core/prompt-resolver.js';\nimport {\n NullPublisher,\n type SessionPublisher,\n type SessionSnapshot,\n} from '../core/session-state.js';\nimport { readDiagnostics } from '../storage/diagnostics.js';\nimport { printInfo, printWarning } from '../ui/logger.js';\nimport type { SessionDirectoryHandle } from './session-directory.js';\n\n/**\n * HTTP server for the web monitoring mode. Plain node:http — no new runtime\n * dependencies. Serves the publisher's in-memory snapshot (it never re-reads\n * issues/N/session.json) and the static UI assets.\n *\n * Resilience contract: nothing here may ever affect the pipeline. Listen\n * failures (EADDRINUSE included) log a warning and the execution continues\n * without a server; the server is unref()'d so it cannot keep the process\n * alive; request handling is wrapped so a handler error answers 500 instead\n * of crashing the process.\n */\n\nconst require = createRequire(import.meta.url);\n\n/** Max length of `issueDescription` on GET /api/sessions (dashboard preview). */\nexport const SESSION_LIST_DESCRIPTION_MAX = 280;\n\nfunction readPackageVersion(): string {\n try {\n return (require('../../package.json') as { version: string }).version;\n } catch {\n return '0.0.0';\n }\n}\n\n/** Collapse whitespace and truncate for the sessions list payload. */\nexport function truncateSessionDescription(\n text: string | null | undefined,\n max = SESSION_LIST_DESCRIPTION_MAX,\n): string | null {\n if (text === null || text === undefined) return null;\n const normalized = text.replace(/\\s+/g, ' ').trim();\n if (normalized.length === 0) return normalized;\n if (normalized.length <= max) return normalized;\n return `${normalized.slice(0, max - 1).trimEnd()}…`;\n}\n\nexport interface WebServerOptions {\n /**\n * Legacy single-session mode (pre-US-003, still used by the US-006\n * fallback): serves this publisher's in-memory snapshot directly, with no\n * directory scan and no lock file involved. Mutually exclusive with\n * `sessions` — when both are given, `sessions` wins.\n */\n publisher?: SessionPublisher;\n /**\n * Multi-session mode: sessions discovered by polling the global storage\n * tree (`web/session-directory.ts`). This is what every current entry\n * point (`web serve`) passes; `publisher` only exists for the legacy path.\n */\n sessions?: SessionDirectoryHandle;\n port: number;\n host: string;\n /** Suggested UI polling interval, exposed via /api/health. */\n refreshSeconds?: number;\n /** Package version reported by /api/health. Default: read from package.json. */\n version?: string;\n /** Directory holding index.html/app.css/app.js. Default: auto-resolved. */\n publicDir?: string;\n /** Info logger. Default: printInfo. */\n info?: (message: string) => void;\n /** Warning logger. Default: printWarning. */\n warn?: (message: string) => void;\n /**\n * Whether to `unref()` the underlying socket so it never keeps the process\n * alive on its own. Default `true`, matching the historical contract for a\n * server bound inline in the pipeline process. The standalone `web serve`\n * process (the detached server itself) passes `false`: it has nothing else\n * to do, so staying alive for as long as the server is bound *is* the job.\n */\n unref?: boolean;\n}\n\nexport interface WebServerHandle {\n /**\n * Absent when this handle represents an existing instance reused through\n * `ensureSingleWebServer` (`web/lock.ts`) instead of one bound locally.\n */\n server?: Server;\n /** Host the server is bound to. */\n host: string;\n /** Actual bound port (relevant when options.port is 0). */\n port: number;\n /** Human-facing access URL. */\n url: string;\n /** Close the server and release signal handlers. Idempotent, never rejects. */\n close(): Promise<void>;\n}\n\ninterface StaticAsset {\n body: string;\n contentType: string;\n}\n\nconst JSON_TYPE = 'application/json; charset=utf-8';\n\n/**\n * Uniform view over \"whatever sessions this server knows about\", so the route\n * handlers below never care whether they are backed by a single in-memory\n * publisher (legacy mode) or by the global directory scan (US-003/US-004).\n */\ninterface SessionSource {\n /** Every session currently considered active, in no particular order. */\n list(): SessionSnapshot[];\n /** A specific session by id, or undefined when it is not currently active. */\n get(sessionId: string): SessionSnapshot | undefined;\n events(sessionId: string): Promise<JournalEntry[] | undefined>;\n}\n\nfunction publisherSessionSource(publisher: SessionPublisher): SessionSource {\n return {\n list: () => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === null ? [] : [snapshot];\n },\n get: (sessionId) => {\n const snapshot = publisher.snapshot();\n return snapshot.sessionId === sessionId ? snapshot : undefined;\n },\n events: async (sessionId) => (publisher.snapshot().sessionId === sessionId ? [] : undefined),\n };\n}\n\nfunction directorySessionSource(handle: SessionDirectoryHandle): SessionSource {\n return {\n list: () => handle.sessions().map((entry) => entry.snapshot),\n get: (sessionId) => handle.getSession(sessionId)?.snapshot,\n events: (sessionId) => handle.events(sessionId),\n };\n}\n\nfunction resolveSessionSource(options: WebServerOptions): SessionSource {\n if (options.sessions) return directorySessionSource(options.sessions);\n return publisherSessionSource(options.publisher ?? new NullPublisher());\n}\n\nconst STATIC_ROUTES: Record<string, { file: string; contentType: string }> = {\n '/': { file: 'index.html', contentType: 'text/html; charset=utf-8' },\n '/app.css': { file: 'app.css', contentType: 'text/css; charset=utf-8' },\n '/app.js': { file: 'app.js', contentType: 'text/javascript; charset=utf-8' },\n};\n\n/** Assets are read once at startup; missing files simply 404. */\nasync function loadStaticAssets(publicDir: string | null): Promise<Map<string, StaticAsset>> {\n const assets = new Map<string, StaticAsset>();\n if (publicDir === null) return assets;\n for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {\n try {\n const body = await readFile(join(publicDir, file), 'utf-8');\n assets.set(route, { body, contentType });\n } catch {\n // Asset not present (e.g. UI not built yet) — route answers 404.\n }\n }\n return assets;\n}\n\n/** Headers applied to every response, including 304s and errors. */\nfunction baseHeaders(res: ServerResponse): void {\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('Referrer-Policy', 'no-referrer');\n res.setHeader('X-Robots-Tag', 'noindex, nofollow');\n}\n\nfunction respond(res: ServerResponse, status: number, contentType: string, body: string): void {\n res.statusCode = status;\n res.setHeader('Content-Type', contentType);\n res.end(body);\n}\n\nfunction respondJson(res: ServerResponse, status: number, payload: unknown): void {\n respond(res, status, JSON_TYPE, JSON.stringify(payload));\n}\n\nfunction isLoopbackHost(host: string): boolean {\n return host === '127.0.0.1' || host === '::1' || host === 'localhost';\n}\n\nasync function readJsonBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n size += buffer.length;\n if (size > 64 * 1024) throw new Error('Request body too large.');\n chunks.push(buffer);\n }\n return JSON.parse(Buffer.concat(chunks).toString('utf-8') || '{}');\n}\n\n/**\n * Start the monitoring HTTP server. Returns null when the server could not\n * listen (port in use, invalid host, ...) — the pipeline continues without\n * monitoring, it is never brought down by the server.\n */\nexport async function startWebServer(options: WebServerOptions): Promise<WebServerHandle | null> {\n const info = options.info ?? printInfo;\n const warn = options.warn ?? printWarning;\n const version = options.version ?? readPackageVersion();\n const startedAtMs = Date.now();\n\n // The UI ships at the package root as web/public/ (sibling of prompts/),\n // resolved the same way from src/ and from the published dist/ layout.\n const assets = await loadStaticAssets(\n options.publicDir ?? resolvePackageDir(join('web', 'public')),\n );\n\n const source = resolveSessionSource(options);\n\n // JSON serialization memoized per session id: an unchanged poll answers 304\n // with an empty body. Content-hashed rather than counter-based, unlike the\n // pre-multi-session version — a directory-backed session has no in-process\n // publisher to hand out a monotonic version(), and a hash works uniformly\n // for both sources.\n const statusCache = new Map<string, { body: string; etag: string }>();\n const statusPayload = (snapshot: SessionSnapshot): { body: string; etag: string } => {\n const key = snapshot.sessionId ?? '';\n const body = JSON.stringify(snapshot);\n const cached = statusCache.get(key);\n if (cached !== undefined && cached.body === body) return cached;\n const etag = `\"${createHash('sha1').update(body).digest('hex')}\"`;\n const entry = { body, etag };\n statusCache.set(key, entry);\n return entry;\n };\n\n const handleRequest = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n baseHeaders(res);\n\n const requestUrl = new URL(req.url ?? '/', 'http://localhost');\n const path = requestUrl.pathname;\n\n if (req.method === 'POST' && path === '/api/config/agent') {\n if (!isLoopbackHost(options.host)) {\n respondJson(res, 403, {\n error: 'Configuration writes are disabled when the monitor is not bound to loopback.',\n });\n return;\n }\n const body = await readJsonBody(req);\n if (body === null || typeof body !== 'object' || Array.isArray(body)) {\n respondJson(res, 400, { error: 'Expected a JSON object.' });\n return;\n }\n const input = body as { provider?: unknown; model?: unknown; phase?: unknown };\n if (typeof input.provider !== 'string' || !isAgentProviderId(input.provider)) {\n respondJson(res, 400, { error: 'Invalid provider.' });\n return;\n }\n if (\n input.phase !== undefined &&\n (typeof input.phase !== 'string' || !isAgentPhase(input.phase))\n ) {\n respondJson(res, 400, { error: 'Invalid phase.' });\n return;\n }\n if (input.model !== undefined && typeof input.model !== 'string') {\n respondJson(res, 400, { error: 'Invalid model.' });\n return;\n }\n const file = await writeAgentPreference({\n target: 'global',\n provider: input.provider,\n ...(input.model ? { model: input.model } : {}),\n ...(input.phase ? { phase: input.phase } : {}),\n });\n respondJson(res, 200, { ok: true, file, appliesTo: 'future executions' });\n return;\n }\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n respondJson(res, 404, { error: 'Not found' });\n return;\n }\n\n if (path === '/api/status' || path === '/status.json') {\n const sessionId = requestUrl.searchParams.get('session');\n let snapshot: SessionSnapshot | undefined;\n if (sessionId !== null) {\n snapshot = source.get(sessionId);\n if (snapshot === undefined) {\n respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });\n return;\n }\n } else {\n // Back-compat shorthand (US-005): with exactly one active session,\n // GET /api/status with no query string still answers it directly, the\n // same behavior a single-session run always had. Zero or several\n // active sessions is genuinely ambiguous without an id and answers an\n // explicit error instead of guessing.\n const active = source.list();\n if (active.length === 0) {\n respondJson(res, 404, {\n error: 'No active session. Pass ?session=<id> — see GET /api/sessions.',\n });\n return;\n }\n if (active.length > 1) {\n respondJson(res, 409, {\n error: 'Multiple active sessions; specify ?session=<id>.',\n sessions: active.map((entry) => entry.sessionId),\n });\n return;\n }\n snapshot = active[0];\n }\n\n const { body, etag } = statusPayload(snapshot);\n res.setHeader('ETag', etag);\n if (req.headers['if-none-match'] === etag) {\n res.statusCode = 304;\n res.end();\n return;\n }\n respond(res, 200, JSON_TYPE, body);\n return;\n }\n\n if (path === '/api/sessions') {\n // Summary fields for the multi-session dashboard (issue #35): the full\n // SessionSnapshot is already in memory, so the client can render cards\n // from this single list without N× /api/status fetches per poll.\n // issueDescription is truncated — cards only need a short preview.\n respondJson(\n res,\n 200,\n source.list().map((snapshot) => ({\n sessionId: snapshot.sessionId,\n issueNumber: snapshot.issue.number,\n issueTitle: snapshot.issue.title,\n issueDescription: truncateSessionDescription(snapshot.issue.description),\n repositoryName: snapshot.repository.name,\n currentPhase: snapshot.currentPhase,\n progressPercent: snapshot.progress.percent,\n elapsedSeconds: snapshot.elapsedSeconds,\n status: snapshot.status,\n startedAt: snapshot.startedAt,\n updatedAt: snapshot.updatedAt,\n // Resilience fields, for a card that has to answer \"is this still\n // moving\" during a six-hour run. `updatedAt` is already the last\n // activity; these two say how hard the run has had to work for it.\n retries: snapshot.execution.retries,\n correctionCycle: snapshot.execution.correctionCycle,\n attempt: snapshot.resilience.attempt,\n provider: snapshot.resilience.provider,\n lastFailureKind: snapshot.resilience.lastFailureKind,\n cooldownUntil: snapshot.resilience.cooldownUntil,\n lastActivityAt: snapshot.resilience.lastActivityAt,\n statusUrl: `/api/status?session=${encodeURIComponent(snapshot.sessionId ?? '')}`,\n eventsUrl: `/api/events?session=${encodeURIComponent(snapshot.sessionId ?? '')}`,\n })),\n );\n return;\n }\n\n if (path === '/api/events') {\n const sessionId = requestUrl.searchParams.get('session');\n if (sessionId === null) {\n respondJson(res, 400, { error: 'Pass ?session=<id>.' });\n return;\n }\n const entries = await source.events(sessionId);\n if (entries === undefined) {\n respondJson(res, 404, { error: `No active session with id '${sessionId}'.` });\n return;\n }\n respondJson(res, 200, entries);\n return;\n }\n\n if (path === '/api/diagnostics') {\n const sessionId = requestUrl.searchParams.get('session') ?? undefined;\n respondJson(res, 200, await readDiagnostics({ sessionId, limit: 500 }));\n return;\n }\n\n if (path === '/api/config') {\n const sessionId = requestUrl.searchParams.get('session');\n const snapshot = sessionId === null ? undefined : source.get(sessionId);\n respondJson(res, 200, {\n effective: snapshot?.configuration ?? null,\n capturedForSession: snapshot?.sessionId ?? null,\n writable: isLoopbackHost(options.host),\n writeScope: 'global preferences for future executions',\n });\n return;\n }\n\n if (path === '/api/health') {\n respondJson(res, 200, {\n ok: true,\n uptime: Math.round((Date.now() - startedAtMs) / 1000),\n version,\n refreshSeconds: options.refreshSeconds ?? 5,\n capabilities: isLoopbackHost(options.host) ? ['config:agent:write'] : [],\n });\n return;\n }\n\n const asset = assets.get(path);\n if (asset) {\n respond(res, 200, asset.contentType, asset.body);\n return;\n }\n\n respondJson(res, 404, { error: 'Not found' });\n };\n\n const server = createServer((req, res) => {\n void handleRequest(req, res).catch((err) => {\n try {\n respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });\n } catch {\n // Response already destroyed — nothing to do, never crash the process.\n }\n });\n });\n\n const listening = await new Promise<boolean>((resolve) => {\n const onError = (err: NodeJS.ErrnoException): void => {\n if (err.code === 'EADDRINUSE') {\n warn(\n `Web monitor port ${options.port} is already in use. Continuing without the web server.`,\n );\n } else {\n warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);\n }\n resolve(false);\n };\n server.once('error', onError);\n server.listen(options.port, options.host, () => {\n server.removeListener('error', onError);\n resolve(true);\n });\n });\n\n if (!listening) return null;\n\n // Never keep the process alive on its own, unless explicitly told not to\n // unref: the pipeline ending must end the process. The one exception is the\n // detached `web serve` process (options.unref === false) — it has no other\n // work, so staying alive for as long as the server is bound is the point.\n if (options.unref !== false) {\n server.unref();\n }\n // Post-listen errors must never become uncaught exceptions.\n server.on('error', (err) => {\n warn(`Web monitor server error: ${err.message}`);\n });\n\n const address = server.address() as AddressInfo;\n const port = address.port;\n const displayHost = options.host === '0.0.0.0' ? 'localhost' : options.host;\n const url = `http://${displayHost}:${port}`;\n\n if (options.host === '0.0.0.0') {\n warn('Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.');\n }\n info(`Web monitor running at ${chalk.bold.cyan(url)}`);\n\n let closed = false;\n let closePromise: Promise<void> | null = null;\n\n const doClose = (): Promise<void> =>\n new Promise<void>((resolve) => {\n process.removeListener('SIGINT', onSignal);\n process.removeListener('SIGTERM', onSignal);\n server.close(() => resolve());\n // Drop idle keep-alive connections so close() never hangs.\n server.closeAllConnections();\n });\n\n const close = (): Promise<void> => {\n if (closed) return closePromise ?? Promise.resolve();\n closed = true;\n closePromise = doClose();\n return closePromise;\n };\n\n // `handle.close` is built here, but ensureSingleWebServer/ensureWebMonitor\n // and the `web serve` command each wrap it afterwards (to also remove the\n // lock file, stop the session-directory poller, ...). onSignal below calls\n // `handle.close()` — the *current* value of the property — rather than the\n // `close` closed over here, so a `kill -TERM <pid>` (including `issue-flow\n // web stop`) always runs the fully wrapped shutdown, not just this raw\n // socket close. Capturing `close` directly here was a real bug: the wrapped\n // behavior only ever ran for an *explicit* `handle.close()` call, leaving\n // the lock file (and the poller) behind on every signal-driven shutdown.\n const handle: WebServerHandle = { server, host: options.host, port, url, close };\n\n // Explicit close on SIGINT/SIGTERM, then re-raise so the default\n // termination behavior still applies.\n const onSignal = (signal: NodeJS.Signals): void => {\n void handle.close().finally(() => {\n process.kill(process.pid, signal);\n });\n };\n process.once('SIGINT', onSignal);\n process.once('SIGTERM', onSignal);\n\n return handle;\n}\n"],"mappings":"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","names":["readFile","join","chalk","require","join","readFile","chalk"]}
|