frizz 0.1.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 +119 -61
  2. package/dist/claude-agent-broker.js +63 -23
  3. package/dist/dev-child.js +6492 -3945
  4. package/dist/frizz.js +745 -495
  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,409 +1427,157 @@ 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";
1430
+ // packages/server/src/project-root.ts
1431
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
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";
1627
1433
  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;
1434
+ import { dirname as dirname5, join as join7, parse, resolve as resolve2 } from "node:path";
1435
+ function projectIdPath(root) {
1436
+ return join7(root, FRIZZ_DIR, ID_FILE);
1632
1437
  }
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") });
1438
+ function readProjectIdFile(root) {
1439
+ let raw;
1440
+ try {
1441
+ raw = readFileSync5(projectIdPath(root), "utf8");
1442
+ } catch {
1443
+ return void 0;
1646
1444
  }
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") });
1445
+ try {
1446
+ return validateProjectId(raw.trim());
1447
+ } catch {
1448
+ throw new Error(`${projectIdPath(root)} is invalid; expected exactly one UUID`);
1650
1449
  }
1651
- return moves;
1652
1450
  }
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;
1451
+ function writeProjectIdFile(root, id) {
1452
+ const dir = join7(root, FRIZZ_DIR);
1453
+ mkdirSync5(dir, { recursive: true });
1454
+ const ignore = join7(dir, SELF_IGNORE);
1455
+ if (!existsSync2(ignore)) {
1663
1456
  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);
1457
+ writeFileSync5(ignore, "*\n", { flag: "wx" });
1670
1458
  } catch {
1671
1459
  }
1672
1460
  }
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;
1461
+ const path = projectIdPath(root);
1462
+ const temp = join7(dir, `.${ID_FILE}.${process.pid}.${randomUUID4()}.tmp`);
1463
+ let fd;
1464
+ try {
1465
+ fd = openSync4(temp, "wx", 384);
1466
+ writeFileSync5(fd, `${id}
1467
+ `, "utf8");
1468
+ fsyncSync3(fd);
1469
+ closeSync3(fd);
1470
+ fd = void 0;
1471
+ renameSync4(temp, path);
1472
+ } catch (error) {
1473
+ if (fd !== void 0) {
1474
+ try {
1475
+ closeSync3(fd);
1476
+ } catch {
1477
+ }
1684
1478
  }
1685
- if (entry.isDirectory() && lstatSync(dst).isDirectory() && mergeInto(src, dst)) moved = true;
1479
+ try {
1480
+ rmSync5(temp, { force: true });
1481
+ } catch {
1482
+ }
1483
+ throw error;
1686
1484
  }
1485
+ return id;
1486
+ }
1487
+ function projectRootLockName(root) {
1488
+ return `identity-path-${createHash3("sha256").update(root).digest("hex")}.lock`;
1489
+ }
1490
+ function ensureProjectIdFile(root, home = homedir5(), seed) {
1491
+ const existing = readProjectIdFile(root);
1492
+ if (existing) return existing;
1493
+ const release = acquireNamedLaunchLockSync(home, projectRootLockName(root));
1687
1494
  try {
1688
- rmdirSync(from);
1689
- } catch {
1495
+ const raced = readProjectIdFile(root);
1496
+ if (raced) return raced;
1497
+ return writeProjectIdFile(root, seed ? validateProjectId(seed) : randomUUID4());
1498
+ } finally {
1499
+ release();
1690
1500
  }
1691
- return moved;
1692
1501
  }
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;
1502
+ function isNotAGitWorktree(error) {
1503
+ const code = error?.code;
1504
+ if (code === "ENOENT" || code === "EACCES") return true;
1505
+ if (!error || typeof error !== "object" || !("stderr" in error)) return false;
1506
+ const stderr = String(error.stderr);
1507
+ return /not a git repository/iu.test(stderr) || /must be run in a work ?tree/iu.test(stderr);
1508
+ }
1509
+ function hasAny(dir, names) {
1510
+ return names.some((name) => existsSync2(join7(dir, name)));
1511
+ }
1512
+ function isHomeDirectory(dir, home = homedir5()) {
1513
+ const canonical = (value) => {
1514
+ try {
1515
+ return realpathSync2(resolve2(value));
1516
+ } catch {
1517
+ return resolve2(value);
1701
1518
  }
1702
- return lstatSync(to).isDirectory() ? mergeInto(from, to) : false;
1519
+ };
1520
+ try {
1521
+ return canonical(dir) === canonical(home);
1703
1522
  } catch {
1704
1523
  return false;
1705
1524
  }
1706
1525
  }
1707
- function readId(dir, scope, key) {
1708
- let output;
1526
+ function existingProjectId(dir) {
1527
+ const fromFile = readProjectIdFile(dir);
1528
+ if (fromFile) return fromFile;
1709
1529
  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
- });
1530
+ return readGitProjectId(dir);
1716
1531
  } catch {
1717
1532
  return void 0;
1718
1533
  }
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
1534
  }
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;
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);
1734
1540
  }
