frizz 0.2.0 → 0.3.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 (115) hide show
  1. package/README.md +17 -17
  2. package/dist/claude-agent-broker.js +63 -23
  3. package/dist/dev-child.js +6877 -4459
  4. package/dist/frizz.js +654 -529
  5. package/package.json +6 -2
  6. package/runtime/cc-worker/.claude-plugin/plugin.json +1 -1
  7. package/runtime/cc-worker/DECISIONS.md +64 -4
  8. package/runtime/cc-worker/bin/frizz-mcp.mjs +494 -57
  9. package/runtime/cc-worker/hooks/agent-dispatch.mjs +1 -1
  10. package/runtime/cc-worker/hooks/deny-plan.mjs +1 -1
  11. package/runtime/cc-worker/hooks/hooks.json +0 -7
  12. package/runtime/cc-worker/hooks/precompact-instructions.mjs +39 -28
  13. package/runtime/cc-worker/hooks/scratchpad.mjs +142 -169
  14. package/runtime/cc-worker/hooks/session-seed.mjs +16 -15
  15. package/runtime/cc-worker/skills/waits/SKILL.md +2 -2
  16. package/web-dist/apple-touch-icon.png +0 -0
  17. package/web-dist/assets/{TerminalPane-ROKHp1ib.js → TerminalPane-CTdetDJJ.js} +2 -2
  18. package/web-dist/assets/{abnfDiagram-VRR7QNED-DcpdhBs3.js → abnfDiagram-VRR7QNED-BDKrjCMs.js} +1 -1
  19. package/web-dist/assets/architecture-TIHT7OUA-ChUMo004.js +1 -0
  20. package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-CUAKf0mn.js → architectureDiagram-ZJ3FMSHR-BJebpUUM.js} +1 -1
  21. package/web-dist/assets/{blockDiagram-677ZJIJ3-BPwpJIzx.js → blockDiagram-677ZJIJ3-BNbuk25k.js} +1 -1
  22. package/web-dist/assets/{c4Diagram-LMCZKHZV-1lptuHzZ.js → c4Diagram-LMCZKHZV-lCfyotdU.js} +1 -1
  23. package/web-dist/assets/channel-pr7r6raB.js +1 -0
  24. package/web-dist/assets/{chunk-32BRIVSS-CFR9AKjY.js → chunk-32BRIVSS-DP72SEkr.js} +1 -1
  25. package/web-dist/assets/{chunk-52WLFC77-CM9uct7m.js → chunk-52WLFC77-DxO3gN-p.js} +1 -1
  26. package/web-dist/assets/{chunk-C7G6YPKG-DiveJARw.js → chunk-C7G6YPKG-BXFx9Vlr.js} +1 -1
  27. package/web-dist/assets/{chunk-EX3LRPZG-BE1CBw8F.js → chunk-EX3LRPZG-CR3sHpPf.js} +1 -1
  28. package/web-dist/assets/{chunk-FWX5IMBZ-DL42uXiO.js → chunk-FWX5IMBZ-CJ8L__oG.js} +2 -2
  29. package/web-dist/assets/{chunk-HOUHSVGY-DPhJWgDw.js → chunk-HOUHSVGY-BCl3JSWr.js} +1 -1
  30. package/web-dist/assets/{chunk-ICXQ74PX-CwYy-6AP.js → chunk-ICXQ74PX-CZxFMci1.js} +1 -1
  31. package/web-dist/assets/{chunk-MOJQB5TN-Ds5I9wxq.js → chunk-MOJQB5TN-Ck3_47dB.js} +1 -1
  32. package/web-dist/assets/{chunk-OGEWGWER-DHiZJwQD.js → chunk-OGEWGWER-CVXjES-F.js} +1 -1
  33. package/web-dist/assets/{chunk-PUDLZKDR-B-eyQTsF.js → chunk-PUDLZKDR-JjrVgaR7.js} +1 -1
  34. package/web-dist/assets/{chunk-Q4XR5HBZ-DK7dB3Ti.js → chunk-Q4XR5HBZ-C5YMRAQ0.js} +1 -1
  35. package/web-dist/assets/{chunk-V7JOEXUC-Dn59m74L.js → chunk-V7JOEXUC-DK0mfUHU.js} +1 -1
  36. package/web-dist/assets/{chunk-VAUOI2AC-DK7x36hd.js → chunk-VAUOI2AC-BKBntpid.js} +1 -1
  37. package/web-dist/assets/{chunk-VR4S4FIN-D7-CI3Yl.js → chunk-VR4S4FIN-zgE1dG6U.js} +1 -1
  38. package/web-dist/assets/{chunk-WYO6CB5R-B3l-mLCs.js → chunk-WYO6CB5R-CZhh1IBq.js} +1 -1
  39. package/web-dist/assets/{chunk-ZGVPDNZ5-Dobxlxie.js → chunk-ZGVPDNZ5-CrNjIEem.js} +1 -1
  40. package/web-dist/assets/classDiagram-OUVF2IWQ-CvGbPMn_.js +1 -0
  41. package/web-dist/assets/classDiagram-v2-EOCWNBFH-CvGbPMn_.js +1 -0
  42. package/web-dist/assets/{cynefin-VYW2F7L2-Dh7RuEUJ.js → cynefin-VYW2F7L2-Ca_BPfTG.js} +1 -1
  43. package/web-dist/assets/{cynefinDiagram-TSTJHNR4-ClPi2mZZ.js → cynefinDiagram-TSTJHNR4-CsINQf6g.js} +1 -1
  44. package/web-dist/assets/{dagre-VKFMJZFB-52_WP1QV.js → dagre-VKFMJZFB-CE0DYMFy.js} +1 -1
  45. package/web-dist/assets/{diagram-FQU43EPY-D_1zVsTL.js → diagram-FQU43EPY-BqTduEYE.js} +1 -1
  46. package/web-dist/assets/{diagram-G47NLZAW-CdZxuGUy.js → diagram-G47NLZAW-Cho718s6.js} +1 -1
  47. package/web-dist/assets/{diagram-NH7WQ7WH-C8pSFu0P.js → diagram-NH7WQ7WH-D0Z8t9UC.js} +1 -1
  48. package/web-dist/assets/{diagram-OA4YK3LP-C5bjZLre.js → diagram-OA4YK3LP-CBacyeDx.js} +1 -1
  49. package/web-dist/assets/{diagram-WEI45ONY-Bxzhiuzn.js → diagram-WEI45ONY-CozpXa4j.js} +1 -1
  50. package/web-dist/assets/{ebnfDiagram-CCIWWBDH-g-Z0J2wP.js → ebnfDiagram-CCIWWBDH-ba4NbQQo.js} +1 -1
  51. package/web-dist/assets/{erDiagram-Q63AITRT-DVNkgIHp.js → erDiagram-Q63AITRT-B3BciOYa.js} +1 -1
  52. package/web-dist/assets/eventmodeling-45OFAUF4-DKmyo-jd.js +1 -0
  53. package/web-dist/assets/flowDiagram-23GEKE2U-DluCBvT4.js +1 -0
  54. package/web-dist/assets/{ganttDiagram-NO4QXBWP-_71pQYEK.js → ganttDiagram-NO4QXBWP-lPmrRit4.js} +1 -1
  55. package/web-dist/assets/{gitGraph-TEB2WS4Q-ChIZiGZS.js → gitGraph-TEB2WS4Q-BjyclOq0.js} +1 -1
  56. package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-DCHAFI0l.js → gitGraphDiagram-IHSO6WYX-pzeb4Yrw.js} +1 -1
  57. package/web-dist/assets/index-BQtjYMpV.css +1 -0
  58. package/web-dist/assets/index-CLW1Q49U.js +360 -0
  59. package/web-dist/assets/{info-DKCQHKI2-BW-n_T1j.js → info-DKCQHKI2-Bwycegvf.js} +1 -1
  60. package/web-dist/assets/{infoDiagram-FWYZ7A6U-CgDYsKi9.js → infoDiagram-FWYZ7A6U-BCvRGj_5.js} +1 -1
  61. package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-ClzGNt9N.js → ishikawaDiagram-FXEZZL3T-Ofw1RMj3.js} +1 -1
  62. package/web-dist/assets/{journeyDiagram-5HDEW3XC-DSCQxkHC.js → journeyDiagram-5HDEW3XC-C5ROwFio.js} +1 -1
  63. package/web-dist/assets/{kanban-definition-HUTT4EX6-CdrdX9N8.js → kanban-definition-HUTT4EX6-YLPLkpeT.js} +1 -1
  64. package/web-dist/assets/{line-ha38Dc-1.js → line-KtkNqRgI.js} +1 -1
  65. package/web-dist/assets/{mermaid-parser.core-Z4uMcpip.js → mermaid-parser.core-D_FfqBe7.js} +3 -3
  66. package/web-dist/assets/{mermaid.core-iZRq3hbu.js → mermaid.core-Ffv8anVf.js} +3 -3
  67. package/web-dist/assets/{mindmap-definition-LN4V7U3C-DmhInJO_.js → mindmap-definition-LN4V7U3C-B2jj4vfm.js} +1 -1
  68. package/web-dist/assets/{packet-7NZHBO7P-DBPB36Kl.js → packet-7NZHBO7P-BjmWHwra.js} +1 -1
  69. package/web-dist/assets/{pegDiagram-2B236MQR-CAH3ljfj.js → pegDiagram-2B236MQR-Dq3iJDyq.js} +1 -1
  70. package/web-dist/assets/{pie-RZYD4A2V-_h_eX4Ca.js → pie-RZYD4A2V-jBbH1lv9.js} +1 -1
  71. package/web-dist/assets/{pieDiagram-ENE6RG2P-DFBPus8j.js → pieDiagram-ENE6RG2P-C3ETW9lq.js} +1 -1
  72. package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-DMvOCjt8.js → quadrantDiagram-ABIIQ3AL-DItSmme7.js} +1 -1
  73. package/web-dist/assets/{radar-I7S5WNFK-2EzoPHEZ.js → radar-I7S5WNFK-6ey6crgP.js} +1 -1
  74. package/web-dist/assets/{railroad-3IZDKUUU-BPJnn-hm.js → railroad-3IZDKUUU-Cii-Mn0E.js} +1 -1
  75. package/web-dist/assets/railroad-abnf-AHOZXSZD-U_vb4BrX.js +1 -0
  76. package/web-dist/assets/railroad-ebnf-EBAXGLYW-BIHG7gNU.js +1 -0
  77. package/web-dist/assets/railroad-peg-LSFZ7HO6-Cpd9r-tB.js +1 -0
  78. package/web-dist/assets/{railroadDiagram-RFXS5EU6-DKq5FagA.js → railroadDiagram-RFXS5EU6-M363ils_.js} +1 -1
  79. package/web-dist/assets/{requirementDiagram-TGXJPOKE-BJ5tGazp.js → requirementDiagram-TGXJPOKE-CTs2_V6T.js} +1 -1
  80. package/web-dist/assets/{sankeyDiagram-HTMAVEWB-XSJjcBhX.js → sankeyDiagram-HTMAVEWB-QTLLcDPD.js} +1 -1
  81. package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-CBb8emSe.js → sequenceDiagram-DBY2YBRQ-Bxw9Tr6e.js} +1 -1
  82. package/web-dist/assets/{stateDiagram-2N3HPSRC-DDfRW94V.js → stateDiagram-2N3HPSRC-BecB6roG.js} +1 -1
  83. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-JXw9T96l.js +1 -0
  84. package/web-dist/assets/{swimlanes-5IMT3BWC-DvRYbkZi.js → swimlanes-5IMT3BWC-COiYgS0w.js} +1 -1
  85. package/web-dist/assets/swimlanesDiagram-G3AALYLV--1Wv7FQQ.js +8 -0
  86. package/web-dist/assets/{timeline-definition-FHXFAJF6-BNUa_DwI.js → timeline-definition-FHXFAJF6-Ci22coeH.js} +1 -1
  87. package/web-dist/assets/{treeView-QDETBFTQ-I6-IW6nJ.js → treeView-QDETBFTQ-4DW35czh.js} +1 -1
  88. package/web-dist/assets/{treemap-6X3UGDF4-CWWmEUYJ.js → treemap-6X3UGDF4-Dq_-Z6-Y.js} +1 -1
  89. package/web-dist/assets/{vennDiagram-L72KCM5P-DTDrPGLk.js → vennDiagram-L72KCM5P-CHQSIEYq.js} +1 -1
  90. package/web-dist/assets/{wardley-OPB4EBWU-CNsdgXXA.js → wardley-OPB4EBWU-Bwlh7HCY.js} +1 -1
  91. package/web-dist/assets/{wardleyDiagram-EHGQE667-YE0tq3Kh.js → wardleyDiagram-EHGQE667-DctjuPYn.js} +1 -1
  92. package/web-dist/assets/{xychartDiagram-FW5EYKEG-D0ofMX8C.js → xychartDiagram-FW5EYKEG-ZoIGosv8.js} +1 -1
  93. package/web-dist/favicon-16.png +0 -0
  94. package/web-dist/favicon-32.png +0 -0
  95. package/web-dist/favicon.svg +17 -61
  96. package/web-dist/icon-192.png +0 -0
  97. package/web-dist/icon-512.png +0 -0
  98. package/web-dist/icon-maskable-512.png +0 -0
  99. package/web-dist/index.html +19 -8
  100. package/web-dist/manifest.webmanifest +3 -3
  101. package/runtime/cc-worker/hooks/scratchpad-stop.mjs +0 -125
  102. package/runtime/cc-worker/skills/handoff/SKILL.md +0 -209
  103. package/web-dist/assets/architecture-TIHT7OUA-CAviNivx.js +0 -1
  104. package/web-dist/assets/channel-CqKDIFQF.js +0 -1
  105. package/web-dist/assets/classDiagram-OUVF2IWQ-B_-6iXYY.js +0 -1
  106. package/web-dist/assets/classDiagram-v2-EOCWNBFH-B_-6iXYY.js +0 -1
  107. package/web-dist/assets/eventmodeling-45OFAUF4-MpmeH5YZ.js +0 -1
  108. package/web-dist/assets/flowDiagram-23GEKE2U-D-QgjjhF.js +0 -1
  109. package/web-dist/assets/index-w4v-GZEc.js +0 -358
  110. package/web-dist/assets/index-zyi22LPz.css +0 -1
  111. package/web-dist/assets/railroad-abnf-AHOZXSZD-YeUoiySk.js +0 -1
  112. package/web-dist/assets/railroad-ebnf-EBAXGLYW-Ddw1SuGG.js +0 -1
  113. package/web-dist/assets/railroad-peg-LSFZ7HO6-Dd8BOGeW.js +0 -1
  114. package/web-dist/assets/stateDiagram-v2-6OUMAXLB-hc41W5Lx.js +0 -1
  115. package/web-dist/assets/swimlanesDiagram-G3AALYLV-BZyGdgSG.js +0 -8
