@dotobokuri/fleet-console 1.22.1 → 1.24.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.
Files changed (69) hide show
  1. package/AGENTS.md +41 -255
  2. package/README.md +8 -0
  3. package/dist/cli.d.ts +11 -1
  4. package/dist/cli.mjs +1866 -515
  5. package/dist/client/assets/{_baseUniq-cap0P4FK.js → _baseUniq-yooYbTgN.js} +1 -1
  6. package/dist/client/assets/{arc-yBqa09aK.js → arc-D4mme5Ts.js} +1 -1
  7. package/dist/client/assets/{architectureDiagram-Q4EWVU46-Bug4qdr-.js → architectureDiagram-Q4EWVU46-BAJOviEX.js} +1 -1
  8. package/dist/client/assets/{blockDiagram-DXYQGD6D-BkiIL2xt.js → blockDiagram-DXYQGD6D-DuObpxQ3.js} +1 -1
  9. package/dist/client/assets/{c4Diagram-AHTNJAMY-DYY200Al.js → c4Diagram-AHTNJAMY-BOT-SD34.js} +1 -1
  10. package/dist/client/assets/channel-TZYyAq1v.js +1 -0
  11. package/dist/client/assets/{chunk-4BX2VUAB-DQKmhVxn.js → chunk-4BX2VUAB-BF_aMTes.js} +1 -1
  12. package/dist/client/assets/{chunk-4TB4RGXK-DHpmA3Zl.js → chunk-4TB4RGXK-M4brBcnD.js} +1 -1
  13. package/dist/client/assets/{chunk-55IACEB6-DeRDuQXE.js → chunk-55IACEB6-CrG-4b2Z.js} +1 -1
  14. package/dist/client/assets/{chunk-EDXVE4YY-CNpnhSJm.js → chunk-EDXVE4YY-BxDSygH1.js} +1 -1
  15. package/dist/client/assets/{chunk-FMBD7UC4-BXGVABL-.js → chunk-FMBD7UC4-Bh7MEX7E.js} +1 -1
  16. package/dist/client/assets/{chunk-OYMX7WX6-WbJs4VwZ.js → chunk-OYMX7WX6-DBnBwxer.js} +1 -1
  17. package/dist/client/assets/{chunk-QZHKN3VN-u3BLl6Sl.js → chunk-QZHKN3VN-bjAogQwW.js} +1 -1
  18. package/dist/client/assets/{chunk-YZCP3GAM-BFESuhuJ.js → chunk-YZCP3GAM-l4FGlk_c.js} +1 -1
  19. package/dist/client/assets/classDiagram-6PBFFD2Q-Dh6hZUl4.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-HSJHXN6E-Dh6hZUl4.js +1 -0
  21. package/dist/client/assets/clone-DgWGnVWH.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-S5V4N54A-rouOsZjT.js → cose-bilkent-S5V4N54A-Bm5QA4zo.js} +1 -1
  23. package/dist/client/assets/{dagre-KV5264BT-QCUeCVpM.js → dagre-KV5264BT-PzXQN-5Q.js} +1 -1
  24. package/dist/client/assets/{diagram-5BDNPKRD-DqerlFb_.js → diagram-5BDNPKRD-KhaAKPtX.js} +1 -1
  25. package/dist/client/assets/{diagram-G4DWMVQ6-BS3a1YGN.js → diagram-G4DWMVQ6-DBbjfiR8.js} +1 -1
  26. package/dist/client/assets/{diagram-MMDJMWI5-BeauTcSV.js → diagram-MMDJMWI5-QWro7PRX.js} +1 -1
  27. package/dist/client/assets/{diagram-TYMM5635-Ci2wLUUE.js → diagram-TYMM5635-B9LqWrbF.js} +1 -1
  28. package/dist/client/assets/{erDiagram-SMLLAGMA-9MnmiKch.js → erDiagram-SMLLAGMA-b0VkMMN4.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-DWJPFMVM-CiV9p5EU.js → flowDiagram-DWJPFMVM-edxBAbe6.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-D6oNoENl.js → ganttDiagram-T4ZO3ILL-ks2Gg1_V.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-DESO385F.js → gitGraphDiagram-UUTBAWPF-DymUyYNl.js} +1 -1
  32. package/dist/client/assets/{graph-BZdl-RHy.js → graph-B-B_mqfh.js} +1 -1
  33. package/dist/client/assets/index-BgLzfg-v.css +1 -0
  34. package/dist/client/assets/index-pDnb-q6f.js +380 -0
  35. package/dist/client/assets/{infoDiagram-42DDH7IO-lc8vZnyi.js → infoDiagram-42DDH7IO-B5iLIYAc.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-BfzvQXhn.js → ishikawaDiagram-UXIWVN3A-DnztLLn7.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-VCZTEJTY-BS_Dr0EV.js → journeyDiagram-VCZTEJTY-BKDRfH1Y.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-6JOO6SKY-Cw7oTQJN.js → kanban-definition-6JOO6SKY-DPB3Phvr.js} +1 -1
  39. package/dist/client/assets/{layout-ChwcDQm8.js → layout-BEeRhYTG.js} +1 -1
  40. package/dist/client/assets/{linear-C7GLbmND.js → linear-QzmfWKSq.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-D9phfpQo.js → mermaid.core-BVka5dgm.js} +4 -4
  42. package/dist/client/assets/{min-p7Whc_Wi.js → min-CfXk9kSN.js} +1 -1
  43. package/dist/client/assets/{mindmap-definition-QFDTVHPH-fl_SJKrw.js → mindmap-definition-QFDTVHPH-tGrGhr6Z.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-DEJITSTG-Btw7ckA5.js → pieDiagram-DEJITSTG-ZRBqRiRK.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-DB4iJlTI.js → quadrantDiagram-34T5L4WZ-VCINZgct.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-MS252O5E-hXwV5Nhy.js → requirementDiagram-MS252O5E-DFLzADh3.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-XADWPNL6-DC-fizCk.js → sankeyDiagram-XADWPNL6-BbouN8Ji.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-FGHM5R23-RrfrroR5.js → sequenceDiagram-FGHM5R23-B58_LF0R.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-FHFEXIEX-q8FUPV_e.js → stateDiagram-FHFEXIEX-pk-OHHuL.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-xsN122JA.js +1 -0
  51. package/dist/client/assets/{timeline-definition-GMOUNBTQ-tegMd8Od.js → timeline-definition-GMOUNBTQ-4jQdCvaj.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-DHZGUBPP-CXx5oN2X.js → vennDiagram-DHZGUBPP-CUr1fH6N.js} +1 -1
  53. package/dist/client/assets/{wardley-RL74JXVD-CxSJ3P_7.js → wardley-RL74JXVD-D0cfvvNA.js} +1 -1
  54. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-DN1P180Z.js → wardleyDiagram-NUSXRM2D-D0l_Rp_Z.js} +1 -1
  55. package/dist/client/assets/{xychartDiagram-5P7HB3ND-Dcn9ukv5.js → xychartDiagram-5P7HB3ND-BdR8OEuJ.js} +1 -1
  56. package/dist/client/index.html +3 -3
  57. package/dist/desktop-protocol.d.ts +49 -0
  58. package/dist/desktop-protocol.mjs +99 -0
  59. package/dist/fleet-plugins/diff/routes.mjs +124 -225
  60. package/dist/fleet-plugins/skills/routes.mjs +218 -85
  61. package/dist/fleet-plugins/terminal/routes.mjs +573 -536
  62. package/package.json +7 -2
  63. package/dist/client/assets/channel-B3EqY8ya.js +0 -1
  64. package/dist/client/assets/classDiagram-6PBFFD2Q-BekEZLVJ.js +0 -1
  65. package/dist/client/assets/classDiagram-v2-HSJHXN6E-BekEZLVJ.js +0 -1
  66. package/dist/client/assets/clone-CsVi9-Ni.js +0 -1
  67. package/dist/client/assets/index-BKpCMFrm.js +0 -380
  68. package/dist/client/assets/index-cg8Uxys9.css +0 -1
  69. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BErzXGq7.js +0 -1
package/dist/cli.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, execFileSync, execFile, execSync } from 'child_process';
3
- import * as fs11 from 'fs';
4
- import fs11__default, { realpathSync, accessSync, constants, existsSync, lstatSync } from 'fs';
5
- import * as path25 from 'path';
6
- import path25__default, { join, resolve, relative, sep, dirname } from 'path';
2
+ import { execFile, spawn, execFileSync, execSync } from 'child_process';
3
+ import * as fs14 from 'fs';
4
+ import fs14__default, { realpathSync, accessSync, constants, existsSync, lstatSync } from 'fs';
5
+ import * as path27 from 'path';
6
+ import path27__default, { join, resolve, relative, sep, dirname } from 'path';
7
7
  import process4 from 'process';
8
8
  import { fileURLToPath, pathToFileURL } from 'url';
9
9
  import * as os4 from 'os';
@@ -12,9 +12,11 @@ import crypto5, { createHash, randomUUID } from 'crypto';
12
12
  import { builtinModules, createRequire } from 'module';
13
13
  import http2 from 'http';
14
14
  import { EventEmitter } from 'events';
15
- import { stat, readFile, realpath, readdir, mkdir, mkdtemp, writeFile, appendFile, rm, rename, access } from 'fs/promises';
15
+ import { stat, mkdir, readdir, readFile, rm, realpath, mkdtemp, writeFile, appendFile, rename, access } from 'fs/promises';
16
16
  import { Writable, Readable } from 'stream';
17
+ import fontList from 'font-list';
17
18
  import net2 from 'net';
19
+ import { promisify } from 'util';
18
20
 
19
21
  var __defProp = Object.defineProperty;