1735
- function hasBoard(id, home) {
1736
- const db = join7(projectStateDir(id, home), "ui.db");
1737
- if (!existsSync2(db)) return false;
1738
- let sql;
1541
+ function discoverProjectRoot(cwd = process.cwd(), home = homedir5()) {
1542
+ let dir;
1739
1543
  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;
1544
+ dir = resolve2(cwd);
1743
1545
  } 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"() {
1546
+ return resolve2(cwd);
1547
+ }
1548
+ const stop = resolve2(home);
1549
+ const filesystemRoot = parse(dir).root;
1550
+ for (let at = dir; ; at = dirname5(at)) {
1551
+ if (at === stop || at === filesystemRoot) break;
1552
+ if (existsSync2(projectIdPath(at))) return at;
1553
+ if (hasAny(at, REPO_MARKERS)) return at;
1554
+ if (hasAny(at, PROJECT_MARKERS)) return at;
1555
+ if (dirname5(at) === at) break;
1556
+ }
1557
+ return dir;
1558
+ }
1559
+ var FRIZZ_DIR, ID_FILE, SELF_IGNORE, REPO_MARKERS, PROJECT_MARKERS;
1560
+ var init_project_root = __esm({
1561
+ "packages/server/src/project-root.ts"() {
1797
1562
  "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";
1563
+ init_project_identity();
1564
+ FRIZZ_DIR = ".frizz";
1565
+ ID_FILE = ".id";
1566
+ SELF_IGNORE = ".gitignore";
1567
+ REPO_MARKERS = [".git", ".jj", ".hg", ".svn"];
1568
+ PROJECT_MARKERS = [
1569
+ "package.json",
1570
+ "pyproject.toml",
1571
+ "go.mod",
1572
+ "Cargo.toml",
1573
+ "deno.json",
1574
+ "deno.jsonc",
1575
+ "composer.json",
1576
+ "Gemfile",
1577
+ "pom.xml",
1578
+ "build.gradle",
1579
+ "Makefile"
1580
+ ];
1803
1581
  }
1804
1582
  });
1805
1583
 
@@ -5910,6 +5688,10 @@ var init_zod = __esm({
5910
5688
  });
5911
5689
 
5912
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
+ }
5913
5695
  var THREAD_SLUG_MAX_CHARS, ThreadSlug;