package/dist/frizz.js CHANGED
@@ -12,8 +12,8 @@ var __export = (target2, all) => {
12
12
 
13
13
  // packages/server/src/frizz-paths.ts
14
14
  import { existsSync } from "node:fs";
15
- import { homedir as homedir2 } from "node:os";
16
- import { join as join2 } from "node:path";
15
+ import { homedir as homedir2, tmpdir } from "node:os";
16
+ import { dirname, join as join2 } from "node:path";
17
17
  function legacyFrizzRoot(home = homedir2()) {
18
18
  return join2(home, LEGACY_DIR_NAME);
19
19
  }
@@ -387,7 +387,7 @@ import {
387
387
  writeFileSync as writeFileSync2
388
388
  } from "node:fs";
389
389
  import { homedir as homedir4 } from "node:os";
390
- import { dirname, join as join4, resolve } from "node:path";
390
+ import { dirname as dirname2, join as join4, resolve } from "node:path";
391
391
  import { setTimeout as delay } from "node:timers/promises";
392
392
  function errorCode2(error) {
393
393
  return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
@@ -492,7 +492,7 @@ function quarantineLock(lockPath) {
492
492
  return true;
493
493
  }
494
494
  function tryAcquireLock(lockPath, adapter) {
495
- mkdirSync2(dirname(lockPath), { recursive: true, mode: 448 });
495
+ mkdirSync2(dirname2(lockPath), { recursive: true, mode: 448 });
496
496
  let fd;
497
497
  try {
498
498
  fd = openSync2(lockPath, "wx", 384);
@@ -513,7 +513,7 @@ function tryAcquireLock(lockPath, adapter) {
513
513
  `, "utf8");
514
514
  fsyncSync(fd);
515
515
  closeSync(fd);
516
- syncDirectory(dirname(lockPath));
516
+ syncDirectory(dirname2(lockPath));
517
517
  } catch (error) {
518
518
  try {
519
519
  closeSync(fd);
@@ -663,7 +663,7 @@ function syncGitConfigPath(path) {
663
663
  } finally {
664
664
  closeSync(fd);
665
665
  }
666
- syncDirectory(dirname(path));
666
+ syncDirectory(dirname2(path));
667
667
  }
668
668
  function syncGitConfig(dir) {
669
669
  let raw;
@@ -744,6 +744,35 @@ function resolveGitProjectIdentity(dir, home = homedir4()) {
744
744
  release();
745
745
  }
746
746
  }
747
+ function parseRepoLabel(remoteUrl) {
748
+ const url = remoteUrl.trim();
749
+ if (!url) return null;
750
+ const scp = url.match(/^[^/@]+@[^:/]+:(.+?)(?:\.git)?\/?$/);
751
+ if (scp) return normalizeOwnerRepo(scp[1]);
752
+ const m = url.match(/^[a-z][a-z0-9+.-]*:\/\/[^/]+\/(.+?)(?:\.git)?\/?$/i);
753
+ if (m) return normalizeOwnerRepo(m[1]);
754
+ return null;
755
+ }
756
+ function normalizeOwnerRepo(path) {
757
+ const parts = path.split("/").filter(Boolean);
758
+ if (parts.length < 2) return null;
759
+ return parts.slice(-2).join("/");
760
+ }
761
+ function originRemoteUrl(dir) {
762
+ try {
763
+ return execFileSync2("git", ["remote", "get-url", "origin"], {
764
+ cwd: dir,
765
+ encoding: "utf8",
766
+ stdio: ["ignore", "pipe", "ignore"]
767
+ }).trim() || null;
768
+ } catch {
769
+ return null;
770
+ }
771
+ }
772
+ function resolveProjectLabel(dir) {
773
+ const url = originRemoteUrl(dir);
774
+ return url ? parseRepoLabel(url) : null;
775
+ }
747
776
  var GLOBAL_LOCK_NAME, GLOBAL_LOCK_OWNER, PORT_LOCK_DIR, INCOMPLETE_LOCK_GRACE_MS, LOCK_POLL_MS, PROJECT_ID_RE, SYNC_WAIT2;
748
777
  var init_project_identity = __esm({
749
778
  "packages/server/src/project-identity.ts"() {
@@ -773,7 +802,7 @@ import {
773
802
  statSync as statSync3,
774
803
  writeFileSync as writeFileSync3
775
804
  } from "node:fs";
776
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join5 } from "node:path";
805
+ import { basename as basename2, dirname as dirname3, isAbsolute, join as join5 } from "node:path";
777
806
  function errorCode3(error) {
778
807
  return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
779
808
  }
@@ -796,8 +825,8 @@ function syncDirectory2(path) {
796
825
  }
797
826
  }
798
827
  function atomicJson(path, value) {
799
- mkdirSync3(dirname2(path), { recursive: true, mode: 448 });
800
- const temp = join5(dirname2(path), `.${basename2(path)}.${process.pid}.${randomUUID3()}.tmp`);
828
+ mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
829
+ const temp = join5(dirname3(path), `.${basename2(path)}.${process.pid}.${randomUUID3()}.tmp`);
801
830
  let fd;
802
831
  try {
803
832
  fd = openSync3(temp, "wx", 384);
@@ -807,7 +836,7 @@ function atomicJson(path, value) {
807
836
  closeSync2(fd);
808
837
  fd = void 0;
809
838
  renameSync2(temp, path);
810
- syncDirectory2(dirname2(path));
839
+ syncDirectory2(dirname3(path));
811
840
  } catch (error) {
812
841
  if (fd !== void 0) {
813
842
  try {
@@ -984,7 +1013,7 @@ function writeNewOwner(path, record) {
984
1013
  fsyncSync2(fd);
985
1014
  closeSync2(fd);
986
1015
  fd = void 0;
987
- syncDirectory2(dirname2(path));
1016
+ syncDirectory2(dirname3(path));
988
1017
  } catch (error) {
989
1018
  if (fd !== void 0) {
990
1019
  try {
@@ -1200,6 +1229,7 @@ var OWNER_NAME, MUTATION_GUARD_NAME, OWNER_VERSION, PARTIAL_GRACE_MS, GUARD_TIME
1200
1229
  var init_project_launch = __esm({
1201
1230
  "packages/server/src/project-launch.ts"() {
1202
1231
  "use strict";
1232
+ init_project_identity();
1203
1233
  init_process_generation();
1204
1234
  init_process_generation();
1205
1235
  OWNER_NAME = "project-launch.owner";
@@ -1376,7 +1406,7 @@ var init_local_origin = __esm({
1376
1406
 
1377
1407
  // packages/server/src/boot-progress.ts
1378
1408
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
1379
- import { basename as basename3, dirname as dirname3, join as join6 } from "node:path";
1409
+ import { basename as basename3, dirname as dirname4, join as join6 } from "node:path";
1380
1410
  function bootProgressPath(stateDir) {
1381
1411
  return join6(stateDir, BOOT_PROGRESS_NAME);
1382
1412
  }
@@ -1397,419 +1427,13 @@ var init_boot_progress = __esm({
1397
1427
  }
1398
1428
  });
1399
1429
 
1400
- // packages/server/src/sqlite-quiet.ts
1401
- function isSqliteExperimentalNotice(warning, type) {
1402
- const name = typeof warning === "string" ? type : warning?.name;
1403
- if (name !== "ExperimentalWarning") return false;
1404
- const text = typeof warning === "string" ? warning : warning?.message ?? "";
1405
- return text.includes("SQLite");
1406
- }
1407
- var emitWarning;
1408
- var init_sqlite_quiet = __esm({
1409
- "packages/server/src/sqlite-quiet.ts"() {
1410
- "use strict";
1411
- emitWarning = process.emitWarning.bind(process);
1412
- process.emitWarning = ((warning, ...rest) => {
1413
- const type = typeof rest[0] === "string" ? rest[0] : void 0;
1414
- if (isSqliteExperimentalNotice(warning, type)) return;
1415
- return emitWarning(warning, ...rest);
1416
- });
1417
- }
1418
- });
1419
-
1420
- // packages/server/src/sqlite.ts
1421
- import { createRequire } from "node:module";
1422
- function namedParameters(sql) {
1423
- const found = /* @__PURE__ */ new Set();
1424
- for (let index = 0; index < sql.length; index++) {
1425
- const char = sql[index];
1426
- if (char === "'" || char === '"' || char === "`") {
1427
- const quote = char;
1428
- index++;
1429
- while (index < sql.length) {
1430
- if (sql[index] === quote) {
1431
- if (sql[index + 1] === quote) index++;
1432
- else break;
1433
- }
1434
- index++;
1435
- }
1436
- continue;
1437
- }
1438
- if (char === "[") {
1439
- while (index < sql.length && sql[index] !== "]") index++;
1440
- continue;
1441
- }
1442
- if (char === "-" && sql[index + 1] === "-") {
1443
- while (index < sql.length && sql[index] !== "\n") index++;
1444
- continue;
1445
- }
1446
- if (char === "/" && sql[index + 1] === "*") {
1447
- index += 2;
1448
- while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) index++;
1449
- index++;
1450
- continue;
1451
- }
1452
- if (char === "@" || char === ":" || char === "$") {
1453
- let end = index + 1;
1454
- while (end < sql.length && /[A-Za-z0-9_]/.test(sql[end])) end++;
1455
- if (end > index + 1) found.add(sql.slice(index + 1, end));
1456
- index = end - 1;
1457
- }
1458
- }
1459
- return [...found];
1460
- }
1461
- function isNamedBagCandidate(value) {
1462
- return typeof value === "object" && value !== null && !Array.isArray(value) && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer) && !(value instanceof Date);
1463
- }
1464
- function plainRow(row) {
1465
- return row === void 0 || row === null ? row : Object.assign({}, row);
1466
- }
1467
- function hasBinding(bag, name) {
1468
- return name in bag || `@${name}` in bag || `:${name}` in bag || `$${name}` in bag;
1469
- }
1470
- function transactionControl(sql) {
1471
- let last = null;
1472
- for (const statement of sql.split(";")) {
1473
- const text = statement.trim().toUpperCase();
1474
- if (/^BEGIN\b/.test(text)) last = "open";
1475
- else if (/^(COMMIT|END)\b/.test(text)) last = "close";
1476
- else if (/^ROLLBACK\b/.test(text) && !/^ROLLBACK\s+TO\b/.test(text)) last = "close";
1477
- }
1478
- return last;
1479
- }
1480
- var DatabaseSync, PreparedStatement, Database;
1481
- var init_sqlite = __esm({
1482
- "packages/server/src/sqlite.ts"() {
1483
- "use strict";
1484
- init_sqlite_quiet();
1485
- ({ DatabaseSync } = createRequire(import.meta.url)("node:sqlite"));
1486
- PreparedStatement = class {
1487
- #statement;
1488
- #required;
1489
- #sql;
1490
- constructor(statement, sql) {
1491
- this.#statement = statement;
1492
- this.#sql = sql;
1493
- this.#required = namedParameters(sql);
1494
- }
1495
- /**
1496
- * Build exactly the bindings this statement asks for, and nothing else.
1497
- *
1498
- * Frizz hands whole row objects to statements that bind a subset of their fields, which raw
1499
- * node:sqlite rejects with "Unknown named parameter". The obvious fix is
1500
- * `setAllowUnknownNamedParameters(true)` — but that method only exists from Node 23.11, and using it
1501
- * would have raised Frizz's floor above the release that first shipped `node:sqlite` at all, for no
1502
- * reason. Selecting the required names from the bag needs no runtime API, and gets the same result.
1503
- *
1504
- * A name the caller never supplied throws, rather than binding NULL the way node:sqlite would.
1505
- * See the file header: silently writing NULL where a value was expected is the one difference that
1506
- * could quietly corrupt data instead of failing.
1507
- */
1508
- #bind(params) {
1509
- if (params.length === 1 && this.#required.length > 0 && isNamedBagCandidate(params[0])) {
1510
- const bag = params[0];
1511
- const bound = {};
1512
- for (const name of this.#required) {
1513
- if (!hasBinding(bag, name)) {
1514
- throw new RangeError(
1515
- `Missing named parameter "${name}" for statement: ${this.#sql.trim().slice(0, 120)}`
1516
- );
1517
- }
1518
- const value = name in bag ? bag[name] : bag[`@${name}`] ?? bag[`:${name}`] ?? bag[`$${name}`];
1519
- bound[name] = value === void 0 ? null : value;
1520
- }
1521
- return [bound];
1522
- }
1523
- return params.map((value) => value === void 0 ? null : value);
1524
- }
1525
- run(...params) {
1526
- const result = this.#statement.run(...this.#bind(params));
1527
- return { changes: Number(result.changes), lastInsertRowid: Number(result.lastInsertRowid) };
1528
- }
1529
- get(...params) {
1530
- return plainRow(this.#statement.get(...this.#bind(params)));
1531
- }
1532
- all(...params) {
1533
- return this.#statement.all(...this.#bind(params)).map(plainRow);
1534
- }
1535
- };
1536
- Database = class {
1537
- #db;
1538
- /** Transaction nesting depth, so a nested transaction becomes a SAVEPOINT rather than an error. */
1539
- #depth = 0;
1540
- /** Fallback transaction state for runtimes without `isTransaction` (added in Node 24). */
1541
- #openTransaction = false;
1542
- /** better-sqlite3 exposes the database file here; the scheduler tests reopen a store through it. */
1543
- name;
1544
- constructor(path, options2 = {}) {
1545
- this.name = path;
1546
- this.#db = new DatabaseSync(path, options2.readonly ? { readOnly: true } : {});
1547
- }
1548
- /** True while a transaction is open, however it was opened. */
1549
- get inTransaction() {
1550
- const native = this.#db.isTransaction;
1551
- return typeof native === "boolean" ? native : this.#openTransaction;
1552
- }
1553
- /**
1554
- * Type parameters are `<BindParameters, Row>`, matching better-sqlite3 — 53 call sites already
1555
- * spell them that way (`prepare<[string], SessionRow>`), and silently swapping the order would
1556
- * type every one of them as `any` rather than failing loudly.
1557
- */
1558
- prepare(sql) {
1559
- return new PreparedStatement(this.#db.prepare(sql), sql);
1560
- }
1561
- /** Returns the database, matching better-sqlite3, so existing `db.exec(a).exec(b)` chains hold. */
1562
- exec(sql) {
1563
- this.#db.exec(sql);
1564
- const control = transactionControl(sql);
1565
- if (control === "open") this.#openTransaction = true;
1566
- else if (control === "close") this.#openTransaction = false;
1567
- return this;
1568
- }
1569
- /**
1570
- * `PRAGMA` with better-sqlite3's calling convention. Frizz only ever SETS pragmas (`journal_mode`,
1571
- * `busy_timeout`, `foreign_keys`) and ignores the result, but returning the rows keeps the shape
1572
- * honest for a read like `pragma("journal_mode")`.
1573
- */
1574
- pragma(source) {
1575
- const sql = `PRAGMA ${source}`;
1576
- try {
1577
- return this.#db.prepare(sql).all();
1578
- } catch {
1579
- this.#db.exec(sql);
1580
- return void 0;
1581
- }
1582
- }
1583
- /**
1584
- * better-sqlite3's `transaction()`, including the part that is easy to miss: it is RE-ENTRANT.
1585
- * Calling one transaction-wrapped function from inside another must not throw "cannot start a
1586
- * transaction within a transaction" — better-sqlite3 quietly switches to a SAVEPOINT, and Frizz's
1587
- * storage layer composes these freely, so the nesting has to survive the port.
1588
- */
1589
- transaction(fn) {
1590
- const runWith = (mode) => (...args2) => {
1591
- const depth = this.#depth;
1592
- const savepoint = `frizz_txn_${depth}`;
1593
- this.#depth = depth + 1;
1594
- try {
1595
- this.exec(depth === 0 ? `BEGIN ${mode}` : `SAVEPOINT ${savepoint}`);
1596
- let result;
1597
- try {
1598
- result = fn(...args2);
1599
- } catch (error) {
1600
- this.exec(depth === 0 ? "ROLLBACK" : `ROLLBACK TO ${savepoint}`);
1601
- if (depth > 0) this.exec(`RELEASE ${savepoint}`);
1602
- throw error;
1603
- }
1604
- this.exec(depth === 0 ? "COMMIT" : `RELEASE ${savepoint}`);
1605
- return result;
1606
- } finally {
1607
- this.#depth = depth;
1608
- }
1609
- };
1610
- const wrapper = runWith("IMMEDIATE");
1611
- wrapper.default = wrapper;
1612
- wrapper.deferred = runWith("DEFERRED");
1613
- wrapper.immediate = runWith("IMMEDIATE");
1614
- wrapper.exclusive = runWith("EXCLUSIVE");
1615
- return wrapper;
1616
- }
1617
- close() {
1618
- this.#db.close();
1619
- }
1620
- };
1621
- }
1622
- });
1623
-
1624
- // packages/server/src/migrate-fray.ts
1625
- import { execFileSync as execFileSync3 } from "node:child_process";
1626
- import { existsSync as existsSync2, lstatSync, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync4, rmdirSync } from "node:fs";
1627
- import { homedir as homedir5 } from "node:os";
1628
- import { join as join7, resolve as resolve2 } from "node:path";
1629
- function xdg2(env, name) {
1630
- const value = env[name];
1631
- return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
1632
- }
1633
- function movesFor(env, platform, home) {
1634
- const moves = [{ from: join7(home, ".fray"), to: join7(home, ".frizz") }];
1635
- if (platform === "win32") {
1636
- const local = env.LOCALAPPDATA || join7(env.USERPROFILE || home, "AppData", "Local");
1637
- moves.push({ from: join7(local, "Fray"), to: join7(local, "Frizz") });
1638
- } else if (platform === "darwin") {
1639
- const support = join7(home, "Library", "Application Support");
1640
- moves.push({ from: join7(support, "Fray"), to: join7(support, "Frizz") });
1641
- moves.push({ from: join7(home, "Library", "Caches", "Fray"), to: join7(home, "Library", "Caches", "Frizz") });
1642
- } else {
1643
- moves.push({ from: join7(home, ".local", "share", "fray"), to: join7(home, ".local", "share", "frizz") });
1644
- moves.push({ from: join7(home, ".local", "state", "fray"), to: join7(home, ".local", "state", "frizz") });
1645
- moves.push({ from: join7(home, ".cache", "fray"), to: join7(home, ".cache", "frizz") });
1646
- }
1647
- for (const name of ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) {
1648
- const base = xdg2(env, name);
1649
- if (base) moves.push({ from: join7(base, "fray"), to: join7(base, "frizz") });
1650
- }
1651
- return moves;
1652
- }
1653
- function migrateFrayGlobalRoots(options2 = {}) {
1654
- const env = options2.env ?? process.env;
1655
- const platform = options2.platform ?? process.platform;
1656
- const home = options2.home ?? homedir5();
1657
- const exists = options2.exists ?? existsSync2;
1658
- const rename = options2.rename ?? renameSync4;
1659
- const merge = options2.merge ?? mergeInto;
1660
- const moved = [];
1661
- for (const move of movesFor(env, platform, home)) {
1662
- if (!exists(move.from)) continue;
1663
- try {
1664
- if (exists(move.to)) {
1665
- if (merge(move.from, move.to)) moved.push(move);
1666
- continue;
1667
- }
1668
- rename(move.from, move.to);
1669
- moved.push(move);
1670
- } catch {
1671
- }
1672
- }
1673
- return moved;
1674
- }
1675
- function mergeInto(from, to) {
1676
- let moved = false;
1677
- for (const entry of readdirSync2(from, { withFileTypes: true })) {
1678
- const src = join7(from, entry.name);
1679
- const dst = join7(to, entry.name);
1680
- if (!existsSync2(dst)) {
1681
- renameSync4(src, dst);
1682
- moved = true;
1683
- continue;
1684
- }
1685
- if (entry.isDirectory() && lstatSync(dst).isDirectory() && mergeInto(src, dst)) moved = true;
1686
- }
1687
- try {
1688
- rmdirSync(from);
1689
- } catch {
1690
- }
1691
- return moved;
1692
- }
1693
- function migrateFrayProjectDir(projectDir) {
1694
- const from = join7(projectDir, ".fray");
1695
- const to = join7(projectDir, ".frizz");
1696
- try {
1697
- if (!lstatSync(from).isDirectory()) return false;
1698
- if (!existsSync2(to)) {
1699
- renameSync4(from, to);
1700
- return true;
1701
- }
1702
- return lstatSync(to).isDirectory() ? mergeInto(from, to) : false;
1703
- } catch {
1704
- return false;
1705
- }
1706
- }
1707
- function readId(dir, scope, key) {
1708
- let output;
1709
- try {
1710
- output = execFileSync3("git", ["config", ...scope, "--get-all", key], {
1711
- cwd: dir,
1712
- encoding: "utf8",
1713
- env: { ...process.env, LC_ALL: "C" },
1714
- stdio: ["ignore", "pipe", "ignore"]
1715
- });
1716
- } catch {
1717
- return void 0;
1718
- }
1719
- const values = output.replace(/\r?\n$/u, "").split(/\r?\n/u).filter((value) => value.length > 0);
1720
- if (values.length !== 1) return void 0;
1721
- const id = (values[0] ?? "").trim();
1722
- return PROJECT_ID_RE2.test(id) ? id : void 0;
1723
- }
1724
- function writeId(dir, scope, id) {
1725
- try {
1726
- execFileSync3("git", ["config", ...scope, "--replace-all", CURRENT_KEY, id], {
1727
- cwd: dir,
1728
- stdio: ["ignore", "ignore", "ignore"]
1729
- });
1730
- } catch {
1731
- return false;
1732
- }
1733
- return readId(dir, scope, CURRENT_KEY) === id;
1734
- }
1735
- function hasBoard(id, home) {
1736
- const db = join7(projectStateDir(id, home), "ui.db");
1737
- if (!existsSync2(db)) return false;
1738
- let sql;
1739
- try {
1740
- sql = new Database(db, { readonly: true });
1741
- const row = sql.prepare("select count(*) as n from session").get();
1742
- return Number(row?.n ?? 0) > 0;
1743
- } catch {
1744
- return true;
1745
- } finally {
1746
- try {
1747
- sql?.close();
1748
- } catch {
1749
- }
1750
- }
1751
- }
1752
- function linkedWorktreeScopes(dir) {
1753
- const gitPath = (flag) => {
1754
- try {
1755
- const raw = execFileSync3("git", ["rev-parse", flag], {
1756
- cwd: dir,
1757
- encoding: "utf8",
1758
- env: { ...process.env, LC_ALL: "C" },
1759
- stdio: ["ignore", "pipe", "ignore"]
1760
- }).trim();
1761
- return realpathSync2(resolve2(dir, raw));
1762
- } catch {
1763
- return void 0;
1764
- }
1765
- };
1766
- const gitDir = gitPath("--git-dir");
1767
- const commonGitDir = gitPath("--git-common-dir");
1768
- if (!gitDir || !commonGitDir || gitDir === commonGitDir) return void 0;
1769
- return {
1770
- legacy: ["--file", join7(gitDir, "fray.config")],
1771
- current: ["--file", join7(gitDir, "frizz.config")]
1772
- };
1773
- }
1774
- function adopt(dir, legacy, current, home) {
1775
- const legacyId = readId(dir, legacy, LEGACY_KEY);
1776
- if (!legacyId) return void 0;
1777
- const currentId = readId(dir, current, CURRENT_KEY);
1778
- if (currentId === legacyId) return void 0;
1779
- if (currentId && hasBoard(currentId, home)) return void 0;
1780
- if (!writeId(dir, current, legacyId)) return void 0;
1781
- return currentId ? { id: legacyId, replaced: currentId } : { id: legacyId };
1782
- }
1783
- function migrateFrayProjectId(projectDir, { home } = {}) {
1784
- const adopted = [];
1785
- const repository = adopt(projectDir, ["--local"], ["--local"], home);
1786
- if (repository) adopted.push(repository);
1787
- const worktree = linkedWorktreeScopes(projectDir);
1788
- if (worktree) {
1789
- const own = adopt(projectDir, worktree.legacy, worktree.current, home);
1790
- if (own) adopted.push({ ...own, worktree: true });
1791
- }
1792
- return adopted;
1793
- }
1794
- var PROJECT_ID_RE2, LEGACY_KEY, CURRENT_KEY;
1795
- var init_migrate_fray = __esm({
1796
- "packages/server/src/migrate-fray.ts"() {
1797
- "use strict";
1798
- init_frizz_paths();
1799
- init_sqlite();
1800
- PROJECT_ID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
1801
- LEGACY_KEY = "fray.id";
1802
- CURRENT_KEY = "frizz.id";
1803
- }
1804
- });
1805
-
1806
1430
  // packages/server/src/project-root.ts
1807
1431
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
1808
- import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync5, openSync as openSync4, readFileSync as readFileSync5, renameSync as renameSync5, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
1809
- import { homedir as homedir6 } from "node:os";
1810
- import { dirname as dirname4, join as join8, parse, resolve as resolve3 } from "node:path";
1432
+ import { closeSync as closeSync3, existsSync as existsSync2, fsyncSync as fsyncSync3, mkdirSync as mkdirSync5, openSync as openSync4, readFileSync as readFileSync5, realpathSync as realpathSync2, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
1433
+ import { homedir as homedir5 } from "node:os";
1434
+ import { dirname as dirname5, join as join7, parse, resolve as resolve2 } from "node:path";
1811
1435
  function projectIdPath(root) {
1812
- return join8(root, FRIZZ_DIR, ID_FILE);
1436
+ return join7(root, FRIZZ_DIR, ID_FILE);
1813
1437
  }
1814
1438
  function readProjectIdFile(root) {
1815
1439
  let raw;
@@ -1825,17 +1449,17 @@ function readProjectIdFile(root) {
1825
1449
  }
1826
1450
  }
1827
1451
  function writeProjectIdFile(root, id) {
1828
- const dir = join8(root, FRIZZ_DIR);
1452
+ const dir = join7(root, FRIZZ_DIR);
1829
1453
  mkdirSync5(dir, { recursive: true });
1830
- const ignore = join8(dir, SELF_IGNORE);
1831
- if (!existsSync3(ignore)) {
1454
+ const ignore = join7(dir, SELF_IGNORE);
1455
+ if (!existsSync2(ignore)) {
1832
1456
  try {
1833
1457
  writeFileSync5(ignore, "*\n", { flag: "wx" });
1834
1458
  } catch {
1835
1459
  }
1836
1460
  }
1837
1461
  const path = projectIdPath(root);
1838
- const temp = join8(dir, `.${ID_FILE}.${process.pid}.${randomUUID4()}.tmp`);
1462
+ const temp = join7(dir, `.${ID_FILE}.${process.pid}.${randomUUID4()}.tmp`);
1839
1463
  let fd;
1840
1464
  try {
1841
1465
  fd = openSync4(temp, "wx", 384);
@@ -1844,7 +1468,7 @@ function writeProjectIdFile(root, id) {
1844
1468
  fsyncSync3(fd);
1845
1469
  closeSync3(fd);
1846
1470
  fd = void 0;
1847
- renameSync5(temp, path);
1471
+ renameSync4(temp, path);
1848
1472
  } catch (error) {
1849
1473
  if (fd !== void 0) {
1850
1474
  try {
@@ -1863,7 +1487,7 @@ function writeProjectIdFile(root, id) {
1863
1487
  function projectRootLockName(root) {
1864
1488
  return `identity-path-${createHash3("sha256").update(root).digest("hex")}.lock`;
1865
1489
  }
1866
- function ensureProjectIdFile(root, home = homedir6(), seed) {
1490
+ function ensureProjectIdFile(root, home = homedir5(), seed) {
1867
1491
  const existing = readProjectIdFile(root);
1868
1492
  if (existing) return existing;
1869
1493
  const release = acquireNamedLaunchLockSync(home, projectRootLockName(root));
@@ -1883,23 +1507,52 @@ function isNotAGitWorktree(error) {
1883
1507
  return /not a git repository/iu.test(stderr) || /must be run in a work ?tree/iu.test(stderr);
1884
1508
  }
1885
1509
  function hasAny(dir, names) {
1886
- return names.some((name) => existsSync3(join8(dir, name)));
1510
+ return names.some((name) => existsSync2(join7(dir, name)));
1887
1511
  }
1888
- function discoverProjectRoot(cwd = process.cwd(), home = homedir6()) {
1512
+ function isHomeDirectory(dir, home = homedir5()) {
1513
+ const canonical = (value) => {
1514
+ try {
1515
+ return realpathSync2(resolve2(value));
1516
+ } catch {
1517
+ return resolve2(value);
1518
+ }
1519
+ };
1520
+ try {
1521
+ return canonical(dir) === canonical(home);
1522
+ } catch {
1523
+ return false;
1524
+ }
1525
+ }
1526
+ function existingProjectId(dir) {
1527
+ const fromFile = readProjectIdFile(dir);
1528
+ if (fromFile) return fromFile;
1529
+ try {
1530
+ return readGitProjectId(dir);
1531
+ } catch {
1532
+ return void 0;
1533
+ }
1534
+ }
1535
+ function isExistingProjectRoot(dir) {
1536
+ return existingProjectId(dir) !== void 0;
1537
+ }
1538
+ function hasProjectMarker(dir) {
1539
+ return hasAny(dir, REPO_MARKERS) || hasAny(dir, PROJECT_MARKERS);
1540
+ }
1541
+ function discoverProjectRoot(cwd = process.cwd(), home = homedir5()) {
1889
1542
  let dir;
1890
1543
  try {
1891
- dir = resolve3(cwd);
1544
+ dir = resolve2(cwd);
1892
1545
  } catch {
1893
- return resolve3(cwd);
1546
+ return resolve2(cwd);
1894
1547
  }
1895
- const stop = resolve3(home);
1548
+ const stop = resolve2(home);
1896
1549
  const filesystemRoot = parse(dir).root;
1897
- for (let at = dir; ; at = dirname4(at)) {
1550
+ for (let at = dir; ; at = dirname5(at)) {
1898
1551
  if (at === stop || at === filesystemRoot) break;
1899
- if (existsSync3(projectIdPath(at))) return at;
1552
+ if (existsSync2(projectIdPath(at))) return at;
1900
1553
  if (hasAny(at, REPO_MARKERS)) return at;
1901
1554
  if (hasAny(at, PROJECT_MARKERS)) return at;
1902
- if (dirname4(at) === at) break;
1555
+ if (dirname5(at) === at) break;
1903
1556
  }
1904
1557
  return dir;
1905
1558
  }
@@ -6035,6 +5688,10 @@ var init_zod = __esm({
6035
5688
  });
6036
5689
 
6037
5690
  // packages/shared/src/thread-slug.ts
5691
+ function slugify(title) {
5692
+ const s = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5693
+ return s.slice(0, THREAD_SLUG_MAX_CHARS).replace(/-+$/g, "") || "thread";
5694
+ }
6038
5695
  var THREAD_SLUG_MAX_CHARS, ThreadSlug;
6039
5696
  var init_thread_slug = __esm({
6040
5697
  "packages/shared/src/thread-slug.ts"() {
@@ -6595,7 +6252,10 @@ var init_receipt_bus = __esm({
6595
6252
  });
6596
6253
 
6597
6254
  // packages/shared/src/index.ts
6598
- var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, ALLDONE_SENTINEL, OPT_OUT_NOTE, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, DEFAULT_PORT;
6255
+ function fallbackPort(base) {
6256
+ return base + 1e4;
6257
+ }
6258
+ var ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, NativeInputRequired, AwaitingHint, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, ThreadWatchKind, RegisterableThreadWatchKind, ThreadWatchTarget, ThreadWatchView, ThreadTimerView, ThreadFence, PlanView, LimitWindow, LimitPause, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountLogoutInput, AccountLogoutResult, AccountLoginStartInput, AccountLoginStartResult, AccountLoginStatusInput, AccountLoginStatusResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, FollowUpInput, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, SetThreadSnoozeInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, AddOwnThreadWatchInput, PromoteOwnThreadWatchInput, PromoteOwnThreadWatchResult, DropOwnThreadWatchInput, ListOwnThreadWatchesInput, OwnThreadWatchesResult, AddOwnThreadWatchResult, DropOwnThreadWatchResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, ConfirmAwaitingInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, SetThreadProfileInput, SetThreadProfileResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, GithubStatus, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketClientMsg, ProjectCard, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
6599
6259
  var init_src = __esm({
6600
6260
  "packages/shared/src/index.ts"() {
6601
6261
  "use strict";
@@ -6863,21 +6523,57 @@ var init_src = __esm({
6863
6523
  RecurringIntervalSeconds = external_exports.number().int().min(RECURRING_MIN_INTERVAL_SECONDS).max(RECURRING_MAX_INTERVAL_SECONDS);
6864
6524
  ThreadRecurringPrompt = external_exports.object({
6865
6525
  prompt: external_exports.string(),
6866
- /** The two mechanisms, named as the panel labels them. `stopHook` fires at every rest; `heartbeat`
6867
- * fires on `intervalSeconds` and reaches the agent mid-turn. */
6526
+ /** The three mechanisms, named as the panel labels them. `stopHook` fires at every rest; `heartbeat`
6527
+ * fires on `intervalSeconds`; `postCompaction` fires whenever the harness summarizes the thread's
6528
+ * context away. The latter two both reach the agent mid-turn. */
6868
6529
  stopHook: external_exports.boolean(),
6869
6530
  heartbeat: external_exports.boolean(),
6531
+ postCompaction: external_exports.boolean(),
6532
+ /** NOT a fourth trigger — a HOLD over all three. While it is on, nothing is sent for as long as the
6533
+ * thread is blocked on the human: an unanswered ```question fence, a native ask, or a permission
6534
+ * prompt. The footer panel seeds it ON for a fresh arming, with the stop hook, because they are one
6535
+ * intent — see scheduler.ts, "WHAT A PENDING QUESTION DOES TO ALL THREE TRIGGERS". The stored column
6536
+ * still defaults OFF, so an existing row and an older caller both keep the behaviour they had. */
6537
+ pauseOnQuestions: external_exports.boolean(),
6870
6538
  intervalSeconds: external_exports.number().int().positive().optional(),
6871
6539
  armedAt: external_exports.string(),
6872
- /** Last delivery of the ON REST trigger; the two are stamped separately so each reads its own clock. */
6540
+ /** Last delivery per trigger; stamped separately so each reads its own clock. */
6873
6541
  lastRestFiredAt: external_exports.string().optional(),
6874
- lastScheduleFiredAt: external_exports.string().optional()
6542
+ lastScheduleFiredAt: external_exports.string().optional(),
6543
+ lastCompactFiredAt: external_exports.string().optional()
6875
6544
  }).strict();
6876
- ALLDONE_SENTINEL = "ALLDONE";
6877
- OPT_OUT_NOTE = `If there is genuinely no further work, reply ${ALLDONE_SENTINEL} on its own line to stop these prompts \u2014 but be sure, because it permanently stalls this run.`;
6878
6545
  TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
6879
6546
  TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
6880
6547
  TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
6548
+ SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
6549
+ SIGNOFF_NUDGE_MESSAGE = [
6550
+ `${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and this is not a request to do more work.`,
6551
+ "",
6552
+ "You rested without a fence, so this thread cannot be triaged. Add one at the END of your next message:",
6553
+ "",
6554
+ "- `` ```question `` \u2014 you need the human. One question per fence, lettered options, one recommended.",
6555
+ "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
6556
+ " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
6557
+ " **bolded verb phrase**.",
6558
+ "- Blocked on a PR, CI, or your own background shell? Register it with `mcp__frizz__watch` and rest.",
6559
+ "",
6560
+ "**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
6561
+ "fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
6562
+ "",
6563
+ "Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
6564
+ "seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
6565
+ "frizz \u2014 so anything you assumed they had followed, they have not."
6566
+ ].join("\n");
6567
+ ThreadWatchKind = external_exports.enum(["shell", "github"]);
6568
+ RegisterableThreadWatchKind = external_exports.enum(["shell"]);
6569
+ ThreadWatchTarget = external_exports.string().trim().min(1).max(200);
6570
+ ThreadWatchView = external_exports.object({
6571
+ id: external_exports.string(),
6572
+ kind: ThreadWatchKind,
6573
+ target: external_exports.string(),
6574
+ state: external_exports.enum(["armed", "fired", "dropped"]),
6575
+ createdAt: external_exports.string()
6576
+ }).strict();
6881
6577
  ThreadTimerView = external_exports.object({
6882
6578
  id: external_exports.string(),
6883
6579
  prompt: external_exports.string(),
@@ -6982,6 +6678,14 @@ var init_src = __esm({
6982
6678
  // Live background SHELLS the worker launched (tailer-derived). Same default-[] discipline. Rendered
6983
6679
  // in the anchored background-ops strip alongside sub-agents; ids make current rows drillable.
6984
6680
  bgShells: external_exports.array(BgShellView).default([]),
6681
+ // The thread's ARMED WATCHERS — registry-derived, not folded from the transcript, which is what makes
6682
+ // them survive the worker saying one more sentence. Same default-[] discipline as the two above.
6683
+ //
6684
+ // These NEVER park the thread: a row with an armed watcher stays a visible queue handoff, and Snooze
6685
+ // remains the only way to hide one (maintainer 2026-08-12, choosing this over auto-Held for every
6686
+ // kind). So this field is for the operator to SEE what a thread is waiting on — it deliberately feeds
6687
+ // no queue-membership rule.
6688
+ watches: external_exports.array(ThreadWatchView).default([]),
6985
6689
  // A pending native AskUserQuestion the session is frozen on (tailer-derived). Optional — absent
6986
6690
  // when there's no unanswered ask. Feeds needsAction + the read-only question render + "Answer in Terminal".
6987
6691
  pendingAsk: PendingAsk.optional(),
@@ -7068,9 +6772,6 @@ var init_src = __esm({
7068
6772
  // ISO8601 read/seen telemetry (threadSeen RPC — recorded when the human opens the thread). Kept for
7069
6773
  // compatibility and analytics only; viewing never acknowledges or removes a queue handoff.
7070
6774
  seenAt: external_exports.string().optional(),
7071
- // Project-relative scratchpad path (.frizz/threads/<session-id>/scratch.md) once provisioned — the worker's
7072
- // compaction-proof working memory, rendered as the thread's doc tab.
7073
- scratchpadPath: external_exports.string().optional(),
7074
6775
  // Project-relative plan artifact this thread was dispatched from (.frizz/plans/*.md), if any.
7075
6776
  planPath: external_exports.string().optional(),
7076
6777
  // Which agent backend runs this thread (Codex-support epic, Phase 3) — drives the subtle per-row
@@ -7087,13 +6788,13 @@ var init_src = __esm({
7087
6788
  // Raw durable barrier bit. Unlike permissionPending this remains true for a future/corrupt value,
7088
6789
  // so rolling clients fail closed instead of enabling another composer while ownership is unknown.
7089
6790
  permissionChangePending: external_exports.boolean().optional(),
7090
- // The last decision the worker's permission POLICY made for this thread (cc-worker/hooks/
7091
- // perm-policy.mjs), and how many times it has DENIED. A policy decision never blocks anyone, so it
7092
- // has no other way of being noticed an allow is not written to the transcript at all. Surfacing it
7093
- // is what keeps "frizz silently approved something on your behalf" from being invisible.
7094
- // Deferred requests are deliberately absent: those already show as a permission prompt / Needs you.
6791
+ // The last DENIAL the worker's permission POLICY made for this thread (cc-worker/hooks/
6792
+ // perm-policy.mjs), and how many times it has denied. A refusal changes what the worker can do, so
6793
+ // it earns a card. Approvals and deferrals are deliberately absent: a deferral already shows as a
6794
+ // permission prompt / Needs you, and an approval blocks nobody the quiet line that used to report
6795
+ // one stuck to the bottom of the thread forever, describing a command the reader had long moved past.
7095
6796
  permPolicy: external_exports.object({
7096
- decision: external_exports.enum(["allow", "deny"]),
6797
+ decision: external_exports.literal("deny"),
7097
6798
  rule: external_exports.string(),
7098
6799
  reason: external_exports.string(),
7099
6800
  tool: external_exports.string().nullable(),
@@ -7135,6 +6836,18 @@ var init_src = __esm({
7135
6836
  projectName: external_exports.string(),
7136
6837
  projectLabel: external_exports.string(),
7137
6838
  // "owner/repo" from the git origin remote; falls back to projectName
6839
+ // "owner/repo" ONLY when that origin remote is github.com — the link target the rendered-markdown
6840
+ // autolinker turns `#123` and a bare commit hash into. Deliberately NOT projectLabel, which is a
6841
+ // host-agnostic DISPLAY name: a GitLab origin yields an owner/repo there too, and pointing its `#12`
6842
+ // at github.com would be a wrong destination rather than a missing one. Absent means the
6843
+ // augmentation stays off (no remote, another forge, or a pre-restart server).
6844
+ githubRepo: external_exports.string().optional(),
6845
+ // This project's URL slug — the `<slug>` in `/project/<slug>`. The client cannot derive it: a
6846
+ // PREFIXED page reads it off its own path, but the LAUNCHING project is served unprefixed and so has
6847
+ // nothing to read, which left `/` — the all-projects GRID — as the only URL its queue could name.
6848
+ // Optional so a pre-restart server keeps working; absent means "fall back to `/`", i.e. the old
6849
+ // behaviour. Registry-derived, so it is the same slug every other surface links to.
6850
+ projectSlug: external_exports.string().optional(),
7138
6851
  // (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
7139
6852
  // board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
7140
6853
  // was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
@@ -7223,6 +6936,17 @@ var init_src = __esm({
7223
6936
  // presents it; resolveClaudeEffort (server/backend/claude-effort.ts) translates it at the spawn edge.
7224
6937
  effort: external_exports.enum(["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"]).optional(),
7225
6938
  notifications: external_exports.boolean(),
6939
+ /**
6940
+ * The permanent column of project icons. OFF by default, on purpose.
6941
+ *
6942
+ * A rail of every project on the machine is a standing invitation to leave the thread you are in —
6943
+ * "just too tempting" (maintainer 2026-08-06). Frizz's home is one board; the grid is a page you go
6944
+ * to, not furniture you sit beside. Hidden, the way back is a breadcrumb in the status bar, which
6945
+ * costs a click exactly when you meant to switch and nothing when you did not.
6946
+ *
6947
+ * Machine-level, like the font: which chrome you want is a property of the person, not the repo.
6948
+ */
6949
+ projectRail: external_exports.boolean(),
7226
6950
  // UI type family. `mono` (default) is the mono-forward system; `sans` swaps prose/UI chrome to a
7227
6951
  // sans stack while code / tool lines / the terminal stay mono. Optional so an old settings blob
7228
6952
  // parses; defaultSettings pins "mono".
@@ -7361,6 +7085,13 @@ var init_src = __esm({
7361
7085
  prompt: RecurringPromptText.nullable(),
7362
7086
  stopHook: external_exports.boolean(),
7363
7087
  heartbeat: external_exports.boolean(),
7088
+ // The POST-COMPACTION trigger (scheduler SOURCE 7, added 2026-08-06). Defaulted rather than required
7089
+ // so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
7090
+ // with the trigger off, which is the honest reading of a caller that has never heard of it.
7091
+ postCompaction: external_exports.boolean().default(false),
7092
+ // The QUESTION HOLD (2026-08-11). Defaulted for the same reason as the trigger above: a caller that
7093
+ // has never heard of it means "don't hold", which is also this option's own default.
7094
+ pauseOnQuestions: external_exports.boolean().default(false),
7364
7095
  intervalSeconds: RecurringIntervalSeconds.optional()
7365
7096
  }).strict();
7366
7097
  SetOwnThreadRecurringPromptInput = external_exports.object({
@@ -7368,8 +7099,23 @@ var init_src = __esm({
7368
7099
  prompt: RecurringPromptText.nullable(),
7369
7100
  stopHook: external_exports.boolean(),
7370
7101
  heartbeat: external_exports.boolean(),
7102
+ // The POST-COMPACTION trigger (scheduler SOURCE 7, added 2026-08-06). Defaulted rather than required
7103
+ // so a client that predates it — an older tab, an older MCP server — keeps writing the row correctly
7104
+ // with the trigger off, which is the honest reading of a caller that has never heard of it.
7105
+ postCompaction: external_exports.boolean().default(false),
7106
+ // The QUESTION HOLD (2026-08-11), defaulted exactly as the trigger above and for the same reason.
7107
+ pauseOnQuestions: external_exports.boolean().default(false),
7371
7108
  intervalSeconds: RecurringIntervalSeconds.optional()
7372
7109
  }).strict();
7110
+ SetOwnThreadRecurringPromptResult = external_exports.object({
7111
+ replaced: ThreadRecurringPrompt.nullable()
7112
+ }).strict();
7113
+ GetOwnThreadRecurringPromptInput = external_exports.object({
7114
+ slug: ThreadSlug
7115
+ }).strict();
7116
+ OwnThreadRecurringPromptResult = external_exports.object({
7117
+ recurringPrompt: ThreadRecurringPrompt.nullable()
7118
+ }).strict();
7373
7119
  SetOwnThreadTimerInput = external_exports.object({
7374
7120
  slug: ThreadSlug,
7375
7121
  prompt: TimerPromptText,
@@ -7394,6 +7140,49 @@ var init_src = __esm({
7394
7140
  cancelled: external_exports.boolean(),
7395
7141
  timers: external_exports.array(ThreadTimerView)
7396
7142
  }).strict();
7143
+ AddOwnThreadWatchInput = external_exports.object({
7144
+ slug: ThreadSlug,
7145
+ // REGISTERABLE kinds only, which is `shell` and nothing else — deliberately narrower than
7146
+ // `ThreadWatchKind`, whose `github` member is a board-derived VIEW row with no registration behind it.
7147
+ // The MCP tool already refuses a PR here in words ("a pr wait does not belong here — use an ```awaiting
7148
+ // fence"); this is the same refusal at the wire, where it cannot be talked past.
7149
+ kind: RegisterableThreadWatchKind,
7150
+ target: ThreadWatchTarget,
7151
+ /** The worker is BLOCKING on this one inside its tool call, so the scheduler settles it silently
7152
+ * instead of delivering a wake — see the column comment in storage.ts. Defaulted, so a caller that
7153
+ * has never heard of it registers an ordinary background watch. */
7154
+ foreground: external_exports.boolean().default(false)
7155
+ }).strict();
7156
+ PromoteOwnThreadWatchInput = external_exports.object({
7157
+ slug: ThreadSlug,
7158
+ id: external_exports.string().min(1).max(64)
7159
+ }).strict();
7160
+ PromoteOwnThreadWatchResult = external_exports.object({
7161
+ promoted: external_exports.boolean(),
7162
+ watches: external_exports.array(ThreadWatchView)
7163
+ }).strict();
7164
+ DropOwnThreadWatchInput = external_exports.object({
7165
+ slug: ThreadSlug,
7166
+ id: external_exports.string().min(1).max(64)
7167
+ }).strict();
7168
+ ListOwnThreadWatchesInput = external_exports.object({
7169
+ slug: ThreadSlug
7170
+ }).strict();
7171
+ OwnThreadWatchesResult = external_exports.object({
7172
+ watches: external_exports.array(ThreadWatchView)
7173
+ }).strict();
7174
+ AddOwnThreadWatchResult = external_exports.object({
7175
+ id: external_exports.string(),
7176
+ /** True when this exact (kind, target) was ALREADY armed on the thread, so the call registered
7177
+ * nothing new. A worker re-registering after a compaction is the common case, and silently minting a
7178
+ * duplicate would mean two wakes for one event. */
7179
+ alreadyArmed: external_exports.boolean(),
7180
+ watches: external_exports.array(ThreadWatchView)
7181
+ }).strict();
7182
+ DropOwnThreadWatchResult = external_exports.object({
7183
+ dropped: external_exports.boolean(),
7184
+ watches: external_exports.array(ThreadWatchView)
7185
+ }).strict();
7397
7186
  SetOwnThreadStopHookInput = external_exports.object({
7398
7187
  slug: ThreadSlug,
7399
7188
  prompt: RecurringPromptText.nullable(),
@@ -7434,6 +7223,11 @@ var init_src = __esm({
7434
7223
  // attempted a write after the flip to danger-full-access was still refused). So the change is real and
7435
7224
  // durable, yet it does not reach work already executing. Distinct from "next-resume", which means
7436
7225
  // nothing was applied to the live session at all.
7226
+ //
7227
+ // It is ALSO the Claude answer, arrived at from the opposite direction: a permission mode is a LAUNCH
7228
+ // flag there, so frizz retires the idle worker process and the next turn cold-resumes under the new
7229
+ // one (router `setThreadPermission`). Same promise to the operator — stored, and true from the next
7230
+ // turn on — reached by restarting rather than by retuning.
7437
7231
  effect: external_exports.enum(["applied", "next-turn", "next-resume"])
7438
7232
  });
7439
7233
  ThreadProfileOptionsInput = external_exports.object({ slug: ThreadSlug }).strict();
@@ -7853,7 +7647,25 @@ ${DISPATCH_TASK_BANNER}
7853
7647
  // there and stores the DISPATCH id here. Additive + optional: absent when the ack was never seen (a
7854
7648
  // resumed session whose dispatch scrolled out), and the line then renders as plain text, not a dead link.
7855
7649
  peerFrom: external_exports.string().optional(),
7856
- peerDispatchId: external_exports.string().optional()
7650
+ peerDispatchId: external_exports.string().optional(),
7651
+ // …and the tell that `peerFrom` is ONLY that subagent_type — that the parser could not resolve the
7652
+ // dispatch's own description for this sender. It matters because a profile cell is not a name: every
7653
+ // child dispatched at `frizz:opus-high` reports under the identical string, so a divider reading
7654
+ // «frizz:opus-high» names the MODEL, not the work, and two siblings are indistinguishable
7655
+ // (maintainer 2026-08-06: "I'm also still occasionally seeing things like 'Agent <OPUS:HIGH>
7656
+ // rested'"). The client renders an unnamed sender as "Sub-agent reported" and keeps the cell in the
7657
+ // tooltip, rather than promoting a profile to a title.
7658
+ //
7659
+ // Resolution is genuinely late-arriving, not merely missing: the description comes from the DISPATCH
7660
+ // record, so a report rendered while the window has not yet reached that record is unnamed and gains
7661
+ // its title once it has. Set only on the Claude path — a codex peer names a real task.
7662
+ peerUnnamed: external_exports.literal(true).optional(),
7663
+ // The sender's own RUNTIME agent id (`origin.senderTaskId`) — kept so a LATER pass can finish the job
7664
+ // the fold could not. The paged transcript RPC folds a bounded window, so a report whose dispatch
7665
+ // scrolled above the page start has no description available at fold time; the tailer still holds the
7666
+ // pairing, and `projectTranscriptPeerNames` uses this id to ask it. Never a drawer key on its own —
7667
+ // that is `peerDispatchId`, which the same pass can also supply once this resolves.
7668
+ peerSenderTaskId: external_exports.string().optional()
7857
7669
  });
7858
7670
  TranscriptPageCursor = external_exports.string().min(1).max(2048).regex(/^[A-Za-z0-9_-]+$/);
7859
7671
  TranscriptPage = external_exports.object({
@@ -7872,12 +7684,240 @@ ${DISPATCH_TASK_BANNER}
7872
7684
  external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict(),
7873
7685
  external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict()
7874
7686
  ]);
7875
- DEFAULT_PORT = 4917;
7687
+ ProjectCard = external_exports.object({
7688
+ id: external_exports.string(),
7689
+ slug: external_exports.string(),
7690
+ name: external_exports.string(),
7691
+ path: external_exports.string(),
7692
+ lastOpenedAt: external_exports.string(),
7693
+ /** The directory is gone — moved or deleted. The card stays so it can be reopened or forgotten. */
7694
+ stale: external_exports.boolean(),
7695
+ /**
7696
+ * When this project's icon was last established, or absent if it never has been.
7697
+ *
7698
+ * Carried purely so the client can hang it off the `/_frizz/project-icon` URL: the icon bytes are
7699
+ * cached hard (a rail of forty squares is forty requests, and none of them should recur), which
7700
+ * means a newly uploaded icon would otherwise stay invisible behind the cached old one. A changed
7701
+ * version is a changed URL, so the swap is immediate without weakening the caching for everyone.
7702
+ *
7703
+ * Deliberately NOT "does this project have an icon". Answering that for a project nobody has
7704
+ * scanned yet would mean scanning it, and this list is one file read on purpose.
7705
+ */
7706
+ iconVersion: external_exports.string().optional(),
7707
+ /**
7708
+ * Whether this project HAS an icon — and crucially, whether we have even looked.
7709
+ *
7710
+ * Three states, not a boolean, because the two "no icon to draw right now" cases must behave
7711
+ * differently and a boolean collapses them:
7712
+ * · `icon` — one is stored; draw it.
7713
+ * · `none` — scanned, nothing found. Draw the monogram and DO NOT request the icon route,
7714
+ * which is what stops an iconless project flashing its initials and then swapping.
7715
+ * · `unknown` — never scanned. The monogram shows, but the request MUST still go out, because
7716
+ * that request is what triggers the (lazy, cached) scan in the first place.
7717
+ *
7718
+ * Collapsing `unknown` into `none` deadlocks the whole feature: no image element is rendered, so
7719
+ * the icon route is never called, so the scan never runs, so the project stays `unknown` forever.
7720
+ * Measured 2026-08-06 — a rail of 29 projects had scanned exactly ONE, and only because a probe
7721
+ * had fetched that one's URL by hand.
7722
+ */
7723
+ iconStatus: external_exports.enum(["icon", "none", "unknown"]),
7724
+ /** An operator's uploaded icon, rather than one the scan found. Drives what the menu offers. */
7725
+ iconIsCustom: external_exports.boolean().optional()
7726
+ });
7727
+ PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
7728
+ DirectoryPickResult = external_exports.discriminatedUnion("kind", [
7729
+ external_exports.object({ kind: external_exports.literal("picked"), project: ProjectCard }),
7730
+ external_exports.object({ kind: external_exports.literal("cancelled") }),
7731
+ external_exports.object({ kind: external_exports.literal("unavailable"), reason: external_exports.string() })
7732
+ ]);
7733
+ ThreadLocation = external_exports.object({ projectSlug: external_exports.string(), projectName: external_exports.string() });
7734
+ FRIZZ_ROUTE_PREFIX = "/_frizz";
7735
+ DEFAULT_PORT = 9393;
7736
+ }
7737
+ });
7738
+
7739
+ // packages/server/src/project-registry.ts
7740
+ import { closeSync as closeSync4, existsSync as existsSync3, fsyncSync as fsyncSync4, mkdirSync as mkdirSync6, openSync as openSync5, readFileSync as readFileSync6, readdirSync as readdirSync2, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync6 } from "node:fs";
7741
+ import { homedir as homedir6 } from "node:os";
7742
+ import { basename as basename4, dirname as dirname6, join as join8 } from "node:path";
7743
+ function registryPath(home = homedir6()) {
7744
+ return join8(frizzPaths({ home }).data, "registry.json");
7745
+ }
7746
+ function readRegistry(home = homedir6()) {
7747
+ let raw;
7748
+ try {
7749
+ raw = readFileSync6(registryPath(home), "utf8");
7750
+ } catch {
7751
+ return { ...EMPTY, projects: [] };
7752
+ }
7753
+ try {
7754
+ const parsed = JSON.parse(raw);
7755
+ if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
7756
+ return {
7757
+ version: 1,
7758
+ projects: parsed.projects.filter(
7759
+ (p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
7760
+ )
7761
+ };
7762
+ } catch {
7763
+ return { ...EMPTY, projects: [] };
7764
+ }
7765
+ }
7766
+ function writeRegistry(registry, home = homedir6()) {
7767
+ const path = registryPath(home);
7768
+ mkdirSync6(dirname6(path), { recursive: true });
7769
+ const temp = `${path}.${process.pid}.tmp`;
7770
+ let fd;
7771
+ try {
7772
+ fd = openSync5(temp, "w", 384);
7773
+ writeFileSync6(fd, `${JSON.stringify(registry, null, 2)}
7774
+ `, "utf8");
7775
+ fsyncSync4(fd);
7776
+ closeSync4(fd);
7777
+ fd = void 0;
7778
+ renameSync5(temp, path);
7779
+ } catch (error) {
7780
+ if (fd !== void 0) {
7781
+ try {
7782
+ closeSync4(fd);
7783
+ } catch {
7784
+ }
7785
+ }
7786
+ try {
7787
+ rmSync6(temp, { force: true });
7788
+ } catch {
7789
+ }
7790
+ throw error;
7791
+ }
7792
+ }
7793
+ function slugifyProject(value) {
7794
+ const s = slugify(value);
7795
+ return s === "thread" ? "project" : s;
7796
+ }
7797
+ function deriveSlug(dir, taken, options2 = {}) {
7798
+ const base = slugifyProject(basename4(dir));
7799
+ const parent = slugifyProject(basename4(dirname6(dir)));
7800
+ const qualified = GENERIC.has(base) && parent ? `${parent}-${base}` : base;
7801
+ const candidates = [
7802
+ qualified,
7803
+ ...options2.remoteOwner ? [`${slugifyProject(options2.remoteOwner)}-${base}`] : [],
7804
+ ...parent && parent !== qualified ? [`${parent}-${base}`] : []
7805
+ ];
7806
+ for (const candidate of candidates) {
7807
+ if (candidate && !taken.has(candidate) && !RESERVED.has(candidate)) return candidate;
7808
+ }
7809
+ const root = candidates[0] || "project";
7810
+ for (let n = 2; ; n++) {
7811
+ const candidate = `${root}-${n}`;
7812
+ if (!taken.has(candidate) && !RESERVED.has(candidate)) return candidate;
7813
+ }
7814
+ }
7815
+ function canonicalPath(dir) {
7816
+ try {
7817
+ return realpathSync3(dir);
7818
+ } catch {
7819
+ return dir;
7820
+ }
7821
+ }
7822
+ function registerProject(input, home = homedir6()) {
7823
+ input = { ...input, dir: canonicalPath(input.dir) };
7824
+ const registry = readRegistry(home);
7825
+ const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
7826
+ const byId = registry.projects.find((p) => p.id === input.id);
7827
+ if (byId) {
7828
+ if (byId.path === input.dir) {
7829
+ byId.lastOpenedAt = at;
7830
+ writeRegistry(registry, home);
7831
+ return { entry: byId, action: "reopened" };
7832
+ }
7833
+ if (existsSync3(join8(byId.path, ".frizz", ".id"))) return { action: "duplicate" };
7834
+ byId.path = input.dir;
7835
+ byId.lastOpenedAt = at;
7836
+ writeRegistry(registry, home);
7837
+ return { entry: byId, action: "moved" };
7838
+ }
7839
+ const byPath = registry.projects.find((p) => p.path === input.dir);
7840
+ if (byPath) {
7841
+ byPath.id = input.id;
7842
+ byPath.lastOpenedAt = at;
7843
+ writeRegistry(registry, home);
7844
+ return { entry: byPath, action: "rekeyed" };
7845
+ }
7846
+ const taken = new Set(registry.projects.map((p) => p.slug));
7847
+ const entry = {
7848
+ id: input.id,
7849
+ path: input.dir,
7850
+ slug: deriveSlug(input.dir, taken, { remoteOwner: input.remoteOwner }),
7851
+ lastOpenedAt: at
7852
+ };
7853
+ registry.projects.push(entry);
7854
+ writeRegistry(registry, home);
7855
+ return { entry, action: "created" };
7856
+ }
7857
+ function listProjects(home = homedir6()) {
7858
+ const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
7859
+ return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync3(p.path) })).sort((a, b) => {
7860
+ if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
7861
+ if (a.order === void 0) return 1;
7862
+ if (b.order === void 0) return -1;
7863
+ return a.order - b.order;
7864
+ });
7865
+ }
7866
+ function findByPath(path, home = homedir6()) {
7867
+ const canonical = canonicalPath(path);
7868
+ return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
7869
+ }
7870
+ var EMPTY, GENERIC, RESERVED;
7871
+ var init_project_registry = __esm({
7872
+ "packages/server/src/project-registry.ts"() {
7873
+ "use strict";
7874
+ init_src();
7875
+ init_frizz_paths();
7876
+ init_project_root();
7877
+ EMPTY = { version: 1, projects: [] };
7878
+ GENERIC = /* @__PURE__ */ new Set([
7879
+ "app",
7880
+ "src",
7881
+ "web",
7882
+ "www",
7883
+ "main",
7884
+ "repo",
7885
+ "code",
7886
+ "server",
7887
+ "client",
7888
+ "packages",
7889
+ "site",
7890
+ "scratch",
7891
+ "tmp",
7892
+ "test"
7893
+ ]);
7894
+ RESERVED = /* @__PURE__ */ new Set([
7895
+ "_frizz",
7896
+ "rpc",
7897
+ "events",
7898
+ "ws",
7899
+ "term",
7900
+ "attach",
7901
+ "local-image",
7902
+ "local-visualization",
7903
+ // `control` answers `/control/stop` and `/control/status` — the supervisor channel the board polls
7904
+ // every few seconds. It was missing from this list, so a directory called `control` could have taken
7905
+ // the segment and routed the whole machine's supervisor polling into one project's app, which has no
7906
+ // such route. Only newly minted slugs consult this, so adding it strands nothing already registered.
7907
+ "control",
7908
+ "project-icon",
7909
+ "assets",
7910
+ "favicon",
7911
+ "manifest",
7912
+ "index",
7913
+ "api",
7914
+ "health"
7915
+ ]);
7876
7916
  }
7877
7917
  });
7878
7918
 
7879
7919
  // packages/server/src/local-image.ts
7880
- import { readFileSync as readFileSync7, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
7920
+ import { readFileSync as readFileSync8, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
7881
7921
  import { extname, isAbsolute as isAbsolute2 } from "node:path";
7882
7922
  function resolveLocalImage(rawPath) {
7883
7923
  if (!rawPath || !isAbsolute2(rawPath)) return { status: 400 };
@@ -7885,13 +7925,13 @@ function resolveLocalImage(rawPath) {
7885
7925
  if (!contentType) return { status: 400 };
7886
7926
  let real;
7887
7927
  try {
7888
- real = realpathSync4(rawPath);
7928
+ real = realpathSync5(rawPath);
7889
7929
  } catch {
7890
7930
  return { status: 404 };
7891
7931
  }
7892
7932
  try {
7893
7933
  if (!statSync4(real).isFile()) return { status: 404 };
7894
- return { status: 200, contentType, body: readFileSync7(real) };
7934
+ return { status: 200, contentType, body: readFileSync8(real) };
7895
7935
  } catch {
7896
7936
  return { status: 404 };
7897
7937
  }
@@ -7911,7 +7951,7 @@ var init_local_image = __esm({
7911
7951
  });
7912
7952
 
7913
7953
  // src/production.ts
7914
- import { readFileSync as readFileSync9 } from "node:fs";
7954
+ import { readFileSync as readFileSync10 } from "node:fs";
7915
7955
  import { homedir as homedir8 } from "node:os";
7916
7956
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7917
7957
  import { join as join11 } from "node:path";
@@ -8076,12 +8116,12 @@ function manifestIdFor(url) {
8076
8116
  return url.endsWith("/") ? url : `${url}/`;
8077
8117
  }
8078
8118
  function execFileP(cmd, args2) {
8079
- return new Promise((resolve6, reject) => {
8119
+ return new Promise((resolve5, reject) => {
8080
8120
  execFile(
8081
8121
  cmd,
8082
8122
  args2,
8083
8123
  { encoding: "utf8", timeout: 1e4 },
8084
- (err, stdout) => err ? reject(err) : resolve6(stdout)
8124
+ (err, stdout) => err ? reject(err) : resolve5(stdout)
8085
8125
  );
8086
8126
  });
8087
8127
  }
@@ -8160,9 +8200,9 @@ function openCdpSession(browserPath, dataPath) {
8160
8200
  cmdPipe.on("error", failAll);
8161
8201
  resPipe.on("error", failAll);
8162
8202
  child.on("exit", () => failAll(new Error("chrome exited before the CDP handshake completed")));
8163
- const call = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve6, reject) => {
8203
+ const call = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve5, reject) => {
8164
8204
  const id = ++seq;
8165
- pending.set(id, { resolve: resolve6, reject });
8205
+ pending.set(id, { resolve: resolve5, reject });
8166
8206
  cmdPipe.write(JSON.stringify({ id, method, params }) + "\0");
8167
8207
  setTimeout(() => {
8168
8208
  if (pending.has(id)) {
@@ -8175,15 +8215,15 @@ function openCdpSession(browserPath, dataPath) {
8175
8215
  await call("Browser.close").catch(() => {
8176
8216
  });
8177
8217
  if (child.exitCode !== null || child.signalCode !== null) return;
8178
- await new Promise((resolve6) => {
8218
+ await new Promise((resolve5) => {
8179
8219
  const t = setTimeout(() => {
8180
8220
  child.kill();
8181
- resolve6();
8221
+ resolve5();
8182
8222
  }, 1e4);
8183
8223
  t.unref();
8184
8224
  child.once("exit", () => {
8185
8225
  clearTimeout(t);
8186
- resolve6();
8226
+ resolve5();
8187
8227
  });
8188
8228
  });
8189
8229
  };
@@ -8661,15 +8701,15 @@ init_project_launch();
8661
8701
  init_local_origin();
8662
8702
  init_boot_progress();
8663
8703
  init_frizz_paths();
8664
- init_migrate_fray();
8665
8704
  init_project_root();
8666
8705
  init_logging();
8667
8706
  init_src();
8668
- import { execFileSync as execFileSync4 } from "node:child_process";
8707
+ init_project_registry();
8708
+ import { execFileSync as execFileSync3 } from "node:child_process";
8669
8709
  import { createServer } from "node:net";
8670
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync6, realpathSync as realpathSync3, writeFileSync as writeFileSync6 } from "node:fs";
8710
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
8671
8711
  import { homedir as homedir7, networkInterfaces } from "node:os";
8672
- import { basename as basename4, join as join9, resolve as resolve4 } from "node:path";
8712
+ import { basename as basename5, join as join9, resolve as resolve3 } from "node:path";
8673
8713
  import { setTimeout as delay2 } from "node:timers/promises";
8674
8714
  var PORT_SCAN_COUNT = 100;
8675
8715
  var LAUNCH_TIMEOUT_MS = 3e4;
@@ -8816,6 +8856,9 @@ function resolveBindSelection(options2, env = process.env) {
8816
8856
  const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
8817
8857
  return { host, exposed: bindHostIsExposed(host), allowedHosts, ...publicOrigin ? { publicOrigin } : {} };
8818
8858
  }
8859
+ function boardAddress(url) {
8860
+ return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
8861
+ }
8819
8862
  function networkUrls(port, host, interfaces = networkInterfaces) {
8820
8863
  if (!bindHostIsExposed(host)) return [];
8821
8864
  const wildcard = host === ALL_INTERFACES_BIND_HOST || host === "::";
@@ -8837,10 +8880,30 @@ function networkUrls(port, host, interfaces = networkInterfaces) {
8837
8880
  }
8838
8881
  return urls;
8839
8882
  }
8840
- function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.env, { migrate = false } = {}) {
8883
+ function resolveLaunchIntent(cwd = process.cwd(), home = homedir7(), env = process.env) {
8884
+ const candidate = realpathSync4(discoverProjectRoot(cwd, home));
8885
+ const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
8886
+ if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
8887
+ return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
8888
+ const host = mostRecentProject(home, env);
8889
+ const reason = isHomeDirectory(candidate, home) ? "home" : "unadopted";
8890
+ if (!host) return { kind: "empty", directory: candidate, reason };
8891
+ return reason === "home" ? { kind: "grid", workspace: host } : { kind: "offer", workspace: host, directory: candidate };
8892
+ }
8893
+ function mostRecentProject(home, env) {
8894
+ for (const entry of listProjects(home)) {
8895
+ if (entry.stale || isHomeDirectory(entry.path, home)) continue;
8896
+ try {
8897
+ return resolveWorkspace(entry.path, home, env);
8898
+ } catch {
8899
+ }
8900
+ }
8901
+ return void 0;
8902
+ }
8903
+ function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.env) {
8841
8904
  let gitRoot;
8842
8905
  try {
8843
- gitRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
8906
+ gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
8844
8907
  cwd,
8845
8908
  encoding: "utf8",
8846
8909
  // stderr is CAPTURED, not ignored: it is the only thing that distinguishes "no worktree here"
@@ -8851,15 +8914,12 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.
8851
8914
  if (!isNotAGitWorktree(error)) throw new Error("unable to resolve Git repository root");
8852
8915
  gitRoot = void 0;
8853
8916
  }
8854
- if (migrate) migrateFrayGlobalRoots({ env, home });
8855
- const root0 = realpathSync3(gitRoot ?? discoverProjectRoot(cwd, home));
8856
- if (migrate) migrateFrayProjectId(root0, { home });
8917
+ const root0 = realpathSync4(gitRoot ?? discoverProjectRoot(cwd, home));
8857
8918
  const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
8858
8919
  const root = identity?.root ?? root0;
8859
- if (migrate) migrateFrayProjectDir(root);
8860
8920
  const id = ensureProjectIdFile(root, home, identity?.id);
8861
8921
  const stateDir = projectStateDir(id, home);
8862
- mkdirSync6(stateDir, { recursive: true });
8922
+ mkdirSync7(stateDir, { recursive: true });
8863
8923
  const target2 = {
8864
8924
  projectId: id,
8865
8925
  projectDir: root,
@@ -8870,7 +8930,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir7(), env = process.
8870
8930
  root,
8871
8931
  id,
8872
8932
  stateDir,
8873
- name: basename4(root),
8933
+ name: basename5(root),
8874
8934
  identityScope: identity?.scope ?? "repository"
8875
8935
  };
8876
8936
  }
@@ -8885,7 +8945,7 @@ function workspaceLaunchTarget(workspace2) {
8885
8945
  function workspaceFromLaunchTarget(target2, env = process.env) {
8886
8946
  let root;
8887
8947
  try {
8888
- root = realpathSync3(target2.projectDir);
8948
+ root = realpathSync4(target2.projectDir);
8889
8949
  } catch {
8890
8950
  throw new Error("pinned Frizz workspace is no longer available");
8891
8951
  }
@@ -8895,14 +8955,14 @@ function workspaceFromLaunchTarget(target2, env = process.env) {
8895
8955
  root,
8896
8956
  id: target2.projectId,
8897
8957
  stateDir: target2.stateDir,
8898
- name: basename4(root),
8958
+ name: basename5(root),
8899
8959
  identityScope: target2.identityScope === "worktree" ? "worktree" : "repository"
8900
8960
  };
8901
8961
  }
8902
8962
  function parseStatusFile(path, authoritative, expected2, adapter = defaultProcessPlatformAdapter) {
8903
8963
  try {
8904
8964
  const value = JSON.parse(
8905
- readFileSync6(path, "utf8")
8965
+ readFileSync7(path, "utf8")
8906
8966
  );
8907
8967
  if (!Number.isInteger(value.pid) || value.pid <= 0 || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
8908
8968
  return null;
@@ -8944,19 +9004,21 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
8944
9004
  function readPreferredPort(stateDir) {
8945
9005
  try {
8946
9006
  const value = JSON.parse(
8947
- readFileSync6(join9(stateDir, "launcher.json"), "utf8")
9007
+ readFileSync7(join9(stateDir, "launcher.json"), "utf8")
8948
9008
  );
8949
9009
  return Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 ? value.port : void 0;
8950
9010
  } catch {
8951
9011
  return void 0;
8952
9012
  }
8953
9013
  }
8954
- async function probeFrizz(port, expected2, fetcher = fetch) {
9014
+ var HEALTH_PROBE_TIMEOUT_MS = 1e3;
9015
+ async function probeFrizz(port, expected2, fetcher = fetch, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
8955
9016
  const controller = new AbortController();
8956
- const timeout = setTimeout(() => controller.abort(), 1e3);
9017
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
8957
9018
  timeout.unref?.();
8958
9019
  try {
8959
- const response = await fetcher(`http://127.0.0.1:${port}/health`, {
9020
+ const scope = expected2.slug ? `${FRIZZ_ROUTE_PREFIX}/${expected2.slug}` : FRIZZ_ROUTE_PREFIX;
9021
+ const response = await fetcher(`http://127.0.0.1:${port}${scope}/health`, {
8960
9022
  signal: controller.signal
8961
9023
  });
8962
9024
  if (!response.ok) return null;
@@ -8981,26 +9043,36 @@ function expectedOwnerHealth(target2, owner) {
8981
9043
  ...owner ? { ownerProof: projectLaunchTokenProof(target2, owner.token) } : {}
8982
9044
  };
8983
9045
  }
8984
- async function canBindPort(port, host = LOOPBACK_BIND_HOST) {
8985
- const bind2 = (candidate) => new Promise((resolveBind) => {
9046
+ async function probeBindPort(port, host = LOOPBACK_BIND_HOST) {
9047
+ return new Promise((resolveBind) => {
8986
9048
  const server = createServer();
8987
9049
  server.unref();
8988
- server.once("error", () => resolveBind(false));
8989
- server.listen(candidate, host, () => server.close(() => resolveBind(true)));
9050
+ server.once(
9051
+ "error",
9052
+ (error) => resolveBind(error.code ?? "EADDRINUSE")
9053
+ );
9054
+ server.listen(port, host, () => server.close(() => resolveBind(void 0)));
8990
9055
  });
8991
- return bind2(port);
8992
9056
  }
8993
- function portCandidates(preferred) {
9057
+ async function canBindPort(port, host = LOOPBACK_BIND_HOST) {
9058
+ return await probeBindPort(port, host) === void 0;
9059
+ }
9060
+ function portUnavailableMessage(port, code) {
9061
+ if (code === "EACCES" || code === "EPERM")
9062
+ return `port ${port} is reserved by the system, not held by a process (bind returned ${code}); on Windows list the reservations with: netsh int ipv4 show excludedportrange protocol=tcp`;
9063
+ return `port ${port} is already in use`;
9064
+ }
9065
+ function portCandidates(preferred, base = DEFAULT_PORT) {
9066
+ const fallback = fallbackPort(base);
8994
9067
  return [
9068
+ base,
8995
9069
  preferred,
8996
- ...Array.from(
8997
- { length: PORT_SCAN_COUNT },
8998
- (_, index) => DEFAULT_PORT + index
8999
- )
9070
+ ...Array.from({ length: PORT_SCAN_COUNT }, (_, index) => fallback + index)
9000
9071
  ].filter((port) => !!port && port <= 65535);
9001
9072
  }
9002
- function noFreePortMessage() {
9003
- return `no free Frizz development port found in ${DEFAULT_PORT}-${DEFAULT_PORT + PORT_SCAN_COUNT - 1}`;
9073
+ function noFreePortMessage(base = DEFAULT_PORT) {
9074
+ const fallback = fallbackPort(base);
9075
+ return `no free Frizz port: ${base} is taken, and so is every port in ${fallback}-${fallback + PORT_SCAN_COUNT - 1}`;
9004
9076
  }
9005
9077
  async function allocatePort(explicit, preferred, options2 = {}) {
9006
9078
  const host = options2.host ?? LOOPBACK_BIND_HOST;
@@ -9015,14 +9087,17 @@ async function allocatePort(explicit, preferred, options2 = {}) {
9015
9087
  };
9016
9088
  if (explicit !== void 0) {
9017
9089
  const allocated = await claim(explicit);
9018
- if (!allocated) throw new Error(`port ${explicit} is already in use`);
9090
+ if (!allocated)
9091
+ throw new Error(
9092
+ portUnavailableMessage(explicit, await probeBindPort(explicit, host))
9093
+ );
9019
9094
  return allocated;
9020
9095
  }
9021
- for (const port of portCandidates(preferred)) {
9096
+ for (const port of portCandidates(preferred, options2.base)) {
9022
9097
  const allocated = await claim(port);
9023
9098
  if (allocated) return allocated;
9024
9099
  }
9025
- throw new Error(noFreePortMessage());
9100
+ throw new Error(noFreePortMessage(options2.base));
9026
9101
  }
9027
9102
  async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS, options2 = {}) {
9028
9103
  const started = Date.now();
@@ -9059,15 +9134,16 @@ init_project_launch();
9059
9134
  init_project_launch();
9060
9135
  import { fork } from "node:child_process";
9061
9136
  import { randomUUID as randomUUID5 } from "node:crypto";
9062
- import { existsSync as existsSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync8 } from "node:fs";
9137
+ import { existsSync as existsSync4, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
9063
9138
  import { createServer as createNetServer } from "node:net";
9064
- import { basename as basename5, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, relative, resolve as resolve5, sep as sep2 } from "node:path";
9139
+ import { basename as basename6, dirname as dirname7, extname as extname2, isAbsolute as isAbsolute3, relative, resolve as resolve4, sep as sep2 } from "node:path";
9065
9140
  import { fileURLToPath } from "node:url";
9066
9141
  import watcher from "@parcel/watcher";
9067
9142
 
9068
9143
  // packages/server/src/restart-supervisor.ts
9069
9144
  init_local_origin();
9070
9145
  init_local_image();
9146
+ init_src();
9071
9147
  import { request as requestHttp, createServer as createServer2 } from "node:http";
9072
9148
  import { connect } from "node:net";
9073
9149
  var SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
@@ -9097,8 +9173,11 @@ function isControlRequest(req) {
9097
9173
  return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH;
9098
9174
  }
9099
9175
  function isLocalImageRequest(req) {
9100
- return (req.method === "GET" || req.method === "HEAD") && new URL(req.url ?? "/", "http://frizz.invalid").pathname === "/local-image";
9176
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
9177
+ const { pathname } = new URL(req.url ?? "/", "http://frizz.invalid");
9178
+ return LOCAL_IMAGE_PATH.test(pathname);
9101
9179
  }
9180
+ var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
9102
9181
  var RestartSupervisorProxy = class {
9103
9182
  server = null;
9104
9183
  restartInFlight = null;
@@ -9436,8 +9515,8 @@ function createSupervisorShutdownHandler(options2) {
9436
9515
  );
9437
9516
  };
9438
9517
  }
9439
- var packagesDir = resolve5(import.meta.dirname, "..", "..");
9440
- var workspaceDir = resolve5(packagesDir, "..");
9518
+ var packagesDir = resolve4(import.meta.dirname, "..", "..");
9519
+ var workspaceDir = resolve4(packagesDir, "..");
9441
9520
  var INITIAL_BOOT_REASON = "initial boot";
9442
9521
  function defaultDevWatchRoots() {
9443
9522
  return [workspaceDir];
@@ -9447,17 +9526,17 @@ function isWithin(path, root) {
9447
9526
  return rel === "" || rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute3(rel);
9448
9527
  }
9449
9528
  function ignoredDevPath(path) {
9450
- const parts = resolve5(path).split(sep2);
9529
+ const parts = resolve4(path).split(sep2);
9451
9530
  const name = parts.at(-1) ?? "";
9452
9531
  return parts.some((part) => GENERATED_DIRS.has(part) || part === "fixtures" || part.endsWith(".fixtures")) || /\.(?:test|spec)\.[^.]+$/.test(name) || name.includes(".golden.");
9453
9532
  }
9454
9533
  function classifyDevChange(path, roots = defaultDevWatchRoots()) {
9455
- const absolute = resolve5(path);
9456
- const root = roots.find((candidate) => isWithin(absolute, resolve5(candidate)));
9534
+ const absolute = resolve4(path);
9535
+ const root = roots.find((candidate) => isWithin(absolute, resolve4(candidate)));
9457
9536
  if (!root) return null;
9458
9537
  if (ignoredDevPath(absolute)) return null;
9459
- const name = basename5(absolute);
9460
- const relToWorkspace = relative(resolve5(root), absolute);
9538
+ const name = basename6(absolute);
9539
+ const relToWorkspace = relative(resolve4(root), absolute);
9461
9540
  const parts = relToWorkspace.split(sep2);
9462
9541
  const packageName = parts[0] === "packages" ? parts[1] : void 0;
9463
9542
  if (name === "package.json" && parts.length === 3 && packageName && CHILD_PACKAGE_METADATA.has(packageName)) {
@@ -9478,12 +9557,12 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
9478
9557
  return null;
9479
9558
  }
9480
9559
  function devConfigSyntaxError(path) {
9481
- const name = basename5(path);
9560
+ const name = basename6(path);
9482
9561
  const isTsconfig = /^tsconfig(?:\.[^.]+)?\.json$/.test(name);
9483
9562
  if (name !== "package.json" && !isTsconfig) return null;
9484
9563
  let text;
9485
9564
  try {
9486
- text = readFileSync8(path, "utf8");
9565
+ text = readFileSync9(path, "utf8");
9487
9566
  } catch (err) {
9488
9567
  return `${name}: ${err instanceof Error ? err.message : err}`;
9489
9568
  }
@@ -9598,10 +9677,10 @@ var Supervisor = class {
9598
9677
  if (launchOwner.pid !== callerGeneration.pid || launchOwner.processStart !== callerGeneration.processStart) throw new Error("dev supervisor caller is not the exact project launch owner");
9599
9678
  this.port = opts.port;
9600
9679
  this.launchTarget = opts.launchTarget;
9601
- this.cwd = resolve5(opts.cwd ?? opts.launchTarget.projectDir);
9680
+ this.cwd = resolve4(opts.cwd ?? opts.launchTarget.projectDir);
9602
9681
  if (this.cwd !== opts.launchTarget.projectDir) throw new Error("dev supervisor cwd does not match its owned project");
9603
9682
  this.parentEnv = projectLaunchEnvironment(opts.env ?? process.env, opts.launchTarget, opts.launchOwnerToken);
9604
- this.roots = (opts.watchRoots ?? defaultDevWatchRoots()).map((root) => resolve5(root));
9683
+ this.roots = (opts.watchRoots ?? defaultDevWatchRoots()).map((root) => resolve4(root));
9605
9684
  this.watchEnabled = opts.watch !== false;
9606
9685
  this.childEnvironment = opts.childEnvironment ?? (() => ({}));
9607
9686
  this.debounceMs = opts.debounceMs ?? DEV_RESTART_DEBOUNCE_MS;
@@ -9611,10 +9690,10 @@ var Supervisor = class {
9611
9690
  this.childArgs = opts.childArgs ?? [];
9612
9691
  this.watchSubscribe = opts.watchSubscribe ?? ((root, callback, options2) => watcher.subscribe(root, callback, options2));
9613
9692
  this.reexec = opts.reexec ?? (typeof process.execve === "function" ? (request) => process.execve(request.executable, request.argv, request.env) : void 0);
9614
- if (opts.stateDir && resolve5(opts.stateDir) !== opts.launchTarget.stateDir) {
9693
+ if (opts.stateDir && resolve4(opts.stateDir) !== opts.launchTarget.stateDir) {
9615
9694
  throw new Error("dev supervisor state directory does not match its owned project");
9616
9695
  }
9617
- this.supervisorLock = resolve5(opts.launchTarget.stateDir, "dev-supervisor.lock");
9696
+ this.supervisorLock = resolve4(opts.launchTarget.stateDir, "dev-supervisor.lock");
9618
9697
  this.ownerToken = opts.launchOwnerToken;
9619
9698
  this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
9620
9699
  this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
@@ -9650,7 +9729,7 @@ var Supervisor = class {
9650
9729
  }
9651
9730
  async start() {
9652
9731
  if (this.supervisorLock) {
9653
- mkdirSync7(dirname5(this.supervisorLock), { recursive: true });
9732
+ mkdirSync8(dirname7(this.supervisorLock), { recursive: true });
9654
9733
  this.writeStatus("starting", "watcher initializing");
9655
9734
  }
9656
9735
  try {
@@ -10145,8 +10224,8 @@ var npmRegistryReleaseAdapter = {
10145
10224
  // src/preflight.ts
10146
10225
  import { spawnSync } from "node:child_process";
10147
10226
  import { chmodSync, statSync as statSync5 } from "node:fs";
10148
- import { createRequire as createRequire2 } from "node:module";
10149
- import { dirname as dirname6, join as join10 } from "node:path";
10227
+ import { createRequire } from "node:module";
10228
+ import { dirname as dirname8, join as join10 } from "node:path";
10150
10229
  var SUPPORTED_NODE_LINES = [
10151
10230
  { major: 22, minor: 13 },
10152
10231
  { major: 23, minor: 4 }
@@ -10195,10 +10274,10 @@ function ensureNativeHelperPermissions(options2 = {}) {
10195
10274
  if (platform === "win32") return;
10196
10275
  const stat = options2.stat ?? ((path) => statSync5(path));
10197
10276
  const chmod = options2.chmod ?? ((path, mode) => chmodSync(path, mode));
10198
- const resolvePty = options2.resolvePty ?? (() => createRequire2(import.meta.url).resolve("node-pty/package.json"));
10277
+ const resolvePty = options2.resolvePty ?? (() => createRequire(import.meta.url).resolve("node-pty/package.json"));
10199
10278
  try {
10200
10279
  const helper = join10(
10201
- dirname6(resolvePty()),
10280
+ dirname8(resolvePty()),
10202
10281
  "prebuilds",
10203
10282
  `${platform}-${options2.arch ?? process.arch}`,
10204
10283
  "spawn-helper"
@@ -10211,10 +10290,13 @@ function ensureNativeHelperPermissions(options2 = {}) {
10211
10290
  }
10212
10291
 
10213
10292
  // src/production.ts
10293
+ init_src();
10294
+ init_project_registry();
10295
+ init_project_identity();
10214
10296
  var PACKAGE_NAME = process.env.FRIZZ_REGISTRY_PACKAGE ?? "frizz";
10215
10297
  function resolvePackageVersion() {
10216
10298
  try {
10217
- const manifest = JSON.parse(readFileSync9(join11(import.meta.dirname, "..", "package.json"), "utf8"));
10299
+ const manifest = JSON.parse(readFileSync10(join11(import.meta.dirname, "..", "package.json"), "utf8"));
10218
10300
  if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
10219
10301
  } catch {
10220
10302
  }
@@ -10277,6 +10359,7 @@ var bind = (() => {
10277
10359
  return fail(error);
10278
10360
  }
10279
10361
  })();
10362
+ var launchIntent;
10280
10363
  var workspace = (() => {
10281
10364
  try {
10282
10365
  if (!reexec) {
@@ -10284,11 +10367,17 @@ var workspace = (() => {
10284
10367
  else assertLaunchPrerequisites();
10285
10368
  }
10286
10369
  const pinned = projectLaunchTargetFromEnvironment(process.env);
10287
- if (reexec) {
10370
+ if (reexec || process.env.FRIZZ_PRODUCTION_SUPERVISOR === "1") {
10288
10371
  if (!pinned) throw new Error("registry successor is missing its pinned project identity");
10289
10372
  return workspaceFromLaunchTarget(pinned);
10290
10373
  }
10291
- return resolveWorkspace(options.repoPath);
10374
+ const intent = resolveLaunchIntent(options.repoPath);
10375
+ if (intent.kind === "empty")
10376
+ throw new Error(
10377
+ intent.reason === "home" ? "frizz cannot open your home directory as a project, and there is no other project to show yet. cd into a repository and run frizz there." : `${intent.directory} is not a Frizz project yet, and there is no other project to show. Run frizz inside a repository, or add this one from the projects page once a board is open.`
10378
+ );
10379
+ launchIntent = intent;
10380
+ return intent.workspace;
10292
10381
  } catch (error) {
10293
10382
  return fail(error);
10294
10383
  }
@@ -10313,8 +10402,39 @@ async function existingPort() {
10313
10402
  for (const port of new Set(ports)) if (await probeFrizz(port, expected)) return port;
10314
10403
  return void 0;
10315
10404
  }
10316
- async function openOrPrint(port, reused) {
10317
- const url = `http://127.0.0.1:${port}`;
10405
+ function ownSlug() {
10406
+ try {
10407
+ return registerProject(
10408
+ { dir: workspace.root, id: target.projectId, remoteOwner: resolveProjectLabel(workspace.root)?.split("/")[0] },
10409
+ homedir8()
10410
+ ).entry?.slug;
10411
+ } catch {
10412
+ return void 0;
10413
+ }
10414
+ }
10415
+ var cachedSlugPath;
10416
+ function slugPath() {
10417
+ if (cachedSlugPath === void 0) {
10418
+ if (launchIntent?.kind === "grid") cachedSlugPath = "/";
10419
+ else if (launchIntent?.kind === "offer") cachedSlugPath = `/?add=${encodeURIComponent(launchIntent.directory)}`;
10420
+ else {
10421
+ const slug = ownSlug();
10422
+ cachedSlugPath = slug ? `/project/${slug}` : "";
10423
+ }
10424
+ }
10425
+ return cachedSlugPath;
10426
+ }
10427
+ async function joinRunningFrizz() {
10428
+ const slug = ownSlug();
10429
+ if (!slug) return void 0;
10430
+ for (const port of /* @__PURE__ */ new Set([DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])) {
10431
+ if (await probeFrizz(port, { projectId: target.projectId, projectDir: target.projectDir, slug }))
10432
+ return { port, slug };
10433
+ }
10434
+ return void 0;
10435
+ }
10436
+ async function openOrPrint(port, reused, path = "") {
10437
+ const url = `http://127.0.0.1:${port}${path}`;
10318
10438
  logger.info("launcher", `${reused ? "reusing" : "started"} Frizz at ${url}`);
10319
10439
  readout?.settle("server", "done", reused ? `already running on port ${port}` : `port ${port}`);
10320
10440
  let browser;
@@ -10354,7 +10474,7 @@ async function openOrPrint(port, reused) {
10354
10474
  const home = homedir8();
10355
10475
  readout.ready(
10356
10476
  [
10357
- { label: "Local", value: `${url}/`, accent: true },
10477
+ { label: "Local", value: boardAddress(url), accent: true },
10358
10478
  ...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
10359
10479
  ...publicOrigin ? [{ label: "Public", value: `${publicOrigin}/`, accent: true }] : [],
10360
10480
  { label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
@@ -10444,7 +10564,7 @@ async function runSupervisor(port, token) {
10444
10564
  log: ${tildePath(logger.file, homedir8())}` : "";
10445
10565
  process.stdout.write(
10446
10566
  code === 0 ? `
10447
- Frizz stopped. Agent sessions in tmux keep running.${suffix}
10567
+ Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
10448
10568
 
10449
10569
  ` : `
10450
10570
  Frizz stopped with errors (exit ${code}).${suffix}
@@ -10471,7 +10591,12 @@ try {
10471
10591
  }
10472
10592
  const existing = await existingPort();
10473
10593
  if (existing) {
10474
- await openOrPrint(existing, true);
10594
+ await openOrPrint(existing, true, slugPath());
10595
+ process.exit(0);
10596
+ }
10597
+ const joined = await joinRunningFrizz();
10598
+ if (joined) {
10599
+ await openOrPrint(joined.port, true, slugPath());
10475
10600
  process.exit(0);
10476
10601
  }
10477
10602
  const claim = tryAcquireProjectLaunchOwner(target, "launcher");
@@ -10496,7 +10621,7 @@ try {
10496
10621
  ]);
10497
10622
  portReservation();
10498
10623
  portReservation = void 0;
10499
- await openOrPrint(port, false);
10624
+ await openOrPrint(port, false, slugPath());
10500
10625
  await running;
10501
10626
  } finally {
10502
10627
  release?.();