20
22
  var __export = (target, all) => {
@@ -37,7 +39,7 @@ function findBinaryPath(command2, env, options = {}) {
37
39
  return findOnPath(command2, pathValue, pathExts, platform);
38
40
  }
39
41
  function wrapWindowsShim(resolved, env, platform) {
40
- if (platform !== "win32" || !WINDOWS_SHIM_EXTENSIONS.has(path25__default.extname(resolved).toLowerCase())) {
42
+ if (platform !== "win32" || !WINDOWS_SHIM_EXTENSIONS.has(path27__default.extname(resolved).toLowerCase())) {
41
43
  return { bin: resolved, prefixArgs: [] };
42
44
  }
43
45
  rejectCmdExpansionSensitiveShim(resolved);
@@ -55,13 +57,13 @@ function forceWindowsArgQuoting(value) {
55
57
  return `${value} `;
56
58
  }
57
59
  function findOnPath(bin, pathValue, pathExts, platform) {
58
- const pathSeparator = platform === "win32" ? WINDOWS_PATH_SEPARATOR : path25__default.delimiter;
59
- if (hasPathSeparator(bin, platform) || path25__default.isAbsolute(bin)) {
60
+ const pathSeparator = platform === "win32" ? WINDOWS_PATH_SEPARATOR : path27__default.delimiter;
61
+ if (hasPathSeparator(bin, platform) || path27__default.isAbsolute(bin)) {
60
62
  return resolveWithExtensions(bin, pathExts, platform);
61
63
  }
62
64
  for (const entry of pathValue.split(pathSeparator)) {
63
65
  const searchDir = entry.length === 0 ? "." : entry;
64
- const candidate = resolveWithExtensions(path25__default.join(searchDir, bin), pathExts, platform);
66
+ const candidate = resolveWithExtensions(path27__default.join(searchDir, bin), pathExts, platform);
65
67
  if (candidate) {
66
68
  return candidate;
67
69
  }
@@ -69,7 +71,7 @@ function findOnPath(bin, pathValue, pathExts, platform) {
69
71
  return void 0;
70
72
  }
71
73
  function resolveWithExtensions(candidate, pathExts, platform) {
72
- if (platform !== "win32" || path25__default.extname(candidate).length > 0) {
74
+ if (platform !== "win32" || path27__default.extname(candidate).length > 0) {
73
75
  return isAccessibleExecutable(candidate, platform) ? candidate : void 0;
74
76
  }
75
77
  for (const ext of pathExts) {
@@ -304,7 +306,7 @@ function isC1Control(code) {
304
306
  var LOCK_DIR_MODE = 448;
305
307
  var LOCK_FILE_MODE = 384;
306
308
  function createConsoleLock(deps = {}) {
307
- const fsImpl = deps.fs ?? fs11__default;
309
+ const fsImpl = deps.fs ?? fs14__default;
308
310
  const now = deps.now ?? Date.now;
309
311
  const randomToken = deps.randomToken ?? (() => crypto5.randomBytes(32).toString("base64url"));
310
312
  const hostname5 = deps.hostname ?? (() => "127.0.0.1");
@@ -333,9 +335,10 @@ function createConsoleLock(deps = {}) {
333
335
  endpoint: input.endpoint,
334
336
  startedAt: now(),
335
337
  token: randomToken(),
336
- version: input.version
338
+ version: input.version,
339
+ ...input.owner ? { owner: input.owner } : {}
337
340
  };
338
- const fd = fsImpl.openSync(input.lockFile, fs11__default.constants.O_CREAT | fs11__default.constants.O_EXCL | fs11__default.constants.O_WRONLY, LOCK_FILE_MODE);
341
+ const fd = fsImpl.openSync(input.lockFile, fs14__default.constants.O_CREAT | fs14__default.constants.O_EXCL | fs14__default.constants.O_WRONLY, LOCK_FILE_MODE);
339
342
  try {
340
343
  fsImpl.writeFileSync(fd, `${JSON.stringify(payload, null, 2)}
341
344
  `, "utf8");
@@ -361,7 +364,7 @@ function createConsoleLock(deps = {}) {
361
364
  }
362
365
  function assertLockModes(lockFile) {
363
366
  if (typeof process.getuid !== "function") return;
364
- const dir = path25__default.dirname(lockFile);
367
+ const dir = path27__default.dirname(lockFile);
365
368
  const dirMode = fsImpl.statSync(dir).mode & 511;
366
369
  const fileMode = fsImpl.statSync(lockFile).mode & 511;
367
370
  if (dirMode !== LOCK_DIR_MODE) {
@@ -403,14 +406,14 @@ function createConsoleLock(deps = {}) {
403
406
  }
404
407
  var FLEET_DATA_DIR_NAME = ".fleet";
405
408
  function getFleetDataDir() {
406
- return path25.join(os4.homedir(), FLEET_DATA_DIR_NAME);
409
+ return path27.join(os4.homedir(), FLEET_DATA_DIR_NAME);
407
410
  }
408
411
  var SECURE_DIR_MODE = 448;
409
412
  var SECURE_FILE_MODE = 384;
410
- var NOFOLLOW_FLAG = fs11.constants.O_NOFOLLOW ?? 0;
413
+ var NOFOLLOW_FLAG = fs14.constants.O_NOFOLLOW ?? 0;
411
414
  function safeLstat(targetPath) {
412
415
  try {
413
- return fs11.lstatSync(targetPath);
416
+ return fs14.lstatSync(targetPath);
414
417
  } catch (error51) {
415
418
  if (error51.code === "ENOENT") {
416
419
  return null;
@@ -424,15 +427,15 @@ function ensureSafeDirectory(dirPath) {
424
427
  if (!stat5.isDirectory() || stat5.isSymbolicLink()) {
425
428
  throw new Error(`Unsafe Fleet directory: ${dirPath}`);
426
429
  }
427
- fs11.chmodSync(dirPath, SECURE_DIR_MODE);
430
+ fs14.chmodSync(dirPath, SECURE_DIR_MODE);
428
431
  return;
429
432
  }
430
- fs11.mkdirSync(dirPath, { recursive: true, mode: SECURE_DIR_MODE });
433
+ fs14.mkdirSync(dirPath, { recursive: true, mode: SECURE_DIR_MODE });
431
434
  const created = safeLstat(dirPath);
432
435
  if (!created?.isDirectory() || created.isSymbolicLink()) {
433
436
  throw new Error(`Unsafe Fleet directory: ${dirPath}`);
434
437
  }
435
- fs11.chmodSync(dirPath, SECURE_DIR_MODE);
438
+ fs14.chmodSync(dirPath, SECURE_DIR_MODE);
436
439
  }
437
440
 
438
441
  // ../../packages/core-infra/src/fs-store/atomic-write.ts
@@ -451,17 +454,17 @@ function writeAtomicSync(filePath, content, options = {}) {
451
454
  const tempPath = buildTempPath(filePath);
452
455
  let tempCreated = false;
453
456
  try {
454
- fs11.writeFileSync(tempPath, content, { encoding: "utf-8", flag: "wx", mode });
457
+ fs14.writeFileSync(tempPath, content, { encoding: "utf-8", flag: "wx", mode });
455
458
  tempCreated = true;
456
459
  if (doFsync) {
457
- const fd = fs11.openSync(tempPath, "r");
460
+ const fd = fs14.openSync(tempPath, "r");
458
461
  try {
459
462
  fsyncSyncBestEffort(fd);
460
463
  } finally {
461
- fs11.closeSync(fd);
464
+ fs14.closeSync(fd);
462
465
  }
463
466
  }
464
- fs11.renameSync(tempPath, filePath);
467
+ fs14.renameSync(tempPath, filePath);
465
468
  return;
466
469
  } catch (error51) {
467
470
  const code = error51.code;
@@ -471,7 +474,7 @@ function writeAtomicSync(filePath, content, options = {}) {
471
474
  }
472
475
  if (tempCreated) {
473
476
  try {
474
- fs11.unlinkSync(tempPath);
477
+ fs14.unlinkSync(tempPath);
475
478
  } catch {
476
479
  }
477
480
  }
@@ -483,7 +486,7 @@ function writeAtomicSync(filePath, content, options = {}) {
483
486
  function cleanupTempFiles(dir, prefix, minAgeMs, now) {
484
487
  let entries;
485
488
  try {
486
- entries = fs11.readdirSync(dir);
489
+ entries = fs14.readdirSync(dir);
487
490
  } catch {
488
491
  return;
489
492
  }
@@ -491,11 +494,11 @@ function cleanupTempFiles(dir, prefix, minAgeMs, now) {
491
494
  if (!entry.startsWith(prefix)) {
492
495
  continue;
493
496
  }
494
- const tempPath = path25.join(dir, entry);
497
+ const tempPath = path27.join(dir, entry);
495
498
  try {
496
- const stat5 = fs11.lstatSync(tempPath);
499
+ const stat5 = fs14.lstatSync(tempPath);
497
500
  if (stat5?.isFile() && !stat5.isSymbolicLink() && now - stat5.mtimeMs >= minAgeMs) {
498
- fs11.unlinkSync(tempPath);
501
+ fs14.unlinkSync(tempPath);
499
502
  }
500
503
  } catch {
501
504
  }
@@ -507,7 +510,7 @@ function isIgnoredFsyncError(error51) {
507
510
  }
508
511
  function fsyncSyncBestEffort(fd) {
509
512
  try {
510
- fs11.fsyncSync(fd);
513
+ fs14.fsyncSync(fd);
511
514
  } catch (error51) {
512
515
  if (!isIgnoredFsyncError(error51)) {
513
516
  throw error51;
@@ -527,7 +530,7 @@ function withDirectoryLock(deps, operation) {
527
530
  const startedAt = now();
528
531
  while (true) {
529
532
  try {
530
- fs11.mkdirSync(deps.lockDir, { mode: 448 });
533
+ fs14.mkdirSync(deps.lockDir, { mode: 448 });
531
534
  writeLockOwner(deps.lockDir, ownerFileName, now());
532
535
  try {
533
536
  return operation();
@@ -547,7 +550,7 @@ function withDirectoryLock(deps, operation) {
547
550
  }
548
551
  function releaseLock(lockDir) {
549
552
  try {
550
- fs11.rmSync(lockDir, { recursive: true, force: true });
553
+ fs14.rmSync(lockDir, { recursive: true, force: true });
551
554
  } catch {
552
555
  }
553
556
  }
@@ -557,14 +560,14 @@ function recoverStaleLock(lockDir, ownerFileName, now, staleLockMs) {
557
560
  if (!isRecoverableSnapshot(initialSnapshot, now, staleLockMs)) return;
558
561
  const quarantineDir = buildQuarantinePath(lockDir);
559
562
  try {
560
- fs11.renameSync(lockDir, quarantineDir);
563
+ fs14.renameSync(lockDir, quarantineDir);
561
564
  } catch {
562
565
  return;
563
566
  }
564
567
  try {
565
568
  const quarantinedSnapshot = readLockSnapshot(quarantineDir, ownerFileName);
566
569
  if (sameLockSnapshot(initialSnapshot, quarantinedSnapshot) && isRecoverableSnapshot(quarantinedSnapshot, now, staleLockMs)) {
567
- fs11.rmSync(quarantineDir, { recursive: true, force: true });
570
+ fs14.rmSync(quarantineDir, { recursive: true, force: true });
568
571
  } else {
569
572
  restoreQuarantinedLock(quarantineDir, lockDir);
570
573
  }
@@ -578,7 +581,7 @@ function recoverStaleLock(lockDir, ownerFileName, now, staleLockMs) {
578
581
  }
579
582
  }
580
583
  function readLockSnapshot(lockDir, ownerFileName) {
581
- const stat5 = fs11.lstatSync(lockDir);
584
+ const stat5 = fs14.lstatSync(lockDir);
582
585
  if (stat5.isSymbolicLink()) {
583
586
  return { owner: null, mtimeMs: 0 };
584
587
  }
@@ -597,18 +600,18 @@ function writeLockOwner(lockDir, ownerFileName, startedAt) {
597
600
  hostname: os4.hostname(),
598
601
  startedAt
599
602
  };
600
- const ownerPath = path25.join(lockDir, ownerFileName);
603
+ const ownerPath = path27.join(lockDir, ownerFileName);
601
604
  try {
602
- fs11.writeFileSync(ownerPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
605
+ fs14.writeFileSync(ownerPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
603
606
  } catch (error51) {
604
607
  releaseLock(lockDir);
605
608
  throw error51;
606
609
  }
607
610
  }
608
611
  function readLockOwner(lockDir, ownerFileName) {
609
- const ownerPath = path25.join(lockDir, ownerFileName);
612
+ const ownerPath = path27.join(lockDir, ownerFileName);
610
613
  try {
611
- return sanitizeLockOwner(JSON.parse(fs11.readFileSync(ownerPath, "utf-8")));
614
+ return sanitizeLockOwner(JSON.parse(fs14.readFileSync(ownerPath, "utf-8")));
612
615
  } catch (error51) {
613
616
  const code = error51.code;
614
617
  if (code === "ENOENT") return null;
@@ -630,7 +633,7 @@ function sameLockSnapshot(a, b) {
630
633
  }
631
634
  function restoreQuarantinedLock(quarantineDir, lockDir) {
632
635
  try {
633
- fs11.renameSync(quarantineDir, lockDir);
636
+ fs14.renameSync(quarantineDir, lockDir);
634
637
  } catch {
635
638
  }
636
639
  }
@@ -665,17 +668,17 @@ function sleepSync(ms) {
665
668
  function readAuthStoreFile(filePath) {
666
669
  let fd;
667
670
  try {
668
- fd = fs11.openSync(filePath, fs11.constants.O_RDONLY | NOFOLLOW_FLAG);
669
- if (!fs11.fstatSync(fd).isFile()) {
671
+ fd = fs14.openSync(filePath, fs14.constants.O_RDONLY | NOFOLLOW_FLAG);
672
+ if (!fs14.fstatSync(fd).isFile()) {
670
673
  return {};
671
674
  }
672
- return JSON.parse(fs11.readFileSync(fd, "utf-8"));
675
+ return JSON.parse(fs14.readFileSync(fd, "utf-8"));
673
676
  } catch {
674
677
  return {};
675
678
  } finally {
676
679
  if (fd !== void 0) {
677
680
  try {
678
- fs11.closeSync(fd);
681
+ fs14.closeSync(fd);
679
682
  } catch {
680
683
  }
681
684
  }
@@ -683,7 +686,7 @@ function readAuthStoreFile(filePath) {
683
686
  }
684
687
 
685
688
  // ../../packages/core-infra/src/auth/auth-storage.ts
686
- var DEFAULT_AUTH_PATH = path25.join(getFleetDataDir(), "auth.json");
689
+ var DEFAULT_AUTH_PATH = path27.join(getFleetDataDir(), "auth.json");
687
690
  var AUTH_LOCK_OWNER_FILE_NAME = "owner.json";
688
691
  var AUTH_LOCK_TIMEOUT_MS = 5e3;
689
692
  function createAuthService(deps = {}) {
@@ -694,7 +697,7 @@ function createAuthService(deps = {}) {
694
697
  async deleteApiKey(providerId) {
695
698
  let deleted = false;
696
699
  withAuthLock(authPath, lockDir, () => {
697
- if (!fs11.existsSync(authPath)) return;
700
+ if (!fs14.existsSync(authPath)) return;
698
701
  const data = readAuthStoreFile(authPath);
699
702
  if (!Object.prototype.hasOwnProperty.call(data, providerId)) return;
700
703
  delete data[providerId];
@@ -707,21 +710,21 @@ function createAuthService(deps = {}) {
707
710
  async getApiKey(providerId) {
708
711
  let fd;
709
712
  try {
710
- if (!fs11.existsSync(authPath)) {
713
+ if (!fs14.existsSync(authPath)) {
711
714
  return void 0;
712
715
  }
713
- fd = fs11.openSync(authPath, fs11.constants.O_RDONLY | NOFOLLOW_FLAG);
714
- if (!fs11.fstatSync(fd).isFile()) {
716
+ fd = fs14.openSync(authPath, fs14.constants.O_RDONLY | NOFOLLOW_FLAG);
717
+ if (!fs14.fstatSync(fd).isFile()) {
715
718
  return void 0;
716
719
  }
717
- const data = JSON.parse(fs11.readFileSync(fd, "utf-8"));
720
+ const data = JSON.parse(fs14.readFileSync(fd, "utf-8"));
718
721
  return typeof data[providerId]?.key === "string" ? data[providerId].key : void 0;
719
722
  } catch {
720
723
  return void 0;
721
724
  } finally {
722
725
  if (fd !== void 0) {
723
726
  try {
724
- fs11.closeSync(fd);
727
+ fs14.closeSync(fd);
725
728
  } catch {
726
729
  }
727
730
  }
@@ -729,7 +732,7 @@ function createAuthService(deps = {}) {
729
732
  },
730
733
  // [HIGH #2] 읽기 경로: fd 기반 O_RDONLY|O_NOFOLLOW 통일
731
734
  async listProviderIds() {
732
- if (!fs11.existsSync(authPath)) {
735
+ if (!fs14.existsSync(authPath)) {
733
736
  return [];
734
737
  }
735
738
  try {
@@ -739,10 +742,10 @@ function createAuthService(deps = {}) {
739
742
  }
740
743
  },
741
744
  async setApiKey(providerId, key) {
742
- const dir = path25.dirname(authPath);
745
+ const dir = path27.dirname(authPath);
743
746
  ensureSafeDirectory(dir);
744
747
  withAuthLock(authPath, lockDir, () => {
745
- const data = fs11.existsSync(authPath) ? readAuthStoreFile(authPath) : {};
748
+ const data = fs14.existsSync(authPath) ? readAuthStoreFile(authPath) : {};
746
749
  data[providerId] = {
747
750
  ...data[providerId] ?? {},
748
751
  key
@@ -775,7 +778,7 @@ function createDurableJsonStore(deps) {
775
778
  const sensitivity = deps.sensitivity;
776
779
  const mode = sensitivity === "sensitive" ? SECURE_FILE_MODE : 420;
777
780
  const minAgeMs = deps.tempCleanupMinAgeMs ?? DEFAULT_TEMP_FILE_MIN_AGE_MS;
778
- const dir = path25.dirname(deps.filePath);
781
+ const dir = path27.dirname(deps.filePath);
779
782
  return {
780
783
  path: deps.filePath,
781
784
  load() {
@@ -825,21 +828,21 @@ function createDurableJsonStore(deps) {
825
828
  function readJsonFile(filePath, sanitize) {
826
829
  let fd;
827
830
  try {
828
- const stat5 = fs11.lstatSync(filePath);
831
+ const stat5 = fs14.lstatSync(filePath);
829
832
  if (!stat5 || !stat5.isFile() || stat5.isSymbolicLink()) {
830
833
  return sanitize(void 0);
831
834
  }
832
- fd = fs11.openSync(filePath, fs11.constants.O_RDONLY | NOFOLLOW_FLAG);
833
- if (!fs11.fstatSync(fd).isFile()) {
835
+ fd = fs14.openSync(filePath, fs14.constants.O_RDONLY | NOFOLLOW_FLAG);
836
+ if (!fs14.fstatSync(fd).isFile()) {
834
837
  return sanitize(void 0);
835
838
  }
836
- return sanitize(JSON.parse(fs11.readFileSync(fd, "utf-8")));
839
+ return sanitize(JSON.parse(fs14.readFileSync(fd, "utf-8")));
837
840
  } catch {
838
841
  return sanitize(void 0);
839
842
  } finally {
840
843
  if (fd !== void 0) {
841
844
  try {
842
- fs11.closeSync(fd);
845
+ fs14.closeSync(fd);
843
846
  } catch {
844
847
  }
845
848
  }
@@ -849,7 +852,7 @@ function ensureSafeDirectoryIfSensitive(dir, sensitivity) {
849
852
  if (sensitivity === "sensitive") {
850
853
  ensureSafeDirectory(dir);
851
854
  } else {
852
- fs11.mkdirSync(dir, { recursive: true });
855
+ fs14.mkdirSync(dir, { recursive: true });
853
856
  }
854
857
  }
855
858
 
@@ -861,8 +864,8 @@ var LOCK_OWNER_FILE_NAME = "owner";
861
864
  var TEMP_FILE_PREFIX = `.tmp-${GLOBAL_OPTIONS_FILE_NAME}-`;
862
865
  function createGlobalOptionsStore(deps = {}) {
863
866
  const dataDir = deps.dataDir ?? getFleetDataDir();
864
- const optionsPath = path25.join(dataDir, GLOBAL_OPTIONS_FILE_NAME);
865
- const lockDir = path25.join(dataDir, LOCK_DIR_NAME);
867
+ const optionsPath = path27.join(dataDir, GLOBAL_OPTIONS_FILE_NAME);
868
+ const lockDir = path27.join(dataDir, LOCK_DIR_NAME);
866
869
  const store2 = createDurableJsonStore({
867
870
  filePath: optionsPath,
868
871
  lockDir,
@@ -895,10 +898,11 @@ function sanitizeGlobalOptionsData(value) {
895
898
  }
896
899
  const data = {
897
900
  version: GLOBAL_OPTIONS_VERSION,
898
- ...typeof value.enableMetaphor === "boolean" ? { enableMetaphor: value.enableMetaphor } : {}
901
+ ...typeof value.enableMetaphor === "boolean" ? { enableMetaphor: value.enableMetaphor } : {},
902
+ ...value.codexLaunchMode === "acp" || value.codexLaunchMode === "app-server" ? { codexLaunchMode: value.codexLaunchMode } : {}
899
903
  };
900
- const allowedKeys = /* @__PURE__ */ new Set(["version", "enableMetaphor"]);
901
- const changed = Object.keys(value).some((key) => !allowedKeys.has(key)) || "enableMetaphor" in value && typeof value.enableMetaphor !== "boolean";
904
+ const allowedKeys = /* @__PURE__ */ new Set(["version", "enableMetaphor", "codexLaunchMode"]);
905
+ const changed = Object.keys(value).some((key) => !allowedKeys.has(key)) || "enableMetaphor" in value && typeof value.enableMetaphor !== "boolean" || "codexLaunchMode" in value && value.codexLaunchMode !== "acp" && value.codexLaunchMode !== "app-server";
902
906
  return { data, changed };
903
907
  }
904
908
  function isRecord(value) {
@@ -930,16 +934,100 @@ function createInfraServices(deps = {}) {
930
934
  globalOptionsService
931
935
  };
932
936
  }
933
- function readFleetConsoleRelease() {
937
+ var DESKTOP_PROTOCOL_VERSION = 1;
938
+ var DESKTOP_RESOURCE_ROOT_ENV = "FLEET_CONSOLE_RESOURCE_ROOT";
939
+ var DESKTOP_OWNER_ID_ENV = "FLEET_CONSOLE_OWNER_ID";
940
+ var DESKTOP_OWNER_KIND_ENV = "FLEET_CONSOLE_OWNER_KIND";
941
+ var DESKTOP_PROTOCOL_VERSION_ENV = "FLEET_CONSOLE_PROTOCOL_VERSION";
942
+ var DESKTOP_RESOURCE_ROOT_MARKER = ".fleet-console-resource-root";
943
+ var DESKTOP_DEVELOPMENT_ENV = "FLEET_CONSOLE_DESKTOP_DEVELOPMENT";
944
+ var LOCK_DIR_NAME2 = "fleet-console";
945
+ var LOCK_FILE_NAME = "console.lock";
946
+ var CONSOLE_DATA_DIR_NAME = "console";
947
+ var CONSOLE_STATE_FILE_NAME = "state.json";
948
+ var CONSOLE_SETTINGS_FILE_NAME = "settings.json";
949
+ var CONSOLE_CAPTURES_DIR_NAME = "captures";
950
+ function readDesktopProtocolEnvironment(env = process.env, deps = {}) {
951
+ const resourceRoot = env[DESKTOP_RESOURCE_ROOT_ENV];
952
+ const ownerId = env[DESKTOP_OWNER_ID_ENV];
953
+ const ownerKind = env[DESKTOP_OWNER_KIND_ENV];
954
+ const protocolVersion = env[DESKTOP_PROTOCOL_VERSION_ENV];
955
+ const values = [resourceRoot, ownerId, ownerKind, protocolVersion];
956
+ if (values.every((value) => value === void 0)) return null;
957
+ if (values.some((value) => !value)) throw new Error("desktop_protocol_environment_incomplete");
958
+ if (ownerKind !== "desktop") throw new Error("desktop_owner_kind_invalid");
959
+ if (!isSafeOwnerId(ownerId)) throw new Error("desktop_owner_id_invalid");
960
+ if (protocolVersion !== String(DESKTOP_PROTOCOL_VERSION)) throw new Error("desktop_protocol_version_unsupported");
961
+ const resolvedRoot = isDesktopDevelopmentEnvironment(env) ? validateDesktopDevelopmentResourceRoot(resourceRoot) : validateDesktopResourceRoot(resourceRoot, deps.expectedPackageRoot);
962
+ return { owner: { kind: "desktop", id: ownerId, protocolVersion: DESKTOP_PROTOCOL_VERSION }, resourceRoot: resolvedRoot };
963
+ }
964
+ function isDesktopDevelopmentEnvironment(env = process.env) {
965
+ return env[DESKTOP_DEVELOPMENT_ENV] === "1";
966
+ }
967
+ function resolveCanonicalStableConsolePaths(input) {
968
+ const dir = input.consoleDirOverride ?? path27__default.join(input.tmpDir, `${LOCK_DIR_NAME2}-${input.uid}-stable`);
969
+ const dataDir = input.consoleDirOverride ?? path27__default.join(input.fleetDataDir, CONSOLE_DATA_DIR_NAME);
970
+ return {
971
+ dir,
972
+ lockFile: path27__default.join(dir, LOCK_FILE_NAME),
973
+ dataDir,
974
+ stateFile: path27__default.join(dataDir, CONSOLE_STATE_FILE_NAME),
975
+ settingsFile: path27__default.join(dataDir, CONSOLE_SETTINGS_FILE_NAME),
976
+ capturesDir: path27__default.join(dataDir, CONSOLE_CAPTURES_DIR_NAME)
977
+ };
978
+ }
979
+ function validateDesktopResourceRoot(root, expectedPackageRoot = resolveDesktopProtocolPackageRoot()) {
980
+ if (!path27__default.isAbsolute(root)) throw new Error("desktop_resource_root_not_absolute");
981
+ const resolvedRoot = fs14__default.realpathSync(root);
982
+ const expectedRoot = fs14__default.realpathSync(expectedPackageRoot);
983
+ if (resolvedRoot !== expectedRoot) throw new Error("desktop_resource_root_invalid");
984
+ const markerPath = path27__default.join(resolvedRoot, DESKTOP_RESOURCE_ROOT_MARKER);
985
+ const marker = fs14__default.readFileSync(markerPath, "utf8").trim();
986
+ if (marker !== String(DESKTOP_PROTOCOL_VERSION)) throw new Error("desktop_resource_root_marker_invalid");
987
+ return resolvedRoot;
988
+ }
989
+ function validateDesktopDevelopmentResourceRoot(root) {
990
+ if (!path27__default.isAbsolute(root)) throw new Error("desktop_resource_root_not_absolute");
991
+ let resolvedRoot;
992
+ try {
993
+ resolvedRoot = fs14__default.realpathSync(root);
994
+ } catch {
995
+ throw new Error("desktop_development_resource_root_invalid");
996
+ }
997
+ const expectedRoot = fs14__default.realpathSync(resolveDesktopProtocolPackageRoot());
998
+ if (resolvedRoot !== expectedRoot) throw new Error("desktop_development_resource_root_invalid");
999
+ return resolvedRoot;
1000
+ }
1001
+ function resolveDesktopProtocolPackageRoot() {
1002
+ const requireFromHere = createRequire(import.meta.url);
1003
+ for (const candidate of ["../package.json", "../../package.json"]) {
1004
+ try {
1005
+ return path27__default.dirname(requireFromHere.resolve(candidate));
1006
+ } catch (error51) {
1007
+ if (error51.code !== "MODULE_NOT_FOUND") throw error51;
1008
+ }
1009
+ }
1010
+ throw new Error("fleet_console_package_json_not_found");
1011
+ }
1012
+ function isSafeOwnerId(value) {
1013
+ return value.length > 0 && value.length <= 256 && /^[A-Za-z0-9._-]+$/.test(value);
1014
+ }
1015
+ function readFleetConsoleRelease(env = process.env) {
1016
+ const desktop = readDesktopProtocolEnvironment(env);
1017
+ if (desktop) return readReleaseAt(desktop.resourceRoot, isDesktopDevelopmentEnvironment(env) ? "local" : "desktop");
934
1018
  const requireFromHere = createRequire(import.meta.url);
935
1019
  const packageJsonPath = resolvePackageJsonPath(requireFromHere);
1020
+ const packageRoot = path27__default.dirname(packageJsonPath);
936
1021
  const pkg = requireFromHere(packageJsonPath);
937
- const packageRoot = path25__default.dirname(packageJsonPath);
938
- const version2 = pkg.version ?? "";
939
1022
  if (pkg.private === true) {
940
- return { channel: "local", version: version2, packageRoot };
1023
+ return readReleaseAt(packageRoot, "local", requireFromHere);
941
1024
  }
942
- return { channel: "stable", version: version2, packageRoot };
1025
+ return readReleaseAt(packageRoot, "stable", requireFromHere);
1026
+ }
1027
+ function readReleaseAt(packageRoot, channel, requireFromHere = createRequire(import.meta.url)) {
1028
+ const packageJsonPath = path27__default.join(packageRoot, "package.json");
1029
+ const pkg = requireFromHere(packageJsonPath);
1030
+ return { channel, version: pkg.version ?? "", packageRoot };
943
1031
  }
944
1032
  function resolvePackageJsonPath(requireFromHere) {
945
1033
  for (const candidate of ["../package.json", "../../package.json"]) {
@@ -953,25 +1041,23 @@ function resolvePackageJsonPath(requireFromHere) {
953
1041
  }
954
1042
 
955
1043
  // core/host/paths.ts
956
- var LOCK_DIR_NAME2 = "fleet-console";
957
1044
  var CONSOLE_RUNTIME_DIR_NAME = "console";
958
- var LOCK_FILE_NAME = "console.lock";
959
- var CONSOLE_DATA_DIR_NAME = "console";
960
- var CONSOLE_STATE_FILE_NAME = "state.json";
961
- var CONSOLE_SETTINGS_FILE_NAME = "settings.json";
962
- var CONSOLE_CAPTURES_DIR_NAME = "captures";
1045
+ var CONSOLE_DATA_DIR_NAME2 = "console";
1046
+ var CONSOLE_STATE_FILE_NAME2 = "state.json";
1047
+ var CONSOLE_SETTINGS_FILE_NAME2 = "settings.json";
1048
+ var CONSOLE_CAPTURES_DIR_NAME2 = "captures";
963
1049
  function createConsolePaths(deps = {}) {
964
1050
  const env = deps.env ?? process.env;
965
1051
  const base = env.FLEET_CONSOLE_DIR ?? defaultConsoleBaseDir(deps);
966
- return { dir: base, lockFile: path25__default.join(base, LOCK_FILE_NAME) };
1052
+ return { dir: base, lockFile: path27__default.join(base, "console.lock") };
967
1053
  }
968
1054
  function createConsoleDataPaths(deps = {}) {
969
1055
  const dir = defaultConsoleDataBaseDir(deps);
970
1056
  return {
971
1057
  dir,
972
- stateFile: path25__default.join(dir, CONSOLE_STATE_FILE_NAME),
973
- settingsFile: path25__default.join(dir, CONSOLE_SETTINGS_FILE_NAME),
974
- capturesDir: path25__default.join(dir, CONSOLE_CAPTURES_DIR_NAME)
1058
+ stateFile: path27__default.join(dir, CONSOLE_STATE_FILE_NAME2),
1059
+ settingsFile: path27__default.join(dir, CONSOLE_SETTINGS_FILE_NAME2),
1060
+ capturesDir: path27__default.join(dir, CONSOLE_CAPTURES_DIR_NAME2)
975
1061
  };
976
1062
  }
977
1063
  function defaultConsoleBaseDir(deps) {
@@ -982,11 +1068,15 @@ function defaultConsoleBaseDir(deps) {
982
1068
  return localConsoleDir(packageRoot);
983
1069
  }
984
1070
  const uid = deps.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
985
- return path25__default.join(os4__default.tmpdir(), `${LOCK_DIR_NAME2}-${uid}-${channel}`);
1071
+ return resolveCanonicalStableConsolePaths({
1072
+ tmpDir: os4__default.tmpdir(),
1073
+ uid,
1074
+ fleetDataDir: getFleetDataDir()
1075
+ }).dir;
986
1076
  }
987
1077
  function defaultConsoleDataBaseDir(deps) {
988
1078
  if (deps.fleetDataDir !== void 0) {
989
- return path25__default.join(deps.fleetDataDir, CONSOLE_DATA_DIR_NAME);
1079
+ return path27__default.join(deps.fleetDataDir, CONSOLE_DATA_DIR_NAME2);
990
1080
  }
991
1081
  const env = deps.env ?? process.env;
992
1082
  if (env.FLEET_CONSOLE_DIR) {
@@ -998,10 +1088,14 @@ function defaultConsoleDataBaseDir(deps) {
998
1088
  const packageRoot = deps.packageRoot ?? (release ??= readFleetConsoleRelease()).packageRoot;
999
1089
  return localConsoleDir(packageRoot);
1000
1090
  }
1001
- return path25__default.join(getFleetDataDir(), CONSOLE_DATA_DIR_NAME);
1091
+ return resolveCanonicalStableConsolePaths({
1092
+ tmpDir: os4__default.tmpdir(),
1093
+ uid: typeof process.getuid === "function" ? process.getuid() : 0,
1094
+ fleetDataDir: getFleetDataDir()
1095
+ }).dataDir;
1002
1096
  }
1003
1097
  function localConsoleDir(packageRoot) {
1004
- return path25__default.join(path25__default.resolve(packageRoot, "..", ".."), ".fleet", CONSOLE_RUNTIME_DIR_NAME);
1098
+ return path27__default.join(path27__default.resolve(packageRoot, "..", ".."), ".fleet", CONSOLE_RUNTIME_DIR_NAME);
1005
1099
  }
1006
1100
 
1007
1101
  // ../../packages/fleet-carriers/src/dispatch/framework.ts
@@ -1837,10 +1931,10 @@ function mergeDefs(...defs) {
1837
1931
  function cloneDef(schema) {
1838
1932
  return mergeDefs(schema._zod.def);
1839
1933
  }
1840
- function getElementAtPath(obj, path46) {
1841
- if (!path46)
1934
+ function getElementAtPath(obj, path50) {
1935
+ if (!path50)
1842
1936
  return obj;
1843
- return path46.reduce((acc, key) => acc?.[key], obj);
1937
+ return path50.reduce((acc, key) => acc?.[key], obj);
1844
1938
  }
1845
1939
  function promiseAllObject(promisesObj) {
1846
1940
  const keys = Object.keys(promisesObj);
@@ -2249,11 +2343,11 @@ function explicitlyAborted(x, startIndex = 0) {
2249
2343
  }
2250
2344
  return false;
2251
2345
  }
2252
- function prefixIssues(path46, issues) {
2346
+ function prefixIssues(path50, issues) {
2253
2347
  return issues.map((iss) => {
2254
2348
  var _a3;
2255
2349
  (_a3 = iss).path ?? (_a3.path = []);
2256
- iss.path.unshift(path46);
2350
+ iss.path.unshift(path50);
2257
2351
  return iss;
2258
2352
  });
2259
2353
  }
@@ -2400,16 +2494,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2400
2494
  }
2401
2495
  function formatError(error51, mapper = (issue2) => issue2.message) {
2402
2496
  const fieldErrors = { _errors: [] };
2403
- const processError = (error52, path46 = []) => {
2497
+ const processError = (error52, path50 = []) => {
2404
2498
  for (const issue2 of error52.issues) {
2405
2499
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2406
- issue2.errors.map((issues) => processError({ issues }, [...path46, ...issue2.path]));
2500
+ issue2.errors.map((issues) => processError({ issues }, [...path50, ...issue2.path]));
2407
2501
  } else if (issue2.code === "invalid_key") {
2408
- processError({ issues: issue2.issues }, [...path46, ...issue2.path]);
2502
+ processError({ issues: issue2.issues }, [...path50, ...issue2.path]);
2409
2503
  } else if (issue2.code === "invalid_element") {
2410
- processError({ issues: issue2.issues }, [...path46, ...issue2.path]);
2504
+ processError({ issues: issue2.issues }, [...path50, ...issue2.path]);
2411
2505
  } else {
2412
- const fullpath = [...path46, ...issue2.path];
2506
+ const fullpath = [...path50, ...issue2.path];
2413
2507
  if (fullpath.length === 0) {
2414
2508
  fieldErrors._errors.push(mapper(issue2));
2415
2509
  } else {
@@ -2436,17 +2530,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2436
2530
  }
2437
2531
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2438
2532
  const result = { errors: [] };
2439
- const processError = (error52, path46 = []) => {
2533
+ const processError = (error52, path50 = []) => {
2440
2534
  var _a3, _b;
2441
2535
  for (const issue2 of error52.issues) {
2442
2536
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2443
- issue2.errors.map((issues) => processError({ issues }, [...path46, ...issue2.path]));
2537
+ issue2.errors.map((issues) => processError({ issues }, [...path50, ...issue2.path]));
2444
2538
  } else if (issue2.code === "invalid_key") {
2445
- processError({ issues: issue2.issues }, [...path46, ...issue2.path]);
2539
+ processError({ issues: issue2.issues }, [...path50, ...issue2.path]);
2446
2540
  } else if (issue2.code === "invalid_element") {
2447
- processError({ issues: issue2.issues }, [...path46, ...issue2.path]);
2541
+ processError({ issues: issue2.issues }, [...path50, ...issue2.path]);
2448
2542
  } else {
2449
- const fullpath = [...path46, ...issue2.path];
2543
+ const fullpath = [...path50, ...issue2.path];
2450
2544
  if (fullpath.length === 0) {
2451
2545
  result.errors.push(mapper(issue2));
2452
2546
  continue;
@@ -2478,8 +2572,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2478
2572
  }
2479
2573
  function toDotPath(_path) {
2480
2574
  const segs = [];
2481
- const path46 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2482
- for (const seg of path46) {
2575
+ const path50 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2576
+ for (const seg of path50) {
2483
2577
  if (typeof seg === "number")
2484
2578
  segs.push(`[${seg}]`);
2485
2579
  else if (typeof seg === "symbol")
@@ -15143,13 +15237,13 @@ function resolveRef(ref, ctx) {
15143
15237
  if (!ref.startsWith("#")) {
15144
15238
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15145
15239
  }
15146
- const path46 = ref.slice(1).split("/").filter(Boolean);
15147
- if (path46.length === 0) {
15240
+ const path50 = ref.slice(1).split("/").filter(Boolean);
15241
+ if (path50.length === 0) {
15148
15242
  return ctx.rootSchema;
15149
15243
  }
15150
15244
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15151
- if (path46[0] === defsKey) {
15152
- const key = path46[1];
15245
+ if (path50[0] === defsKey) {
15246
+ const key = path50[1];
15153
15247
  if (!key || !ctx.defs[key]) {
15154
15248
  throw new Error(`Reference not found: ${ref}`);
15155
15249
  }
@@ -20652,11 +20746,24 @@ function getYoloModeId(cli) {
20652
20746
  return "yolo";
20653
20747
  }
20654
20748
  }
20655
- function buildCodexDeveloperInstructionConfig(systemPrompt) {
20656
- if (!systemPrompt) {
20657
- return [];
20749
+ function buildCodexConfigEnv(systemPrompt, configOverrides, baseConfigJson) {
20750
+ const map2 = parseBaseConfigMap(baseConfigJson);
20751
+ for (const override of configOverrides ?? []) {
20752
+ const eqIndex = override.indexOf("=");
20753
+ if (eqIndex < 0) {
20754
+ continue;
20755
+ }
20756
+ const key = override.slice(0, eqIndex);
20757
+ const rawValue = override.slice(eqIndex + 1);
20758
+ assignNestedKey(map2, key, parseOverrideValue(rawValue));
20759
+ }
20760
+ if (systemPrompt) {
20761
+ map2.developer_instructions = systemPrompt;
20762
+ }
20763
+ if (Object.keys(map2).length === 0) {
20764
+ return void 0;
20658
20765
  }
20659
- return [`developer_instructions="${escapeTomlBasicString(systemPrompt)}"`];
20766
+ return JSON.stringify(map2);
20660
20767
  }
20661
20768
  function buildConfigOverrideArgs(overrides) {
20662
20769
  return overrides.flatMap((override) => ["-c", override]);
@@ -20684,27 +20791,44 @@ function mcpServerConfigsToAcp(servers) {
20684
20791
  headers: server.headers ?? []
20685
20792
  }));
20686
20793
  }
20687
- function escapeTomlBasicString(value) {
20688
- return value.replace(/[\u0000-\u001f"\\\u007f]/g, (char) => {
20689
- switch (char) {
20690
- case "\b":
20691
- return "\\b";
20692
- case " ":
20693
- return "\\t";
20694
- case "\n":
20695
- return "\\n";
20696
- case "\f":
20697
- return "\\f";
20698
- case "\r":
20699
- return "\\r";
20700
- case '"':
20701
- return '\\"';
20702
- case "\\":
20703
- return "\\\\";
20704
- default:
20705
- return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
20794
+ function parseBaseConfigMap(baseConfigJson) {
20795
+ if (!baseConfigJson) {
20796
+ return {};
20797
+ }
20798
+ try {
20799
+ const parsed = JSON.parse(baseConfigJson);
20800
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
20801
+ return parsed;
20706
20802
  }
20707
- });
20803
+ } catch {
20804
+ }
20805
+ return {};
20806
+ }
20807
+ function parseOverrideValue(raw) {
20808
+ try {
20809
+ return JSON.parse(raw);
20810
+ } catch {
20811
+ if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
20812
+ return raw.slice(1, -1);
20813
+ }
20814
+ return raw;
20815
+ }
20816
+ }
20817
+ function assignNestedKey(target, dottedKey, value) {
20818
+ const segments = dottedKey.split(".");
20819
+ let cursor = target;
20820
+ for (let i = 0; i < segments.length - 1; i += 1) {
20821
+ const segment = segments[i];
20822
+ const existing = cursor[segment];
20823
+ if (typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
20824
+ cursor = existing;
20825
+ } else {
20826
+ const nested = {};
20827
+ cursor[segment] = nested;
20828
+ cursor = nested;
20829
+ }
20830
+ }
20831
+ cursor[segments[segments.length - 1]] = value;
20708
20832
  }
20709
20833
 
20710
20834
  // ../../packages/core-unified-agent/src/detector/CliDetector.ts
@@ -21965,12 +22089,12 @@ var CodexAppServerConnection = class extends BaseConnection {
21965
22089
  );
21966
22090
  }
21967
22091
  findRolloutPathForThreadId(threadId) {
21968
- const codexHome = this.codexHome ?? this.env.CODEX_HOME ?? path25.join(this.env.HOME ?? "", ".codex");
22092
+ const codexHome = this.codexHome ?? this.env.CODEX_HOME ?? path27.join(this.env.HOME ?? "", ".codex");
21969
22093
  if (!codexHome) {
21970
22094
  return null;
21971
22095
  }
21972
22096
  for (const rootName of ["sessions", "archived_sessions"]) {
21973
- const found = findRolloutPath(path25.join(codexHome, rootName), threadId);
22097
+ const found = findRolloutPath(path27.join(codexHome, rootName), threadId);
21974
22098
  if (found) {
21975
22099
  return found;
21976
22100
  }
@@ -22077,11 +22201,11 @@ function isMissingRolloutError(error51, threadId) {
22077
22201
  return error51 instanceof Error && error51.message.includes(`no rollout found for thread id ${threadId}`);
22078
22202
  }
22079
22203
  function findRolloutPath(root, threadId) {
22080
- if (!fs11.existsSync(root)) {
22204
+ if (!fs14.existsSync(root)) {
22081
22205
  return null;
22082
22206
  }
22083
- for (const entry of fs11.readdirSync(root, { withFileTypes: true })) {
22084
- const entryPath = path25.join(root, entry.name);
22207
+ for (const entry of fs14.readdirSync(root, { withFileTypes: true })) {
22208
+ const entryPath = path27.join(root, entry.name);
22085
22209
  if (entry.isDirectory()) {
22086
22210
  const found = findRolloutPath(entryPath, threadId);
22087
22211
  if (found) {
@@ -22099,13 +22223,11 @@ function findRolloutPath(root, threadId) {
22099
22223
  // ../../packages/core-unified-agent/src/client/UnifiedCodexAgentClient.ts
22100
22224
  var CODEX_TURN_LEVEL_CONFIG_KEYS = /* @__PURE__ */ new Set(["effort", "model"]);
22101
22225
  var CODEX_THREAD_POLICY_CONFIG_KEYS = /* @__PURE__ */ new Set(["approvalPolicy", "sandbox"]);
22102
- var CODEX_ACP_PROMPT_FALLBACK = process.platform === "win32";
22103
22226
  var UnifiedCodexAgentClient = class extends EventEmitter {
22104
22227
  connection = null;
22105
22228
  sessionId = null;
22106
22229
  sessionCwd = null;
22107
22230
  currentSystemPrompt = null;
22108
- firstPromptPending = null;
22109
22231
  pendingOverrides = null;
22110
22232
  detector = new CliDetector();
22111
22233
  on(event, listener) {
@@ -22126,9 +22248,10 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22126
22248
  throw new Error("UnifiedCodexAgentClient\uB294 codex CLI\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.");
22127
22249
  }
22128
22250
  this.validateModelEffort(options.model, options.effort);
22129
- {
22251
+ if (shouldUseCodexAcp(options)) {
22130
22252
  return this.connectAcp(options);
22131
22253
  }
22254
+ return this.connectAppServer(options);
22132
22255
  }
22133
22256
  async connectAppServer(options) {
22134
22257
  const backend = getBackendConfig("codex");
@@ -22197,22 +22320,17 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22197
22320
  async connectAcp(options) {
22198
22321
  const cleanEnv = cleanEnvironment(process.env, options.env);
22199
22322
  const spawnConfig = createSpawnConfig("codex", options);
22200
- const developerInstructions = CODEX_ACP_PROMPT_FALLBACK ? void 0 : options.systemPrompt ?? void 0;
22201
- const args = [
22202
- ...spawnConfig.args,
22203
- ...this.buildStartupConfigArgs(
22204
- options.configOverrides,
22205
- void 0,
22206
- void 0,
22207
- developerInstructions
22208
- )
22209
- ];
22323
+ const codexConfigEnv = buildCodexConfigEnv(
22324
+ options.systemPrompt,
22325
+ options.configOverrides,
22326
+ cleanEnv.CODEX_CONFIG
22327
+ );
22210
22328
  const connection = new AcpConnection({
22211
22329
  command: spawnConfig.command,
22212
- args,
22330
+ args: spawnConfig.args,
22213
22331
  cliType: "codex",
22214
22332
  cwd: options.cwd,
22215
- env: { ...cleanEnv },
22333
+ env: { ...cleanEnv, ...codexConfigEnv ? { CODEX_CONFIG: codexConfigEnv } : {} },
22216
22334
  requestTimeout: options.timeout ?? 6e5,
22217
22335
  initTimeout: options.timeout ?? 6e4,
22218
22336
  promptIdleTimeout: options.promptIdleTimeout ?? 6e5,
@@ -22309,17 +22427,7 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22309
22427
  if (!this.sessionId) {
22310
22428
  throw new Error("\uC5F0\uACB0\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
22311
22429
  }
22312
- const systemPrompt = this.firstPromptPending;
22313
- if (!systemPrompt) {
22314
- return this.connection.sendPrompt(this.sessionId, content);
22315
- }
22316
- const userBlocks = typeof content === "string" ? [{ type: "text", text: content }] : content;
22317
- const response = await this.connection.sendPrompt(this.sessionId, [
22318
- { type: "text", text: systemPrompt },
22319
- ...userBlocks
22320
- ]);
22321
- this.firstPromptPending = null;
22322
- return response;
22430
+ return this.connection.sendPrompt(this.sessionId, content);
22323
22431
  }
22324
22432
  this.applyPendingOverrides();
22325
22433
  const input = typeof content === "string" ? [{ type: "text", text: content, text_elements: [] }] : content.map((block) => "text" in block ? { type: "text", text: block.text, text_elements: [] } : { type: "text", text: JSON.stringify(block), text_elements: [] });
@@ -22401,8 +22509,6 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22401
22509
  mcpServers: this.resolveAcpMcpServers(mcpServers)
22402
22510
  });
22403
22511
  this.sessionId = sessionId;
22404
- this.currentSystemPrompt = null;
22405
- this.firstPromptPending = null;
22406
22512
  return;
22407
22513
  }
22408
22514
  if (mcpServers?.length) {
@@ -22439,7 +22545,6 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22439
22545
  const session = await this.connection.reconnectSession(targetCwd);
22440
22546
  this.sessionId = session.sessionId;
22441
22547
  this.sessionCwd = targetCwd;
22442
- this.firstPromptPending = this.currentSystemPrompt;
22443
22548
  return {
22444
22549
  cli: "codex",
22445
22550
  protocol: "acp",
@@ -22467,7 +22572,6 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22467
22572
  this.sessionId = session.sessionId;
22468
22573
  this.sessionCwd = options.cwd;
22469
22574
  this.currentSystemPrompt = options.systemPrompt ?? null;
22470
- this.firstPromptPending = CODEX_ACP_PROMPT_FALLBACK && !options.sessionId ? options.systemPrompt ?? null : null;
22471
22575
  this.pendingOverrides = {
22472
22576
  turnConfig: {},
22473
22577
  threadConfig: {}
@@ -22485,7 +22589,6 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22485
22589
  this.sessionId = null;
22486
22590
  this.sessionCwd = null;
22487
22591
  this.currentSystemPrompt = null;
22488
- this.firstPromptPending = null;
22489
22592
  this.pendingOverrides = null;
22490
22593
  }
22491
22594
  validateModelEffort(modelId, effort) {
@@ -22696,9 +22799,8 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22696
22799
  return "read-only";
22697
22800
  }
22698
22801
  }
22699
- buildStartupConfigArgs(overrides, servers, modeMapping, developerInstructions) {
22802
+ buildStartupConfigArgs(overrides, servers, modeMapping) {
22700
22803
  const configArgs = [
22701
- ...buildCodexDeveloperInstructionConfig(developerInstructions),
22702
22804
  ...modeMapping ? [
22703
22805
  `approval_policy="${modeMapping.approvalPolicy}"`,
22704
22806
  `sandbox_mode="${modeMapping.sandbox}"`
@@ -22787,6 +22889,11 @@ var UnifiedCodexAgentClient = class extends EventEmitter {
22787
22889
  return patterns.some((pattern) => pattern.test(text));
22788
22890
  }
22789
22891
  };
22892
+ function shouldUseCodexAcp(options) {
22893
+ const value = options.env?.CODEX_USE_ACP ?? process.env.CODEX_USE_ACP;
22894
+ const normalized = value?.trim().toLowerCase();
22895
+ return normalized !== "false" && normalized !== "0";
22896
+ }
22790
22897
  var UnifiedCursorAgentClient = class extends EventEmitter {
22791
22898
  connection = null;
22792
22899
  sessionId = null;
@@ -24912,7 +25019,7 @@ function formatInstallCommand(packageNames, command2, versionOrChannel) {
24912
25019
  return `${command2} i -g ${packageNames.map((name) => `${name}@${versionOrChannel}`).join(" ")}`;
24913
25020
  }
24914
25021
  function isPathInside(deps, child, parent) {
24915
- const separator = deps.platform === "win32" ? "\\" : path25__default.sep;
25022
+ const separator = deps.platform === "win32" ? "\\" : path27__default.sep;
24916
25023
  return child === parent || child.startsWith(`${parent}${separator}`);
24917
25024
  }
24918
25025
  function normalizeExistingPath(deps, value) {
@@ -24927,10 +25034,10 @@ function normalizePath(deps, value) {
24927
25034
  return deps.platform === "win32" ? resolved.toLowerCase() : resolved;
24928
25035
  }
24929
25036
  function resolvePathForPlatform(platform, value) {
24930
- return platform === "win32" ? path25__default.win32.resolve(value) : path25__default.resolve(value);
25037
+ return platform === "win32" ? path27__default.win32.resolve(value) : path27__default.resolve(value);
24931
25038
  }
24932
25039
  function joinPathForPlatform(platform, ...parts) {
24933
- return platform === "win32" ? path25__default.win32.join(...parts) : path25__default.join(...parts);
25040
+ return platform === "win32" ? path27__default.win32.join(...parts) : path27__default.join(...parts);
24934
25041
  }
24935
25042
  function defaultExecFile(file2, args) {
24936
25043
  return execFileSync(file2, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -25039,7 +25146,7 @@ function readRawCarriersOrDefaultStore() {
25039
25146
  return readCarriers();
25040
25147
  }
25041
25148
  function getCarriersFilePath() {
25042
- return path25.join(resolveStoreDir(), FILENAME);
25149
+ return path27.join(resolveStoreDir(), FILENAME);
25043
25150
  }
25044
25151
  function updateCarriers(mutator) {
25045
25152
  withStoreLock(() => {
@@ -25053,7 +25160,7 @@ function updateCarriers(mutator) {
25053
25160
  function withStoreLock(operation) {
25054
25161
  const dir = resolveStoreDir();
25055
25162
  ensureSafeDirectory(dir);
25056
- const lockDir = path25.join(dir, LOCK_DIRNAME);
25163
+ const lockDir = path27.join(dir, LOCK_DIRNAME);
25057
25164
  return withDirectoryLock(
25058
25165
  { lockDir, ownerFileName: LOCK_OWNER_FILENAME },
25059
25166
  operation
@@ -25066,20 +25173,20 @@ function readCarriers() {
25066
25173
  return readCarriersFromDir(resolveStoreDir());
25067
25174
  }
25068
25175
  function readCarriersFromDir(dir) {
25069
- const filePath = path25.join(dir, FILENAME);
25176
+ const filePath = path27.join(dir, FILENAME);
25070
25177
  let fd;
25071
25178
  try {
25072
- const stat5 = fs11.lstatSync(filePath);
25179
+ const stat5 = fs14.lstatSync(filePath);
25073
25180
  if (!stat5.isFile() || stat5.isSymbolicLink()) return {};
25074
- fd = fs11.openSync(filePath, fs11.constants.O_RDONLY | NOFOLLOW_FLAG);
25075
- if (!fs11.fstatSync(fd).isFile()) return {};
25076
- return sanitizeFleetCarriers(JSON.parse(fs11.readFileSync(fd, "utf-8")));
25181
+ fd = fs14.openSync(filePath, fs14.constants.O_RDONLY | NOFOLLOW_FLAG);
25182
+ if (!fs14.fstatSync(fd).isFile()) return {};
25183
+ return sanitizeFleetCarriers(JSON.parse(fs14.readFileSync(fd, "utf-8")));
25077
25184
  } catch {
25078
25185
  return {};
25079
25186
  } finally {
25080
25187
  if (fd !== void 0) {
25081
25188
  try {
25082
- fs11.closeSync(fd);
25189
+ fs14.closeSync(fd);
25083
25190
  } catch {
25084
25191
  }
25085
25192
  }
@@ -25087,7 +25194,7 @@ function readCarriersFromDir(dir) {
25087
25194
  }
25088
25195
  function recordLastLocalWriteFingerprint(filePath, generation) {
25089
25196
  try {
25090
- const st = fs11.statSync(filePath);
25197
+ const st = fs14.statSync(filePath);
25091
25198
  runtimeState.lastLocalWriteFingerprint = { generation, mtimeMs: st.mtimeMs, size: st.size };
25092
25199
  } catch {
25093
25200
  runtimeState.lastLocalWriteFingerprint = { generation, mtimeMs: 0, size: 0 };
@@ -25095,7 +25202,7 @@ function recordLastLocalWriteFingerprint(filePath, generation) {
25095
25202
  }
25096
25203
  function writeCarriers(s) {
25097
25204
  const dir = resolveStoreDir();
25098
- const filePath = path25.join(dir, FILENAME);
25205
+ const filePath = path27.join(dir, FILENAME);
25099
25206
  const next = serializeFleetCarriers(s);
25100
25207
  writeAtomicSync(filePath, `${JSON.stringify(next, null, 2)}
25101
25208
  `, { fsync: false, mode: 420 });
@@ -25120,7 +25227,7 @@ function sanitizeFleetCarriers(value) {
25120
25227
  }
25121
25228
  function unlinkStaleSubagentModeFile(dir) {
25122
25229
  try {
25123
- fs11.unlinkSync(path25.join(dir, STALE_SUBAGENT_MODE_FILENAME));
25230
+ fs14.unlinkSync(path27.join(dir, STALE_SUBAGENT_MODE_FILENAME));
25124
25231
  } catch {
25125
25232
  }
25126
25233
  }
@@ -25649,7 +25756,7 @@ var CHRONICLE_DEFAULTS = {
25649
25756
  };
25650
25757
  var CARRIER_METADATA = {
25651
25758
  // ── Tier 1: Routing ──
25652
- title: "Captain \xB7 Chief Knowledge Officer",
25759
+ title: "Chief Knowledge Officer",
25653
25760
  summary: "Documentation steward across two surfaces: codebase markdown (AGENTS.md/README/CHANGELOG/inline docs) and Fleet Wiki entries proposed via the wiki patch queue.",
25654
25761
  category: "operations",
25655
25762
  whenToUse: [
@@ -25662,7 +25769,7 @@ var CARRIER_METADATA = {
25662
25769
  "before implementation and verification are complete",
25663
25770
  "code modification (\u2192genesis) or code review (\u2192sentinel)",
25664
25771
  "architectural judgment (\u2192nimitz) or release-scope planning decisions (\u2192kirov)",
25665
- "Fleet Wiki patch approval \u2014 Admiral retains the wiki_patch_queue approve/reject gate"
25772
+ "Fleet Wiki patch approval \u2014 host agent retains the wiki_patch_queue approve/reject gate"
25666
25773
  ],
25667
25774
  requestBlocks: [
25668
25775
  {
@@ -25698,18 +25805,18 @@ var CARRIER_METADATA = {
25698
25805
  ],
25699
25806
  // ── Tier 2: Composition ──
25700
25807
  permissions: [
25701
- "CRITICAL: Fleet Wiki entries are governed by the Fleet Wiki workspace AGENTS.md doctrine \u2014 propose via wiki_ingest only, and NEVER edit wiki entries or system-managed indexes/logs by hand. Admiral holds the wiki_patch_queue approve/reject gate.",
25808
+ "CRITICAL: Fleet Wiki entries are governed by the Fleet Wiki workspace AGENTS.md doctrine \u2014 propose via wiki_ingest only, and NEVER edit wiki entries or system-managed indexes/logs by hand. The host agent holds the wiki_patch_queue approve/reject gate.",
25702
25809
  "CRITICAL: Owns codebase markdown \u2014 AGENTS.md, README, CHANGELOG, and inline doc files outside the Fleet Wiki workspace \u2014 and may write them directly. NEVER modify source code logic (report issues instead).",
25703
- "MUST treat the Admiral's <doc_type>, <audience>, <scope>, and <change_scope> as binding contracts. NEVER redefine or expand documentation scope autonomously.",
25704
- "Owns expression details (tone, wording, paragraph flow within domain conventions) but MUST NOT silently change structural decisions specified by the Admiral (sections, items, target files).",
25810
+ "MUST treat the host agent's <doc_type>, <audience>, <scope>, and <change_scope> as binding contracts. NEVER redefine or expand documentation scope autonomously.",
25811
+ "Owns expression details (tone, wording, paragraph flow within domain conventions) but MUST NOT silently change structural decisions specified by the host agent (sections, items, target files).",
25705
25812
  "MUST NOT make go/no-go, release timing, or release-scope decisions (escalate to Kirov/Nimitz). May detect and document breaking changes only."
25706
25813
  ],
25707
25814
  outputFormat: `Deliver the documentation artifact directly.
25708
25815
  [Codebase Doc] Write the .md/AGENTS.md/README/CHANGELOG file(s) directly via filesystem tools.
25709
- [Fleet Wiki] Compose the entry body and enqueue via wiki_ingest. Report the patch_id; never edit wiki entries or system-managed files by hand. Admiral approves via wiki_patch_queue.
25816
+ [Fleet Wiki] Compose the entry body and enqueue via wiki_ingest. Report the patch_id; never edit wiki entries or system-managed files by hand. The host agent approves via wiki_patch_queue.
25710
25817
  After delivery, provide a brief completion report.
25711
25818
  [Required] always include:
25712
- **Workflow** \u2014 One of: 'Codebase Doc' or 'Fleet Wiki' (state which workflow this sortie used).
25819
+ **Workflow** \u2014 One of: 'Codebase Doc' or 'Fleet Wiki' (state which workflow this task used).
25713
25820
  **Documents written / Patches enqueued** \u2014 Each file created/modified (Codebase Doc) OR each patch_id enqueued (Fleet Wiki) with target path.
25714
25821
  **Cascade .md audit** \u2014 (Codebase Doc only) Each .md inspected: path, status (updated / already consistent / not applicable), 1-line summary if updated.
25715
25822
  [If applicable] omit if not relevant:
@@ -25718,10 +25825,10 @@ After delivery, provide a brief completion report.
25718
25825
  Keep the completion report concise \u2014 the documentation/patch itself is the primary deliverable.`,
25719
25826
  principles: [
25720
25827
  CARRIER_JOBS_SELF_CALL_HINT,
25721
- "Two workflows, one carrier \u2014 never mix the two surfaces. (A) Codebase Doc: direct filesystem edits to .md/AGENTS.md/README/CHANGELOG outside the Fleet Wiki workspace. (B) Fleet Wiki: orient \u2192 consult \u2192 lint \u2192 wiki_ingest to enqueue \u2192 Admiral approves via wiki_patch_queue. Decide the workflow from <doc_type>; if ambiguous, escalate before acting.",
25722
- "[Fleet Wiki] Read the Fleet Wiki workspace AGENTS.md before the first wiki operation in a sortie \u2014 it is the authoritative doctrine for boundaries, roles, gates, schema reference, and escalation. The active entry schema may evolve; always consult the workspace doctrine and current schema each sortie rather than relying on prior memory.",
25828
+ "Two workflows, one carrier \u2014 never mix the two surfaces. (A) Codebase Doc: direct filesystem edits to .md/AGENTS.md/README/CHANGELOG outside the Fleet Wiki workspace. (B) Fleet Wiki: orient \u2192 consult \u2192 lint \u2192 wiki_ingest to enqueue \u2192 host agent approves via wiki_patch_queue. Decide the workflow from <doc_type>; if ambiguous, escalate before acting.",
25829
+ "[Fleet Wiki] Read the Fleet Wiki workspace AGENTS.md before the first wiki operation in a task \u2014 it is the authoritative doctrine for boundaries, roles, gates, schema reference, and escalation. The active entry schema may evolve; always consult the workspace doctrine and current schema each task rather than relying on prior memory.",
25723
25830
  "[Fleet Wiki] Provide raw evidence (source) alongside every wiki_ingest call \u2014 synthesized entry body must cite raw substance without copying verbatim. The system writes raw, queue, and index automatically.",
25724
- "[Codebase Doc] Every sortie must include a cascade .md audit \u2014 identify all .md files within the change scope, verify they reflect current state, and cross-reference parent/child AGENTS.md to prevent doctrinal conflicts.",
25831
+ "[Codebase Doc] Every task must include a cascade .md audit \u2014 identify all .md files within the change scope, verify they reflect current state, and cross-reference parent/child AGENTS.md to prevent doctrinal conflicts.",
25725
25832
  "[Codebase Doc] CRITICAL: README.md files must ONLY be updated where they already exist \u2014 NEVER create new README.md files. If a directory lacks a README.md, leave it as-is and note the absence in the audit report.",
25726
25833
  "If additional documentation scope seems needed, MUST report it as a follow-up suggestion in the completion report. NEVER silently expand the audit/update scope.",
25727
25834
  "Change-impact documentation must be factual and observable \u2014 never recommend whether a change should ship, be reverted, or be delayed."
@@ -25743,8 +25850,8 @@ var GENESIS_DEFAULTS = {
25743
25850
  };
25744
25851
  var CARRIER_METADATA2 = {
25745
25852
  // ── Tier 1: Routing ──
25746
- title: "Captain \xB7 Chief Engineer",
25747
- summary: "Full-stack implementation workhorse \u2014 builds features, writes production-quality clean code, and maintains structural integrity throughout. As the Captain (\uD568\uC7A5) of this Carrier, Genesis takes direct command of build and implementation operations.",
25853
+ title: "Chief Engineer",
25854
+ summary: "Full-stack implementation workhorse \u2014 builds features, writes production-quality clean code, and maintains structural integrity throughout.",
25748
25855
  category: "operations",
25749
25856
  whenToUse: [
25750
25857
  "new features",
@@ -25771,14 +25878,14 @@ var CARRIER_METADATA2 = {
25771
25878
  // ── Tier 2: Composition ──
25772
25879
  permissions: [
25773
25880
  "Full access to the codebase \u2014 read, write, and execute commands.",
25774
- "Owns implementation details (internal helper structure, code organization, local naming) ONLY within the design boundaries set by the Admiral's instructions.",
25775
- "MUST NOT substitute autonomous design judgment for the Admiral's explicit design decisions \u2014 interface unification vs separation, type/function names, directory structure, public surface shape, and any choice the Admiral has specified are BINDING contracts, not suggestions.",
25881
+ "Owns implementation details (internal helper structure, code organization, local naming) ONLY within the design boundaries set by the host agent's instructions.",
25882
+ "MUST NOT substitute autonomous design judgment for the host agent's explicit design decisions \u2014 interface unification vs separation, type/function names, directory structure, public surface shape, and any choice the host agent has specified are BINDING contracts, not suggestions.",
25776
25883
  "MUST NOT silently re-plan, expand scope, invent alternative workflows, or shrink the assigned work beyond what the instructions specify.",
25777
25884
  "MUST NOT silently absorb Kirov's planning role or Nimitz's architecture arbitration role when those inputs are clearly missing."
25778
25885
  ],
25779
25886
  principles: [
25780
25887
  CARRIER_JOBS_SELF_CALL_HINT,
25781
- "MUST treat the Admiral's <objective>, <scope>, <constraints>, and <references> as binding design contracts. Specific design decisions stated in the instructions MUST be implemented as-instructed, not as 'cleaner' or 'better' substitutions.",
25888
+ "MUST treat the host agent's <objective>, <scope>, <constraints>, and <references> as binding design contracts. Specific design decisions stated in the instructions MUST be implemented as-instructed, not as 'cleaner' or 'better' substitutions.",
25782
25889
  "If an alternative design seems superior, MUST complete the assigned work AS-INSTRUCTED first, then report the alternative ONLY as a follow-up suggestion. NEVER substitute the alternative silently.",
25783
25890
  "On ambiguity or apparent conflict in the instructions, MUST report back and request clarification instead of choosing autonomously.",
25784
25891
  "Follow planning artifacts when provided \u2014 do not re-plan work that Kirov has already structured unless the input is clearly invalid.",
@@ -25812,8 +25919,8 @@ var KIROV_DEFAULTS = {
25812
25919
  };
25813
25920
  var CARRIER_METADATA3 = {
25814
25921
  // ── Tier 1: Routing ──
25815
- title: "Captain \xB7 Operational Planning Bridge",
25816
- summary: "Clarifies requirements, closes planning gaps, and writes executable .fleet/plans/*.md plan_files with ordered waves, ownership, dependencies, QA gates, acceptance criteria, documentation impacts, and escalation triggers.",
25922
+ title: "Operational Planning",
25923
+ summary: "Clarifies requirements, closes planning gaps, and writes one executable .fleet/plans/*.md plan_file SSoT with explicit parallel lanes, ownership, dependencies, QA gates, acceptance criteria, documentation impacts, and escalation triggers.",
25817
25924
  category: "planning",
25818
25925
  whenToUse: [
25819
25926
  "structured .fleet/plans/*.md plan_file requests, PRD decomposition, or Ohio-executable execution plans",
@@ -25821,14 +25928,14 @@ var CARRIER_METADATA3 = {
25821
25928
  "medium/large refactors, migrations, cross-module work, or materially ambiguous requirements where planning must close gaps before execution"
25822
25929
  ],
25823
25930
  whenNotToUse: [
25824
- "single-Carrier tasks with \u22643 dependent steps and clear acceptance criteria (\u2192Admiral plans directly)",
25931
+ "single-Carrier tasks with \u22643 dependent steps and clear acceptance criteria (\u2192host agent plans directly)",
25825
25932
  "direct code implementation, single-shot (\u2192genesis) or plan-driven (\u2192ohio)",
25826
- "final architecture arbitration (\u2192nimitz) or sorties needing reconnaissance first (\u2192vanguard/tempest)"
25933
+ "final architecture arbitration (\u2192nimitz) or dispatches needing reconnaissance first (\u2192vanguard/tempest)"
25827
25934
  ],
25828
25935
  requestBlocks: [
25829
25936
  { tag: "goal", hint: "What the user wants to build, fix, or achieve \u2014 specific feature, PRD, behavior, and any stated constraints.", required: true },
25830
25937
  { tag: "plan_file", hint: "If provided, exact repo-relative .fleet/plans/{name}.md path Kirov must create or update. Do not choose a different filename.", required: false },
25831
- { tag: "context", hint: "Relevant codebase context \u2014 files, modules, patterns, prior Admiral direction, or implementation realities the planner should respect.", required: false },
25938
+ { tag: "context", hint: "Relevant codebase context \u2014 files, modules, patterns, prior host-agent direction, or implementation realities the planner should respect.", required: false },
25832
25939
  { tag: "constraints", hint: "Business rules, tech stack requirements, scope boundaries, fixed decisions, or explicit exclusions the plan must respect.", required: false },
25833
25940
  { tag: "intent_type", hint: "If known: Refactoring | Build from Scratch | Mid-sized | Collaborative | Architecture Follow-through | Research-to-Plan.", required: false }
25834
25941
  ],
@@ -25836,27 +25943,31 @@ var CARRIER_METADATA3 = {
25836
25943
  // ── Tier 2: Composition ──
25837
25944
  permissions: [
25838
25945
  "CRITICAL: Write access strictly limited to .fleet/plans/*.md and .fleet/drafts/*.md. NEVER modify source code, configs, or any non-markdown file.",
25839
- "MUST honor exact provided .fleet/plans/*.md paths. Success means creating or updating an executable plan_file unless the Admiral explicitly requests draft-only work.",
25840
- "MUST return unresolved architecture choices, system-design trade-offs, and technical path decisions to the Admiral for direction instead of silently deciding them.",
25841
- "MUST report Blockers or Admiral Direction Needed instead of claiming completion when the plan file cannot be written or the schema cannot be filled."
25946
+ "MUST honor exact provided .fleet/plans/*.md paths. Keep one plan_file as the execution SSoT: Success means creating or updating that executable plan_file unless the host agent explicitly requests draft-only work. Do not create split plan files for parallel lanes.",
25947
+ "MUST return unresolved architecture choices, system-design trade-offs, and technical path decisions to the host agent for direction instead of silently deciding them.",
25948
+ "MUST report Blockers or Host Direction Needed instead of claiming completion when the plan file cannot be written or the schema cannot be filled."
25842
25949
  ],
25843
25950
  outputFormat: `After completing the plan, provide a structured plan summary.
25844
25951
  [Required] always include:
25845
25952
  **Plan file** \u2014 Exact generated or updated .fleet/plans/{name}.md path.
25846
- **Execution Waves** \u2014 Ordered waves with parallel markers (e.g., "W1 \u2192 W2 || W3 \u2192 W4") and critical dependencies.
25953
+ **Execution Topology** \u2014 Ordered waves, stable Wave/Lane IDs, parallel markers (e.g., "W1 \u2192 W2 || W3 \u2192 W4"), and critical dependencies.
25954
+ **Dispatch Manifest** \u2014 Each lane's exact write set, start condition, eligible concurrent lanes, integration gate, handoff, and rollback unit.
25847
25955
  **Scope: IN** \u2014 What is explicitly included in the plan.
25848
25956
  **Scope: OUT** \u2014 What is explicitly excluded.
25849
25957
  **Next step** \u2014 Run \`/start-work {name}\` to execute the plan.
25850
25958
  [If applicable] omit if not relevant:
25851
25959
  **Blockers** \u2014 Why no plan file was written or why the schema cannot be filled.
25852
- **Admiral Direction Needed** \u2014 Architecture, trade-off, or path choices needing confirmation.
25960
+ **Host Direction Needed** \u2014 Architecture, trade-off, or path choices needing confirmation.
25853
25961
  Keep the summary concise \u2014 bullets and short lines only. No narrative paragraphs.`,
25854
25962
  principles: [
25855
25963
  CARRIER_JOBS_SELF_CALL_HINT,
25856
25964
  "Clarify only to unlock planning \u2014 ask the minimum questions needed to produce a reliable execution plan.",
25857
25965
  "Pre-plan gap analysis is mandatory internal input, never a substitute final output. May launch background explore/librarian sub-agents for context gathering. Use incremental write protocol: Write() skeleton first, then Edit() in 2-4 task batches.",
25858
- 'The .fleet/plans/*.md file MUST contain this exact default Markdown template unless the Admiral provides a different template: # Objective, # File Ownership, # Waves, ## Wave N \u2014 <name>, - Target files/modules:, - Dependencies:, - Parallelizable:, - Implementation summary:, - Verification/static checks:, - Escalation triggers:, # QA Gates, # Acceptance Criteria, # Documentation Updates, # Final Review Loop. Required headings must not be renamed, reordered, or omitted; extra sections allowed only after them. Write headings into the plan_file itself, not just mentioned in the response. For tiny tasks, mark non-applicable fields "Not applicable" rather than deleting them.',
25859
- "Return unresolved architecture and deep trade-off decisions to the Admiral for direction.",
25966
+ 'The .fleet/plans/*.md file MUST contain this exact default Markdown template unless the host agent provides a different template: # Objective, # File Ownership, # Execution Topology, - Execution mode: Sequential | Parallel, - Shared mutable resources:, # Waves, ## Wave N \u2014 <name>, ### Lane WN-X \u2014 <name>, - Exact write set:, - Read dependencies:, - Dependency/start condition:, - Eligible concurrent lanes: (use "none" for serialized work), - Integration gate:, - Handoff:, - Rollback unit:, - Implementation summary:, - Verification/static checks:, - Escalation triggers:, # Dispatch Manifest, - Full-plan Ohio invocation (execution_scope omitted or all): allowed sequentially only when Execution mode is Sequential or Execution Topology is absent; for Parallel, dispatch exact Lane IDs only and never combine a full-plan invocation with lane jobs, - Lane WN-X \u2014 <name>: exact write set, read dependencies, dependency/start condition, eligible concurrent lanes, integration gate, handoff, and rollback unit summary for dispatch, # QA Gates, # Acceptance Criteria, # Documentation Updates, # Final Review Loop. Required headings must not be renamed, reordered, or omitted; extra sections allowed only after them. Write headings into the plan_file itself, not just mentioned in the response. For tiny tasks, mark non-applicable fields "Not applicable" rather than deleting them.',
25967
+ "Execution Topology is mandatory for every plan. It MUST declare Execution mode: Sequential | Parallel, shared mutable resources, ordered waves, and stable Wave/Lane IDs; a lane may be marked parallel only when its exact non-overlapping write set and read dependencies prove it is safe to run concurrently.",
25968
+ "Dispatch Manifest is mandatory for every plan. For each parallel lane, declare: stable Wave/Lane ID; exact non-overlapping write set; read dependencies; dependency/start condition; eligible concurrent lanes; integration gate; handoff; and rollback unit. It MUST also state that full-plan Ohio invocation (execution_scope omitted or all) is allowed sequentially only for Sequential or absent Execution Topology; Parallel requires exact Lane IDs, makes full-plan invocation unavailable as an alternative dispatch path, and never combines it with lane jobs. If disjoint lanes cannot be proven safe, mark the work sequential rather than calling it parallel.",
25969
+ "Under each lane's '- Implementation summary:' (or the wave's when a wave declares no lanes), enumerate the concrete implementation steps as a Markdown checkbox task list \u2014 one '- [ ]' line per step, roughly 3-7 per lane; use '- [x]' only for work already completed at planning time. These checkboxes are the machine-readable progress contract consumed by the execution carrier and the Console Plans surface \u2014 do not encode task state in any other notation (no emoji, no status words, no strikethrough).",
25970
+ "Return unresolved architecture and deep trade-off decisions to the host agent for direction.",
25860
25971
  "Optimize for direct execution from the resulting plan_file."
25861
25972
  ]
25862
25973
  };
@@ -25876,8 +25987,8 @@ var NIMITZ_DEFAULTS = {
25876
25987
  };
25877
25988
  var CARRIER_METADATA4 = {
25878
25989
  // ── Tier 1: Routing ──
25879
- title: "Captain \xB7 Strategic Command & Judgment",
25880
- summary: "Read-only strategic command \u2014 decides the technical path through doctrinal judgment, architecture decisions, deep analysis, and trade-off adjudication. As the Captain (\uD568\uC7A5) of this Carrier, Nimitz commands strategic technical judgment from the flagship bridge without entering the implementation path.",
25990
+ title: "Strategic Command & Judgment",
25991
+ summary: "Read-only strategic command \u2014 decides the technical path through doctrinal judgment, architecture decisions, deep analysis, and trade-off adjudication without entering the implementation path.",
25881
25992
  category: "strategy",
25882
25993
  whenToUse: [
25883
25994
  "architecture and design decisions",
@@ -25900,7 +26011,7 @@ var CARRIER_METADATA4 = {
25900
26011
  // ── Tier 2: Composition ──
25901
26012
  permissions: [
25902
26013
  "CRITICAL: Strictly read-only. NEVER delegate code modification or file editing to this carrier.",
25903
- "CRITICAL: NEVER sortie Nimitz without prior reconnaissance \u2014 if recon is needed, sortie vanguard/tempest FIRST. Hard prerequisite, not a suggestion.",
26014
+ "CRITICAL: NEVER dispatch Nimitz without prior reconnaissance \u2014 if recon is needed, dispatch vanguard/tempest FIRST. Hard prerequisite, not a suggestion.",
25904
26015
  "Full access to read the codebase and execute read-only commands for analysis.",
25905
26016
  "MUST NOT decompose work into task waves, delivery schedules, or implementation checklists \u2014 handoff belongs to Kirov."
25906
26017
  ],
@@ -25941,8 +26052,8 @@ var OHIO_DEFAULTS = {
25941
26052
  };
25942
26053
  var CARRIER_METADATA5 = {
25943
26054
  // ── Tier 1: Routing ──
25944
- title: "Captain \xB7 Multi-Wave Strike Execution",
25945
- summary: "Receives a Kirov-authored plan_file and executes it wave-by-wave to completion \u2014 silent, patient, sequential delivery of plan steps. As the Captain (\uD568\uC7A5) of this Carrier, Ohio commands multi-wave strike execution and is the sole carrier authorised to consume plan_file inputs.",
26055
+ title: "Multi-Wave Execution",
26056
+ summary: "Receives a Kirov-authored plan_file and executes it wave-by-wave to completion, or one manifest-declared lane when explicitly scoped. Ohio is the sole carrier authorised to consume plan_file inputs.",
25946
26057
  category: "planning",
25947
26058
  whenToUse: [
25948
26059
  "multi-wave builds driven by an explicit plan_file",
@@ -25951,13 +26062,14 @@ var CARRIER_METADATA5 = {
25951
26062
  "cross-module coordinated changes following a Kirov plan"
25952
26063
  ],
25953
26064
  whenNotToUse: [
25954
- "single-file edits or admiral-direct single-shot tasks (\u2192genesis)",
26065
+ "single-file edits or host-directed single-shot tasks (\u2192genesis)",
25955
26066
  "architecture decisions (\u2192nimitz)",
25956
26067
  "planning work itself (\u2192kirov)",
25957
26068
  "reconnaissance before planning (\u2192vanguard/tempest)"
25958
26069
  ],
25959
26070
  requestBlocks: [
25960
26071
  { tag: "plan_file", hint: "Required repo-relative path to a Markdown plan file under .fleet/plans/*.md only. Ohio reads this file and follows it as the authoritative execution plan.", required: true },
26072
+ { tag: "execution_scope", hint: "Optional: for legacy plans without Execution Topology or plans marked Execution mode: Sequential, omitted or `all` executes the full plan sequentially. For Execution mode: Parallel, provide one exact Wave/Lane ID declared by the Dispatch Manifest; omitted or `all` is rejected. Never combine a full-plan invocation with scoped-lane Ohio invocation(s).", required: false },
25961
26073
  { tag: "objective", hint: "Optional brief restatement of the overarching goal for context anchoring.", required: false },
25962
26074
  { tag: "scope", hint: "Optional explicit scope boundaries if narrower than the plan_file's full coverage.", required: false },
25963
26075
  { tag: "constraints", hint: "Optional hard constraints, deadlines, or compatibility requirements that override or supplement the plan.", required: false }
@@ -25967,7 +26079,9 @@ var CARRIER_METADATA5 = {
25967
26079
  permissions: [
25968
26080
  "Full access to the codebase \u2014 read, write, and execute commands.",
25969
26081
  "MUST consult plan_file as the authoritative execution contract \u2014 plan steps are not optional or negotiable.",
25970
- "MUST treat the Admiral's <objective>, <scope>, and <constraints> as binding ALONGSIDE the plan_file. Even if a step or constraint seems suboptimal, MUST NOT substitute autonomous design judgment.",
26082
+ "MUST read the plan's Execution Topology before resolving execution_scope. For legacy plans without Execution Topology or plans marked Execution mode: Sequential, omitted scope or `all` executes the full plan sequentially. For Execution mode: Parallel, require one exact Dispatch Manifest Wave/Lane ID and reject omitted or `all` scope rather than silently serializing available parallelism. A full-plan invocation (omitted or `all`) MUST NEVER be used alongside scoped-lane Ohio invocation(s).",
26083
+ "When executing a scoped lane, it MUST be exactly one Wave/Lane ID declared by the plan's Dispatch Manifest. A scoped Ohio may change only that lane's declared write set; it MUST NOT edit plan_file, execute unassigned lanes, or guess an ambiguous scope. Before execution, it MUST satisfy the lane's dependency/start condition and required predecessor integration gates; its own QA/integration gate occurs after execution and MUST be satisfied before Ohio reports the lane eligible to release downstream work.",
26084
+ "MUST treat the host agent's <objective>, <scope>, and <constraints> as binding ALONGSIDE the plan_file. Even if a step or constraint seems suboptimal, MUST NOT substitute autonomous design judgment.",
25971
26085
  "MUST NOT silently re-plan, skip steps, invent new workflow paths, or expand scope beyond what the plan_file specifies.",
25972
26086
  "On genuine blockers (ambiguous step, missing dependency, environmental failure), MUST report back and request re-direction instead of fabricating workarounds."
25973
26087
  ],
@@ -25975,13 +26089,17 @@ var CARRIER_METADATA5 = {
25975
26089
  CARRIER_JOBS_SELF_CALL_HINT,
25976
26090
  "Read plan_file as the binding execution contract \u2014 do not deviate, re-plan, or skip steps.",
25977
26091
  "Accept only repo-relative Markdown plan paths under .fleet/plans/*.md. If the path is missing, unreadable, outside .fleet/plans/, not repo-relative, or not a .md file, do not guess, do not silently re-plan, and do not invent a replacement workflow \u2014 report the problem back and ask for re-direction.",
26092
+ "Read Execution Topology before resolving execution_scope. For legacy plans without Execution Topology or Execution mode: Sequential, omitted scope or `all` retains full-plan sequential compatibility. For Execution mode: Parallel, require one exact manifest-declared Wave/Lane ID and reject omitted or `all` scope rather than silently serializing available parallelism. Never use a full-plan invocation alongside scoped-lane Ohio invocation(s).",
26093
+ "For a lane scope, change only that lane's declared write set. Never edit plan_file or execute another lane. Before execution, satisfy the lane's dependency/start condition and required predecessor integration gates; after execution, satisfy that lane's own QA/integration gate before reporting it eligible to release downstream work.",
25978
26094
  "Execute waves in the declared order; preserve QA checkpoints between waves and do not collapse them.",
25979
- "If a wave or constraint should be redesigned, MUST escalate to the Admiral via the completion report instead of silently altering the wave's intent.",
26095
+ "Progress write-back applies to full-plan scope only (execution_scope omitted or `all`): immediately after a wave's QA checkpoint passes, update the plan_file by flipping that wave's completed task checkboxes from '- [ ]' to '- [x]'. This is a state-marker edit only \u2014 never reword, reorder, add, or delete any other plan content while doing it. Flip only steps actually completed; if a completed step has no matching checkbox, leave the text untouched and record the mismatch under Deviations in the completion report. A scoped-lane Ohio never edits plan_file (lane jobs may run concurrently) \u2014 instead it lists the lane's completed task checkboxes in its completion report so the integrator flips them after the lane's integration gate.",
26096
+ "If a wave or constraint should be redesigned, MUST escalate to the host agent via the completion report instead of silently altering the wave's intent.",
25980
26097
  "Escalate genuine blockers (ambiguous step, missing dependency, environmental failure) instead of fabricating workarounds.",
25981
26098
  "Do not absorb planning, architecture, or QA roles \u2014 if the plan demands a decision Ohio cannot make, escalate to the appropriate carrier (Kirov/Nimitz/Sentinel)."
25982
26099
  ],
25983
26100
  outputFormat: `After completing the assigned wave(s), provide a structured wave-completion report.
25984
26101
  [Required] always include:
26102
+ **Execution scope** \u2014 \`all\` or the exact Wave/Lane ID executed from the plan's Dispatch Manifest.
25985
26103
  **Wave(s) executed** \u2014 Ordered list of wave/step IDs from the plan_file actually completed.
25986
26104
  **Changes** \u2014 Every file created/modified/deleted with a 1-line summary each.
25987
26105
  **QA results** \u2014 Outcome of each wave's QA checkpoint (pass/fail with detail).
@@ -26007,8 +26125,8 @@ var SENTINEL_DEFAULTS = {
26007
26125
  };
26008
26126
  var CARRIER_METADATA6 = {
26009
26127
  // ── Tier 1: Routing ──
26010
- title: "Captain \xB7 The Inquisitor / QA & Security Lead",
26011
- summary: "Bug hunter and security specialist \u2014 code review, defect detection, quality audits, vulnerability hunting, and penetration testing with ruthless precision. As the Captain (\uD568\uC7A5) of this Carrier, Sentinel commands the fleet's quality and security inspections.",
26128
+ title: "QA & Security Lead",
26129
+ summary: "Bug hunter and security specialist \u2014 code review, defect detection, quality audits, vulnerability hunting, and penetration testing with ruthless precision.",
26012
26130
  category: "strategy",
26013
26131
  whenToUse: [
26014
26132
  "code review, bug hunting, quality audits, test execution",
@@ -26064,8 +26182,8 @@ var TEMPEST_DEFAULTS = {
26064
26182
  };
26065
26183
  var CARRIER_METADATA7 = {
26066
26184
  // ── Tier 1: Routing ──
26067
- title: "Captain \xB7 External Intelligence Strike",
26068
- summary: "GitHub intelligence \u2014 external repo investigation via API and clone-based deep analysis. As the Captain (\uD568\uC7A5) of this Carrier, Tempest sprints across foreign waters to collect remote intelligence from upstream repositories and external codebases.",
26185
+ title: "External Intelligence",
26186
+ summary: "GitHub intelligence \u2014 external repo investigation via API and clone-based deep analysis of upstream repositories and unfamiliar codebases.",
26069
26187
  category: "operations",
26070
26188
  whenToUse: [
26071
26189
  "external GitHub repo investigation (library internals, upstream dependencies)",
@@ -26120,8 +26238,8 @@ var VANGUARD_DEFAULTS = {
26120
26238
  };
26121
26239
  var CARRIER_METADATA8 = {
26122
26240
  // ── Tier 1: Routing ──
26123
- title: "Captain \xB7 Scout Specialist",
26124
- summary: "Fast reconnaissance \u2014 codebase exploration, symbol tracing, web research. As the Captain (\uD568\uC7A5) of this Carrier, Vanguard leads forward scouting and fast intelligence gathering before heavier operations begin.",
26241
+ title: "Reconnaissance Specialist",
26242
+ summary: "Fast reconnaissance \u2014 codebase exploration, symbol tracing, web research, and external knowledge gathering.",
26125
26243
  category: "operations",
26126
26244
  whenToUse: [
26127
26245
  "codebase reconnaissance \u2014 exploration, multi-file scanning, symbol tracing",
@@ -31940,7 +32058,7 @@ function resolveDispatchCwd(rawCwd, fallbackCwd) {
31940
32058
  if (rawCwd === void 0) return { ok: true, cwd: fallbackCwd };
31941
32059
  const trimmed = rawCwd.trim();
31942
32060
  if (trimmed.length === 0) return { ok: true, cwd: fallbackCwd };
31943
- if (!path25__default.isAbsolute(trimmed)) {
32061
+ if (!path27__default.isAbsolute(trimmed)) {
31944
32062
  return {
31945
32063
  ok: false,
31946
32064
  error: `Invalid cwd "${rawCwd}": must be an absolute path. Provide an absolute directory or omit cwd to use the host session cwd.`
@@ -32415,10 +32533,103 @@ function requireCarrierConfig(registry3, carrierId) {
32415
32533
  if (!config2) throw new Error(`Carrier not found: ${carrierId}`);
32416
32534
  return config2;
32417
32535
  }
32418
-
32419
- // core/host/global-settings-routes.ts
32536
+ var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
32537
+ var UI_FONT_SIZE_RANGE = { min: 12, max: 18};
32538
+ var DEFAULT_UI_FONT_SETTINGS = { source: "builtin", id: "manrope", size: 14 };
32539
+ var SETTINGS_VERSION = 1;
32540
+ var SETTINGS_LOCK_DIR_NAME = "settings.lock";
32541
+ var SETTINGS_LOCK_OWNER_FILE_NAME = "owner.json";
32542
+ var SETTINGS_TEMP_PREFIX = ".settings.";
32420
32543
  var MIN_CONSOLE_STATIC_PORT = 1024;
32421
32544
  var MAX_CONSOLE_STATIC_PORT = 65535;
32545
+ function createConsoleSettingsStore(deps = {}) {
32546
+ const paths = deps.paths ?? createConsoleDataPaths();
32547
+ const createStore = deps.createStore ?? createDurableJsonStore;
32548
+ return createStore({
32549
+ filePath: paths.settingsFile,
32550
+ lockDir: path27__default.join(paths.dir, SETTINGS_LOCK_DIR_NAME),
32551
+ lockOwnerFileName: SETTINGS_LOCK_OWNER_FILE_NAME,
32552
+ now: deps.now,
32553
+ sanitize: sanitizeConsoleSettingsData,
32554
+ sensitivity: "sensitive",
32555
+ tempCleanupPrefix: SETTINGS_TEMP_PREFIX
32556
+ });
32557
+ }
32558
+ function sanitizeConsoleSettingsData(value) {
32559
+ if (!isRecord4(value)) return emptyConsoleSettingsData();
32560
+ if (value.version !== SETTINGS_VERSION) return emptyConsoleSettingsData();
32561
+ const general = readConsoleGeneralSettings(value.general);
32562
+ const plugins = readConsolePluginSettings(value.plugins);
32563
+ return {
32564
+ version: SETTINGS_VERSION,
32565
+ general: general ?? {},
32566
+ plugins: plugins ?? {}
32567
+ };
32568
+ }
32569
+ function emptyConsoleSettingsData() {
32570
+ return { version: 1, general: {}, plugins: {} };
32571
+ }
32572
+ function sanitizeUiFontSettings(value) {
32573
+ if (isConsoleUiFontId(value)) return { source: "builtin", id: value, size: DEFAULT_UI_FONT_SETTINGS.size };
32574
+ if (!isRecord4(value) || !isValidUiFontSize(value.size)) return void 0;
32575
+ if (value.source === "builtin" && isConsoleUiFontId(value.id)) {
32576
+ return { source: "builtin", id: value.id, size: value.size };
32577
+ }
32578
+ if (value.source === "system" && typeof value.familyName === "string") {
32579
+ const familyName = sanitizeSystemFontFamily(value.familyName);
32580
+ return familyName ? { source: "system", familyName, size: value.size } : void 0;
32581
+ }
32582
+ return void 0;
32583
+ }
32584
+ function isUiFontSettings(value) {
32585
+ if (!isRecord4(value) || !isValidUiFontSize(value.size)) return false;
32586
+ if (value.source === "builtin") return isConsoleUiFontId(value.id);
32587
+ return value.source === "system" && typeof value.familyName === "string" && value.familyName.length > 0 && value.familyName === sanitizeSystemFontFamily(value.familyName);
32588
+ }
32589
+ function readConsoleGeneralSettings(value) {
32590
+ if (!isRecord4(value)) return null;
32591
+ const consolePortMode = value.consolePortMode === "dynamic" || value.consolePortMode === "static" ? value.consolePortMode : void 0;
32592
+ const consoleStaticPort = isValidConsoleStaticPort(value.consoleStaticPort) ? value.consoleStaticPort : void 0;
32593
+ const language = value.language === "auto" || value.language === "en" || value.language === "ko" ? value.language : void 0;
32594
+ const theme = value.theme === "instrument" || value.theme === "maritime" || value.theme === "carbon" ? value.theme : void 0;
32595
+ const uiFont = sanitizeUiFontSettings(value.uiFont);
32596
+ return {
32597
+ ...consolePortMode !== void 0 ? { consolePortMode } : {},
32598
+ ...consoleStaticPort !== void 0 ? { consoleStaticPort } : {},
32599
+ ...language !== void 0 ? { language } : {},
32600
+ ...theme !== void 0 ? { theme } : {},
32601
+ ...uiFont !== void 0 ? { uiFont } : {}
32602
+ };
32603
+ }
32604
+ function readConsolePluginSettings(value) {
32605
+ if (!isRecord4(value)) return null;
32606
+ const result = {};
32607
+ for (const [key, entry] of Object.entries(value)) {
32608
+ if (!PLUGIN_ID_PATTERN.test(key)) continue;
32609
+ if (!isRecord4(entry)) continue;
32610
+ result[key] = entry;
32611
+ }
32612
+ return result;
32613
+ }
32614
+ function isRecord4(value) {
32615
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32616
+ }
32617
+ function isValidConsoleStaticPort(value) {
32618
+ return typeof value === "number" && Number.isInteger(value) && value >= MIN_CONSOLE_STATIC_PORT && value <= MAX_CONSOLE_STATIC_PORT;
32619
+ }
32620
+ function isConsoleUiFontId(value) {
32621
+ return value === "manrope" || value === "jetbrains-mono" || value === "source-code-pro";
32622
+ }
32623
+ function isValidUiFontSize(value) {
32624
+ return typeof value === "number" && Number.isInteger(value) && value >= UI_FONT_SIZE_RANGE.min && value <= UI_FONT_SIZE_RANGE.max;
32625
+ }
32626
+ function sanitizeSystemFontFamily(value) {
32627
+ return value.replace(/[\x00-\x1F\x7F]/g, "").trim().slice(0, 128);
32628
+ }
32629
+
32630
+ // core/host/global-settings-routes.ts
32631
+ var MIN_CONSOLE_STATIC_PORT2 = 1024;
32632
+ var MAX_CONSOLE_STATIC_PORT2 = 65535;
32422
32633
  var GLOBAL_SETTINGS_API_CATALOG = [
32423
32634
  {
32424
32635
  method: "GET",
@@ -32474,22 +32685,34 @@ async function mutateGlobalSettings(req, res, deps) {
32474
32685
  deps.writeJson(res, 400, { error: "invalid_console_port_mode" });
32475
32686
  return;
32476
32687
  }
32477
- if (body.consoleStaticPort !== void 0 && body.consoleStaticPort !== null && !isValidConsoleStaticPort(body.consoleStaticPort)) {
32688
+ if (body.consoleStaticPort !== void 0 && body.consoleStaticPort !== null && !isValidConsoleStaticPort2(body.consoleStaticPort)) {
32478
32689
  deps.writeJson(res, 400, { error: "invalid_console_static_port" });
32479
32690
  return;
32480
32691
  }
32481
- if (body.theme !== void 0 && body.theme !== "maritime" && body.theme !== "carbon") {
32692
+ if (body.language !== void 0 && body.language !== "auto" && body.language !== "en" && body.language !== "ko") {
32693
+ deps.writeJson(res, 400, { error: "invalid_language" });
32694
+ return;
32695
+ }
32696
+ if (body.theme !== void 0 && body.theme !== "instrument" && body.theme !== "maritime" && body.theme !== "carbon") {
32482
32697
  deps.writeJson(res, 400, { error: "invalid_theme" });
32483
32698
  return;
32484
32699
  }
32700
+ if (!isUiFontSettingsOrUndefined(body.uiFont)) {
32701
+ deps.writeJson(res, 400, { error: "invalid_ui_font" });
32702
+ return;
32703
+ }
32485
32704
  const updated = deps.consoleSettingsStore.update((current) => ({
32705
+ ...current,
32486
32706
  version: 1,
32487
32707
  general: {
32488
32708
  ...current.general,
32489
32709
  ...body.consolePortMode === "dynamic" || body.consolePortMode === "static" ? { consolePortMode: body.consolePortMode } : {},
32490
- ...isValidConsoleStaticPort(body.consoleStaticPort) ? { consoleStaticPort: body.consoleStaticPort } : {},
32491
- ...body.theme === "maritime" || body.theme === "carbon" ? { theme: body.theme } : {}
32492
- }
32710
+ ...isValidConsoleStaticPort2(body.consoleStaticPort) ? { consoleStaticPort: body.consoleStaticPort } : {},
32711
+ ...body.language === "auto" || body.language === "en" || body.language === "ko" ? { language: body.language } : {},
32712
+ ...body.theme === "instrument" || body.theme === "maritime" || body.theme === "carbon" ? { theme: body.theme } : {},
32713
+ ...isUiFontSettings(body.uiFont) ? { uiFont: body.uiFont } : {}
32714
+ },
32715
+ plugins: current.plugins
32493
32716
  }));
32494
32717
  const response = { state: toGlobalSettingsState(updated) };
32495
32718
  deps.writeJson(res, 200, response);
@@ -32499,77 +32722,21 @@ function toGlobalSettingsState(data) {
32499
32722
  return {
32500
32723
  consolePortMode: general.consolePortMode ?? "dynamic",
32501
32724
  consoleStaticPort: general.consoleStaticPort ?? null,
32502
- theme: general.theme ?? "maritime"
32725
+ language: general.language ?? "auto",
32726
+ theme: general.theme ?? "instrument",
32727
+ uiFont: general.uiFont ?? DEFAULT_UI_FONT_SETTINGS
32503
32728
  };
32504
32729
  }
32505
32730
  function isJsonRequest2(req) {
32506
32731
  const contentType = req.headers["content-type"];
32507
32732
  return typeof contentType === "string" && contentType.toLowerCase().split(";")[0]?.trim() === "application/json";
32508
32733
  }
32509
- function isValidConsoleStaticPort(value) {
32510
- return typeof value === "number" && Number.isInteger(value) && value >= MIN_CONSOLE_STATIC_PORT && value <= MAX_CONSOLE_STATIC_PORT;
32511
- }
32512
- var PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
32513
- var SETTINGS_VERSION = 1;
32514
- var SETTINGS_LOCK_DIR_NAME = "settings.lock";
32515
- var SETTINGS_LOCK_OWNER_FILE_NAME = "owner.json";
32516
- var SETTINGS_TEMP_PREFIX = ".settings.";
32517
- var MIN_CONSOLE_STATIC_PORT2 = 1024;
32518
- var MAX_CONSOLE_STATIC_PORT2 = 65535;
32519
- function createConsoleSettingsStore(deps = {}) {
32520
- const paths = deps.paths ?? createConsoleDataPaths();
32521
- const createStore = deps.createStore ?? createDurableJsonStore;
32522
- return createStore({
32523
- filePath: paths.settingsFile,
32524
- lockDir: path25__default.join(paths.dir, SETTINGS_LOCK_DIR_NAME),
32525
- lockOwnerFileName: SETTINGS_LOCK_OWNER_FILE_NAME,
32526
- now: deps.now,
32527
- sanitize: sanitizeConsoleSettingsData,
32528
- sensitivity: "sensitive",
32529
- tempCleanupPrefix: SETTINGS_TEMP_PREFIX
32530
- });
32531
- }
32532
- function sanitizeConsoleSettingsData(value) {
32533
- if (!isRecord4(value)) return emptyConsoleSettingsData();
32534
- if (value.version !== SETTINGS_VERSION) return emptyConsoleSettingsData();
32535
- const general = readConsoleGeneralSettings(value.general);
32536
- const plugins = readConsolePluginSettings(value.plugins);
32537
- return {
32538
- version: SETTINGS_VERSION,
32539
- general: general ?? {},
32540
- plugins: plugins ?? {}
32541
- };
32542
- }
32543
- function emptyConsoleSettingsData() {
32544
- return { version: 1, general: {}, plugins: {} };
32545
- }
32546
- function readConsoleGeneralSettings(value) {
32547
- if (!isRecord4(value)) return null;
32548
- const consolePortMode = value.consolePortMode === "dynamic" || value.consolePortMode === "static" ? value.consolePortMode : void 0;
32549
- const consoleStaticPort = isValidConsoleStaticPort2(value.consoleStaticPort) ? value.consoleStaticPort : void 0;
32550
- const theme = value.theme === "maritime" || value.theme === "carbon" ? value.theme : void 0;
32551
- return {
32552
- ...consolePortMode !== void 0 ? { consolePortMode } : {},
32553
- ...consoleStaticPort !== void 0 ? { consoleStaticPort } : {},
32554
- ...theme !== void 0 ? { theme } : {}
32555
- };
32556
- }
32557
- function readConsolePluginSettings(value) {
32558
- if (!isRecord4(value)) return null;
32559
- const result = {};
32560
- for (const [key, entry] of Object.entries(value)) {
32561
- if (!PLUGIN_ID_PATTERN.test(key)) continue;
32562
- if (!isRecord4(entry)) continue;
32563
- result[key] = entry;
32564
- }
32565
- return result;
32566
- }
32567
- function isRecord4(value) {
32568
- return typeof value === "object" && value !== null && !Array.isArray(value);
32569
- }
32570
32734
  function isValidConsoleStaticPort2(value) {
32571
32735
  return typeof value === "number" && Number.isInteger(value) && value >= MIN_CONSOLE_STATIC_PORT2 && value <= MAX_CONSOLE_STATIC_PORT2;
32572
32736
  }
32737
+ function isUiFontSettingsOrUndefined(value) {
32738
+ return value === void 0 || isUiFontSettings(value);
32739
+ }
32573
32740
 
32574
32741
  // core/host/plugin-settings-routes.ts
32575
32742
  var PLUGIN_SETTINGS_PREFIX = "/api/v1/settings/plugins/";
@@ -32650,6 +32817,133 @@ function isJsonRequest3(req) {
32650
32817
  const contentType = req.headers["content-type"];
32651
32818
  return typeof contentType === "string" && contentType.toLowerCase().split(";")[0]?.trim() === "application/json";
32652
32819
  }
32820
+ var MAX_FAMILY_LENGTH = 128;
32821
+ var SUCCESS_TTL_MS = 5 * 60 * 1e3;
32822
+ var FAILURE_TTL_MS = 30 * 1e3;
32823
+ var CONTROL_CHARACTER_PATTERN = /[\x00-\x1F\x7F]/g;
32824
+ var NORMAL_FACE_MARKERS = ["normal", "regular", "roman", "book"];
32825
+ var TEXT_FAMILY_ALLOWLIST = /* @__PURE__ */ new Set([
32826
+ "arial",
32827
+ "arial nova",
32828
+ "avenir",
32829
+ "avenir next",
32830
+ "calibri",
32831
+ "candara",
32832
+ "helvetica",
32833
+ "helvetica neue",
32834
+ "inter",
32835
+ "manrope",
32836
+ "noto sans",
32837
+ "noto serif",
32838
+ "segoe ui",
32839
+ "sf pro text",
32840
+ "system ui",
32841
+ "times new roman",
32842
+ "verdana",
32843
+ "apple sd gothic neo",
32844
+ "hiragino sans",
32845
+ "hiragino kaku gothic pro",
32846
+ "malgun gothic",
32847
+ "meiryo",
32848
+ "microsoft yahei",
32849
+ "noto sans cjk",
32850
+ "noto serif cjk",
32851
+ "pingfang sc",
32852
+ "pingfang tc",
32853
+ "yu gothic"
32854
+ ]);
32855
+ var TEXT_FAMILY_MARKERS = ["sans", "serif", "text", "grotesk", "gothic", "roman", "book", "humanist"];
32856
+ var DENY_FAMILY_MARKERS = ["hidden", "vertical", "symbol", "icon", "emoji", "dingbat", "ornament", "music", "math", "display", "decorative"];
32857
+ var EMPTY_SYSTEM_FONTS_ERROR = new Error("system font enumeration returned no usable families");
32858
+ function createSystemFontsService(deps = {}) {
32859
+ const loadFonts = deps.loadFonts ?? (() => fontList.getFonts2({ disableQuoting: true }));
32860
+ const now = deps.now ?? Date.now;
32861
+ const successTtlMs = deps.successTtlMs ?? SUCCESS_TTL_MS;
32862
+ const failureTtlMs = deps.failureTtlMs ?? FAILURE_TTL_MS;
32863
+ let cached2 = null;
32864
+ let cachedFailure = null;
32865
+ let inFlight = null;
32866
+ const getFonts = () => {
32867
+ if (cached2 && now() - cached2.cachedAt < successTtlMs) return Promise.resolve(cached2.fonts);
32868
+ if (cachedFailure && now() - cachedFailure.cachedAt < failureTtlMs) return Promise.reject(cachedFailure.error);
32869
+ if (inFlight) return inFlight;
32870
+ inFlight = loadFonts().then((fonts) => {
32871
+ const normalized = normalizeSystemFonts(fonts);
32872
+ if (normalized.length === 0) throw EMPTY_SYSTEM_FONTS_ERROR;
32873
+ cached2 = { fonts: normalized, cachedAt: now() };
32874
+ cachedFailure = null;
32875
+ return normalized;
32876
+ }).catch((error51) => {
32877
+ cachedFailure = { error: error51, cachedAt: now() };
32878
+ throw error51;
32879
+ }).finally(() => {
32880
+ inFlight = null;
32881
+ });
32882
+ return inFlight;
32883
+ };
32884
+ return { getFonts };
32885
+ }
32886
+ function normalizeSystemFonts(fonts) {
32887
+ const groups = /* @__PURE__ */ new Map();
32888
+ for (const font of fonts) {
32889
+ if (!isFontInfo(font)) continue;
32890
+ const family = sanitizeFamilyName(font.familyName);
32891
+ if (!family) continue;
32892
+ const key = family.toLocaleLowerCase();
32893
+ const group = groups.get(key) ?? { family, faces: [] };
32894
+ group.faces.push(font);
32895
+ groups.set(key, group);
32896
+ }
32897
+ return [...groups.values()].map(toSystemFontRecord).sort((left, right) => left.family.localeCompare(right.family, void 0, { sensitivity: "base" }) || left.family.localeCompare(right.family));
32898
+ }
32899
+ function buildSystemFontsResponse(fonts) {
32900
+ return { version: 1, fonts };
32901
+ }
32902
+ function isFontInfo(value) {
32903
+ return typeof value === "object" && value !== null && typeof value.familyName === "string" && typeof value.monospace === "boolean" && typeof value.style === "string";
32904
+ }
32905
+ function sanitizeFamilyName(value) {
32906
+ return value.replace(CONTROL_CHARACTER_PATTERN, "").trim().slice(0, MAX_FAMILY_LENGTH);
32907
+ }
32908
+ function toSystemFontRecord(group) {
32909
+ const normalizedFamily = group.family.toLocaleLowerCase();
32910
+ const monospace = group.faces.length > 0 && group.faces.every((face) => face.monospace);
32911
+ const hasNormalNonMonospaceFace = group.faces.some((face) => !face.monospace && isNormalFace(face));
32912
+ const denied = DENY_FAMILY_MARKERS.some((marker) => normalizedFamily.includes(marker));
32913
+ const textFamily = TEXT_FAMILY_ALLOWLIST.has(normalizedFamily) || TEXT_FAMILY_MARKERS.some((marker) => normalizedFamily.includes(marker));
32914
+ return { family: group.family, monospace, uiSuitable: !denied && hasNormalNonMonospaceFace && textFamily };
32915
+ }
32916
+ function isNormalFace(face) {
32917
+ const style = face.style.toLocaleLowerCase();
32918
+ return NORMAL_FACE_MARKERS.some((marker) => style.includes(marker));
32919
+ }
32920
+
32921
+ // core/host/system-fonts-routes.ts
32922
+ var SYSTEM_FONTS_PATH = "/api/v1/settings/fonts/system";
32923
+ var SYSTEM_FONTS_API_CATALOG = [
32924
+ {
32925
+ method: "GET",
32926
+ path: SYSTEM_FONTS_PATH,
32927
+ summary: "List sanitized system font families for built-in settings.",
32928
+ category: "Settings",
32929
+ gate: "loopback"
32930
+ }
32931
+ ];
32932
+ function createSystemFontsRouter(deps) {
32933
+ return async function handleSystemFontsRoute(context) {
32934
+ if (context.pathname !== SYSTEM_FONTS_PATH) return false;
32935
+ if (context.req.method !== "GET") {
32936
+ deps.writeJson(context.res, 405, { error: "Method not allowed" });
32937
+ return true;
32938
+ }
32939
+ try {
32940
+ deps.writeJson(context.res, 200, buildSystemFontsResponse(await deps.systemFonts.getFonts()));
32941
+ } catch {
32942
+ deps.writeJson(context.res, 503, { error: "system_fonts_unavailable" });
32943
+ }
32944
+ return true;
32945
+ };
32946
+ }
32653
32947
 
32654
32948
  // core/host/api-catalog.ts
32655
32949
  var compareApiCatalogEntries = (left, right) => left.category.localeCompare(right.category) || left.path.localeCompare(right.path) || left.method.localeCompare(right.method);
@@ -32658,9 +32952,115 @@ function buildApiCatalog() {
32658
32952
  ...SERVER_API_CATALOG,
32659
32953
  ...CARRIER_SETTINGS_API_CATALOG,
32660
32954
  ...GLOBAL_SETTINGS_API_CATALOG,
32661
- ...PLUGIN_SETTINGS_API_CATALOG
32955
+ ...PLUGIN_SETTINGS_API_CATALOG,
32956
+ ...SYSTEM_FONTS_API_CATALOG
32662
32957
  ].slice().sort(compareApiCatalogEntries);
32663
32958
  }
32959
+ var TheaterPathContextError = class extends Error {
32960
+ code;
32961
+ constructor(code) {
32962
+ super(code);
32963
+ this.code = code;
32964
+ }
32965
+ };
32966
+ var DIRECTORY_ENTRY_CAP = 500;
32967
+ async function resolveTheaterPathContext(theaterRoot, relPath) {
32968
+ if (relPath !== null && !isSafeRelativePath(relPath)) throw new TheaterPathContextError("invalid_path");
32969
+ const nominalRoot = path27__default.resolve(theaterRoot);
32970
+ const nominalTarget = relPath === null ? nominalRoot : path27__default.resolve(nominalRoot, ...relPath.split("/"));
32971
+ if (!isWithinRoot(nominalTarget, nominalRoot)) throw new TheaterPathContextError("forbidden");
32972
+ let realRoot;
32973
+ let realPath;
32974
+ try {
32975
+ [realRoot, realPath] = await Promise.all([fs14__default.promises.realpath(nominalRoot), fs14__default.promises.realpath(nominalTarget)]);
32976
+ } catch (error51) {
32977
+ throw mapFsError(error51);
32978
+ }
32979
+ if (!isWithinRoot(realPath, realRoot)) throw new TheaterPathContextError("forbidden");
32980
+ let stat5;
32981
+ try {
32982
+ stat5 = await fs14__default.promises.stat(realPath);
32983
+ } catch (error51) {
32984
+ throw mapFsError(error51);
32985
+ }
32986
+ if (!stat5.isDirectory()) throw new TheaterPathContextError("invalid_path");
32987
+ const canonicalRel = path27__default.relative(realRoot, realPath).split(path27__default.sep).join("/");
32988
+ return { realRoot, realPath, relPath: canonicalRel === "" ? null : canonicalRel, label: path27__default.basename(realPath) || path27__default.basename(realRoot) || "Theater" };
32989
+ }
32990
+ async function listTheaterDirectories(theaterRoot, parentRelPath) {
32991
+ const parent = await resolveTheaterPathContext(theaterRoot, parentRelPath);
32992
+ let entries;
32993
+ try {
32994
+ entries = await fs14__default.promises.readdir(parent.realPath, { withFileTypes: true });
32995
+ } catch (error51) {
32996
+ throw mapFsError(error51);
32997
+ }
32998
+ const result = [];
32999
+ for (const entry of entries) {
33000
+ if (result.length >= DIRECTORY_ENTRY_CAP) break;
33001
+ const candidate = path27__default.join(parent.realPath, entry.name);
33002
+ try {
33003
+ const realCandidate = await fs14__default.promises.realpath(candidate);
33004
+ if (!isWithinRoot(realCandidate, parent.realRoot)) continue;
33005
+ const stat5 = await fs14__default.promises.stat(realCandidate);
33006
+ if (!stat5.isDirectory()) continue;
33007
+ const relPath = path27__default.relative(parent.realRoot, realCandidate).split(path27__default.sep).join("/");
33008
+ if (relPath) result.push({ relPath, label: path27__default.basename(realCandidate) });
33009
+ } catch {
33010
+ }
33011
+ }
33012
+ return result.sort((left, right) => left.label.localeCompare(right.label));
33013
+ }
33014
+ function isSafeRelativePath(value) {
33015
+ return value.length > 0 && value.length <= 4096 && !value.includes("\0") && !value.includes("\\") && !value.startsWith("/") && !/^[A-Za-z]:/.test(value) && !value.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
33016
+ }
33017
+ function isWithinRoot(candidate, root) {
33018
+ const normalizedRoot = root.endsWith(path27__default.sep) ? root : `${root}${path27__default.sep}`;
33019
+ return candidate === root || candidate.startsWith(normalizedRoot);
33020
+ }
33021
+ function mapFsError(error51) {
33022
+ return error51.code === "ENOENT" ? new TheaterPathContextError("not_found") : new TheaterPathContextError("invalid_path");
33023
+ }
33024
+
33025
+ // core/host/codex/context-routes.ts
33026
+ function createCodexWorkspaceContextRouter(deps) {
33027
+ return async function handleCodexWorkspaceContextRoute({ req, res, pathname }) {
33028
+ const match = pathname.match(/^\/api\/v1\/theaters\/([^/]+)\/codex-workspace$/u);
33029
+ if (!match) return false;
33030
+ const theater = deps.getTheater(decodeURIComponent(match[1] ?? ""));
33031
+ if (!theater) {
33032
+ deps.writeJson(res, 400, { error: "invalid_theater" });
33033
+ return true;
33034
+ }
33035
+ if (req.method !== "POST") {
33036
+ deps.writeJson(res, 405, { error: "method_not_allowed" });
33037
+ return true;
33038
+ }
33039
+ if (!deps.isAuthorized(req)) {
33040
+ deps.writeJson(res, 401, { error: "unauthorized" });
33041
+ return true;
33042
+ }
33043
+ const body = await deps.readJsonBody(req);
33044
+ if (!isResolveWorkspaceBody(body)) {
33045
+ deps.writeJson(res, 400, { error: "invalid_request" });
33046
+ return true;
33047
+ }
33048
+ try {
33049
+ deps.writeJson(res, 200, await deps.resolveWorkspace(theater.id, theater.realpath, body.relPath));
33050
+ } catch (error51) {
33051
+ writePathError(res, deps, error51);
33052
+ }
33053
+ return true;
33054
+ };
33055
+ }
33056
+ function isResolveWorkspaceBody(value) {
33057
+ return typeof value === "object" && value !== null && Object.keys(value).length === 1 && (value.relPath === null || typeof value.relPath === "string");
33058
+ }
33059
+ function writePathError(res, deps, error51) {
33060
+ if (!(error51 instanceof TheaterPathContextError)) throw error51;
33061
+ const status = error51.code === "not_found" ? 404 : error51.code === "forbidden" ? 403 : 400;
33062
+ deps.writeJson(res, status, { error: error51.code });
33063
+ }
32664
33064
 
32665
33065
  // ../../packages/fleet-wiki/src/boundaries.ts
32666
33066
  var FLEET_WIKI_ENTRY_BEGIN = "<<<FLEET_WIKI_ENTRY_BEGIN";
@@ -33068,18 +33468,18 @@ Always read \`schema/wiki-schema.md\` directly before composing or revising entr
33068
33468
  `;
33069
33469
  async function ensureWorkspaceSchema(paths) {
33070
33470
  await mkdir(paths.schemaDir, { recursive: true });
33071
- await writeDefaultFileIfMissing(path25__default.join(paths.schemaDir, WORKSPACE_SCHEMA_AGENTS_FILENAME), DEFAULT_WORKSPACE_SCHEMA_AGENTS);
33072
- await writeDefaultFileIfMissing(path25__default.join(paths.schemaDir, WORKSPACE_SCHEMA_FILENAME), DEFAULT_WORKSPACE_WIKI_SCHEMA);
33471
+ await writeDefaultFileIfMissing(path27__default.join(paths.schemaDir, WORKSPACE_SCHEMA_AGENTS_FILENAME), DEFAULT_WORKSPACE_SCHEMA_AGENTS);
33472
+ await writeDefaultFileIfMissing(path27__default.join(paths.schemaDir, WORKSPACE_SCHEMA_FILENAME), DEFAULT_WORKSPACE_WIKI_SCHEMA);
33073
33473
  await writeDefaultFileIfMissing(buildTemplatePath(paths, "prd"), DEFAULT_TEMPLATE_PRD);
33074
33474
  return readWorkspaceSchemaSummary(paths);
33075
33475
  }
33076
33476
  async function ensureWorkspaceDoctrine(paths) {
33077
33477
  await mkdir(paths.root, { recursive: true });
33078
- await writeDefaultFileIfMissing(path25__default.join(paths.root, WORKSPACE_KNOWLEDGE_AGENTS_FILENAME), DEFAULT_WORKSPACE_KNOWLEDGE_AGENTS);
33478
+ await writeDefaultFileIfMissing(path27__default.join(paths.root, WORKSPACE_KNOWLEDGE_AGENTS_FILENAME), DEFAULT_WORKSPACE_KNOWLEDGE_AGENTS);
33079
33479
  }
33080
33480
  async function readWorkspaceSchemaSummary(paths) {
33081
- const agentsPath = path25__default.join(paths.schemaDir, WORKSPACE_SCHEMA_AGENTS_FILENAME);
33082
- const wikiSchemaPath = path25__default.join(paths.schemaDir, WORKSPACE_SCHEMA_FILENAME);
33481
+ const agentsPath = path27__default.join(paths.schemaDir, WORKSPACE_SCHEMA_AGENTS_FILENAME);
33482
+ const wikiSchemaPath = path27__default.join(paths.schemaDir, WORKSPACE_SCHEMA_FILENAME);
33083
33483
  const wikiSchemaContent = await tryReadFile(wikiSchemaPath);
33084
33484
  const templates = await scanTemplates(paths);
33085
33485
  if (wikiSchemaContent === null) {
@@ -33118,7 +33518,7 @@ async function scanTemplates(paths) {
33118
33518
  if (!entry.name.startsWith(WORKSPACE_TEMPLATE_PREFIX) || !entry.name.endsWith(WORKSPACE_TEMPLATE_SUFFIX)) continue;
33119
33519
  const id = entry.name.slice(WORKSPACE_TEMPLATE_PREFIX.length, -WORKSPACE_TEMPLATE_SUFFIX.length);
33120
33520
  if (!id) continue;
33121
- const filePath = path25__default.join(paths.schemaDir, entry.name);
33521
+ const filePath = path27__default.join(paths.schemaDir, entry.name);
33122
33522
  try {
33123
33523
  const parsed = parseTemplateMarkdown(await readFile(filePath, "utf8"));
33124
33524
  templates.push({
@@ -33152,7 +33552,7 @@ async function validateTemplateCompliance(paths, templateId, body) {
33152
33552
  }
33153
33553
  }
33154
33554
  function inferTemplateIdFromTarget(target, knownTemplateIds) {
33155
- const basename = path25__default.basename(target, ".md");
33555
+ const basename = path27__default.basename(target, ".md");
33156
33556
  const ids = knownTemplateIds ?? DEFAULT_TEMPLATE_IDS;
33157
33557
  const sorted = [...ids].sort((a, b) => b.length - a.length);
33158
33558
  for (const id of sorted) {
@@ -33181,7 +33581,7 @@ function extractSchemaSummary(content) {
33181
33581
  return lines.find((line) => line.length > 0 && !line.startsWith("#")) ?? "Workspace schema is present.";
33182
33582
  }
33183
33583
  function buildTemplatePath(paths, id) {
33184
- return path25__default.join(paths.schemaDir, `${WORKSPACE_TEMPLATE_PREFIX}${id}${WORKSPACE_TEMPLATE_SUFFIX}`);
33584
+ return path27__default.join(paths.schemaDir, `${WORKSPACE_TEMPLATE_PREFIX}${id}${WORKSPACE_TEMPLATE_SUFFIX}`);
33185
33585
  }
33186
33586
  function parseTemplateMarkdown(content) {
33187
33587
  const normalized = content.replace(/\r\n/g, "\n");
@@ -33208,16 +33608,16 @@ function isAlreadyExistsError(error51) {
33208
33608
 
33209
33609
  // ../../packages/fleet-wiki/src/paths.ts
33210
33610
  function resolveMemoryPaths(cwd) {
33211
- const root = path25__default.join(cwd, KNOWLEDGE_ROOT_DIRNAME);
33611
+ const root = path27__default.join(cwd, KNOWLEDGE_ROOT_DIRNAME);
33212
33612
  return {
33213
33613
  root,
33214
- rawDir: path25__default.join(root, RAW_DIRNAME),
33215
- wikiDir: path25__default.join(root, WIKI_DIRNAME),
33216
- schemaDir: path25__default.join(root, SCHEMA_DIRNAME),
33217
- queueDir: path25__default.join(root, QUEUE_DIRNAME),
33218
- archiveDir: path25__default.join(root, ARCHIVE_DIRNAME),
33219
- conflictsDir: path25__default.join(root, CONFLICTS_DIRNAME),
33220
- indexFile: path25__default.join(root, INDEX_FILENAME)
33614
+ rawDir: path27__default.join(root, RAW_DIRNAME),
33615
+ wikiDir: path27__default.join(root, WIKI_DIRNAME),
33616
+ schemaDir: path27__default.join(root, SCHEMA_DIRNAME),
33617
+ queueDir: path27__default.join(root, QUEUE_DIRNAME),
33618
+ archiveDir: path27__default.join(root, ARCHIVE_DIRNAME),
33619
+ conflictsDir: path27__default.join(root, CONFLICTS_DIRNAME),
33620
+ indexFile: path27__default.join(root, INDEX_FILENAME)
33221
33621
  };
33222
33622
  }
33223
33623
  async function ensureMemoryRoot(paths) {
@@ -33232,10 +33632,10 @@ async function ensureMemoryRoot(paths) {
33232
33632
  await ensureWorkspaceDoctrine(paths);
33233
33633
  }
33234
33634
  function getIndexMarkdownFile(paths) {
33235
- return path25__default.join(paths.wikiDir, INDEX_MD_FILENAME);
33635
+ return path27__default.join(paths.wikiDir, INDEX_MD_FILENAME);
33236
33636
  }
33237
33637
  function getLogFile(paths) {
33238
- return path25__default.join(paths.root, LOG_MD_FILENAME);
33638
+ return path27__default.join(paths.root, LOG_MD_FILENAME);
33239
33639
  }
33240
33640
 
33241
33641
  // ../../packages/fleet-wiki/src/log.ts
@@ -33301,12 +33701,12 @@ async function readWikiEntry(id, paths) {
33301
33701
  const index = await loadIndex(paths);
33302
33702
  const indexed = index[id];
33303
33703
  if (indexed) {
33304
- const resolved = path25__default.resolve(paths.root, indexed.path);
33305
- const rel = path25__default.relative(paths.wikiDir, resolved);
33306
- if (!rel || rel.startsWith("..") || path25__default.isAbsolute(rel)) return null;
33704
+ const resolved = path27__default.resolve(paths.root, indexed.path);
33705
+ const rel = path27__default.relative(paths.wikiDir, resolved);
33706
+ if (!rel || rel.startsWith("..") || path27__default.isAbsolute(rel)) return null;
33307
33707
  return readMarkdownFile(resolved);
33308
33708
  }
33309
- const fallbackPath = path25__default.join(paths.wikiDir, `${id}.md`);
33709
+ const fallbackPath = path27__default.join(paths.wikiDir, `${id}.md`);
33310
33710
  if (await pathExists(fallbackPath)) {
33311
33711
  return readMarkdownFile(fallbackPath);
33312
33712
  }
@@ -33317,20 +33717,20 @@ async function readWikiEntry(id, paths) {
33317
33717
  async function writeWikiEntryAtTarget(entry, target, paths) {
33318
33718
  await ensureMemoryRoot(paths);
33319
33719
  assertSafeEntryId(entry.id);
33320
- const absoluteTarget = path25__default.resolve(paths.root, target);
33321
- const relativeTarget = path25__default.relative(paths.wikiDir, absoluteTarget);
33322
- if (!relativeTarget || relativeTarget.startsWith("..") || path25__default.isAbsolute(relativeTarget)) {
33720
+ const absoluteTarget = path27__default.resolve(paths.root, target);
33721
+ const relativeTarget = path27__default.relative(paths.wikiDir, absoluteTarget);
33722
+ if (!relativeTarget || relativeTarget.startsWith("..") || path27__default.isAbsolute(relativeTarget)) {
33323
33723
  throw new Error(`[fleet-wiki] wiki target escapes wiki/: ${target}`);
33324
33724
  }
33325
33725
  if (!absoluteTarget.endsWith(".md")) {
33326
33726
  throw new Error(`[fleet-wiki] wiki target must end with .md: ${target}`);
33327
33727
  }
33328
- if (path25__default.basename(absoluteTarget, ".md") !== entry.id) {
33728
+ if (path27__default.basename(absoluteTarget, ".md") !== entry.id) {
33329
33729
  throw new Error("wiki patch body id must match target filename");
33330
33730
  }
33331
- await mkdir(path25__default.dirname(absoluteTarget), { recursive: true });
33731
+ await mkdir(path27__default.dirname(absoluteTarget), { recursive: true });
33332
33732
  await writeMarkdownAtomic(absoluteTarget, serializeWikiEntry(entry), paths);
33333
- return path25__default.join("wiki", relativeTarget).replaceAll(path25__default.sep, "/");
33733
+ return path27__default.join("wiki", relativeTarget).replaceAll(path27__default.sep, "/");
33334
33734
  }
33335
33735
  async function listWiki(paths) {
33336
33736
  const entries = await listWikiRecords(paths);
@@ -33553,7 +33953,7 @@ function parseWikiEntry(content) {
33553
33953
  async function listWikiRecords(paths) {
33554
33954
  return listMarkdownEntriesRecursive(paths.wikiDir, paths, (content, filePath) => ({
33555
33955
  entry: parseWikiEntry(content),
33556
- path: path25__default.relative(paths.root, filePath).replaceAll(path25__default.sep, "/")
33956
+ path: path27__default.relative(paths.root, filePath).replaceAll(path27__default.sep, "/")
33557
33957
  }));
33558
33958
  }
33559
33959
  async function listMarkdownEntriesRecursive(dirPath, paths, parser) {
@@ -33566,7 +33966,7 @@ async function listMarkdownEntriesRecursive(dirPath, paths, parser) {
33566
33966
  }
33567
33967
  const sortedEntries = entries.sort((left, right) => left.name.localeCompare(right.name));
33568
33968
  for (const entry of sortedEntries) {
33569
- const filePath = path25__default.join(dirPath, entry.name);
33969
+ const filePath = path27__default.join(dirPath, entry.name);
33570
33970
  if (entry.isDirectory()) {
33571
33971
  items.push(...await listMarkdownEntriesRecursive(filePath, paths, parser));
33572
33972
  continue;
@@ -33704,8 +34104,8 @@ async function writeJsonAtomic(filePath, value, paths) {
33704
34104
  }
33705
34105
  async function writeAtomic(filePath, content, paths) {
33706
34106
  await ensureMemoryRoot(paths);
33707
- const tempPath = path25__default.join(
33708
- path25__default.dirname(filePath),
34107
+ const tempPath = path27__default.join(
34108
+ path27__default.dirname(filePath),
33709
34109
  `.tmp-${process.pid}-${Date.now()}-${crypto5.randomUUID()}-${os4__default.hostname()}`
33710
34110
  );
33711
34111
  await writeFile(tempPath, content, "utf8");
@@ -33799,7 +34199,7 @@ function toRankedHit(entry, reason, detail, graphBoost, backlinksById) {
33799
34199
  score: basePriority * 100 + statusPriority * 10 + graphScore + (detail.tagBoost ?? 0),
33800
34200
  reason,
33801
34201
  excerpt,
33802
- path: path25__default.join("wiki", `${entry.id}.md`),
34202
+ path: path27__default.join("wiki", `${entry.id}.md`),
33803
34203
  tags: entry.tags,
33804
34204
  updated: entry.updated,
33805
34205
  version: entry.version,
@@ -33909,15 +34309,15 @@ var PATCH_SET_DIRNAME = "_sets";
33909
34309
  var PATCH_SET_META_FILENAME = PATCH_META_FILENAME;
33910
34310
  function getPatchSetDir(paths, patchSetId) {
33911
34311
  assertSafePatchSetId(patchSetId);
33912
- const patchSetDir = path25__default.join(paths.queueDir, PATCH_SET_DIRNAME, patchSetId);
33913
- const relative = path25__default.relative(path25__default.join(paths.queueDir, PATCH_SET_DIRNAME), patchSetDir);
33914
- if (!relative || relative.startsWith("..") || path25__default.isAbsolute(relative)) {
34312
+ const patchSetDir = path27__default.join(paths.queueDir, PATCH_SET_DIRNAME, patchSetId);
34313
+ const relative = path27__default.relative(path27__default.join(paths.queueDir, PATCH_SET_DIRNAME), patchSetDir);
34314
+ if (!relative || relative.startsWith("..") || path27__default.isAbsolute(relative)) {
33915
34315
  throw new Error(`[fleet-wiki] patch set id escapes queue/_sets: ${patchSetId}`);
33916
34316
  }
33917
34317
  return patchSetDir;
33918
34318
  }
33919
34319
  function getPatchSetMetaFile(paths, patchSetId) {
33920
- return path25__default.join(getPatchSetDir(paths, patchSetId), PATCH_SET_META_FILENAME);
34320
+ return path27__default.join(getPatchSetDir(paths, patchSetId), PATCH_SET_META_FILENAME);
33921
34321
  }
33922
34322
  async function readPatchSet(paths, patchSetId) {
33923
34323
  const patchSet = await readJsonFile2(getPatchSetMetaFile(paths, patchSetId));
@@ -33940,7 +34340,7 @@ async function createConflict(input, paths) {
33940
34340
  await ensureMemoryRoot(paths);
33941
34341
  const now = input.now ?? /* @__PURE__ */ new Date();
33942
34342
  const conflictId = buildConflictId(now, input.wikiId, input.reason, input.proposed);
33943
- const conflictDir = path25__default.join(paths.conflictsDir, conflictId);
34343
+ const conflictDir = path27__default.join(paths.conflictsDir, conflictId);
33944
34344
  await mkdir(conflictDir, { recursive: true });
33945
34345
  const meta3 = {
33946
34346
  id: conflictId,
@@ -33960,13 +34360,13 @@ async function createConflict(input, paths) {
33960
34360
  currentHash: input.currentHash,
33961
34361
  warnings: input.warnings
33962
34362
  };
33963
- await writeJsonFile(path25__default.join(conflictDir, CONFLICT_META_FILENAME), meta3, paths);
33964
- await writePatchFile(path25__default.join(conflictDir, CONFLICT_PROPOSED_FILENAME), input.proposed, paths);
34363
+ await writeJsonFile(path27__default.join(conflictDir, CONFLICT_META_FILENAME), meta3, paths);
34364
+ await writePatchFile(path27__default.join(conflictDir, CONFLICT_PROPOSED_FILENAME), input.proposed, paths);
33965
34365
  if (input.current !== void 0) {
33966
- await writePatchFile(path25__default.join(conflictDir, CONFLICT_CURRENT_FILENAME), input.current, paths);
34366
+ await writePatchFile(path27__default.join(conflictDir, CONFLICT_CURRENT_FILENAME), input.current, paths);
33967
34367
  }
33968
34368
  if (input.rawSource !== void 0) {
33969
- await writePatchFile(path25__default.join(conflictDir, CONFLICT_RAW_SOURCE_FILENAME), input.rawSource, paths);
34369
+ await writePatchFile(path27__default.join(conflictDir, CONFLICT_RAW_SOURCE_FILENAME), input.rawSource, paths);
33970
34370
  }
33971
34371
  return {
33972
34372
  meta: meta3,
@@ -34016,11 +34416,11 @@ async function validatePatch(patch, paths) {
34016
34416
  if (!target || !summary || !proposer || !created) throw new Error("patch frontmatter is incomplete");
34017
34417
  if (summary.length > 120) throw new Error("patch summary exceeds 120 chars");
34018
34418
  assertCanonicalPatchTarget(target);
34019
- const absoluteTarget = path25__default.resolve(paths.root, target);
34020
- if (!absoluteTarget.startsWith(`${paths.root}${path25__default.sep}`) && absoluteTarget !== paths.root) {
34419
+ const absoluteTarget = path27__default.resolve(paths.root, target);
34420
+ if (!absoluteTarget.startsWith(`${paths.root}${path27__default.sep}`) && absoluteTarget !== paths.root) {
34021
34421
  throw new Error("patch target escapes wiki root");
34022
34422
  }
34023
- if (!absoluteTarget.startsWith(`${paths.wikiDir}${path25__default.sep}`)) throw new Error("wiki patch must target wiki/");
34423
+ if (!absoluteTarget.startsWith(`${paths.wikiDir}${path27__default.sep}`)) throw new Error("wiki patch must target wiki/");
34024
34424
  assertNoSymlinkPathComponents(target, paths);
34025
34425
  if (op === "create_wiki" && await pathExists(absoluteTarget)) {
34026
34426
  throw new Error(
@@ -34044,7 +34444,7 @@ async function listQueue(paths) {
34044
34444
  for (const id of ids) {
34045
34445
  if (id === "_sets") continue;
34046
34446
  try {
34047
- const meta3 = await readJsonFile2(path25__default.join(paths.queueDir, id, PATCH_META_FILENAME));
34447
+ const meta3 = await readJsonFile2(path27__default.join(paths.queueDir, id, PATCH_META_FILENAME));
34048
34448
  results.push({ id, meta: meta3 });
34049
34449
  } catch {
34050
34450
  continue;
@@ -34069,9 +34469,9 @@ async function resolveQueueSelection(id, paths) {
34069
34469
  }
34070
34470
  async function showQueue(id, paths) {
34071
34471
  const selection = await resolveQueueSelection(id, paths);
34072
- const queueDir = path25__default.join(paths.queueDir, selection.id);
34073
- const patch = await parsePatch(await readPatchFile(path25__default.join(queueDir, PATCH_FILENAME)));
34074
- const meta3 = await readJsonFile2(path25__default.join(queueDir, PATCH_META_FILENAME));
34472
+ const queueDir = path27__default.join(paths.queueDir, selection.id);
34473
+ const patch = await parsePatch(await readPatchFile(path27__default.join(queueDir, PATCH_FILENAME)));
34474
+ const meta3 = await readJsonFile2(path27__default.join(queueDir, PATCH_META_FILENAME));
34075
34475
  return { patch, meta: meta3 };
34076
34476
  }
34077
34477
  async function approvePatch(id, paths) {
@@ -34094,8 +34494,8 @@ async function approvePatch(id, paths) {
34094
34494
  rawSourceRef: meta3.rawSourceRef,
34095
34495
  warnings: meta3.warnings
34096
34496
  });
34097
- const queueDir = path25__default.join(paths.queueDir, id);
34098
- await writeJsonFile(path25__default.join(queueDir, PATCH_META_FILENAME), {
34497
+ const queueDir = path27__default.join(paths.queueDir, id);
34498
+ await writeJsonFile(path27__default.join(queueDir, PATCH_META_FILENAME), {
34099
34499
  ...meta3,
34100
34500
  conflictId
34101
34501
  }, paths);
@@ -34148,7 +34548,7 @@ async function rejectPatch(id, reason, paths) {
34148
34548
  }
34149
34549
  async function normalizeWikiEntryPatch(entry, target, paths) {
34150
34550
  assertSafeEntryId(entry.id);
34151
- if (entry.id !== path25__default.basename(target, ".md")) {
34551
+ if (entry.id !== path27__default.basename(target, ".md")) {
34152
34552
  throw new Error("wiki patch body id must match target filename");
34153
34553
  }
34154
34554
  const inlineRawSourceRef = extractInlineRawSourceRef(entry.body);
@@ -34186,9 +34586,9 @@ function extractInlineRawSourceRef(body) {
34186
34586
  }
34187
34587
  async function validatePatchBase(patch, meta3, paths) {
34188
34588
  if (patch.frontmatter.op !== "update_wiki" || !meta3?.baseVersion && !meta3?.baseHash) return;
34189
- const wikiId = path25__default.basename(patch.frontmatter.target, ".md");
34589
+ const wikiId = path27__default.basename(patch.frontmatter.target, ".md");
34190
34590
  const currentEntry = await readWikiEntry(wikiId, paths);
34191
- const currentPath = path25__default.join(paths.root, patch.frontmatter.target);
34591
+ const currentPath = path27__default.join(paths.root, patch.frontmatter.target);
34192
34592
  const currentMarkdown = await pathExists(currentPath) ? await readPatchFile(currentPath) : void 0;
34193
34593
  if (meta3.baseVersion !== void 0 && currentEntry?.version !== meta3.baseVersion) {
34194
34594
  throw new Error(
@@ -34247,17 +34647,17 @@ function assertCanonicalPatchTarget(target) {
34247
34647
  if (target.endsWith("/")) {
34248
34648
  throw new Error("patch target must not end with a slash");
34249
34649
  }
34250
- if (target.startsWith("/") || path25__default.posix.isAbsolute(target) || path25__default.win32.isAbsolute(target)) {
34650
+ if (target.startsWith("/") || path27__default.posix.isAbsolute(target) || path27__default.win32.isAbsolute(target)) {
34251
34651
  throw new Error("patch target must be relative");
34252
34652
  }
34253
- if (path25__default.posix.normalize(target) !== target) {
34653
+ if (path27__default.posix.normalize(target) !== target) {
34254
34654
  throw new Error("patch target must not contain dot segments or redundant separators");
34255
34655
  }
34256
34656
  }
34257
34657
  function canonicalApprovalLockKey(paths, target) {
34258
34658
  const wikiRoot = realpathSync(paths.wikiDir);
34259
- const relativeTarget = path25__default.posix.relative("wiki", target);
34260
- const targetPath = path25__default.join(wikiRoot, ...relativeTarget.split("/"));
34659
+ const relativeTarget = path27__default.posix.relative("wiki", target);
34660
+ const targetPath = path27__default.join(wikiRoot, ...relativeTarget.split("/"));
34261
34661
  return `${wikiRoot}\0${targetPath}`.toLowerCase();
34262
34662
  }
34263
34663
  function canonicalPatchEditLockKey(paths, patchId) {
@@ -34273,7 +34673,7 @@ function assertNoSymlinkPathComponents(target, paths) {
34273
34673
  throw error51;
34274
34674
  }
34275
34675
  for (const part of parts) {
34276
- current = path25__default.join(current, part);
34676
+ current = path27__default.join(current, part);
34277
34677
  try {
34278
34678
  if (lstatSync(current).isSymbolicLink()) {
34279
34679
  throw new Error("patch target must not include symlink path components");
@@ -34288,8 +34688,8 @@ function assertSafeRawSourceRef(rawSourceRef, paths) {
34288
34688
  if (!rawSourceRef.startsWith("raw/")) {
34289
34689
  throw new Error("raw source provenance must point into raw/");
34290
34690
  }
34291
- const absoluteRef = path25__default.resolve(paths.root, rawSourceRef);
34292
- if (!absoluteRef.startsWith(`${paths.rawDir}${path25__default.sep}`)) {
34691
+ const absoluteRef = path27__default.resolve(paths.root, rawSourceRef);
34692
+ if (!absoluteRef.startsWith(`${paths.rawDir}${path27__default.sep}`)) {
34293
34693
  throw new Error("raw source provenance must point into raw/");
34294
34694
  }
34295
34695
  }
@@ -34305,14 +34705,14 @@ function classifyPatchConflict(error51) {
34305
34705
  }
34306
34706
  async function recordPatchConflict(patch, paths, reason, options) {
34307
34707
  const now = /* @__PURE__ */ new Date();
34308
- const targetPath = path25__default.join(paths.root, patch.frontmatter.target);
34708
+ const targetPath = path27__default.join(paths.root, patch.frontmatter.target);
34309
34709
  const current = await pathExists(targetPath) ? await readPatchFile(targetPath) : void 0;
34310
34710
  const currentEntry = current ? parseStoredWikiEntry(current) : void 0;
34311
34711
  const proposedEntry = parsePatchBodyEntry(patch.body);
34312
34712
  const record2 = await createConflict({
34313
34713
  reason,
34314
34714
  target: patch.frontmatter.target,
34315
- wikiId: proposedEntry?.id ?? path25__default.basename(patch.frontmatter.target, ".md"),
34715
+ wikiId: proposedEntry?.id ?? path27__default.basename(patch.frontmatter.target, ".md"),
34316
34716
  title: proposedEntry?.title,
34317
34717
  proposer: patch.frontmatter.proposer,
34318
34718
  rawSourceRef: options?.rawSourceRef,
@@ -34408,11 +34808,11 @@ function parseInlineArray(value) {
34408
34808
  return inner.split(",").map((item) => item.trim().replace(/^"(.*)"$/, "$1"));
34409
34809
  }
34410
34810
  async function archiveQueueEntry(id, paths, meta3) {
34411
- const fromDir = path25__default.join(paths.queueDir, id);
34412
- const toDir = path25__default.join(paths.archiveDir, id);
34811
+ const fromDir = path27__default.join(paths.queueDir, id);
34812
+ const toDir = path27__default.join(paths.archiveDir, id);
34413
34813
  await removePath(toDir);
34414
34814
  await movePath(fromDir, toDir);
34415
- await writeJsonFile(path25__default.join(toDir, PATCH_META_FILENAME), meta3, paths);
34815
+ await writeJsonFile(path27__default.join(toDir, PATCH_META_FILENAME), meta3, paths);
34416
34816
  }
34417
34817
  function buildQueueIdHelp(prefix, availableIds) {
34418
34818
  if (availableIds.length === 0) {
@@ -35134,15 +35534,15 @@ async function canonicalizeTheaterPath(cwd) {
35134
35534
  return canonicalizeTheaterPathSync(cwd);
35135
35535
  }
35136
35536
  function canonicalizeTheaterPathSync(cwd) {
35137
- const resolved = path25__default.resolve(cwd);
35537
+ const resolved = path27__default.resolve(cwd);
35138
35538
  try {
35139
- return fs11__default.realpathSync.native(resolved);
35539
+ return fs14__default.realpathSync.native(resolved);
35140
35540
  } catch {
35141
35541
  return resolved;
35142
35542
  }
35143
35543
  }
35144
35544
  function theaterLabel(cwd) {
35145
- return path25__default.basename(cwd) || cwd;
35545
+ return path27__default.basename(cwd) || cwd;
35146
35546
  }
35147
35547
 
35148
35548
  // core/host/codex/workspaces.ts
@@ -35150,7 +35550,7 @@ var WorkspaceRegistry = class {
35150
35550
  #items = /* @__PURE__ */ new Map();
35151
35551
  #mruId = null;
35152
35552
  async register(cwdInput, lastOpenedAt) {
35153
- const cwd = path25__default.resolve(cwdInput);
35553
+ const cwd = path27__default.resolve(cwdInput);
35154
35554
  const real = await canonicalizeTheaterPath(cwd);
35155
35555
  const paths = resolveMemoryPaths(real);
35156
35556
  if (!await directoryExists(paths.root)) {
@@ -35166,7 +35566,7 @@ var WorkspaceRegistry = class {
35166
35566
  id,
35167
35567
  cwd,
35168
35568
  realpath: real,
35169
- label: path25__default.basename(cwd),
35569
+ label: path27__default.basename(cwd),
35170
35570
  paths,
35171
35571
  registeredAt: existing?.registeredAt ?? now,
35172
35572
  // 복원 경로는 durable lastOpenedAt을 그대로 보존해 재시작 후에도 워크스페이스 최근성
@@ -35232,6 +35632,7 @@ var LOOPBACK_ACCESS_HOSTS = ["127.0.0.1", "::1"];
35232
35632
  var LOOPBACK_HOST = "127.0.0.1";
35233
35633
  function createCodexGateway(deps) {
35234
35634
  const workspaces = new WorkspaceRegistry();
35635
+ const workspaceOwners = /* @__PURE__ */ new Map();
35235
35636
  let accessSets = null;
35236
35637
  let initialWorkspace = null;
35237
35638
  let initialWorkspaceId = null;
@@ -35261,6 +35662,10 @@ function createCodexGateway(deps) {
35261
35662
  redirect(response, `${CODEX_BASE}${selected.location}`);
35262
35663
  return true;
35263
35664
  }
35665
+ if (selected.kind === "missing-workspace") {
35666
+ sendJson2(response, 404, { error: "workspace_not_found" });
35667
+ return true;
35668
+ }
35264
35669
  if (selected.kind === "no-workspace") {
35265
35670
  if (isJsonRequest4(request)) {
35266
35671
  sendJson2(response, 404, { error: "no_workspace_registered" });
@@ -35291,18 +35696,50 @@ function createCodexGateway(deps) {
35291
35696
  initialWorkspaceId = workspace.id;
35292
35697
  return workspace;
35293
35698
  }
35699
+ async function registerWorkspace(cwd, lastOpenedAt, ownerTheaterId) {
35700
+ const workspace = await workspaces.register(cwd, lastOpenedAt);
35701
+ if (ownerTheaterId) {
35702
+ const owners = workspaceOwners.get(workspace.id) ?? /* @__PURE__ */ new Set();
35703
+ owners.add(ownerTheaterId);
35704
+ workspaceOwners.set(workspace.id, owners);
35705
+ }
35706
+ return workspace;
35707
+ }
35708
+ async function resolveWorkspaceForPath(theaterId, theaterRoot, relPath) {
35709
+ const pathContext = await resolveTheaterPathContext(theaterRoot, relPath);
35710
+ try {
35711
+ const workspace = await registerWorkspace(pathContext.realPath, void 0, theaterId);
35712
+ return { hasWiki: true, id: workspace.id };
35713
+ } catch (error51) {
35714
+ if (error51 instanceof Error && error51.message === "knowledge_root_missing") {
35715
+ return { hasWiki: false, id: null };
35716
+ }
35717
+ throw error51;
35718
+ }
35719
+ }
35720
+ function unregisterWorkspace(id) {
35721
+ if (initialWorkspaceId === id) {
35722
+ initialWorkspace = null;
35723
+ initialWorkspaceId = null;
35724
+ }
35725
+ return workspaces.remove(id);
35726
+ }
35727
+ function unregisterTheaterWorkspaces(theaterId) {
35728
+ for (const [workspaceId, owners] of workspaceOwners) {
35729
+ owners.delete(theaterId);
35730
+ if (owners.size === 0) {
35731
+ workspaceOwners.delete(workspaceId);
35732
+ unregisterWorkspace(workspaceId);
35733
+ }
35734
+ }
35735
+ }
35294
35736
  return {
35295
35737
  getWorkspace: (id) => workspaces.get(id),
35296
35738
  handle,
35297
35739
  listWorkspaceRegistrations: () => workspaces.listRegistrations(),
35298
- registerWorkspace: (cwd, lastOpenedAt) => workspaces.register(cwd, lastOpenedAt),
35299
- unregisterWorkspace: (id) => {
35300
- if (initialWorkspaceId === id) {
35301
- initialWorkspace = null;
35302
- initialWorkspaceId = null;
35303
- }
35304
- return workspaces.remove(id);
35305
- }
35740
+ registerWorkspace,
35741
+ resolveWorkspaceForPath,
35742
+ unregisterTheaterWorkspaces
35306
35743
  };
35307
35744
  }
35308
35745
  function buildAllowedAccessSets(host, port, interfaces = os4__default.networkInterfaces()) {
@@ -35330,8 +35767,10 @@ function selectWorkspace(requestUrl, workspaces, cwd) {
35330
35767
  const prefixed = url2.pathname.match(/^\/w\/([^/]+)(\/.*)?$/);
35331
35768
  if (prefixed) {
35332
35769
  const workspace = workspaces.get(decodeURIComponent(prefixed[1] ?? ""));
35333
- if (!workspace) return { kind: "redirect", location: "/" };
35334
35770
  const suffix = prefixed[2] ?? "/";
35771
+ if (!workspace) {
35772
+ return suffix.startsWith("/api/") ? { kind: "missing-workspace" } : { kind: "redirect", location: "/" };
35773
+ }
35335
35774
  url2.pathname = suffix;
35336
35775
  return { kind: "workspace", workspace, rewrittenUrl: `${url2.pathname}${url2.search}` };
35337
35776
  }
@@ -35484,7 +35923,7 @@ function createConsoleDurableStateStore(deps = {}) {
35484
35923
  const createStore = deps.createStore ?? createDurableJsonStore;
35485
35924
  return createStore({
35486
35925
  filePath: paths.stateFile,
35487
- lockDir: path25__default.join(paths.dir, STATE_LOCK_DIR_NAME),
35926
+ lockDir: path27__default.join(paths.dir, STATE_LOCK_DIR_NAME),
35488
35927
  lockOwnerFileName: STATE_LOCK_OWNER_FILE_NAME,
35489
35928
  now: deps.now,
35490
35929
  sanitize: sanitizeDurableConsoleState,
@@ -35526,7 +35965,7 @@ function migrateV1Operation(value) {
35526
35965
  const cwd = readNonEmptyString(value.cwd);
35527
35966
  const createdAt = readFiniteNumber(value.createdAt);
35528
35967
  if (!sessionId || !theaterId || !cwd || createdAt === null) return null;
35529
- const title = readOptionalString(value.label) ?? (path25__default.basename(cwd) || cwd);
35968
+ const title = readOptionalString(value.label) ?? (path27__default.basename(cwd) || cwd);
35530
35969
  const payload = { cwd };
35531
35970
  const cliId = readOptionalString(value.cliId);
35532
35971
  const cliLabel = readOptionalString(value.cliLabel);
@@ -35573,7 +36012,11 @@ function sanitizeTheaterRegistration(value) {
35573
36012
  const registeredAt = readNonEmptyString(value.registeredAt);
35574
36013
  const lastOpenedAt = readNonEmptyString(value.lastOpenedAt);
35575
36014
  if (!id || !theaterPath || !realpath2 || !label || !registeredAt || !lastOpenedAt) return null;
35576
- return { id, path: theaterPath, realpath: realpath2, label, registeredAt, lastOpenedAt };
36015
+ const pathContext = typeof value.pathContext === "string" && isSafeDurableRelPath(value.pathContext) ? value.pathContext : null;
36016
+ return { id, path: theaterPath, realpath: realpath2, label, registeredAt, lastOpenedAt, pathContext };
36017
+ }
36018
+ function isSafeDurableRelPath(value) {
36019
+ return value.length > 0 && !value.includes("\0") && !value.startsWith("/") && !value.startsWith("\\") && !/^[A-Za-z]:/.test(value) && !value.split("/").some((part) => part === "" || part === "." || part === "..");
35577
36020
  }
35578
36021
  function sanitizeOperationNode(value) {
35579
36022
  if (!isRecord6(value)) return null;
@@ -36110,6 +36553,359 @@ function sanitizeReplacementNodes(nextNodes) {
36110
36553
  return result;
36111
36554
  }
36112
36555
 
36556
+ // core/host/plans/plan-parse.ts
36557
+ var TITLE_PATTERN = /^#\s+(.+)$/;
36558
+ var EXECUTION_MODE_PATTERN = /^-\s*Execution mode:\s*(sequential|parallel)\b/i;
36559
+ var WAVE_PATTERN = /^##\s+wave\s+(\d+)\b/i;
36560
+ var LANE_PATTERN = /^###\s+lane\s+(.+)$/i;
36561
+ var LANE_ID_PATTERN = /^(W\d+-[A-Za-z0-9]+)\b/i;
36562
+ var LANE_BOUNDARY_PATTERN = /^#{1,3}\s/;
36563
+ var SECTION_HEADING_PATTERN = /^#{1,2}\s/;
36564
+ var CHECKBOX_PATTERN = /^\s*-\s*\[( |x|X)\]/;
36565
+ var FENCE_PATTERN = /^\s*(`{3,}|~{3,})/;
36566
+ var CLOSING_FENCE_PATTERN = /^\s*(`{3,}|~{3,})\s*$/;
36567
+ var TEMPLATE_SECTION_TITLES = /* @__PURE__ */ new Set([
36568
+ "objective",
36569
+ "file ownership",
36570
+ "waves",
36571
+ "qa gates",
36572
+ "acceptance criteria",
36573
+ "documentation updates",
36574
+ "final review loop"
36575
+ ]);
36576
+ function parsePlan(content) {
36577
+ const lines = scanPlanLines(content.split(/\r?\n/));
36578
+ const title = findTitle(lines);
36579
+ const executionMode = findExecutionMode(lines);
36580
+ const waveStarts = findWaveStarts(lines);
36581
+ const waves = waveStarts.map(({ index, heading, lineIndex }, waveIndex) => {
36582
+ const nextWaveLineIndex = waveStarts[waveIndex + 1]?.lineIndex ?? lines.length;
36583
+ const endLineIndex = findSectionEnd(lines, lineIndex + 1, nextWaveLineIndex);
36584
+ const waveLines = lines.slice(lineIndex + 1, endLineIndex);
36585
+ const counts = countTasks(waveLines);
36586
+ return { index, heading, lanes: findLanes(waveLines), ...counts };
36587
+ });
36588
+ return { title, executionMode, waves, ...countTasks(lines) };
36589
+ }
36590
+ function scanPlanLines(lines) {
36591
+ let activeFence = null;
36592
+ return lines.map((text) => {
36593
+ if (activeFence !== null) {
36594
+ const closer = CLOSING_FENCE_PATTERN.exec(text)?.[1];
36595
+ if (closer !== void 0 && closer.startsWith(activeFence.char) && closer.length >= activeFence.length) {
36596
+ activeFence = null;
36597
+ }
36598
+ return { text, isFenced: true };
36599
+ }
36600
+ const marker = FENCE_PATTERN.exec(text)?.[1];
36601
+ if (marker !== void 0) {
36602
+ activeFence = { char: marker[0] ?? "`", length: marker.length };
36603
+ return { text, isFenced: true };
36604
+ }
36605
+ return { text, isFenced: false };
36606
+ });
36607
+ }
36608
+ function findTitle(lines) {
36609
+ for (const line of lines) {
36610
+ if (line.isFenced) continue;
36611
+ const match = TITLE_PATTERN.exec(line.text);
36612
+ if (!match?.[1]) continue;
36613
+ const heading = match[1].trim();
36614
+ if (TEMPLATE_SECTION_TITLES.has(heading.toLowerCase())) return null;
36615
+ return heading;
36616
+ }
36617
+ return null;
36618
+ }
36619
+ function findExecutionMode(lines) {
36620
+ for (const line of lines) {
36621
+ if (line.isFenced) continue;
36622
+ const match = EXECUTION_MODE_PATTERN.exec(line.text.trim());
36623
+ if (match?.[1]) return match[1].toLowerCase();
36624
+ }
36625
+ return null;
36626
+ }
36627
+ function findWaveStarts(lines) {
36628
+ const waves = [];
36629
+ for (const [lineIndex, line] of lines.entries()) {
36630
+ if (line.isFenced) continue;
36631
+ const match = WAVE_PATTERN.exec(line.text);
36632
+ if (!match?.[1]) continue;
36633
+ waves.push({
36634
+ index: Number(match[1]),
36635
+ heading: line.text.replace(/^##\s+/, ""),
36636
+ lineIndex
36637
+ });
36638
+ }
36639
+ return waves;
36640
+ }
36641
+ function findLanes(waveLines) {
36642
+ const laneStarts = [];
36643
+ for (const [lineIndex, line] of waveLines.entries()) {
36644
+ if (line.isFenced) continue;
36645
+ const match = LANE_PATTERN.exec(line.text);
36646
+ if (!match?.[1]) continue;
36647
+ const heading = match[1].trim();
36648
+ laneStarts.push({ heading, id: LANE_ID_PATTERN.exec(heading)?.[1]?.toUpperCase() ?? null, lineIndex });
36649
+ }
36650
+ return laneStarts.map(({ heading, id, lineIndex }, laneIndex) => {
36651
+ const nextLaneLineIndex = laneStarts[laneIndex + 1]?.lineIndex ?? waveLines.length;
36652
+ let endLineIndex = nextLaneLineIndex;
36653
+ for (let cursor = lineIndex + 1; cursor < nextLaneLineIndex; cursor++) {
36654
+ const line = waveLines[cursor];
36655
+ if (!line?.isFenced && LANE_BOUNDARY_PATTERN.test(line?.text ?? "")) {
36656
+ endLineIndex = cursor;
36657
+ break;
36658
+ }
36659
+ }
36660
+ return { id, heading, ...countTasks(waveLines.slice(lineIndex + 1, endLineIndex)) };
36661
+ });
36662
+ }
36663
+ function findSectionEnd(lines, startLineIndex, maxLineIndex) {
36664
+ for (let lineIndex = startLineIndex; lineIndex < maxLineIndex; lineIndex++) {
36665
+ const line = lines[lineIndex];
36666
+ if (!line?.isFenced && SECTION_HEADING_PATTERN.test(line?.text ?? "")) return lineIndex;
36667
+ }
36668
+ return maxLineIndex;
36669
+ }
36670
+ function countTasks(lines) {
36671
+ let tasksDone = 0;
36672
+ let tasksTotal = 0;
36673
+ for (const line of lines) {
36674
+ if (line.isFenced) continue;
36675
+ const match = CHECKBOX_PATTERN.exec(line.text);
36676
+ if (!match) continue;
36677
+ tasksTotal++;
36678
+ if (match[1] === "x" || match[1] === "X") tasksDone++;
36679
+ }
36680
+ return { tasksDone, tasksTotal };
36681
+ }
36682
+
36683
+ // core/host/plans/plan-store.ts
36684
+ var PLANS_DIRECTORY_SEGMENTS = [".fleet", "plans"];
36685
+ var MARKDOWN_EXTENSION = ".md";
36686
+ var PLAN_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.md$/;
36687
+ var LIST_READ_CONCURRENCY = 16;
36688
+ var PLAN_READ_SIZE_CAP = 2 * 1024 * 1024;
36689
+ var PlanStoreError = class extends Error {
36690
+ code;
36691
+ constructor(code) {
36692
+ super(code);
36693
+ this.name = "PlanStoreError";
36694
+ this.code = code;
36695
+ }
36696
+ };
36697
+ async function listPlansForTheater(theaterPath) {
36698
+ const plansRoot = await resolvePlansRoot(theaterPath, true);
36699
+ if (plansRoot === null) return [];
36700
+ let entries;
36701
+ try {
36702
+ entries = await fs14__default.promises.readdir(plansRoot.realPlansPath, { withFileTypes: true });
36703
+ } catch (error51) {
36704
+ throw toPlanStoreError(error51);
36705
+ }
36706
+ const names = entries.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(MARKDOWN_EXTENSION) && isValidPlanName(entry.name)).map((entry) => entry.name);
36707
+ const plans = [];
36708
+ for (let startIndex = 0; startIndex < names.length; startIndex += LIST_READ_CONCURRENCY) {
36709
+ const chunk = names.slice(startIndex, startIndex + LIST_READ_CONCURRENCY);
36710
+ const results = await Promise.all(chunk.map((name) => toPlanListItem(plansRoot, name).catch(() => null)));
36711
+ plans.push(...results.filter((plan) => plan !== null));
36712
+ }
36713
+ return plans.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
36714
+ }
36715
+ function isValidPlanName(name) {
36716
+ return PLAN_NAME_PATTERN.test(name) && !name.includes("..");
36717
+ }
36718
+ async function readPlanForTheater(theaterPath, name) {
36719
+ const plansRoot = await resolvePlansRoot(theaterPath, false);
36720
+ if (plansRoot === null) throw new PlanStoreError("not_found");
36721
+ const file2 = await resolvePlanFile(plansRoot, name);
36722
+ if (file2.stat.size > PLAN_READ_SIZE_CAP) throw new PlanStoreError("too_large");
36723
+ let content;
36724
+ try {
36725
+ content = await fs14__default.promises.readFile(file2.realPath, "utf8");
36726
+ } catch (error51) {
36727
+ throw toPlanStoreError(error51);
36728
+ }
36729
+ const parsed = parsePlan(content);
36730
+ return {
36731
+ name,
36732
+ title: parsed.title ?? name,
36733
+ executionMode: parsed.executionMode,
36734
+ updatedAt: file2.stat.mtime.toISOString(),
36735
+ content,
36736
+ waves: parsed.waves,
36737
+ tasksDone: parsed.tasksDone,
36738
+ tasksTotal: parsed.tasksTotal
36739
+ };
36740
+ }
36741
+ async function toPlanListItem(plansRoot, name) {
36742
+ const file2 = await resolvePlanFile(plansRoot, name);
36743
+ if (file2.stat.size > PLAN_READ_SIZE_CAP) {
36744
+ return {
36745
+ name,
36746
+ title: name,
36747
+ executionMode: null,
36748
+ waveCount: 0,
36749
+ tasksDone: 0,
36750
+ tasksTotal: 0,
36751
+ updatedAt: file2.stat.mtime.toISOString(),
36752
+ sizeBytes: file2.stat.size
36753
+ };
36754
+ }
36755
+ const content = await fs14__default.promises.readFile(file2.realPath, "utf8").catch((error51) => {
36756
+ throw toPlanStoreError(error51);
36757
+ });
36758
+ const parsed = parsePlan(content);
36759
+ return {
36760
+ name,
36761
+ title: parsed.title ?? name,
36762
+ executionMode: parsed.executionMode,
36763
+ waveCount: parsed.waves.length,
36764
+ tasksDone: parsed.tasksDone,
36765
+ tasksTotal: parsed.tasksTotal,
36766
+ updatedAt: file2.stat.mtime.toISOString(),
36767
+ sizeBytes: file2.stat.size
36768
+ };
36769
+ }
36770
+ async function resolvePlansRoot(contextPath, allowMissing) {
36771
+ const nominalContextPath = path27__default.resolve(contextPath);
36772
+ const plansPath = path27__default.resolve(nominalContextPath, ...PLANS_DIRECTORY_SEGMENTS);
36773
+ if (!isWithinRoot2(plansPath, nominalContextPath)) throw new PlanStoreError("path_outside_theater");
36774
+ let realContextPath;
36775
+ let realPlansPath;
36776
+ try {
36777
+ [realContextPath, realPlansPath] = await Promise.all([
36778
+ fs14__default.promises.realpath(nominalContextPath),
36779
+ fs14__default.promises.realpath(plansPath)
36780
+ ]);
36781
+ } catch (error51) {
36782
+ if (allowMissing && isNotFoundError(error51)) return null;
36783
+ throw toPlanStoreError(error51);
36784
+ }
36785
+ if (!isWithinRoot2(realPlansPath, realContextPath)) throw new PlanStoreError("path_outside_theater");
36786
+ return { realContextPath, realPlansPath };
36787
+ }
36788
+ async function resolvePlanFile(plansRoot, name) {
36789
+ const candidatePath = path27__default.resolve(plansRoot.realPlansPath, name);
36790
+ if (!isWithinRoot2(candidatePath, plansRoot.realPlansPath)) throw new PlanStoreError("path_outside_theater");
36791
+ let realPath;
36792
+ try {
36793
+ realPath = await fs14__default.promises.realpath(candidatePath);
36794
+ } catch (error51) {
36795
+ throw toPlanStoreError(error51);
36796
+ }
36797
+ if (!isWithinRoot2(realPath, plansRoot.realPlansPath) || !isWithinRoot2(realPath, plansRoot.realContextPath)) {
36798
+ throw new PlanStoreError("path_outside_theater");
36799
+ }
36800
+ let stat5;
36801
+ try {
36802
+ stat5 = await fs14__default.promises.stat(realPath);
36803
+ } catch (error51) {
36804
+ throw toPlanStoreError(error51);
36805
+ }
36806
+ if (!stat5.isFile()) throw new PlanStoreError("not_found");
36807
+ return { realPath, stat: stat5 };
36808
+ }
36809
+ function isWithinRoot2(resolvedPath, rootPath) {
36810
+ const normalizedRoot = rootPath.endsWith(path27__default.sep) ? rootPath : rootPath + path27__default.sep;
36811
+ return resolvedPath === rootPath || resolvedPath.startsWith(normalizedRoot);
36812
+ }
36813
+ function isNotFoundError(error51) {
36814
+ return error51.code === "ENOENT";
36815
+ }
36816
+ function toPlanStoreError(error51) {
36817
+ if (error51 instanceof PlanStoreError) return error51;
36818
+ return new PlanStoreError("not_found");
36819
+ }
36820
+
36821
+ // core/host/plans/routes.ts
36822
+ function createPlansRouter(deps) {
36823
+ return async function handlePlansRoute(context) {
36824
+ const { req, res, pathname } = context;
36825
+ if (pathname === "/api/v1/plans/list") {
36826
+ await handlePlansList(req, res, deps);
36827
+ return true;
36828
+ }
36829
+ if (pathname === "/api/v1/plans/read") {
36830
+ await handlePlansRead(req, res, deps);
36831
+ return true;
36832
+ }
36833
+ return false;
36834
+ };
36835
+ }
36836
+ async function handlePlansList(req, res, deps) {
36837
+ if (req.method !== "POST") {
36838
+ deps.writeJson(res, 405, { error: "Method not allowed" });
36839
+ return;
36840
+ }
36841
+ if (!deps.isAuthorized(req)) {
36842
+ deps.writeJson(res, 401, { error: "unauthorized" });
36843
+ return;
36844
+ }
36845
+ const body = await deps.readJsonBody(req);
36846
+ if (!isPlainObject2(body) || typeof body.theaterId !== "string" || !isValidRelPath(body.relPath)) {
36847
+ deps.writeJson(res, 400, { error: "invalid_request" });
36848
+ return;
36849
+ }
36850
+ const theaterPath = deps.resolveTheaterPath(body.theaterId);
36851
+ if (!theaterPath) {
36852
+ deps.writeJson(res, 404, { error: "theater_not_found" });
36853
+ return;
36854
+ }
36855
+ try {
36856
+ const context = await resolveTheaterPathContext(theaterPath, body.relPath);
36857
+ deps.writeJson(res, 200, { plans: await listPlansForTheater(context.realPath) });
36858
+ } catch (error51) {
36859
+ writePlanStoreError(res, deps, error51);
36860
+ }
36861
+ }
36862
+ async function handlePlansRead(req, res, deps) {
36863
+ if (req.method !== "POST") {
36864
+ deps.writeJson(res, 405, { error: "Method not allowed" });
36865
+ return;
36866
+ }
36867
+ if (!deps.isAuthorized(req)) {
36868
+ deps.writeJson(res, 401, { error: "unauthorized" });
36869
+ return;
36870
+ }
36871
+ const body = await deps.readJsonBody(req);
36872
+ if (!isPlainObject2(body) || typeof body.theaterId !== "string" || !isValidRelPath(body.relPath)) {
36873
+ deps.writeJson(res, 400, { error: "invalid_request" });
36874
+ return;
36875
+ }
36876
+ if (typeof body.name !== "string" || !isValidPlanName(body.name)) {
36877
+ deps.writeJson(res, 400, { error: "invalid_name" });
36878
+ return;
36879
+ }
36880
+ const theaterPath = deps.resolveTheaterPath(body.theaterId);
36881
+ if (!theaterPath) {
36882
+ deps.writeJson(res, 404, { error: "theater_not_found" });
36883
+ return;
36884
+ }
36885
+ try {
36886
+ const context = await resolveTheaterPathContext(theaterPath, body.relPath);
36887
+ deps.writeJson(res, 200, await readPlanForTheater(context.realPath, body.name));
36888
+ } catch (error51) {
36889
+ writePlanStoreError(res, deps, error51);
36890
+ }
36891
+ }
36892
+ function isPlainObject2(value) {
36893
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36894
+ }
36895
+ function isValidRelPath(value) {
36896
+ return value === null || typeof value === "string" && isSafeRelativePath(value);
36897
+ }
36898
+ function writePlanStoreError(res, deps, error51) {
36899
+ if (error51 instanceof TheaterPathContextError) {
36900
+ const httpStatus2 = error51.code === "forbidden" ? 403 : error51.code === "not_found" ? 404 : 400;
36901
+ deps.writeJson(res, httpStatus2, { error: error51.code });
36902
+ return;
36903
+ }
36904
+ if (!(error51 instanceof PlanStoreError)) throw error51;
36905
+ const httpStatus = error51.code === "path_outside_theater" ? 403 : error51.code === "too_large" ? 413 : 404;
36906
+ deps.writeJson(res, httpStatus, { error: error51.code });
36907
+ }
36908
+
36113
36909
  // sdk/version.ts
36114
36910
  var SDK_API_VERSION = 1;
36115
36911
 
@@ -36231,14 +37027,14 @@ function renderShim(definition) {
36231
37027
  ].join("\n");
36232
37028
  }
36233
37029
  function isSubpath(root, target) {
36234
- const relative = path25__default.relative(root, target);
36235
- return relative === "" || !relative.startsWith("..") && !path25__default.isAbsolute(relative);
37030
+ const relative = path27__default.relative(root, target);
37031
+ return relative === "" || !relative.startsWith("..") && !path27__default.isAbsolute(relative);
36236
37032
  }
36237
37033
  function discoverFleetPlugins(options = {}) {
36238
- const builtInSourceRoot = options.builtInSourceRoot ?? (options.cwd ? path25__default.resolve(options.cwd, "runtime/fleet-plugins") : null);
37034
+ const builtInSourceRoot = options.builtInSourceRoot ?? (options.cwd ? path27__default.resolve(options.cwd, "runtime/fleet-plugins") : null);
36239
37035
  const roots = [
36240
37036
  ...builtInSourceRoot ? [{ root: builtInSourceRoot, builtInDistRoot: options.builtInDistRoot ?? null, external: false }] : [],
36241
- { root: path25__default.join(options.homeDir ?? os4__default.homedir(), ".fleet", "plugins"), builtInDistRoot: null, external: true }
37037
+ { root: path27__default.join(options.homeDir ?? os4__default.homedir(), ".fleet", "plugins"), builtInDistRoot: null, external: true }
36242
37038
  ];
36243
37039
  const sourcePlugins = roots.flatMap((root) => discoverPluginRoot(root.root, root.builtInDistRoot, root.external));
36244
37040
  const sourcePluginIds = new Set(sourcePlugins.map((plugin) => plugin.manifest.id));
@@ -36259,14 +37055,14 @@ function parseFleetPluginManifest(value) {
36259
37055
  };
36260
37056
  }
36261
37057
  function discoverPluginRoot(root, builtInDistRoot, external) {
36262
- if (!fs11__default.existsSync(root)) return [];
36263
- const entries = fs11__default.readdirSync(root, { withFileTypes: true });
37058
+ if (!fs14__default.existsSync(root)) return [];
37059
+ const entries = fs14__default.readdirSync(root, { withFileTypes: true });
36264
37060
  const plugins = [];
36265
37061
  for (const entry of entries) {
36266
37062
  if (!entry.isDirectory() || entry.name === "shared") continue;
36267
- const pluginRoot = path25__default.join(root, entry.name);
36268
- const manifestPath = path25__default.join(pluginRoot, "plugin.json");
36269
- if (!fs11__default.existsSync(manifestPath)) continue;
37063
+ const pluginRoot = path27__default.join(root, entry.name);
37064
+ const manifestPath = path27__default.join(pluginRoot, "plugin.json");
37065
+ if (!fs14__default.existsSync(manifestPath)) continue;
36270
37066
  const manifest = readManifest(manifestPath);
36271
37067
  if (!manifest) continue;
36272
37068
  const clientEntry = resolveOptionalManifestEntry(pluginRoot, manifest.client, "client");
@@ -36278,14 +37074,14 @@ function discoverPluginRoot(root, builtInDistRoot, external) {
36278
37074
  return plugins;
36279
37075
  }
36280
37076
  function discoverDistPluginRoot(root) {
36281
- if (!fs11__default.existsSync(root)) return [];
36282
- const entries = fs11__default.readdirSync(root, { withFileTypes: true });
37077
+ if (!fs14__default.existsSync(root)) return [];
37078
+ const entries = fs14__default.readdirSync(root, { withFileTypes: true });
36283
37079
  const plugins = [];
36284
37080
  for (const entry of entries) {
36285
37081
  if (!entry.isDirectory() || entry.name === "shared") continue;
36286
- const pluginRoot = path25__default.join(root, entry.name);
36287
- const manifestPath = path25__default.join(pluginRoot, "plugin.json");
36288
- const manifest = fs11__default.existsSync(manifestPath) ? readManifest(manifestPath) : readDistPluginManifest(entry.name);
37082
+ const pluginRoot = path27__default.join(root, entry.name);
37083
+ const manifestPath = path27__default.join(pluginRoot, "plugin.json");
37084
+ const manifest = fs14__default.existsSync(manifestPath) ? readManifest(manifestPath) : readDistPluginManifest(entry.name);
36289
37085
  if (!manifest) continue;
36290
37086
  const clientEntry = resolveOptionalManifestEntry(pluginRoot, manifest.client, "client");
36291
37087
  if (clientEntry === false) continue;
@@ -36297,7 +37093,7 @@ function discoverDistPluginRoot(root) {
36297
37093
  }
36298
37094
  function readManifest(manifestPath) {
36299
37095
  try {
36300
- const manifest = parseFleetPluginManifest(JSON.parse(fs11__default.readFileSync(manifestPath, "utf8")));
37096
+ const manifest = parseFleetPluginManifest(JSON.parse(fs14__default.readFileSync(manifestPath, "utf8")));
36301
37097
  if (!manifest) console.warn(`[fleet-console] Plugin manifest skipped: invalid manifest at ${manifestPath}`);
36302
37098
  return manifest;
36303
37099
  } catch (error51) {
@@ -36311,11 +37107,11 @@ function resolveRoutesEntry(pluginRoot, manifest, builtInDistRoot) {
36311
37107
  if (manifestRoute2 === false) return false;
36312
37108
  if (manifestRoute2) return manifestRoute2;
36313
37109
  }
36314
- const manifestRoute = path25__default.join(pluginRoot, "routes.ts");
36315
- if (fs11__default.existsSync(manifestRoute)) return manifestRoute;
37110
+ const manifestRoute = path27__default.join(pluginRoot, "routes.ts");
37111
+ if (fs14__default.existsSync(manifestRoute)) return manifestRoute;
36316
37112
  if (!builtInDistRoot) return null;
36317
- const builtRoute = path25__default.join(builtInDistRoot, manifest.id, "routes.mjs");
36318
- return fs11__default.existsSync(builtRoute) ? builtRoute : null;
37113
+ const builtRoute = path27__default.join(builtInDistRoot, manifest.id, "routes.mjs");
37114
+ return fs14__default.existsSync(builtRoute) ? builtRoute : null;
36319
37115
  }
36320
37116
  function resolveDistRoutesEntry(pluginRoot, manifest) {
36321
37117
  const routesEntry = resolveOptionalManifestEntry(pluginRoot, manifest.routes ?? "routes.mjs", "routes");
@@ -36328,10 +37124,10 @@ function resolveOptionalManifestEntry(pluginRoot, entryPath, label) {
36328
37124
  console.warn(`[fleet-console] Plugin ${label} entry skipped: unsafe relative path ${entryPath}`);
36329
37125
  return false;
36330
37126
  }
36331
- const candidate = path25__default.resolve(pluginRoot, entryPath);
36332
- if (!fs11__default.existsSync(candidate)) return null;
36333
- const rootRealpath = fs11__default.realpathSync.native(pluginRoot);
36334
- const entryRealpath = fs11__default.realpathSync.native(candidate);
37127
+ const candidate = path27__default.resolve(pluginRoot, entryPath);
37128
+ if (!fs14__default.existsSync(candidate)) return null;
37129
+ const rootRealpath = fs14__default.realpathSync.native(pluginRoot);
37130
+ const entryRealpath = fs14__default.realpathSync.native(candidate);
36335
37131
  if (!isSubpath2(rootRealpath, entryRealpath)) {
36336
37132
  console.warn(`[fleet-console] Plugin ${label} entry skipped: path escapes plugin root ${entryPath}`);
36337
37133
  return false;
@@ -36346,12 +37142,12 @@ function readDistPluginManifest(id) {
36346
37142
  return pluginId ? { id: pluginId, routes: "routes.mjs" } : null;
36347
37143
  }
36348
37144
  function isSafeRelativeEntry(entryPath) {
36349
- if (path25__default.isAbsolute(entryPath)) return false;
37145
+ if (path27__default.isAbsolute(entryPath)) return false;
36350
37146
  return entryPath.split(/[\\/]+/u).every((segment) => segment !== "..");
36351
37147
  }
36352
37148
  function isSubpath2(root, candidate) {
36353
- const relative = path25__default.relative(root, candidate);
36354
- return relative === "" || !relative.startsWith("..") && !path25__default.isAbsolute(relative);
37149
+ const relative = path27__default.relative(root, candidate);
37150
+ return relative === "" || !relative.startsWith("..") && !path27__default.isAbsolute(relative);
36355
37151
  }
36356
37152
  function isRecord9(value) {
36357
37153
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -36369,17 +37165,22 @@ var PLUGIN_DEV_EXTERNALS = [
36369
37165
  "child_process",
36370
37166
  "node-pty",
36371
37167
  "ws",
36372
- "@fleet-console/sdk",
36373
- "@fleet-console/sdk/*",
36374
- "@dotobokuri/*",
36375
37168
  "@fleet-plugins/*"
36376
37169
  ];
36377
- var __dirname$1 = path25__default.dirname(fileURLToPath(import.meta.url));
37170
+ var PLUGIN_BUNDLE_DIR_PREFIX = "fleet-console-plugin-";
37171
+ var PLUGIN_BUNDLE_OWNER_FILE = ".fleet-console-plugin-owner.json";
37172
+ var PLUGIN_BUNDLE_PID_PATTERN = /^fleet-console-plugin-(\d+)-/u;
37173
+ var MAX_PLUGIN_BUNDLE_OWNER_PID = 2147483647;
37174
+ var __dirname$1 = path27__default.dirname(fileURLToPath(import.meta.url));
36378
37175
  function createFleetPluginHost(deps) {
36379
37176
  const plugins = filterDiscoveredPlugins(discoverFleetPlugins(deps));
36380
37177
  const sensitiveFieldsByPluginId = new Map(plugins.map((plugin) => [plugin.manifest.id, plugin.manifest.sensitiveFields ?? []]));
36381
- const importModule = deps.importModule ?? importPluginModule;
37178
+ const generatedBundleDirs = /* @__PURE__ */ new Set();
37179
+ const isProcessAlive2 = deps.isProcessAlive ?? isPluginBundleOwnerAlive;
37180
+ const removeBundleDir = deps.removeBundleDir ?? removePluginBundleDir;
37181
+ const importModule = deps.importModule ?? ((entry) => importPluginModule(entry, deps.bundleCacheDir, generatedBundleDirs));
36382
37182
  async function boot() {
37183
+ await removeStalePluginBundleDirs(deps.bundleCacheDir, isProcessAlive2, removeBundleDir);
36383
37184
  for (const plugin of plugins) {
36384
37185
  if (!plugin.routesEntry) continue;
36385
37186
  if (!plugin.external) {
@@ -36393,7 +37194,21 @@ function createFleetPluginHost(deps) {
36393
37194
  }
36394
37195
  }
36395
37196
  }
36396
- return { plugins, sensitiveFieldsByPluginId, boot };
37197
+ async function cleanup() {
37198
+ const dirs = [...generatedBundleDirs];
37199
+ const cleanupResults = await Promise.allSettled(dirs.map((dir) => removeBundleDir(dir)));
37200
+ for (const [index, result] of cleanupResults.entries()) {
37201
+ const dir = dirs[index];
37202
+ if (result.status === "fulfilled") {
37203
+ generatedBundleDirs.delete(dir);
37204
+ continue;
37205
+ }
37206
+ if (result.status === "rejected") {
37207
+ console.warn(`[fleet-console] Plugin bundle cache cleanup failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
37208
+ }
37209
+ }
37210
+ }
37211
+ return { plugins, sensitiveFieldsByPluginId, boot, cleanup };
36397
37212
  async function bootPluginRoutes(plugin) {
36398
37213
  const mod = await importModule(plugin.routesEntry);
36399
37214
  const register = resolveRegister(mod);
@@ -36427,7 +37242,7 @@ function filterDiscoveredPlugins(plugins) {
36427
37242
  }
36428
37243
  return accepted;
36429
37244
  }
36430
- async function importPluginModule(entry) {
37245
+ async function importPluginModule(entry, bundleCacheDir, generatedBundleDirs) {
36431
37246
  if (entry.endsWith(".ts")) {
36432
37247
  const { build } = await import('esbuild');
36433
37248
  const output = await build({
@@ -36438,49 +37253,101 @@ async function importPluginModule(entry) {
36438
37253
  target: "node20",
36439
37254
  write: false,
36440
37255
  sourcemap: false,
36441
- external: PLUGIN_DEV_EXTERNALS
37256
+ external: PLUGIN_DEV_EXTERNALS,
37257
+ nodePaths: [resolveConsolePackageNodeModules()].filter((value) => value !== null)
36442
37258
  });
36443
37259
  const bundled = output.outputFiles[0]?.text;
36444
37260
  if (!bundled) throw new Error("plugin_bundle_failed");
36445
- const bundledPath = await writeTemporaryPluginBundle(entry, bundled);
37261
+ const bundledPath = await writeTemporaryPluginBundle(entry, bundled, bundleCacheDir, generatedBundleDirs);
36446
37262
  return await import(pathToFileURL(bundledPath).href);
36447
37263
  }
36448
37264
  return await import(pathToFileURL(entry).href);
36449
37265
  }
36450
- async function writeTemporaryPluginBundle(entry, source) {
36451
- const cacheRoot = path25__default.join(resolvePluginBundleNodeModules(entry), ".cache");
37266
+ async function writeTemporaryPluginBundle(entry, source, bundleCacheDir, generatedBundleDirs) {
37267
+ const cacheRoot = bundleCacheDir ?? path27__default.join(resolvePluginBundleNodeModules(entry), ".cache");
36452
37268
  await mkdir(cacheRoot, { recursive: true });
36453
- const dir = await mkdtemp(path25__default.join(cacheRoot, "fleet-console-plugin-"));
36454
- const file2 = path25__default.join(dir, "routes.mjs");
37269
+ const dir = await mkdtemp(path27__default.join(cacheRoot, `${PLUGIN_BUNDLE_DIR_PREFIX}${process.pid}-`));
37270
+ if (bundleCacheDir) {
37271
+ generatedBundleDirs.add(dir);
37272
+ await writePluginBundleOwner(dir, process.pid);
37273
+ }
37274
+ const file2 = path27__default.join(dir, "routes.mjs");
36455
37275
  await writeFile(file2, source, "utf8");
36456
37276
  return file2;
36457
37277
  }
37278
+ async function removeStalePluginBundleDirs(bundleCacheDir, isProcessAlive2, removeBundleDir) {
37279
+ if (!bundleCacheDir) return;
37280
+ try {
37281
+ await mkdir(bundleCacheDir, { recursive: true });
37282
+ const entries = await readdir(bundleCacheDir, { withFileTypes: true });
37283
+ for (const entry of entries) {
37284
+ if (!entry.name.startsWith(PLUGIN_BUNDLE_DIR_PREFIX)) continue;
37285
+ const entryPath = path27__default.join(bundleCacheDir, entry.name);
37286
+ const ownerPid = await readPluginBundleOwnerPid(entryPath, entry.name);
37287
+ if (ownerPid !== null && isProcessAlive2(ownerPid)) continue;
37288
+ try {
37289
+ await removeBundleDir(entryPath);
37290
+ } catch (error51) {
37291
+ console.warn(`[fleet-console] Plugin bundle cache startup cleanup failed: ${error51 instanceof Error ? error51.message : String(error51)}`);
37292
+ }
37293
+ }
37294
+ } catch (error51) {
37295
+ console.warn(`[fleet-console] Plugin bundle cache startup cleanup failed: ${error51 instanceof Error ? error51.message : String(error51)}`);
37296
+ }
37297
+ }
37298
+ async function removePluginBundleDir(dir) {
37299
+ await rm(dir, { recursive: true, force: true });
37300
+ }
37301
+ async function writePluginBundleOwner(dir, pid) {
37302
+ await writeFile(path27__default.join(dir, PLUGIN_BUNDLE_OWNER_FILE), JSON.stringify({ pid }), "utf8");
37303
+ }
37304
+ async function readPluginBundleOwnerPid(dir, name) {
37305
+ try {
37306
+ const parsed = JSON.parse(await readFile(path27__default.join(dir, PLUGIN_BUNDLE_OWNER_FILE), "utf8"));
37307
+ if (isPluginBundleOwnerPid(parsed.pid)) return parsed.pid;
37308
+ } catch {
37309
+ }
37310
+ const pid = Number(PLUGIN_BUNDLE_PID_PATTERN.exec(name)?.[1]);
37311
+ return isPluginBundleOwnerPid(pid) ? pid : null;
37312
+ }
37313
+ function isPluginBundleOwnerAlive(pid) {
37314
+ if (!isPluginBundleOwnerPid(pid)) return false;
37315
+ try {
37316
+ process.kill(pid, 0);
37317
+ return true;
37318
+ } catch (error51) {
37319
+ return Boolean(error51 && typeof error51 === "object" && "code" in error51 && error51.code === "EPERM");
37320
+ }
37321
+ }
37322
+ function isPluginBundleOwnerPid(value) {
37323
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_PLUGIN_BUNDLE_OWNER_PID;
37324
+ }
36458
37325
  function resolvePluginBundleNodeModules(entry) {
36459
37326
  const packageNodeModules = resolveConsolePackageNodeModules();
36460
- let dir = path25__default.dirname(entry);
37327
+ let dir = path27__default.dirname(entry);
36461
37328
  let fallback = packageNodeModules;
36462
37329
  while (true) {
36463
- const workspaceConsoleNodeModules = path25__default.join(dir, "runtime", "fleet-console", "node_modules");
37330
+ const workspaceConsoleNodeModules = path27__default.join(dir, "runtime", "fleet-console", "node_modules");
36464
37331
  if (hasFleetConsoleSdk(workspaceConsoleNodeModules)) return workspaceConsoleNodeModules;
36465
- const candidate = path25__default.join(dir, "node_modules");
37332
+ const candidate = path27__default.join(dir, "node_modules");
36466
37333
  if (hasFleetConsoleSdk(candidate)) return candidate;
36467
37334
  if (fallback === null && existsSync(candidate)) fallback = candidate;
36468
- const parent = path25__default.dirname(dir);
36469
- if (parent === dir) return fallback ?? path25__default.join(path25__default.dirname(entry), "node_modules");
37335
+ const parent = path27__default.dirname(dir);
37336
+ if (parent === dir) return fallback ?? path27__default.join(path27__default.dirname(entry), "node_modules");
36470
37337
  dir = parent;
36471
37338
  }
36472
37339
  }
36473
37340
  function resolveConsolePackageNodeModules() {
36474
37341
  for (const candidate of [
36475
- path25__default.resolve(__dirname$1, "..", "node_modules"),
36476
- path25__default.resolve(__dirname$1, "..", "..", "..", "node_modules")
37342
+ path27__default.resolve(__dirname$1, "..", "node_modules"),
37343
+ path27__default.resolve(__dirname$1, "..", "..", "..", "node_modules")
36477
37344
  ]) {
36478
37345
  if (hasFleetConsoleSdk(candidate)) return candidate;
36479
37346
  }
36480
37347
  return null;
36481
37348
  }
36482
37349
  function hasFleetConsoleSdk(nodeModules) {
36483
- return existsSync(path25__default.join(nodeModules, "@fleet-console", "sdk"));
37350
+ return existsSync(path27__default.join(nodeModules, "@fleet-console", "sdk"));
36484
37351
  }
36485
37352
  function resolveRegister(mod) {
36486
37353
  if (mod.register) return mod.register;
@@ -36526,6 +37393,8 @@ function normalizePrefix(prefix) {
36526
37393
  var RELEASE_NOTE_HEADINGS = ["Added", "Changed", "Fixed", "Removed", "Breaking Changes"];
36527
37394
  var VERSION_HEADER_PATTERN = /^## \[([^\]]+)\](?: - ([0-9]{4}-[0-9]{2}-[0-9]{2}))?$/;
36528
37395
  var SECTION_HEADER_PATTERN = /^### (Added|Changed|Fixed|Removed|Breaking Changes)$/;
37396
+ var PRODUCT_HEADER_PATTERN = /^### fleet-(cli|console|desktop|plugin|core)$/;
37397
+ var PRODUCT_SECTION_HEADER_PATTERN = /^#### (Added|Changed|Fixed|Removed|Breaking Changes)$/;
36529
37398
  var BULLET_PATTERN = /^- (.+)$/;
36530
37399
  var PACKAGE_TAG_PATTERN = /^\[([^\]]+)\]/;
36531
37400
  function parseConsoleReleaseNotes(changelog) {
@@ -36546,24 +37415,55 @@ function parseConsoleReleaseNotes(changelog) {
36546
37415
  return notes;
36547
37416
  }
36548
37417
  function collectSections(lines) {
36549
- const sections = [];
37418
+ const legacySections = /* @__PURE__ */ new Map();
37419
+ const productSections = /* @__PURE__ */ new Map();
37420
+ const legacyHeadings = /* @__PURE__ */ new Set();
36550
37421
  let current = null;
37422
+ let isProductContext = false;
36551
37423
  for (const line of lines) {
36552
37424
  const sectionMatch = SECTION_HEADER_PATTERN.exec(line);
36553
37425
  if (sectionMatch !== null) {
36554
- current = { heading: sectionMatch[1], items: [] };
36555
- sections.push(current);
37426
+ const heading = sectionMatch[1];
37427
+ isProductContext = false;
37428
+ current = legacyHeadings.has(heading) ? null : getSection(legacySections, heading);
37429
+ legacyHeadings.add(heading);
37430
+ continue;
37431
+ }
37432
+ if (PRODUCT_HEADER_PATTERN.test(line)) {
37433
+ isProductContext = true;
37434
+ current = null;
36556
37435
  continue;
36557
37436
  }
36558
- if (current === null) continue;
36559
37437
  if (line.startsWith("### ")) {
37438
+ isProductContext = false;
37439
+ current = null;
37440
+ continue;
37441
+ }
37442
+ const productSectionMatch = PRODUCT_SECTION_HEADER_PATTERN.exec(line);
37443
+ if (productSectionMatch !== null) {
37444
+ current = isProductContext ? getSection(productSections, productSectionMatch[1]) : null;
37445
+ continue;
37446
+ }
37447
+ if (current === null) continue;
37448
+ if (line.startsWith("### ") || line.startsWith("#### ")) {
36560
37449
  current = null;
36561
37450
  continue;
36562
37451
  }
36563
37452
  const bulletMatch = BULLET_PATTERN.exec(line);
36564
37453
  if (bulletMatch !== null) current.items.push(parseReleaseNoteItem(bulletMatch[1] ?? ""));
36565
37454
  }
36566
- return RELEASE_NOTE_HEADINGS.map((heading) => sections.find((section2) => section2.heading === heading)).filter((section2) => Boolean(section2 && section2.items.length > 0));
37455
+ return RELEASE_NOTE_HEADINGS.map((heading) => combineSections(heading, legacySections.get(heading), productSections.get(heading))).filter((section2) => Boolean(section2 && section2.items.length > 0));
37456
+ }
37457
+ function combineSections(heading, legacySection, productSection) {
37458
+ if (legacySection === void 0 && productSection === void 0) return void 0;
37459
+ return { heading, items: [...legacySection?.items ?? [], ...productSection?.items ?? []] };
37460
+ }
37461
+ function getSection(sections, heading) {
37462
+ const existing = sections.get(heading);
37463
+ if (existing !== void 0) return existing;
37464
+ const section2 = { heading, items: [] };
37465
+ sections.set(heading, section2);
37466
+ return section2;
36567
37467
  }
36568
37468
  function parseReleaseNoteItem(rawText) {
36569
37469
  const packageTags = [];
@@ -36589,10 +37489,11 @@ var ConsoleReleaseNotesUnavailableError = class extends Error {
36589
37489
 
36590
37490
  // core/host/release-notes/service.ts
36591
37491
  var RAW_CHANGELOG_URL = "https://raw.githubusercontent.com/sbluemin/fleet-harness/main/CHANGELOG.md";
37492
+ var RAW_KOREAN_CHANGELOG_URL = "https://raw.githubusercontent.com/sbluemin/fleet-harness/main/CHANGELOG.ko.md";
36592
37493
  var SOURCE_REF = "main";
36593
37494
  var FETCH_TIMEOUT_MS = 3e3;
36594
37495
  var MAX_CHANGELOG_BYTES = 1024 * 1024;
36595
- var SUCCESS_TTL_MS = 60 * 60 * 1e3;
37496
+ var SUCCESS_TTL_MS2 = 60 * 60 * 1e3;
36596
37497
  var NEGATIVE_TTL_MS = 30 * 1e3;
36597
37498
  function createConsoleReleaseNotesService(deps = {}) {
36598
37499
  const fetchImpl = deps.fetchImpl ?? fetch;
@@ -36603,30 +37504,46 @@ function createConsoleReleaseNotesService(deps = {}) {
36603
37504
  let lastFailureAt = 0;
36604
37505
  let inFlight = null;
36605
37506
  let forceInFlight = null;
37507
+ let koreanLastSuccess = null;
37508
+ let koreanLastFailureAt = 0;
37509
+ let koreanInFlight = null;
37510
+ let koreanForceInFlight = null;
37511
+ let koreanRequestGeneration = 0;
36606
37512
  async function refresh(options = {}) {
37513
+ if (options.locale === "ko") return await refreshKoreanOverlay(options);
37514
+ return await refreshEnglish(options);
37515
+ }
37516
+ async function refreshEnglish(options) {
36607
37517
  const currentTime = now();
36608
37518
  if (options.force) {
36609
- if (forceInFlight) return forceInFlight;
37519
+ if (forceInFlight) return await forceInFlight;
36610
37520
  const pending2 = runFetch().finally(() => {
36611
37521
  if (forceInFlight === pending2) forceInFlight = null;
36612
37522
  });
36613
37523
  forceInFlight = pending2;
36614
- return pending2;
37524
+ return await pending2;
36615
37525
  }
36616
- if (lastSuccess && currentTime - lastSuccess.fetchedAt < SUCCESS_TTL_MS) return lastSuccess;
37526
+ if (lastSuccess && currentTime - lastSuccess.fetchedAt < SUCCESS_TTL_MS2) return lastSuccess;
36617
37527
  if (lastFailureAt > 0 && currentTime - lastFailureAt < NEGATIVE_TTL_MS) {
36618
37528
  if (lastSuccess) return { ...lastSuccess, stale: true };
36619
37529
  throw new ConsoleReleaseNotesUnavailableError("negative_cache");
36620
37530
  }
36621
- if (inFlight) return inFlight;
37531
+ if (inFlight) return await inFlight;
36622
37532
  const pending = runFetch().finally(() => {
36623
37533
  if (inFlight === pending) inFlight = null;
36624
37534
  });
36625
37535
  inFlight = pending;
36626
- return pending;
37536
+ return await pending;
37537
+ }
37538
+ async function refreshKoreanOverlay(options) {
37539
+ const english = refreshEnglish(options);
37540
+ const korean = refreshKorean(options).catch(() => null);
37541
+ const canonical = await english;
37542
+ const localized = await korean;
37543
+ return mergeKoreanOverlay(canonical, localized?.notes ?? null);
36627
37544
  }
36628
37545
  function runFetch() {
36629
- return fetchReleaseNotes().then((result) => {
37546
+ return fetchEnglishReleaseNotes().then((result) => {
36630
37547
  lastSuccess = result;
36631
37548
  lastFailureAt = 0;
36632
37549
  return result;
@@ -36637,29 +37554,93 @@ function createConsoleReleaseNotesService(deps = {}) {
36637
37554
  throw new ConsoleReleaseNotesUnavailableError("cold_unavailable");
36638
37555
  });
36639
37556
  }
36640
- async function fetchReleaseNotes() {
37557
+ async function fetchEnglishReleaseNotes() {
37558
+ return {
37559
+ notes: await fetchAndParse(RAW_CHANGELOG_URL),
37560
+ sourceRef: SOURCE_REF,
37561
+ fetchedAt: now(),
37562
+ stale: false
37563
+ };
37564
+ }
37565
+ async function refreshKorean(options) {
37566
+ const currentTime = now();
37567
+ if (options.force) {
37568
+ if (koreanForceInFlight) return await koreanForceInFlight;
37569
+ const pending2 = runKoreanFetch(++koreanRequestGeneration).finally(() => {
37570
+ if (koreanForceInFlight === pending2) koreanForceInFlight = null;
37571
+ });
37572
+ koreanForceInFlight = pending2;
37573
+ return await pending2;
37574
+ }
37575
+ if (koreanLastSuccess && currentTime - koreanLastSuccess.fetchedAt < SUCCESS_TTL_MS2) return koreanLastSuccess;
37576
+ if (koreanLastFailureAt > 0 && currentTime - koreanLastFailureAt < NEGATIVE_TTL_MS) return null;
37577
+ if (koreanInFlight) return await koreanInFlight;
37578
+ const pending = runKoreanFetch(++koreanRequestGeneration).finally(() => {
37579
+ if (koreanInFlight === pending) koreanInFlight = null;
37580
+ });
37581
+ koreanInFlight = pending;
37582
+ return await pending;
37583
+ }
37584
+ function runKoreanFetch(requestGeneration) {
37585
+ return fetchAndParse(RAW_KOREAN_CHANGELOG_URL).then((notes) => {
37586
+ const result = { notes, fetchedAt: now() };
37587
+ if (requestGeneration === koreanRequestGeneration) {
37588
+ koreanLastSuccess = result;
37589
+ koreanLastFailureAt = 0;
37590
+ }
37591
+ return result;
37592
+ }).catch(() => {
37593
+ if (requestGeneration === koreanRequestGeneration) koreanLastFailureAt = now();
37594
+ return null;
37595
+ });
37596
+ }
37597
+ async function fetchAndParse(url2) {
36641
37598
  const controller = new AbortController();
36642
37599
  const timer = setTimer(() => controller.abort(), FETCH_TIMEOUT_MS);
36643
37600
  timer.unref?.();
36644
37601
  try {
36645
- const response = await fetchImpl(RAW_CHANGELOG_URL, {
37602
+ const response = await fetchImpl(url2, {
36646
37603
  headers: { Accept: "text/plain; charset=utf-8" },
36647
37604
  signal: controller.signal
36648
37605
  });
36649
37606
  if (!response.ok) throw new ConsoleReleaseNotesUnavailableError("cold_unavailable");
36650
37607
  const text = await readTextWithByteLimit(response, controller);
36651
- return {
36652
- notes: parseConsoleReleaseNotes(text),
36653
- sourceRef: SOURCE_REF,
36654
- fetchedAt: now(),
36655
- stale: false
36656
- };
37608
+ return parseConsoleReleaseNotes(text).map((note) => ({ ...note, localizationFallback: false }));
36657
37609
  } finally {
36658
37610
  clearTimer(timer);
36659
37611
  }
36660
37612
  }
36661
37613
  return { refresh };
36662
37614
  }
37615
+ function mergeKoreanOverlay(canonical, koreanNotes) {
37616
+ if (koreanNotes === null) return { ...canonical, notes: canonical.notes.map(withFallback) };
37617
+ const occurrences = /* @__PURE__ */ new Map();
37618
+ for (const note of koreanNotes) {
37619
+ const queue = occurrences.get(note.version) ?? [];
37620
+ queue.push(note);
37621
+ occurrences.set(note.version, queue);
37622
+ }
37623
+ return {
37624
+ ...canonical,
37625
+ notes: canonical.notes.map((english) => {
37626
+ const korean = occurrences.get(english.version)?.shift();
37627
+ return korean && hasMatchingStructure(english, korean) ? { ...english, sections: korean.sections, localizationFallback: false } : withFallback(english);
37628
+ })
37629
+ };
37630
+ }
37631
+ function withFallback(note) {
37632
+ return { ...note, localizationFallback: true };
37633
+ }
37634
+ function hasMatchingStructure(english, korean) {
37635
+ return english.date === korean.date && english.sections.length === korean.sections.length && english.sections.every((section2, sectionIndex) => {
37636
+ const localizedSection = korean.sections[sectionIndex];
37637
+ return localizedSection?.heading === section2.heading && localizedSection.items.length === section2.items.length && section2.items.every((item, itemIndex) => sameTags(item.packageTags, localizedSection.items[itemIndex]?.packageTags));
37638
+ });
37639
+ }
37640
+ function sameTags(left, right) {
37641
+ if (right === void 0) return false;
37642
+ return left.length === right.length && left.every((tag, index) => tag === right[index]);
37643
+ }
36663
37644
  async function readTextWithByteLimit(response, controller) {
36664
37645
  const body = response.body;
36665
37646
  if (body === null) return await response.text();
@@ -36756,7 +37737,7 @@ var MIME_TYPES = {
36756
37737
  ".woff2": "font/woff2"
36757
37738
  };
36758
37739
  function createStaticConsoleHandler(packageRoot) {
36759
- const consoleRoot = path25__default.join(packageRoot, "dist", "client");
37740
+ const consoleRoot = path27__default.join(packageRoot, "dist", "client");
36760
37741
  return (req, res, pathname) => tryServeStaticConsole(req, res, pathname, consoleRoot);
36761
37742
  }
36762
37743
  function tryServeStaticConsole(req, res, pathname, consoleRoot) {
@@ -36772,15 +37753,15 @@ function tryServeStaticConsole(req, res, pathname, consoleRoot) {
36772
37753
  res.end(JSON.stringify({ error: "Not found" }));
36773
37754
  return true;
36774
37755
  }
36775
- const absolutePath = path25__default.resolve(consoleRoot, relativePath2);
37756
+ const absolutePath = path27__default.resolve(consoleRoot, relativePath2);
36776
37757
  if (!absolutePath.startsWith(consoleRoot)) {
36777
37758
  res.writeHead(404, withSecurityHeaders2({ "Content-Type": "application/json" }));
36778
37759
  res.end(JSON.stringify({ error: "Not found" }));
36779
37760
  return true;
36780
37761
  }
36781
37762
  try {
36782
- const data = fs11__default.readFileSync(absolutePath);
36783
- const contentType = MIME_TYPES[path25__default.extname(absolutePath)] ?? "application/octet-stream";
37763
+ const data = fs14__default.readFileSync(absolutePath);
37764
+ const contentType = MIME_TYPES[path27__default.extname(absolutePath)] ?? "application/octet-stream";
36784
37765
  res.writeHead(200, withSecurityHeaders2({ "Content-Type": contentType }));
36785
37766
  if (req.method === "HEAD") {
36786
37767
  res.end();
@@ -36800,7 +37781,7 @@ function tryServeStaticConsole(req, res, pathname, consoleRoot) {
36800
37781
  }
36801
37782
  function serveFallbackIndex(req, res, consoleRoot) {
36802
37783
  try {
36803
- const data = fs11__default.readFileSync(path25__default.join(consoleRoot, "index.html"));
37784
+ const data = fs14__default.readFileSync(path27__default.join(consoleRoot, "index.html"));
36804
37785
  res.writeHead(200, withSecurityHeaders2({ "Content-Type": MIME_TYPES[".html"] }));
36805
37786
  if (req.method === "HEAD") {
36806
37787
  res.end();
@@ -36826,7 +37807,7 @@ function resolveConsolePath(pathname) {
36826
37807
  }
36827
37808
  if (withoutPrefix === "") return "index.html";
36828
37809
  if (withoutPrefix.includes("\0")) return null;
36829
- if (!path25__default.extname(withoutPrefix)) return "index.html";
37810
+ if (!path27__default.extname(withoutPrefix)) return "index.html";
36830
37811
  return withoutPrefix;
36831
37812
  }
36832
37813
  var TheaterFolderListError = class extends Error {
@@ -36837,12 +37818,12 @@ var TheaterFolderListError = class extends Error {
36837
37818
  this.code = code;
36838
37819
  }
36839
37820
  };
36840
- var DIRECTORY_ENTRY_CAP = 500;
37821
+ var DIRECTORY_ENTRY_CAP2 = 500;
36841
37822
  var WINDOWS_DRIVE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
36842
37823
  async function listTheaterFolders(requestedPath, deps = {}) {
36843
37824
  const platform = deps.platform ?? process.platform;
36844
- const stat5 = deps.stat ?? fs11__default.promises.stat;
36845
- const opendir = deps.opendir ?? fs11__default.promises.opendir;
37825
+ const stat5 = deps.stat ?? fs14__default.promises.stat;
37826
+ const opendir = deps.opendir ?? fs14__default.promises.opendir;
36846
37827
  const targetPath = normalizeListPath(requestedPath, platform, deps);
36847
37828
  const roots = await listRoots(platform, stat5);
36848
37829
  const targetStat = await statDirectory(targetPath, stat5);
@@ -36860,14 +37841,14 @@ async function listTheaterFolders(requestedPath, deps = {}) {
36860
37841
  }
36861
37842
  function normalizeFolderBrowserPath(value, platform = process.platform) {
36862
37843
  if (typeof value !== "string" || value.length === 0 || value.includes("\0")) throw new TheaterFolderListError("invalid_path");
36863
- if (isWindowsAmbiguousPath(value, platform) || !path25__default.isAbsolute(value)) throw new TheaterFolderListError("invalid_path");
36864
- return path25__default.resolve(value);
37844
+ if (isWindowsAmbiguousPath(value, platform) || !path27__default.isAbsolute(value)) throw new TheaterFolderListError("invalid_path");
37845
+ return path27__default.resolve(value);
36865
37846
  }
36866
37847
  function normalizeListPath(requestedPath, platform, deps) {
36867
37848
  if (requestedPath === null || requestedPath === void 0) {
36868
37849
  const home = deps.homedir?.() ?? os4__default.homedir();
36869
37850
  const start = home || deps.cwd?.() || process.cwd();
36870
- return path25__default.resolve(start);
37851
+ return path27__default.resolve(start);
36871
37852
  }
36872
37853
  return normalizeFolderBrowserPath(requestedPath, platform);
36873
37854
  }
@@ -36887,7 +37868,7 @@ async function statDirectory(targetPath, stat5) {
36887
37868
  try {
36888
37869
  return await stat5(targetPath);
36889
37870
  } catch (error51) {
36890
- throw mapFsError(error51);
37871
+ throw mapFsError2(error51);
36891
37872
  }
36892
37873
  }
36893
37874
  async function collectDirectoryEntries(targetPath, opendir, stat5, entries) {
@@ -36897,14 +37878,14 @@ async function collectDirectoryEntries(targetPath, opendir, stat5, entries) {
36897
37878
  while (dirent !== null) {
36898
37879
  const entry = await toTheaterFolderEntry(targetPath, dirent, stat5);
36899
37880
  if (entry !== null) {
36900
- if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
37881
+ if (entries.length >= DIRECTORY_ENTRY_CAP2) return true;
36901
37882
  entries.push(entry);
36902
37883
  }
36903
37884
  dirent = await directory.read();
36904
37885
  }
36905
37886
  return false;
36906
37887
  } catch (error51) {
36907
- throw mapFsError(error51);
37888
+ throw mapFsError2(error51);
36908
37889
  } finally {
36909
37890
  await directory.close();
36910
37891
  }
@@ -36913,12 +37894,12 @@ async function openDirectory(targetPath, opendir) {
36913
37894
  try {
36914
37895
  return await opendir(targetPath);
36915
37896
  } catch (error51) {
36916
- throw mapFsError(error51);
37897
+ throw mapFsError2(error51);
36917
37898
  }
36918
37899
  }
36919
37900
  async function toTheaterFolderEntry(targetPath, dirent, stat5) {
36920
37901
  if (!dirent.isDirectory() && !dirent.isSymbolicLink()) return null;
36921
- const entryPath = path25__default.join(targetPath, dirent.name);
37902
+ const entryPath = path27__default.join(targetPath, dirent.name);
36922
37903
  if (dirent.isDirectory()) return { name: dirent.name, path: entryPath, kind: "dir", accessible: true };
36923
37904
  const accessible = await statSymlinkDirectory(entryPath, stat5);
36924
37905
  if (accessible === null) return null;
@@ -36931,17 +37912,17 @@ async function statSymlinkDirectory(entryPath, stat5) {
36931
37912
  return false;
36932
37913
  }
36933
37914
  }
36934
- function mapFsError(error51) {
37915
+ function mapFsError2(error51) {
36935
37916
  const code = error51.code;
36936
37917
  if (code === "EACCES" || code === "EPERM") return new TheaterFolderListError("forbidden");
36937
37918
  if (code === "ENOENT" || code === "ENOTDIR") return new TheaterFolderListError("not_found");
36938
37919
  return new TheaterFolderListError("invalid_path");
36939
37920
  }
36940
37921
  function parentPath(targetPath, platform) {
36941
- const parsed = path25__default.parse(targetPath);
36942
- const resolved = path25__default.resolve(targetPath);
36943
- if (resolved === path25__default.resolve(parsed.root)) return null;
36944
- const parent = path25__default.dirname(resolved);
37922
+ const parsed = path27__default.parse(targetPath);
37923
+ const resolved = path27__default.resolve(targetPath);
37924
+ if (resolved === path27__default.resolve(parsed.root)) return null;
37925
+ const parent = path27__default.dirname(resolved);
36945
37926
  if (platform === "win32" && parent === resolved) return null;
36946
37927
  return parent;
36947
37928
  }
@@ -36953,7 +37934,7 @@ var GRANT_ID_BYTES = 24;
36953
37934
  var DEFAULT_GRANT_TTL_MS = 6e4;
36954
37935
  function createFolderGrantStore(deps = {}) {
36955
37936
  const randomId = deps.randomId ?? (() => crypto5.randomBytes(GRANT_ID_BYTES).toString("base64url"));
36956
- const statSync2 = deps.statSync ?? fs11__default.statSync;
37937
+ const statSync2 = deps.statSync ?? fs14__default.statSync;
36957
37938
  const ttlMs = deps.ttlMs ?? DEFAULT_GRANT_TTL_MS;
36958
37939
  const now = deps.now ?? Date.now;
36959
37940
  const grants = /* @__PURE__ */ new Map();
@@ -36979,10 +37960,10 @@ function createFolderGrantStore(deps = {}) {
36979
37960
  }
36980
37961
  return { issue: issue2, consume };
36981
37962
  }
36982
- function validateAbsoluteDirectory(cwd, statSync2 = fs11__default.statSync) {
37963
+ function validateAbsoluteDirectory(cwd, statSync2 = fs14__default.statSync) {
36983
37964
  if (typeof cwd !== "string" || cwd.length === 0 || cwd.includes("\0")) throw new Error("invalid_folder");
36984
- if (isWindowsAmbiguousPath2(cwd) || !path25__default.isAbsolute(cwd)) throw new Error("invalid_folder");
36985
- const normalized = path25__default.resolve(cwd);
37965
+ if (isWindowsAmbiguousPath2(cwd) || !path27__default.isAbsolute(cwd)) throw new Error("invalid_folder");
37966
+ const normalized = path27__default.resolve(cwd);
36986
37967
  if (!statSync2(normalized).isDirectory()) throw new Error("invalid_folder");
36987
37968
  return normalized;
36988
37969
  }
@@ -36990,11 +37971,178 @@ function isWindowsAmbiguousPath2(value) {
36990
37971
  if (process.platform !== "win32") return false;
36991
37972
  return /^[a-zA-Z]:(?![\\/])/.test(value) || /^[\\/](?![\\/])/.test(value);
36992
37973
  }
37974
+ var execFileAsync = promisify(execFile);
37975
+ var GIT_TIMEOUT_MS = 3e3;
37976
+ var GIT_MAX_BUFFER = 1e6;
37977
+ async function listTheaterWorktrees(theaterRoot, deps = {}) {
37978
+ const exec = deps.execFile ?? ((file2, args, options) => execFileAsync(file2, args, options));
37979
+ const realpath2 = deps.realpath ?? fs14__default.promises.realpath;
37980
+ let stdout;
37981
+ try {
37982
+ ({ stdout } = await exec("git", ["worktree", "list", "--porcelain"], { cwd: theaterRoot, env: { ...process.env, LC_ALL: "C" }, shell: false, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER }));
37983
+ } catch (error51) {
37984
+ if (isNotGitError(error51)) return { isGitRepo: false, worktrees: [] };
37985
+ throw error51;
37986
+ }
37987
+ const realRoot = await realpath2(theaterRoot);
37988
+ const records = parseGitWorktreePorcelain(stdout);
37989
+ const worktrees = [];
37990
+ for (const record2 of records) {
37991
+ try {
37992
+ const real = await realpath2(record2.worktree);
37993
+ if (!isWithinRoot3(real, realRoot) || real === realRoot) continue;
37994
+ worktrees.push({ relPath: path27__default.relative(realRoot, real).split(path27__default.sep).join("/"), branch: record2.branch, isCurrent: real === realRoot });
37995
+ } catch {
37996
+ }
37997
+ }
37998
+ return { isGitRepo: true, worktrees };
37999
+ }
38000
+ function parseGitWorktreePorcelain(value) {
38001
+ const records = [];
38002
+ for (const block of value.split("\n\n")) {
38003
+ const fields = block.split("\n");
38004
+ const worktree = fields.find((field) => field.startsWith("worktree "))?.slice("worktree ".length);
38005
+ if (!worktree) continue;
38006
+ const branchRef = fields.find((field) => field.startsWith("branch "))?.slice("branch ".length) ?? null;
38007
+ records.push({ worktree, branch: branchRef?.replace(/^refs\/heads\//, "") ?? null, detached: fields.includes("detached") });
38008
+ }
38009
+ return records;
38010
+ }
38011
+ function isWithinRoot3(candidate, root) {
38012
+ const normalizedRoot = root.endsWith(path27__default.sep) ? root : `${root}${path27__default.sep}`;
38013
+ return candidate === root || candidate.startsWith(normalizedRoot);
38014
+ }
38015
+ function isNotGitError(error51) {
38016
+ const value = error51;
38017
+ const text = `${typeof value.stderr === "string" ? value.stderr : ""}
38018
+ ${typeof value.message === "string" ? value.message : ""}`;
38019
+ return /not a git repository/i.test(text);
38020
+ }
38021
+
38022
+ // core/host/theater-path-context-routes.ts
38023
+ function createTheaterPathContextRouter(deps) {
38024
+ return async function handleTheaterPathContextRoute({ req, res, pathname }) {
38025
+ const match = pathname.match(/^\/api\/v1\/theaters\/([^/]+)\/path-context(?:\/(worktrees|directories))?$/u);
38026
+ if (!match) return false;
38027
+ const theater = deps.getTheater(decodeURIComponent(match[1] ?? ""));
38028
+ if (!theater) {
38029
+ deps.writeJson(res, 400, { error: "invalid_theater" });
38030
+ return true;
38031
+ }
38032
+ const action = match[2] ?? "context";
38033
+ if (action === "context") await handleContext(req, res, theater, deps);
38034
+ else if (action === "worktrees") await handleWorktrees(req, res, theater, deps);
38035
+ else await handleDirectories(req, res, theater, deps);
38036
+ return true;
38037
+ };
38038
+ }
38039
+ async function handleContext(req, res, theater, deps) {
38040
+ if (req.method === "GET") {
38041
+ await writeContext(res, theater, deps);
38042
+ return;
38043
+ }
38044
+ if (req.method !== "PUT") {
38045
+ deps.writeJson(res, 405, { error: "method_not_allowed" });
38046
+ return;
38047
+ }
38048
+ if (!deps.isAuthorized(req)) {
38049
+ deps.writeJson(res, 401, { error: "unauthorized" });
38050
+ return;
38051
+ }
38052
+ const body = await deps.readJsonBody(req);
38053
+ if (!isPlainObject3(body) || !(body.relPath === null || typeof body.relPath === "string") || Object.keys(body).length !== 1) {
38054
+ deps.writeJson(res, 400, { error: "invalid_request" });
38055
+ return;
38056
+ }
38057
+ try {
38058
+ const resolved = await resolveTheaterPathContext(theater.realpath, body.relPath);
38059
+ const updated = deps.setPathContext(theater.id, resolved.relPath);
38060
+ if (!updated) {
38061
+ deps.writeJson(res, 404, { error: "theater_not_found" });
38062
+ return;
38063
+ }
38064
+ deps.persist();
38065
+ deps.writeJson(res, 200, await toContextDto(updated, deps));
38066
+ } catch (error51) {
38067
+ writePathError2(res, deps, error51);
38068
+ }
38069
+ }
38070
+ async function handleWorktrees(req, res, theater, deps) {
38071
+ if (req.method !== "GET") {
38072
+ deps.writeJson(res, 405, { error: "method_not_allowed" });
38073
+ return;
38074
+ }
38075
+ try {
38076
+ deps.writeJson(res, 200, await listTheaterWorktrees(theater.realpath));
38077
+ } catch {
38078
+ deps.writeJson(res, 500, { error: "worktree_discovery_failed" });
38079
+ }
38080
+ }
38081
+ async function handleDirectories(req, res, theater, deps) {
38082
+ if (req.method !== "POST") {
38083
+ deps.writeJson(res, 405, { error: "method_not_allowed" });
38084
+ return;
38085
+ }
38086
+ if (!deps.isAuthorized(req)) {
38087
+ deps.writeJson(res, 401, { error: "unauthorized" });
38088
+ return;
38089
+ }
38090
+ const body = await deps.readJsonBody(req);
38091
+ if (!isPlainObject3(body) || !(body.relativePath === null || typeof body.relativePath === "string") || Object.keys(body).length !== 1) {
38092
+ deps.writeJson(res, 400, { error: "invalid_request" });
38093
+ return;
38094
+ }
38095
+ try {
38096
+ deps.writeJson(res, 200, { directories: await listTheaterDirectories(theater.realpath, body.relativePath) });
38097
+ } catch (error51) {
38098
+ writePathError2(res, deps, error51);
38099
+ }
38100
+ }
38101
+ async function writeContext(res, theater, deps) {
38102
+ try {
38103
+ deps.writeJson(res, 200, await toContextDto(theater, deps));
38104
+ } catch (error51) {
38105
+ if (error51 instanceof TheaterPathContextError && theater.pathContext !== null) {
38106
+ const healed = deps.setPathContext(theater.id, null);
38107
+ if (healed) {
38108
+ deps.persist();
38109
+ try {
38110
+ deps.writeJson(res, 200, await toContextDto(healed, deps));
38111
+ return;
38112
+ } catch (rootError) {
38113
+ writePathError2(res, deps, rootError);
38114
+ return;
38115
+ }
38116
+ }
38117
+ }
38118
+ writePathError2(res, deps, error51);
38119
+ }
38120
+ }
38121
+ async function toContextDto(theater, _deps) {
38122
+ const resolved = await resolveTheaterPathContext(theater.realpath, theater.pathContext);
38123
+ let kind = resolved.relPath === null ? "root" : "directory";
38124
+ if (resolved.relPath !== null) {
38125
+ try {
38126
+ const worktrees = await listTheaterWorktrees(theater.realpath);
38127
+ if (worktrees.worktrees.some((worktree) => worktree.relPath === resolved.relPath)) kind = "worktree";
38128
+ } catch {
38129
+ }
38130
+ }
38131
+ return { kind, relPath: resolved.relPath, label: resolved.relPath === null ? theater.label : resolved.label };
38132
+ }
38133
+ function writePathError2(res, deps, error51) {
38134
+ if (!(error51 instanceof TheaterPathContextError)) throw error51;
38135
+ const status = error51.code === "not_found" ? 404 : error51.code === "forbidden" ? 403 : 400;
38136
+ deps.writeJson(res, status, { error: error51.code });
38137
+ }
38138
+ function isPlainObject3(value) {
38139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
38140
+ }
36993
38141
  var TheaterRegistry = class {
36994
38142
  #items = /* @__PURE__ */ new Map();
36995
38143
  #mruId = null;
36996
38144
  async register(cwd) {
36997
- const resolved = path25__default.resolve(cwd);
38145
+ const resolved = path27__default.resolve(cwd);
36998
38146
  const real = await canonicalizeTheaterPath(resolved);
36999
38147
  const id = workspaceHash(real);
37000
38148
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -37008,7 +38156,8 @@ var TheaterRegistry = class {
37008
38156
  realpath: real,
37009
38157
  label: theaterLabel(resolved),
37010
38158
  registeredAt: existing?.registeredAt ?? now,
37011
- lastOpenedAt: now
38159
+ lastOpenedAt: now,
38160
+ pathContext: existing?.pathContext ?? null
37012
38161
  };
37013
38162
  this.#items.set(id, item);
37014
38163
  this.#mruId = id;
@@ -37048,6 +38197,13 @@ var TheaterRegistry = class {
37048
38197
  if (this.#mruId === id) this.#mruId = this.list()[0]?.id ?? null;
37049
38198
  return removed;
37050
38199
  }
38200
+ setPathContext(id, pathContext) {
38201
+ const current = this.#items.get(id);
38202
+ if (!current) return null;
38203
+ const next = { ...current, pathContext };
38204
+ this.#items.set(id, next);
38205
+ return next;
38206
+ }
37051
38207
  };
37052
38208
  var PACKAGE_NAMES = ["@dotobokuri/fleet-cli", "@dotobokuri/fleet-console"];
37053
38209
  var WORKER_FILE_PREFIX = "fleet-console-update-";
@@ -37059,7 +38215,7 @@ function createConsoleUpdateApplyService(deps = {}) {
37059
38215
  const env = deps.env ?? process4.env;
37060
38216
  const execPath = deps.execPath ?? process4.execPath;
37061
38217
  const makeDir = deps.makeDir ?? ((dirPath, options) => {
37062
- fs11__default.mkdirSync(dirPath, options);
38218
+ fs14__default.mkdirSync(dirPath, options);
37063
38219
  });
37064
38220
  const now = deps.now ?? Date.now;
37065
38221
  const processPid = deps.processPid ?? process4.pid;
@@ -37068,15 +38224,15 @@ function createConsoleUpdateApplyService(deps = {}) {
37068
38224
  const spawnWorker = deps.spawnWorker ?? defaultSpawnWorker;
37069
38225
  const tmpDir = deps.tmpDir ?? os4__default.tmpdir();
37070
38226
  const writeFile5 = deps.writeFile ?? ((filePath, content, options) => {
37071
- fs11__default.writeFileSync(filePath, content, { mode: options.mode });
38227
+ fs14__default.writeFileSync(filePath, content, { mode: options.mode });
37072
38228
  });
37073
38229
  async function start(request) {
37074
38230
  const packageManager = await preflightInstall(request.currentPackageRoot);
37075
38231
  const stamp = `${now()}-${processPid}`;
37076
- const workerPath = path25__default.join(tmpDir, `${WORKER_FILE_PREFIX}${stamp}${WORKER_FILE_SUFFIX}`);
38232
+ const workerPath = path27__default.join(tmpDir, `${WORKER_FILE_PREFIX}${stamp}${WORKER_FILE_SUFFIX}`);
37077
38233
  makeDir(request.dataDir, { recursive: true, mode: 448 });
37078
- const statusFile = path25__default.join(request.dataDir, `${WORKER_FILE_PREFIX}${stamp}${STATUS_FILE_SUFFIX}`);
37079
- const logFile = path25__default.join(request.dataDir, `${WORKER_FILE_PREFIX}${stamp}${LOG_FILE_SUFFIX}`);
38234
+ const statusFile = path27__default.join(request.dataDir, `${WORKER_FILE_PREFIX}${stamp}${STATUS_FILE_SUFFIX}`);
38235
+ const logFile = path27__default.join(request.dataDir, `${WORKER_FILE_PREFIX}${stamp}${LOG_FILE_SUFFIX}`);
37080
38236
  const script = emitConsoleUpdateWorkerScript({
37081
38237
  currentEndpoint: request.currentEndpoint,
37082
38238
  currentPackageRoot: request.currentPackageRoot,
@@ -37419,17 +38575,19 @@ function defaultSpawnWorker(execPath, args, options) {
37419
38575
  }
37420
38576
  function resolveDefaultServerModulePath() {
37421
38577
  const builtPath = fileURLToPath(new URL("../dist/cli.mjs", import.meta.url));
37422
- if (fs11__default.existsSync(builtPath)) return builtPath;
38578
+ if (fs14__default.existsSync(builtPath)) return builtPath;
37423
38579
  const sourcePath = fileURLToPath(import.meta.url);
37424
38580
  if (sourcePath.endsWith(".ts")) {
37425
38581
  return sourcePath.replace(/update-apply\.ts$/, "cli.ts");
37426
38582
  }
37427
- return path25__default.join(path25__default.dirname(sourcePath), "cli.mjs");
38583
+ return path27__default.join(path27__default.dirname(sourcePath), "cli.mjs");
37428
38584
  }
37429
38585
 
37430
38586
  // core/host/update-check.ts
37431
38587
  var FLEET_CONSOLE_PACKAGE_NAME = "@dotobokuri/fleet-console";
37432
38588
  var UPDATE_CHECK_TTL_MS = 60 * 60 * 1e3;
38589
+ var UPDATE_CHECK_ERROR_TTL_MS = 5 * 60 * 1e3;
38590
+ var UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1e3;
37433
38591
  var NO_UPDATE_STATUS = { updateAvailable: false };
37434
38592
  function createConsoleUpdateCheckService(deps = {}) {
37435
38593
  const readRelease = deps.readRelease ?? readFleetConsoleRelease;
@@ -37437,11 +38595,17 @@ function createConsoleUpdateCheckService(deps = {}) {
37437
38595
  const isGreater = deps.isGreater ?? isVersionGreater;
37438
38596
  const now = deps.now ?? Date.now;
37439
38597
  const ttlMs = deps.ttlMs ?? UPDATE_CHECK_TTL_MS;
38598
+ const errorTtlMs = deps.errorTtlMs ?? UPDATE_CHECK_ERROR_TTL_MS;
38599
+ const intervalMs = deps.intervalMs ?? UPDATE_CHECK_INTERVAL_MS;
38600
+ const startInterval = deps.setInterval ?? ((callback, delayMs) => setInterval(callback, delayMs));
38601
+ const stopInterval = deps.clearInterval ?? ((interval2) => clearInterval(interval2));
37440
38602
  let cached2 = null;
37441
38603
  let inFlight = null;
38604
+ let interval = null;
38605
+ const changeListeners = /* @__PURE__ */ new Set();
37442
38606
  const getStatus = () => {
37443
38607
  const current = cached2;
37444
- if (current && now() - current.checkedAt < ttlMs) {
38608
+ if (current && now() - current.checkedAt < current.ttlMs) {
37445
38609
  return current.status;
37446
38610
  }
37447
38611
  void refresh();
@@ -37449,17 +38613,19 @@ function createConsoleUpdateCheckService(deps = {}) {
37449
38613
  };
37450
38614
  const refresh = async (options = {}) => {
37451
38615
  const current = cached2;
37452
- if (options.force !== true && current && now() - current.checkedAt < ttlMs) {
38616
+ if (options.force !== true && current && now() - current.checkedAt < current.ttlMs) {
37453
38617
  return current.status;
37454
38618
  }
37455
38619
  if (inFlight) {
37456
38620
  return inFlight;
37457
38621
  }
37458
38622
  inFlight = resolveUpdateStatus().then((status) => {
37459
- cached2 = { status, checkedAt: now() };
38623
+ const previousStatus = cached2?.status ?? NO_UPDATE_STATUS;
38624
+ cached2 = { status, checkedAt: now(), ttlMs };
38625
+ notifyIfChanged(previousStatus, status);
37460
38626
  return status;
37461
38627
  }).catch(() => {
37462
- cached2 = { status: NO_UPDATE_STATUS, checkedAt: now() };
38628
+ cached2 = { status: NO_UPDATE_STATUS, checkedAt: now(), ttlMs: errorTtlMs };
37463
38629
  return NO_UPDATE_STATUS;
37464
38630
  }).finally(() => {
37465
38631
  inFlight = null;
@@ -37468,16 +38634,46 @@ function createConsoleUpdateCheckService(deps = {}) {
37468
38634
  };
37469
38635
  const resolveUpdateStatus = async () => {
37470
38636
  const release = readRelease();
37471
- if (release.channel === "local") {
38637
+ if (release.channel === "local" || release.channel === "desktop") {
37472
38638
  return NO_UPDATE_STATUS;
37473
38639
  }
37474
38640
  const latestVersion = await fetchLatest(FLEET_CONSOLE_PACKAGE_NAME);
37475
- if (!latestVersion || !isGreater(latestVersion, release.version)) {
38641
+ if (latestVersion === void 0) {
38642
+ throw new Error("registry lookup failed");
38643
+ }
38644
+ if (!isGreater(latestVersion, release.version)) {
37476
38645
  return NO_UPDATE_STATUS;
37477
38646
  }
37478
38647
  return { updateAvailable: true, latestVersion };
37479
38648
  };
37480
- return { getStatus, refresh };
38649
+ const start = () => {
38650
+ if (interval) return;
38651
+ interval = startInterval(() => {
38652
+ void refresh({ force: true });
38653
+ }, intervalMs);
38654
+ interval.unref?.();
38655
+ };
38656
+ const stop = () => {
38657
+ if (!interval) return;
38658
+ stopInterval(interval);
38659
+ interval = null;
38660
+ };
38661
+ const onChange = (listener) => {
38662
+ changeListeners.add(listener);
38663
+ return () => changeListeners.delete(listener);
38664
+ };
38665
+ function notifyIfChanged(previous, next) {
38666
+ const updateBecameAvailable = !previous.updateAvailable && next.updateAvailable;
38667
+ const latestVersionChanged = previous.latestVersion !== next.latestVersion;
38668
+ if (!updateBecameAvailable && !latestVersionChanged) return;
38669
+ for (const listener of changeListeners) {
38670
+ try {
38671
+ listener(next);
38672
+ } catch {
38673
+ }
38674
+ }
38675
+ }
38676
+ return { getStatus, refresh, start, stop, onChange };
37481
38677
  }
37482
38678
 
37483
38679
  // core/host/server.ts
@@ -37533,6 +38729,41 @@ var SERVER_API_CATALOG = [
37533
38729
  category: "Observer",
37534
38730
  gate: "origin-write"
37535
38731
  },
38732
+ {
38733
+ method: "GET",
38734
+ path: "/api/v1/theaters/:theaterId/path-context",
38735
+ summary: "Get the selected Theater path context.",
38736
+ category: "Observer",
38737
+ gate: "loopback"
38738
+ },
38739
+ {
38740
+ method: "PUT",
38741
+ path: "/api/v1/theaters/:theaterId/path-context",
38742
+ summary: "Save the selected Theater path context.",
38743
+ category: "Observer",
38744
+ gate: "origin-write"
38745
+ },
38746
+ {
38747
+ method: "GET",
38748
+ path: "/api/v1/theaters/:theaterId/path-context/worktrees",
38749
+ summary: "List contained Git worktrees for a Theater.",
38750
+ category: "Observer",
38751
+ gate: "loopback"
38752
+ },
38753
+ {
38754
+ method: "POST",
38755
+ path: "/api/v1/theaters/:theaterId/path-context/directories",
38756
+ summary: "List contained directories for a Theater path context.",
38757
+ category: "Observer",
38758
+ gate: "origin-write"
38759
+ },
38760
+ {
38761
+ method: "POST",
38762
+ path: "/api/v1/theaters/:theaterId/codex-workspace",
38763
+ summary: "Resolve the Codex workspace for a Theater path context.",
38764
+ category: "Observer",
38765
+ gate: "origin-write"
38766
+ },
37536
38767
  {
37537
38768
  method: "POST",
37538
38769
  path: "/plugins/terminal/shell/ticket",
@@ -37568,6 +38799,20 @@ var SERVER_API_CATALOG = [
37568
38799
  category: "Observer",
37569
38800
  gate: "origin-write"
37570
38801
  },
38802
+ {
38803
+ method: "POST",
38804
+ path: "/api/v1/plans/list",
38805
+ summary: "Theater\uC758 \uC2E4\uD589 \uACC4\uD68D \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4.",
38806
+ category: "Observer",
38807
+ gate: "origin-write"
38808
+ },
38809
+ {
38810
+ method: "POST",
38811
+ path: "/api/v1/plans/read",
38812
+ summary: "\uC2E4\uD589 \uACC4\uD68D \uBB38\uC11C\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4.",
38813
+ category: "Observer",
38814
+ gate: "origin-write"
38815
+ },
37571
38816
  {
37572
38817
  method: "POST",
37573
38818
  path: "/api/v1/updates/apply",
@@ -37587,6 +38832,7 @@ function createConsoleServer(deps = {}) {
37587
38832
  const host = deps.host ?? DEFAULT_HOST;
37588
38833
  const port = deps.port ?? DEFAULT_PORT;
37589
38834
  const release = deps.release ?? readFleetConsoleRelease();
38835
+ const desktop = readDesktopProtocolEnvironment();
37590
38836
  const tryServeStaticConsole2 = createStaticConsoleHandler(release.packageRoot);
37591
38837
  const version2 = deps.version ?? release.version;
37592
38838
  const channel = release.channel;
@@ -37594,7 +38840,7 @@ function createConsoleServer(deps = {}) {
37594
38840
  registerDefaultCarriers(carrierRegistry);
37595
38841
  const lock = createConsoleLock({ hostname: () => host });
37596
38842
  const releaseNotes = deps.releaseNotes ?? createConsoleReleaseNotesService();
37597
- const updateCheck = deps.updateCheck ?? createConsoleUpdateCheckService();
38843
+ const updateCheck = deps.updateCheck ?? createConsoleUpdateCheckService({ readRelease: () => release });
37598
38844
  const updateApply = deps.updateApply ?? createConsoleUpdateApplyService();
37599
38845
  const theaters = new TheaterRegistry();
37600
38846
  const operations = createOperationStore();
@@ -37617,6 +38863,9 @@ function createConsoleServer(deps = {}) {
37617
38863
  const pluginCleanupCallbacks = /* @__PURE__ */ new Set();
37618
38864
  const pluginEventListeners = /* @__PURE__ */ new Map();
37619
38865
  const operationSseSubscribers = /* @__PURE__ */ new Set();
38866
+ let unsubscribeUpdateCheckChanges = updateCheck.onChange?.(() => {
38867
+ broadcastUpdateAvailable();
38868
+ }) ?? null;
37620
38869
  const pluginHostCapabilities = {
37621
38870
  operations: {
37622
38871
  list: () => operations.list(),
@@ -37687,8 +38936,8 @@ function createConsoleServer(deps = {}) {
37687
38936
  },
37688
38937
  paths: {
37689
38938
  capturesDir: durablePaths.capturesDir,
37690
- pluginDataDir: (pluginId) => path25__default.join(durablePaths.dir, "plugins", pluginId),
37691
- resolveTheaterPath: (theaterId) => theaters.get(theaterId)?.path ?? null,
38939
+ pluginDataDir: (pluginId) => path27__default.join(durablePaths.dir, "plugins", pluginId),
38940
+ resolveTheaterPath: (theaterId) => theaters.get(theaterId)?.realpath ?? null,
37692
38941
  canonicalizeTheaterPath: canonicalizeTheaterPathSync,
37693
38942
  workspaceHash
37694
38943
  },
@@ -37712,8 +38961,10 @@ function createConsoleServer(deps = {}) {
37712
38961
  }
37713
38962
  }
37714
38963
  };
38964
+ process.env.FLEET_CONSOLE_PACKAGE_ROOT = release.packageRoot;
37715
38965
  const pluginHost = createFleetPluginHost({
37716
38966
  ...resolveBuiltInPluginDiscoveryRoots(release.packageRoot),
38967
+ bundleCacheDir: path27__default.join(durablePaths.dir, "plugin-cache"),
37717
38968
  routes: routeRegistry,
37718
38969
  upgrades: upgradeRegistry,
37719
38970
  host: pluginHostCapabilities
@@ -37779,6 +39030,31 @@ function createConsoleServer(deps = {}) {
37779
39030
  readJsonBody,
37780
39031
  writeJson
37781
39032
  });
39033
+ const systemFontsRouter = createSystemFontsRouter({
39034
+ systemFonts: deps.systemFonts ?? createSystemFontsService(),
39035
+ writeJson
39036
+ });
39037
+ const plansRouter = createPlansRouter({
39038
+ isAuthorized: isTerminalAuthorized,
39039
+ readJsonBody,
39040
+ resolveTheaterPath: (theaterId) => theaters.get(theaterId)?.realpath ?? null,
39041
+ writeJson
39042
+ });
39043
+ const theaterPathContextRouter = createTheaterPathContextRouter({
39044
+ getTheater: (theaterId) => theaters.get(theaterId),
39045
+ isAuthorized: isTerminalAuthorized,
39046
+ persist: persistDurableState,
39047
+ readJsonBody,
39048
+ setPathContext: (theaterId, relPath) => theaters.setPathContext(theaterId, relPath),
39049
+ writeJson
39050
+ });
39051
+ const codexWorkspaceContextRouter = createCodexWorkspaceContextRouter({
39052
+ getTheater: (theaterId) => theaters.get(theaterId),
39053
+ isAuthorized: isTerminalAuthorized,
39054
+ readJsonBody,
39055
+ resolveWorkspace: (theaterId, theaterRoot, relPath) => codex.resolveWorkspaceForPath(theaterId, theaterRoot, relPath),
39056
+ writeJson
39057
+ });
37782
39058
  const operationsRouter = createOperationsRouter({
37783
39059
  store: operations,
37784
39060
  isAuthorized: isTerminalAuthorized,
@@ -37807,6 +39083,10 @@ function createConsoleServer(deps = {}) {
37807
39083
  }
37808
39084
  });
37809
39085
  routeRegistry.register("/api/v1/operations", operationsRouter);
39086
+ routeRegistry.register("/api/v1/theaters", async (context) => {
39087
+ if (await theaterPathContextRouter(context)) return true;
39088
+ return codexWorkspaceContextRouter(context);
39089
+ });
37810
39090
  routeRegistry.register("/api/v1/settings", async (ctx) => {
37811
39091
  const { req, res, pathname } = ctx;
37812
39092
  if (pathname === "/api/v1/settings/api-catalog") {
@@ -37815,6 +39095,7 @@ function createConsoleServer(deps = {}) {
37815
39095
  }
37816
39096
  if (await carrierSettingsRouter(ctx)) return true;
37817
39097
  if (await pluginSettingsRouter(ctx)) return true;
39098
+ if (await systemFontsRouter(ctx)) return true;
37818
39099
  return globalSettingsRouter(ctx);
37819
39100
  });
37820
39101
  routeRegistry.register("/plugin-runtime", handlePluginRuntimeRoute);
@@ -37855,6 +39136,10 @@ function createConsoleServer(deps = {}) {
37855
39136
  runAsyncHandler(handleTheaterFolderGrants(req, res), res);
37856
39137
  return;
37857
39138
  }
39139
+ if (pathname === "/api/v1/plans/list" || pathname === "/api/v1/plans/read") {
39140
+ runAsyncBooleanHandler(plansRouter({ req, res, pathname }), res, () => false);
39141
+ return;
39142
+ }
37858
39143
  const theaterItemMatch = pathname.match(/^\/api\/v1\/theaters\/([^/]+)$/);
37859
39144
  if (theaterItemMatch) {
37860
39145
  runAsyncHandler(handleObserverTheaterItem(req, res, decodeURIComponent(theaterItemMatch[1] ?? "")), res);
@@ -37919,6 +39204,8 @@ function createConsoleServer(deps = {}) {
37919
39204
  endpoint: payload.endpoint,
37920
39205
  startedAt: payload.startedAt,
37921
39206
  version: payload.version,
39207
+ ...channel === "desktop" ? { channel } : {},
39208
+ ...payload.owner ? { owner: payload.owner } : {},
37922
39209
  workspaceCount: operations.list().length
37923
39210
  };
37924
39211
  writeJson(res, 200, body);
@@ -37936,7 +39223,7 @@ function createConsoleServer(deps = {}) {
37936
39223
  return;
37937
39224
  }
37938
39225
  const body = await readJsonBody(req);
37939
- if (!isPlainObject2(body) || body.path !== void 0 && body.path !== null && typeof body.path !== "string") {
39226
+ if (!isPlainObject4(body) || body.path !== void 0 && body.path !== null && typeof body.path !== "string") {
37940
39227
  writeJson(res, 400, { error: "invalid_path" });
37941
39228
  return;
37942
39229
  }
@@ -37961,7 +39248,7 @@ function createConsoleServer(deps = {}) {
37961
39248
  return;
37962
39249
  }
37963
39250
  const body = await readJsonBody(req);
37964
- if (!isPlainObject2(body) || typeof body.path !== "string") {
39251
+ if (!isPlainObject4(body) || typeof body.path !== "string") {
37965
39252
  writeJson(res, 400, { error: "invalid_folder" });
37966
39253
  return;
37967
39254
  }
@@ -37985,7 +39272,7 @@ function createConsoleServer(deps = {}) {
37985
39272
  return;
37986
39273
  }
37987
39274
  const body = await readJsonBody(req);
37988
- if (!isPlainObject2(body) || typeof body.folderGrantId !== "string") {
39275
+ if (!isPlainObject4(body) || typeof body.folderGrantId !== "string") {
37989
39276
  writeJson(res, 400, { error: "invalid_folder_grant" });
37990
39277
  return;
37991
39278
  }
@@ -37997,7 +39284,7 @@ function createConsoleServer(deps = {}) {
37997
39284
  const theater = await theaters.register(cwd);
37998
39285
  let hasWiki = false;
37999
39286
  try {
38000
- await codex.registerWorkspace(cwd);
39287
+ await codex.registerWorkspace(theater.realpath, void 0, theater.id);
38001
39288
  hasWiki = true;
38002
39289
  } catch (error51) {
38003
39290
  if (!(error51 instanceof Error && error51.message === "knowledge_root_missing")) {
@@ -38017,7 +39304,7 @@ function createConsoleServer(deps = {}) {
38017
39304
  return;
38018
39305
  }
38019
39306
  theaters.remove(theaterId);
38020
- codex.unregisterWorkspace(theaterId);
39307
+ codex.unregisterTheaterWorkspaces(theaterId);
38021
39308
  for (const operation of operations.listByTheater(theaterId)) {
38022
39309
  pluginHostCapabilities.events.publish(OPERATION_DELETED_EVENT_CHANNEL, {
38023
39310
  operationId: operation.id,
@@ -38058,8 +39345,10 @@ function createConsoleServer(deps = {}) {
38058
39345
  return;
38059
39346
  }
38060
39347
  try {
38061
- const force = readUrl(req).searchParams.get("force") === "true";
38062
- writeJson(res, 200, await releaseNotes.refresh({ force }));
39348
+ const searchParams = readUrl(req).searchParams;
39349
+ const force = searchParams.get("force") === "true";
39350
+ const locale = searchParams.get("locale") === "ko" ? "ko" : "en";
39351
+ writeJson(res, 200, await releaseNotes.refresh({ force, locale }));
38063
39352
  } catch (error51) {
38064
39353
  if (error51 instanceof ConsoleReleaseNotesUnavailableError) {
38065
39354
  writeJson(res, 503, { error: "release_notes_unavailable" });
@@ -38082,10 +39371,14 @@ function createConsoleServer(deps = {}) {
38082
39371
  writeJson(res, 400, { error: "invalid_update_apply_body" });
38083
39372
  return;
38084
39373
  }
38085
- if (body !== null && (!isPlainObject2(body) || hasForbiddenUpdateApplyBodyKeys(body))) {
39374
+ if (body !== null && (!isPlainObject4(body) || hasForbiddenUpdateApplyBodyKeys(body))) {
38086
39375
  writeJson(res, 400, { error: "invalid_update_apply_body" });
38087
39376
  return;
38088
39377
  }
39378
+ if (channel === "desktop") {
39379
+ writeJson(res, 403, { error: "desktop_update_managed" });
39380
+ return;
39381
+ }
38089
39382
  if (channel === "local") {
38090
39383
  writeJson(res, 403, { error: "local_channel" });
38091
39384
  return;
@@ -38180,6 +39473,9 @@ function createConsoleServer(deps = {}) {
38180
39473
  await disposeConsoleResources(currentLock);
38181
39474
  }
38182
39475
  async function disposeConsoleResources(currentLock) {
39476
+ updateCheck.stop?.();
39477
+ unsubscribeUpdateCheckChanges?.();
39478
+ unsubscribeUpdateCheckChanges = null;
38183
39479
  if (consoleResourcesDisposed) {
38184
39480
  currentLock?.release();
38185
39481
  return;
@@ -38191,6 +39487,7 @@ function createConsoleServer(deps = {}) {
38191
39487
  console.warn(`[fleet-console] Plugin cleanup failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`);
38192
39488
  }
38193
39489
  }
39490
+ await pluginHost.cleanup();
38194
39491
  pluginCleanupCallbacks.clear();
38195
39492
  pluginEventListeners.clear();
38196
39493
  currentLock?.release();
@@ -38202,6 +39499,8 @@ function createConsoleServer(deps = {}) {
38202
39499
  theaters.restore(state.theaters);
38203
39500
  operations.replace(state.operations);
38204
39501
  operations.replaceGroups(state.groups ?? []);
39502
+ const healed = await healRestoredPathContexts();
39503
+ if (healed) persistDurableState();
38205
39504
  } catch (error51) {
38206
39505
  console.warn(`[fleet-console] Durable state restore skipped: ${error51 instanceof Error ? error51.message : String(error51)}`);
38207
39506
  state = emptyDurableConsoleState();
@@ -38211,11 +39510,28 @@ function createConsoleServer(deps = {}) {
38211
39510
  }
38212
39511
  await restoreCodexWorkspaces();
38213
39512
  }
39513
+ async function healRestoredPathContexts() {
39514
+ let healed = false;
39515
+ for (const theater of theaters.list()) {
39516
+ if (theater.pathContext === null) continue;
39517
+ try {
39518
+ const resolved = await resolveTheaterPathContext(theater.realpath, theater.pathContext);
39519
+ if (resolved.relPath !== theater.pathContext) {
39520
+ theaters.setPathContext(theater.id, resolved.relPath);
39521
+ healed = true;
39522
+ }
39523
+ } catch {
39524
+ theaters.setPathContext(theater.id, null);
39525
+ healed = true;
39526
+ }
39527
+ }
39528
+ return healed;
39529
+ }
38214
39530
  async function restoreCodexWorkspaces() {
38215
39531
  const ordered = [...theaters.list()].sort((left, right) => left.lastOpenedAt.localeCompare(right.lastOpenedAt));
38216
39532
  for (const theater of ordered) {
38217
39533
  try {
38218
- await codex.registerWorkspace(theater.realpath, theater.lastOpenedAt);
39534
+ await codex.registerWorkspace(theater.realpath, theater.lastOpenedAt, theater.id);
38219
39535
  } catch (error51) {
38220
39536
  if (!(error51 instanceof Error && error51.message === "knowledge_root_missing")) {
38221
39537
  console.warn(`[fleet-console] Codex workspace restore skipped for Theater ${theater.id}: ${error51 instanceof Error ? error51.message : String(error51)}`);
@@ -38235,6 +39551,13 @@ function createConsoleServer(deps = {}) {
38235
39551
  res.write(data);
38236
39552
  }
38237
39553
  }
39554
+ function broadcastUpdateAvailable() {
39555
+ if (operationSseSubscribers.size === 0) return;
39556
+ const data = encodeSseData("update:available", {});
39557
+ for (const res of operationSseSubscribers) {
39558
+ res.write(data);
39559
+ }
39560
+ }
38238
39561
  function persistDurableState() {
38239
39562
  try {
38240
39563
  durableStateStore.save({
@@ -38279,7 +39602,7 @@ function createConsoleServer(deps = {}) {
38279
39602
  server = result.srv;
38280
39603
  loopbackServer = result.localLoopbackServer;
38281
39604
  portState = result.portState;
38282
- lockHandle = lock.writeLock({ dir: lockPaths.dir, lockFile: lockPaths.lockFile, pid: process.pid, port: result.actualPort, endpoint: result.endpoint, version: version2 });
39605
+ lockHandle = lock.writeLock({ dir: lockPaths.dir, lockFile: lockPaths.lockFile, pid: process.pid, port: result.actualPort, endpoint: result.endpoint, version: version2, ...desktop ? { owner: desktop.owner } : {} });
38283
39606
  activeLockFile = lockPaths.lockFile;
38284
39607
  activeEndpoint = result.endpoint;
38285
39608
  } catch (error51) {
@@ -38287,6 +39610,12 @@ function createConsoleServer(deps = {}) {
38287
39610
  throw error51;
38288
39611
  }
38289
39612
  if (!activeEndpoint) throw new Error("Console endpoint unavailable");
39613
+ if (unsubscribeUpdateCheckChanges === null) {
39614
+ unsubscribeUpdateCheckChanges = updateCheck.onChange?.(() => {
39615
+ broadcastUpdateAvailable();
39616
+ }) ?? null;
39617
+ }
39618
+ updateCheck.start?.();
38290
39619
  void updateCheck.refresh();
38291
39620
  return activeEndpoint;
38292
39621
  },
@@ -38307,6 +39636,14 @@ function createConsoleServer(deps = {}) {
38307
39636
  allowFallback: false
38308
39637
  };
38309
39638
  }
39639
+ if (channel === "local") {
39640
+ return {
39641
+ port: DEFAULT_PORT,
39642
+ requestedPort: null,
39643
+ portMode: "dynamic",
39644
+ allowFallback: false
39645
+ };
39646
+ }
38310
39647
  const options = consoleSettingsStore.load().general ?? {};
38311
39648
  if (options.consolePortMode === "static" && isValidConsoleStaticPort3(options.consoleStaticPort)) {
38312
39649
  return {
@@ -38373,11 +39710,11 @@ function createConsoleServer(deps = {}) {
38373
39710
  return returnedServer;
38374
39711
  }
38375
39712
  function resolveBuiltInPluginDiscoveryRoots(packageRoot) {
38376
- const packageRootRepo = path25__default.resolve(packageRoot, "..", "..");
38377
- const sourceRoot = path25__default.join(packageRootRepo, "runtime", "fleet-plugins");
39713
+ const packageRootRepo = path27__default.resolve(packageRoot, "..", "..");
39714
+ const sourceRoot = path27__default.join(packageRootRepo, "runtime", "fleet-plugins");
38378
39715
  return {
38379
- ...fs11__default.existsSync(sourceRoot) ? { builtInSourceRoot: sourceRoot } : {},
38380
- builtInDistRoot: path25__default.join(packageRoot, "dist", "fleet-plugins")
39716
+ ...fs14__default.existsSync(sourceRoot) ? { builtInSourceRoot: sourceRoot } : {},
39717
+ builtInDistRoot: path27__default.join(packageRoot, "dist", "fleet-plugins")
38381
39718
  };
38382
39719
  }
38383
39720
  function createHttpServer(handler, upgradeRegistry) {
@@ -38433,7 +39770,7 @@ function theaterFolderListStatus(error51) {
38433
39770
  if (error51.code === "not_found") return 404;
38434
39771
  return 400;
38435
39772
  }
38436
- function isPlainObject2(value) {
39773
+ function isPlainObject4(value) {
38437
39774
  return typeof value === "object" && value !== null && !Array.isArray(value);
38438
39775
  }
38439
39776
  function hasForbiddenUpdateApplyBodyKeys(body) {
@@ -38443,7 +39780,7 @@ function isValidConsoleStaticPort3(value) {
38443
39780
  return typeof value === "number" && Number.isInteger(value) && value >= MIN_CONSOLE_STATIC_PORT3 && value <= MAX_CONSOLE_STATIC_PORT3;
38444
39781
  }
38445
39782
  function sanitizeLaunchKind(value) {
38446
- if (!isPlainObject2(value) || typeof value.id !== "string" || typeof value.type !== "string" || typeof value.title !== "string") return null;
39783
+ if (!isPlainObject4(value) || typeof value.id !== "string" || typeof value.type !== "string" || typeof value.title !== "string") return null;
38447
39784
  return {
38448
39785
  id: value.id,
38449
39786
  type: value.type,
@@ -38522,7 +39859,7 @@ function isAllowedTerminalOrigin(req, expectedPort) {
38522
39859
  async function readPluginStorageJson(dataDir, pluginId, key) {
38523
39860
  const file2 = resolvePluginStorageFile(dataDir, pluginId, key);
38524
39861
  try {
38525
- return JSON.parse(await fs11__default.promises.readFile(file2, "utf8"));
39862
+ return JSON.parse(await fs14__default.promises.readFile(file2, "utf8"));
38526
39863
  } catch (error51) {
38527
39864
  if (isNodeError(error51) && error51.code === "ENOENT") return null;
38528
39865
  throw error51;
@@ -38530,20 +39867,20 @@ async function readPluginStorageJson(dataDir, pluginId, key) {
38530
39867
  }
38531
39868
  async function writePluginStorageJson(dataDir, pluginId, key, value) {
38532
39869
  const file2 = resolvePluginStorageFile(dataDir, pluginId, key);
38533
- await fs11__default.promises.mkdir(path25__default.dirname(file2), { recursive: true });
38534
- await fs11__default.promises.writeFile(file2, JSON.stringify(value), "utf8");
39870
+ await fs14__default.promises.mkdir(path27__default.dirname(file2), { recursive: true });
39871
+ await fs14__default.promises.writeFile(file2, JSON.stringify(value), "utf8");
38535
39872
  }
38536
39873
  function resolvePluginStorageFile(dataDir, pluginId, key) {
38537
39874
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(pluginId) || !/^[a-z0-9][a-z0-9._-]*$/i.test(key)) {
38538
39875
  throw new Error("invalid_plugin_storage_key");
38539
39876
  }
38540
- return path25__default.join(dataDir, "plugins", pluginId, `${key}.json`);
39877
+ return path27__default.join(dataDir, "plugins", pluginId, `${key}.json`);
38541
39878
  }
38542
39879
  function isNodeError(error51) {
38543
39880
  return error51 instanceof Error && "code" in error51;
38544
39881
  }
38545
39882
  function createConsoleStalePolicy(deps = {}) {
38546
- const fsImpl = deps.fs ?? fs11__default;
39883
+ const fsImpl = deps.fs ?? fs14__default;
38547
39884
  function isBuildStale(lock, buildFile) {
38548
39885
  try {
38549
39886
  return lock.startedAt < fsImpl.statSync(buildFile).mtimeMs;
@@ -38665,6 +40002,7 @@ function createConsoleDaemonLifecycle(deps = {}) {
38665
40002
  async function stop() {
38666
40003
  const payload = readTrustedLock();
38667
40004
  if (!payload) return;
40005
+ if (isLockProcessAlive(payload.pid)) assertCliCanControlDaemon(payload);
38668
40006
  try {
38669
40007
  process4.kill(payload.pid, "SIGTERM");
38670
40008
  } catch (err) {
@@ -38684,6 +40022,7 @@ function createConsoleDaemonLifecycle(deps = {}) {
38684
40022
  const probeResult = await health.probe(current);
38685
40023
  const isBuildStale = current ? stale.isBuildStale(current, serverModulePath) : false;
38686
40024
  if (probeResult.healthy && current) {
40025
+ if (current.owner?.kind === "desktop") return current.endpoint;
38687
40026
  if (!isBuildStale) return current.endpoint;
38688
40027
  if (typeof probeResult.health?.workspaceCount === "number" && probeResult.health.workspaceCount > 0) return current.endpoint;
38689
40028
  }
@@ -38736,11 +40075,12 @@ async function runConsoleStatus(deps = {}) {
38736
40075
  const consoleUrl = `${status.lock.endpoint}console/`;
38737
40076
  const workspaceCount = typeof status.health?.workspaceCount === "number" ? status.health.workspaceCount : 0;
38738
40077
  const staleNote = status.buildStale ? " \xB7 build stale (restart recommended)" : "";
40078
+ const ownerNote = status.lock.owner?.kind === "desktop" ? " \xB7 owned by desktop" : "";
38739
40079
  return [
38740
40080
  `Fleet Console server: running (pid ${status.lock.pid})`,
38741
40081
  ` endpoint ${status.lock.endpoint}`,
38742
40082
  ` console ${consoleUrl}`,
38743
- ` workspaces ${workspaceCount}${staleNote}`
40083
+ ` workspaces ${workspaceCount}${staleNote}${ownerNote}`
38744
40084
  ].join("\n");
38745
40085
  }
38746
40086
  async function runConsoleStop(deps = {}) {
@@ -38748,6 +40088,17 @@ async function runConsoleStop(deps = {}) {
38748
40088
  await lifecycle.stop();
38749
40089
  return "Fleet Console server stopped.";
38750
40090
  }
40091
+ function assertCliCanControlDaemon(payload) {
40092
+ if (payload.owner?.kind === "desktop") throw new Error("Fleet Console is owned by the desktop app; quit it from the native menu.");
40093
+ }
40094
+ function isLockProcessAlive(pid) {
40095
+ try {
40096
+ process4.kill(pid, 0);
40097
+ return true;
40098
+ } catch (err) {
40099
+ return err.code !== "ESRCH";
40100
+ }
40101
+ }
38751
40102
  async function runConsoleRestart(deps = {}) {
38752
40103
  const lifecycle = deps.lifecycle ?? createConsoleDaemonLifecycle();
38753
40104
  await lifecycle.stop();
@@ -38801,7 +40152,7 @@ async function main() {
38801
40152
  }
38802
40153
  function resolveDefaultServerModulePath2() {
38803
40154
  const builtPath = new URL("../dist/cli.mjs", import.meta.url).pathname;
38804
- if (fs11__default.existsSync(builtPath)) return builtPath;
40155
+ if (fs14__default.existsSync(builtPath)) return builtPath;
38805
40156
  return fileURLToPath(import.meta.url);
38806
40157
  }
38807
40158
  function readHookSessionId(env) {
@@ -38855,7 +40206,7 @@ function readStdinBestEffort() {
38855
40206
  });
38856
40207
  });
38857
40208
  }
38858
- var isDirectRun = process4.argv[1] && path25__default.resolve(process4.argv[1]) === fileURLToPath(import.meta.url);
40209
+ var isDirectRun = process4.argv[1] && path27__default.resolve(process4.argv[1]) === fileURLToPath(import.meta.url);
38859
40210
  if (isDirectRun) {
38860
40211
  await main().catch((error51) => {
38861
40212
  process4.stderr.write(`${error51 instanceof Error ? error51.message : String(error51)}
@@ -38864,4 +40215,4 @@ if (isDirectRun) {
38864
40215
  });
38865
40216
  }
38866
40217
 
38867
- export { buildConsoleHelpText, createConsoleDaemonLifecycle, main, openFleetConsole, parseConsoleCliMode, parseConsoleHookCommand, runConsoleRestart, runConsoleStatus, runConsoleStop };
40218
+ export { assertCliCanControlDaemon, buildConsoleHelpText, createConsoleDaemonLifecycle, isLockProcessAlive, main, openFleetConsole, parseConsoleCliMode, parseConsoleHookCommand, runConsoleRestart, runConsoleStatus, runConsoleStop };