5914
5696
  var init_thread_slug = __esm({
5915
5697
  "packages/shared/src/thread-slug.ts"() {
@@ -6470,7 +6252,10 @@ var init_receipt_bus = __esm({
6470
6252
  });
6471
6253
 
6472
6254
  // packages/shared/src/index.ts
6473
- 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;
6474
6259
  var init_src = __esm({
6475
6260
  "packages/shared/src/index.ts"() {
6476
6261
  "use strict";
@@ -6738,21 +6523,57 @@ var init_src = __esm({
6738
6523
  RecurringIntervalSeconds = external_exports.number().int().min(RECURRING_MIN_INTERVAL_SECONDS).max(RECURRING_MAX_INTERVAL_SECONDS);
6739
6524
  ThreadRecurringPrompt = external_exports.object({
6740
6525
  prompt: external_exports.string(),
6741
- /** The two mechanisms, named as the panel labels them. `stopHook` fires at every rest; `heartbeat`
6742
- * 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. */
6743
6529
  stopHook: external_exports.boolean(),
6744
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(),
6745
6538
  intervalSeconds: external_exports.number().int().positive().optional(),
6746
6539
  armedAt: external_exports.string(),
6747
- /** 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. */
6748
6541
  lastRestFiredAt: external_exports.string().optional(),
6749
- lastScheduleFiredAt: external_exports.string().optional()
6542
+ lastScheduleFiredAt: external_exports.string().optional(),
6543
+ lastCompactFiredAt: external_exports.string().optional()
6750
6544
  }).strict();
6751
- ALLDONE_SENTINEL = "ALLDONE";
6752
- 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.`;
6753
6545
  TIMER_PROMPT_MAX = SNOOZE_PROMPT_MAX;
6754
6546
  TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
6755
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();
6756
6577
  ThreadTimerView = external_exports.object({
6757
6578
  id: external_exports.string(),
6758
6579
  prompt: external_exports.string(),
@@ -6857,6 +6678,14 @@ var init_src = __esm({
6857
6678
  // Live background SHELLS the worker launched (tailer-derived). Same default-[] discipline. Rendered
6858
6679
  // in the anchored background-ops strip alongside sub-agents; ids make current rows drillable.
6859
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([]),
6860
6689
  // A pending native AskUserQuestion the session is frozen on (tailer-derived). Optional — absent
6861
6690
  // when there's no unanswered ask. Feeds needsAction + the read-only question render + "Answer in Terminal".
6862
6691
  pendingAsk: PendingAsk.optional(),
@@ -6943,9 +6772,6 @@ var init_src = __esm({
6943
6772
  // ISO8601 read/seen telemetry (threadSeen RPC — recorded when the human opens the thread). Kept for
6944
6773
  // compatibility and analytics only; viewing never acknowledges or removes a queue handoff.
6945
6774
  seenAt: external_exports.string().optional(),
6946
- // Project-relative scratchpad path (.frizz/threads/<session-id>/scratch.md) once provisioned — the worker's
6947
- // compaction-proof working memory, rendered as the thread's doc tab.
6948
- scratchpadPath: external_exports.string().optional(),
6949
6775
  // Project-relative plan artifact this thread was dispatched from (.frizz/plans/*.md), if any.
6950
6776
  planPath: external_exports.string().optional(),
6951
6777
  // Which agent backend runs this thread (Codex-support epic, Phase 3) — drives the subtle per-row
@@ -6962,13 +6788,13 @@ var init_src = __esm({
6962
6788
  // Raw durable barrier bit. Unlike permissionPending this remains true for a future/corrupt value,
6963
6789
  // so rolling clients fail closed instead of enabling another composer while ownership is unknown.
6964
6790
  permissionChangePending: external_exports.boolean().optional(),
6965
- // The last decision the worker's permission POLICY made for this thread (cc-worker/hooks/
6966
- // perm-policy.mjs), and how many times it has DENIED. A policy decision never blocks anyone, so it
6967
- // has no other way of being noticed an allow is not written to the transcript at all. Surfacing it
6968
- // is what keeps "frizz silently approved something on your behalf" from being invisible.
6969
- // 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.
6970
6796
  permPolicy: external_exports.object({
6971
- decision: external_exports.enum(["allow", "deny"]),
6797
+ decision: external_exports.literal("deny"),
6972
6798
  rule: external_exports.string(),
6973
6799
  reason: external_exports.string(),
6974
6800
  tool: external_exports.string().nullable(),
@@ -7010,6 +6836,18 @@ var init_src = __esm({
7010
6836
  projectName: external_exports.string(),
7011
6837
  projectLabel: external_exports.string(),
7012
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(),
7013
6851
  // (No `.frizz/ exists` bit here on purpose. Threads are session-first — the ui.db registry IS the
7014
6852
  // board — so `.frizz/` presence says nothing about whether this project has one. Its only consumer
7015
6853
  // was a shell gate that dead-ended `.frizz`-less repos; the server still probes the directory
@@ -7098,6 +6936,17 @@ var init_src = __esm({
7098
6936
  // presents it; resolveClaudeEffort (server/backend/claude-effort.ts) translates it at the spawn edge.
7099
6937
  effort: external_exports.enum(["low", "medium", "high", "xhigh", "max", "ultra", "ultracode"]).optional(),
7100
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(),
7101
6950
  // UI type family. `mono` (default) is the mono-forward system; `sans` swaps prose/UI chrome to a
7102
6951
  // sans stack while code / tool lines / the terminal stay mono. Optional so an old settings blob
7103
6952
  // parses; defaultSettings pins "mono".
@@ -7236,6 +7085,13 @@ var init_src = __esm({
7236
7085
  prompt: RecurringPromptText.nullable(),
7237
7086
  stopHook: external_exports.boolean(),
7238
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),
7239
7095
  intervalSeconds: RecurringIntervalSeconds.optional()
7240
7096
  }).strict();
7241
7097
  SetOwnThreadRecurringPromptInput = external_exports.object({
@@ -7243,8 +7099,23 @@ var init_src = __esm({
7243
7099
  prompt: RecurringPromptText.nullable(),
7244
7100
  stopHook: external_exports.boolean(),
7245
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),
7246
7108
  intervalSeconds: RecurringIntervalSeconds.optional()
7247
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();
7248
7119
  SetOwnThreadTimerInput = external_exports.object({
7249
7120
  slug: ThreadSlug,
7250
7121
  prompt: TimerPromptText,
@@ -7269,6 +7140,49 @@ var init_src = __esm({
7269
7140
  cancelled: external_exports.boolean(),
7270
7141
  timers: external_exports.array(ThreadTimerView)
7271
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();
7272
7186
  SetOwnThreadStopHookInput = external_exports.object({
7273
7187
  slug: ThreadSlug,
7274
7188
  prompt: RecurringPromptText.nullable(),
@@ -7309,6 +7223,11 @@ var init_src = __esm({
7309
7223
  // attempted a write after the flip to danger-full-access was still refused). So the change is real and
7310
7224
  // durable, yet it does not reach work already executing. Distinct from "next-resume", which means
7311
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.
7312
7231
  effect: external_exports.enum(["applied", "next-turn", "next-resume"])
7313
7232
  });
7314
7233
  ThreadProfileOptionsInput = external_exports.object({ slug: ThreadSlug }).strict();
@@ -7728,7 +7647,25 @@ ${DISPATCH_TASK_BANNER}
7728
7647
  // there and stores the DISPATCH id here. Additive + optional: absent when the ack was never seen (a
7729
7648
  // resumed session whose dispatch scrolled out), and the line then renders as plain text, not a dead link.
7730
7649
  peerFrom: external_exports.string().optional(),
7731
- 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()
7732
7669
  });
7733
7670
  TranscriptPageCursor = external_exports.string().min(1).max(2048).regex(/^[A-Za-z0-9_-]+$/);
7734
7671
  TranscriptPage = external_exports.object({
@@ -7747,12 +7684,240 @@ ${DISPATCH_TASK_BANNER}
7747
7684
  external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict(),
7748
7685
  external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("transcript"), slug: SocketTranscriptSlug }).strict()
7749
7686
  ]);
7750
- 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
+ ]);
7751
7916
  }
7752
7917
  });
7753
7918
 
7754
7919
  // packages/server/src/local-image.ts
7755
- import { readFileSync as readFileSync6, realpathSync as realpathSync4, statSync as statSync4 } from "node:fs";
7920
+ import { readFileSync as readFileSync8, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
7756
7921
  import { extname, isAbsolute as isAbsolute2 } from "node:path";
7757
7922
  function resolveLocalImage(rawPath) {
7758
7923
  if (!rawPath || !isAbsolute2(rawPath)) return { status: 400 };
@@ -7760,13 +7925,13 @@ function resolveLocalImage(rawPath) {
7760
7925
  if (!contentType) return { status: 400 };
7761
7926
  let real;
7762
7927
  try {
7763
- real = realpathSync4(rawPath);
7928
+ real = realpathSync5(rawPath);
7764
7929
  } catch {
7765
7930
  return { status: 404 };
7766
7931
  }
7767
7932
  try {
7768
7933
  if (!statSync4(real).isFile()) return { status: 404 };
7769
- return { status: 200, contentType, body: readFileSync6(real) };
7934
+ return { status: 200, contentType, body: readFileSync8(real) };
7770
7935
  } catch {
7771
7936
  return { status: 404 };
7772
7937
  }
@@ -7786,10 +7951,10 @@ var init_local_image = __esm({
7786
7951
  });
7787
7952
 
7788
7953
  // src/production.ts
7789
- import { readFileSync as readFileSync8 } from "node:fs";
7790
- import { homedir as homedir7 } from "node:os";
7954
+ import { readFileSync as readFileSync10 } from "node:fs";
7955
+ import { homedir as homedir8 } from "node:os";
7791
7956
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7792
- import { join as join10 } from "node:path";
7957
+ import { join as join11 } from "node:path";
7793
7958
 
7794
7959
  // src/browser.ts
7795
7960
  import { spawn, execFile } from "node:child_process";
@@ -8536,14 +8701,15 @@ init_project_launch();
8536
8701
  init_local_origin();
8537
8702
  init_boot_progress();
8538
8703
  init_frizz_paths();
8539
- init_migrate_fray();
8704
+ init_project_root();
8540
8705
  init_logging();
8541
8706
  init_src();
8542
- import { execFileSync as execFileSync4 } from "node:child_process";
8707
+ init_project_registry();
8708
+ import { execFileSync as execFileSync3 } from "node:child_process";
8543
8709
  import { createServer } from "node:net";
8544
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync3, writeFileSync as writeFileSync5 } from "node:fs";
8545
- import { homedir as homedir6, networkInterfaces } from "node:os";
8546
- import { basename as basename4, join as join8, resolve as resolve3 } from "node:path";
8710
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
8711
+ import { homedir as homedir7, networkInterfaces } from "node:os";
8712
+ import { basename as basename5, join as join9, resolve as resolve3 } from "node:path";
8547
8713
  import { setTimeout as delay2 } from "node:timers/promises";
8548
8714
  var PORT_SCAN_COUNT = 100;
8549
8715
  var LAUNCH_TIMEOUT_MS = 3e4;
@@ -8690,6 +8856,9 @@ function resolveBindSelection(options2, env = process.env) {
8690
8856
  const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
8691
8857
  return { host, exposed: bindHostIsExposed(host), allowedHosts, ...publicOrigin ? { publicOrigin } : {} };
8692
8858
  }
8859
+ function boardAddress(url) {
8860
+ return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
8861
+ }
8693
8862
  function networkUrls(port, host, interfaces = networkInterfaces) {
8694
8863
  if (!bindHostIsExposed(host)) return [];
8695
8864
  const wildcard = host === ALL_INTERFACES_BIND_HOST || host === "::";
@@ -8711,40 +8880,58 @@ function networkUrls(port, host, interfaces = networkInterfaces) {
8711
8880
  }
8712
8881
  return urls;
8713
8882
  }
8714
- function resolveWorkspace(cwd = process.cwd(), home = homedir6(), 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) {
8715
8904
  let gitRoot;
8716
8905
  try {
8717
- gitRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
8906
+ gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
8718
8907
  cwd,
8719
8908
  encoding: "utf8",
8720
- stdio: ["ignore", "pipe", "ignore"]
8909
+ // stderr is CAPTURED, not ignored: it is the only thing that distinguishes "no worktree here"
8910
+ // from "this repository is broken", and those two must not be handled the same way.
8911
+ stdio: ["ignore", "pipe", "pipe"]
8721
8912
  }).trim();
8722
- } catch {
8723
- throw new Error(
8724
- `frizz-dev must be run inside a Git repository (cwd: ${cwd})`
8725
- );
8913
+ } catch (error) {
8914
+ if (!isNotAGitWorktree(error)) throw new Error("unable to resolve Git repository root");
8915
+ gitRoot = void 0;
8726
8916
  }
8727
- if (migrate) migrateFrayGlobalRoots({ env, home });
8728
- const root0 = realpathSync3(gitRoot);
8729
- if (migrate) migrateFrayProjectId(root0, { home });
8730
- const identity = resolveGitProjectIdentity(root0, home);
8731
- const root = identity.root;
8732
- if (migrate) migrateFrayProjectDir(root);
8733
- const id = identity.id;
8917
+ const root0 = realpathSync4(gitRoot ?? discoverProjectRoot(cwd, home));
8918
+ const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
8919
+ const root = identity?.root ?? root0;
8920
+ const id = ensureProjectIdFile(root, home, identity?.id);
8734
8921
  const stateDir = projectStateDir(id, home);
8735
- mkdirSync5(stateDir, { recursive: true });
8922
+ mkdirSync7(stateDir, { recursive: true });
8736
8923
  const target2 = {
8737
8924
  projectId: id,
8738
8925
  projectDir: root,
8739
8926
  stateDir,
8740
- ...identity.scope === "worktree" ? { identityScope: "worktree" } : {}
8927
+ ...identity?.scope === "worktree" ? { identityScope: "worktree" } : {}
8741
8928
  };
8742
8929
  return {
8743
8930
  root,
8744
8931
  id,
8745
8932
  stateDir,
8746
- name: basename4(root),
8747
- identityScope: identity.scope
8933
+ name: basename5(root),
8934
+ identityScope: identity?.scope ?? "repository"
8748
8935
  };
8749
8936
  }
8750
8937
  function workspaceLaunchTarget(workspace2) {
@@ -8758,7 +8945,7 @@ function workspaceLaunchTarget(workspace2) {
8758
8945
  function workspaceFromLaunchTarget(target2, env = process.env) {
8759
8946
  let root;
8760
8947
  try {
8761
- root = realpathSync3(target2.projectDir);
8948
+ root = realpathSync4(target2.projectDir);
8762
8949
  } catch {
8763
8950
  throw new Error("pinned Frizz workspace is no longer available");
8764
8951
  }
@@ -8768,14 +8955,14 @@ function workspaceFromLaunchTarget(target2, env = process.env) {
8768
8955
  root,
8769
8956
  id: target2.projectId,
8770
8957
  stateDir: target2.stateDir,
8771
- name: basename4(root),
8958
+ name: basename5(root),
8772
8959
  identityScope: target2.identityScope === "worktree" ? "worktree" : "repository"
8773
8960
  };
8774
8961
  }
8775
8962
  function parseStatusFile(path, authoritative, expected2, adapter = defaultProcessPlatformAdapter) {
8776
8963
  try {
8777
8964
  const value = JSON.parse(
8778
- readFileSync5(path, "utf8")
8965
+ readFileSync7(path, "utf8")
8779
8966
  );
8780
8967
  if (!Number.isInteger(value.pid) || value.pid <= 0 || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
8781
8968
  return null;
@@ -8803,12 +8990,12 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
8803
8990
  if (authoritative && processGenerationIsStale(authoritative, adapter))
8804
8991
  return null;
8805
8992
  return parseStatusFile(
8806
- join8(stateDir, "dev-supervisor.lock"),
8993
+ join9(stateDir, "dev-supervisor.lock"),
8807
8994
  authoritative,
8808
8995
  expected2,
8809
8996
  adapter
8810
8997
  ) ?? parseStatusFile(
8811
- join8(stateDir, "server.lock"),
8998
+ join9(stateDir, "server.lock"),
8812
8999
  authoritative,
8813
9000
  expected2,
8814
9001
  adapter
@@ -8817,19 +9004,21 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
8817
9004
  function readPreferredPort(stateDir) {
8818
9005
  try {
8819
9006
  const value = JSON.parse(
8820
- readFileSync5(join8(stateDir, "launcher.json"), "utf8")
9007
+ readFileSync7(join9(stateDir, "launcher.json"), "utf8")
8821
9008
  );
8822
9009
  return Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 ? value.port : void 0;
8823
9010
  } catch {
8824
9011
  return void 0;
8825
9012
  }
8826
9013
  }
8827
- 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) {
8828
9016
  const controller = new AbortController();
8829
- const timeout = setTimeout(() => controller.abort(), 1e3);
9017
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
8830
9018
  timeout.unref?.();
8831
9019
  try {
8832
- 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`, {
8833
9022
  signal: controller.signal
8834
9023
  });
8835
9024
  if (!response.ok) return null;
@@ -8854,26 +9043,36 @@ function expectedOwnerHealth(target2, owner) {
8854
9043
  ...owner ? { ownerProof: projectLaunchTokenProof(target2, owner.token) } : {}
8855
9044
  };
8856
9045
  }
8857
- async function canBindPort(port, host = LOOPBACK_BIND_HOST) {
8858
- const bind2 = (candidate) => new Promise((resolveBind) => {
9046
+ async function probeBindPort(port, host = LOOPBACK_BIND_HOST) {
9047
+ return new Promise((resolveBind) => {
8859
9048
  const server = createServer();
8860
9049
  server.unref();
8861
- server.once("error", () => resolveBind(false));
8862
- 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)));
8863
9055
  });
8864
- return bind2(port);
8865
9056
  }
8866
- 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);
8867
9067
  return [
9068
+ base,
8868
9069
  preferred,
8869
- ...Array.from(
8870
- { length: PORT_SCAN_COUNT },
8871
- (_, index) => DEFAULT_PORT + index
8872
- )
9070
+ ...Array.from({ length: PORT_SCAN_COUNT }, (_, index) => fallback + index)
8873
9071
  ].filter((port) => !!port && port <= 65535);
8874
9072
  }
8875
- function noFreePortMessage() {
8876
- 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}`;
8877
9076
  }
8878
9077
  async function allocatePort(explicit, preferred, options2 = {}) {
8879
9078
  const host = options2.host ?? LOOPBACK_BIND_HOST;
@@ -8888,14 +9087,17 @@ async function allocatePort(explicit, preferred, options2 = {}) {
8888
9087
  };
8889
9088
  if (explicit !== void 0) {
8890
9089
  const allocated = await claim(explicit);
8891
- 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
+ );
8892
9094
  return allocated;
8893
9095
  }
8894
- for (const port of portCandidates(preferred)) {
9096
+ for (const port of portCandidates(preferred, options2.base)) {
8895
9097
  const allocated = await claim(port);
8896
9098
  if (allocated) return allocated;
8897
9099
  }
8898
- throw new Error(noFreePortMessage());
9100
+ throw new Error(noFreePortMessage(options2.base));
8899
9101
  }
8900
9102
  async function waitForWorkspace(port, expected2, timeoutMs = LAUNCH_TIMEOUT_MS, options2 = {}) {
8901
9103
  const started = Date.now();
@@ -8931,16 +9133,17 @@ init_project_launch();
8931
9133
  // packages/server/src/dev-supervisor.ts
8932
9134
  init_project_launch();
8933
9135
  import { fork } from "node:child_process";
8934
- import { randomUUID as randomUUID4 } from "node:crypto";
8935
- import { existsSync as existsSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "node:fs";
9136
+ import { randomUUID as randomUUID5 } from "node:crypto";
9137
+ import { existsSync as existsSync4, mkdirSync as mkdirSync8, readFileSync as readFileSync9 } from "node:fs";
8936
9138
  import { createServer as createNetServer } from "node:net";
8937
- import { basename as basename5, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, relative, resolve as resolve4, 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";
8938
9140
  import { fileURLToPath } from "node:url";
8939
9141
  import watcher from "@parcel/watcher";
8940
9142
 
8941
9143
  // packages/server/src/restart-supervisor.ts
8942
9144
  init_local_origin();
8943
9145
  init_local_image();
9146
+ init_src();
8944
9147
  import { request as requestHttp, createServer as createServer2 } from "node:http";
8945
9148
  import { connect } from "node:net";
8946
9149
  var SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
@@ -8970,8 +9173,11 @@ function isControlRequest(req) {
8970
9173
  return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH;
8971
9174
  }
8972
9175
  function isLocalImageRequest(req) {
8973
- 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);
8974
9179
  }
9180
+ var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
8975
9181
  var RestartSupervisorProxy = class {
8976
9182
  server = null;
8977
9183
  restartInFlight = null;
@@ -9329,7 +9535,7 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
9329
9535
  const root = roots.find((candidate) => isWithin(absolute, resolve4(candidate)));
9330
9536
  if (!root) return null;
9331
9537
  if (ignoredDevPath(absolute)) return null;
9332
- const name = basename5(absolute);
9538
+ const name = basename6(absolute);
9333
9539
  const relToWorkspace = relative(resolve4(root), absolute);
9334
9540
  const parts = relToWorkspace.split(sep2);
9335
9541
  const packageName = parts[0] === "packages" ? parts[1] : void 0;
@@ -9351,12 +9557,12 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
9351
9557
  return null;
9352
9558
  }
9353
9559
  function devConfigSyntaxError(path) {
9354
- const name = basename5(path);
9560
+ const name = basename6(path);
9355
9561
  const isTsconfig = /^tsconfig(?:\.[^.]+)?\.json$/.test(name);
9356
9562
  if (name !== "package.json" && !isTsconfig) return null;
9357
9563
  let text;
9358
9564
  try {
9359
- text = readFileSync7(path, "utf8");
9565
+ text = readFileSync9(path, "utf8");
9360
9566
  } catch (err) {
9361
9567
  return `${name}: ${err instanceof Error ? err.message : err}`;
9362
9568
  }
@@ -9452,7 +9658,7 @@ var Supervisor = class {
9452
9658
  watchSubscribe;
9453
9659
  reexec;
9454
9660
  supervisorLock;
9455
- statusPublisherToken = randomUUID4();
9661
+ statusPublisherToken = randomUUID5();
9456
9662
  processGeneration = currentProcessGeneration();
9457
9663
  ownerToken;
9458
9664
  launchTarget;
@@ -9523,14 +9729,14 @@ var Supervisor = class {
9523
9729
  }
9524
9730
  async start() {
9525
9731
  if (this.supervisorLock) {
9526
- mkdirSync6(dirname4(this.supervisorLock), { recursive: true });
9732
+ mkdirSync8(dirname7(this.supervisorLock), { recursive: true });
9527
9733
  this.writeStatus("starting", "watcher initializing");
9528
9734
  }
9529
9735
  try {
9530
9736
  await this.publicProxy.listen();
9531
9737
  if (this.watchEnabled) {
9532
9738
  const settled = await Promise.allSettled(
9533
- this.roots.filter(existsSync3).map((root) => this.watchSubscribe(root, (err, events) => this.onWatch(err, events), {
9739
+ this.roots.filter(existsSync4).map((root) => this.watchSubscribe(root, (err, events) => this.onWatch(err, events), {
9534
9740
  ignore: DEV_WATCH_IGNORE
9535
9741
  }))
9536
9742
  );
@@ -9948,13 +10154,13 @@ async function startDevSupervisor(opts) {
9948
10154
  import { execFile as execFile2, spawn as spawn2 } from "node:child_process";
9949
10155
  var PRODUCTION_REEXEC_FLAG = "--_frizz-production-reexec";
9950
10156
  function compareReleaseVersions(a, b) {
9951
- const parse = (value) => {
10157
+ const parse2 = (value) => {
9952
10158
  const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
9953
10159
  if (!match) return null;
9954
10160
  return { numeric: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4] };
9955
10161
  };
9956
- const left = parse(a);
9957
- const right = parse(b);
10162
+ const left = parse2(a);
10163
+ const right = parse2(b);
9958
10164
  if (!left || !right) return null;
9959
10165
  for (let index = 0; index < left.numeric.length; index++) {
9960
10166
  const delta = left.numeric[index] - right.numeric[index];
@@ -10018,8 +10224,8 @@ var npmRegistryReleaseAdapter = {
10018
10224
  // src/preflight.ts
10019
10225
  import { spawnSync } from "node:child_process";
10020
10226
  import { chmodSync, statSync as statSync5 } from "node:fs";
10021
- import { createRequire as createRequire2 } from "node:module";
10022
- import { dirname as dirname5, join as join9 } from "node:path";
10227
+ import { createRequire } from "node:module";
10228
+ import { dirname as dirname8, join as join10 } from "node:path";
10023
10229
  var SUPPORTED_NODE_LINES = [
10024
10230
  { major: 22, minor: 13 },
10025
10231
  { major: 23, minor: 4 }
@@ -10037,9 +10243,7 @@ function nodeVersionIsSupported(major, minor) {
10037
10243
  if (line) return minor >= line.minor;
10038
10244
  return major > SUPPORTED_NODE_LINES[SUPPORTED_NODE_LINES.length - 1].major;
10039
10245
  }
10040
- var REQUIRED_EXECUTABLES = [
10041
- { name: "git", need: "Frizz identifies a project by its Git repository" }
10042
- ];
10246
+ var REQUIRED_EXECUTABLES = [];
10043
10247
  function assertRequiredExecutables(command = commandIsAvailable) {
10044
10248
  for (const { name, need } of REQUIRED_EXECUTABLES) {
10045
10249
  if (command(name)) continue;
@@ -10070,10 +10274,10 @@ function ensureNativeHelperPermissions(options2 = {}) {
10070
10274
  if (platform === "win32") return;
10071
10275
  const stat = options2.stat ?? ((path) => statSync5(path));
10072
10276
  const chmod = options2.chmod ?? ((path, mode) => chmodSync(path, mode));
10073
- 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"));
10074
10278
  try {
10075
- const helper = join9(
10076
- dirname5(resolvePty()),
10279
+ const helper = join10(
10280
+ dirname8(resolvePty()),
10077
10281
  "prebuilds",
10078
10282
  `${platform}-${options2.arch ?? process.arch}`,
10079
10283
  "spawn-helper"
@@ -10086,10 +10290,13 @@ function ensureNativeHelperPermissions(options2 = {}) {
10086
10290
  }
10087
10291
 
10088
10292
  // src/production.ts
10293
+ init_src();
10294
+ init_project_registry();
10295
+ init_project_identity();
10089
10296
  var PACKAGE_NAME = process.env.FRIZZ_REGISTRY_PACKAGE ?? "frizz";
10090
10297
  function resolvePackageVersion() {
10091
10298
  try {
10092
- const manifest = JSON.parse(readFileSync8(join10(import.meta.dirname, "..", "package.json"), "utf8"));
10299
+ const manifest = JSON.parse(readFileSync10(join11(import.meta.dirname, "..", "package.json"), "utf8"));
10093
10300
  if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
10094
10301
  } catch {
10095
10302
  }
@@ -10152,6 +10359,7 @@ var bind = (() => {
10152
10359
  return fail(error);
10153
10360
  }
10154
10361
  })();
10362
+ var launchIntent;
10155
10363
  var workspace = (() => {
10156
10364
  try {
10157
10365
  if (!reexec) {
@@ -10159,11 +10367,17 @@ var workspace = (() => {
10159
10367
  else assertLaunchPrerequisites();
10160
10368
  }
10161
10369
  const pinned = projectLaunchTargetFromEnvironment(process.env);
10162
- if (reexec) {
10370
+ if (reexec || process.env.FRIZZ_PRODUCTION_SUPERVISOR === "1") {
10163
10371
  if (!pinned) throw new Error("registry successor is missing its pinned project identity");
10164
10372
  return workspaceFromLaunchTarget(pinned);
10165
10373
  }
10166
- 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;
10167
10381
  } catch (error) {
10168
10382
  return fail(error);
10169
10383
  }
@@ -10188,8 +10402,39 @@ async function existingPort() {
10188
10402
  for (const port of new Set(ports)) if (await probeFrizz(port, expected)) return port;
10189
10403
  return void 0;
10190
10404
  }
10191
- async function openOrPrint(port, reused) {
10192
- 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}`;
10193
10438
  logger.info("launcher", `${reused ? "reusing" : "started"} Frizz at ${url}`);
10194
10439
  readout?.settle("server", "done", reused ? `already running on port ${port}` : `port ${port}`);
10195
10440
  let browser;
@@ -10197,7 +10442,7 @@ async function openOrPrint(port, reused) {
10197
10442
  readout?.begin("browser", options.appMode ? "requesting app window" : "requesting default browser");
10198
10443
  try {
10199
10444
  if (options.appMode) {
10200
- await launchApp(url, { dataPath: join10(workspace.stateDir, "browser-profile") });
10445
+ await launchApp(url, { dataPath: join11(workspace.stateDir, "browser-profile") });
10201
10446
  browser = reused ? "focused the Frizz app window" : "opened the Frizz app window";
10202
10447
  } else {
10203
10448
  await launchBrowserTab(url);
@@ -10226,10 +10471,10 @@ async function openOrPrint(port, reused) {
10226
10471
  for (const warning of warnings) console.log(warning);
10227
10472
  return;
10228
10473
  }
10229
- const home = homedir7();
10474
+ const home = homedir8();
10230
10475
  readout.ready(
10231
10476
  [
10232
- { label: "Local", value: `${url}/`, accent: true },
10477
+ { label: "Local", value: boardAddress(url), accent: true },
10233
10478
  ...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
10234
10479
  ...publicOrigin ? [{ label: "Public", value: `${publicOrigin}/`, accent: true }] : [],
10235
10480
  { label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
@@ -10259,10 +10504,10 @@ async function runSupervisor(port, token) {
10259
10504
  target,
10260
10505
  owner.token
10261
10506
  );
10262
- const webDist = join10(import.meta.dirname, "..", "web-dist");
10263
- const runtimeDir = join10(import.meta.dirname, "..", "runtime");
10264
- const scriptsDir = join10(runtimeDir, "board");
10265
- const workerPluginDir = join10(runtimeDir, "cc-worker");
10507
+ const webDist = join11(import.meta.dirname, "..", "web-dist");
10508
+ const runtimeDir = join11(import.meta.dirname, "..", "runtime");
10509
+ const scriptsDir = join11(runtimeDir, "board");
10510
+ const workerPluginDir = join11(runtimeDir, "cc-worker");
10266
10511
  const childEntry = fileURLToPath2(new URL("./dev-child.js", import.meta.url));
10267
10512
  let plannedUpdate;
10268
10513
  let updateAvailable = true;
@@ -10316,10 +10561,10 @@ async function runSupervisor(port, token) {
10316
10561
  exit: (code) => {
10317
10562
  logger.info("launcher", `stopped with code ${code}`);
10318
10563
  const suffix = logger.file ? `
10319
- log: ${tildePath(logger.file, homedir7())}` : "";
10564
+ log: ${tildePath(logger.file, homedir8())}` : "";
10320
10565
  process.stdout.write(
10321
10566
  code === 0 ? `
10322
- Frizz stopped. Agent sessions in tmux keep running.${suffix}
10567
+ Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
10323
10568
 
10324
10569
  ` : `
10325
10570
  Frizz stopped with errors (exit ${code}).${suffix}
@@ -10346,7 +10591,12 @@ try {
10346
10591
  }
10347
10592
  const existing = await existingPort();
10348
10593
  if (existing) {
10349
- 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());
10350
10600
  process.exit(0);
10351
10601
  }
10352
10602
  const claim = tryAcquireProjectLaunchOwner(target, "launcher");
@@ -10371,7 +10621,7 @@ try {
10371
10621
  ]);
10372
10622
  portReservation();
10373
10623
  portReservation = void 0;
10374
- await openOrPrint(port, false);
10624
+ await openOrPrint(port, false, slugPath());
10375
10625
  await running;
10376
10626
  } finally {
10377
10627
  release?.();