frizz 0.13.12 → 0.13.14

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 (3) hide show
  1. package/README.md +21 -13
  2. package/dist/frizz.js +888 -233
  3. package/package.json +3 -2
package/dist/frizz.js CHANGED
@@ -182,7 +182,7 @@ var init_local_origin = __esm({
182
182
 
183
183
  // packages/server/src/access-codes.ts
184
184
  import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
185
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
185
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
186
186
  import { dirname, join as join2 } from "node:path";
187
187
  function memorySessionDirectory(seed = []) {
188
188
  const records = new Map(seed.map((r) => [r.id, r]));
@@ -227,7 +227,7 @@ function secretsMatch(a, b) {
227
227
  return left.length === right.length && timingSafeEqual(left, right);
228
228
  }
229
229
  function fileSessionDirectory(stateDir, now = Date.now) {
230
- const path = join2(stateDir, "sessions.json");
230
+ const path = join2(stateDir, SESSION_DIRECTORY_FILE);
231
231
  let records = [];
232
232
  try {
233
233
  const parsed = JSON.parse(readFileSync(path, "utf8"));
@@ -283,6 +283,39 @@ function loadOrCreateSessionKey(stateDir) {
283
283
  }
284
284
  return rotateSessionKey(stateDir);
285
285
  }
286
+ function adoptSessionState(stateDir, others) {
287
+ if (existsSync(join2(stateDir, SESSION_KEY_FILE))) return void 0;
288
+ const usedAt = (dir) => {
289
+ try {
290
+ const key = statSync(join2(dir, SESSION_KEY_FILE));
291
+ if (!key.isFile() || key.size < 32) return void 0;
292
+ try {
293
+ return Math.max(key.mtimeMs, statSync(join2(dir, SESSION_DIRECTORY_FILE)).mtimeMs);
294
+ } catch {
295
+ return key.mtimeMs;
296
+ }
297
+ } catch {
298
+ return void 0;
299
+ }
300
+ };
301
+ let newest;
302
+ for (const dir of others) {
303
+ if (dir === stateDir) continue;
304
+ const at = usedAt(dir);
305
+ if (at !== void 0 && (!newest || at > newest.at)) newest = { dir, at };
306
+ }
307
+ if (!newest) return void 0;
308
+ mkdirSync(stateDir, { recursive: true });
309
+ for (const name of [SESSION_EPOCH_FILE, SESSION_DIRECTORY_FILE, SESSION_KEY_FILE]) {
310
+ try {
311
+ copyFileSync(join2(newest.dir, name), join2(stateDir, name));
312
+ chmodSync(join2(stateDir, name), 384);
313
+ } catch (error) {
314
+ if (name === SESSION_KEY_FILE) throw error;
315
+ }
316
+ }
317
+ return newest.dir;
318
+ }
286
319
  function rotateSessionKey(stateDir) {
287
320
  const key = randomBytes(32);
288
321
  writeSessionKey(join2(stateDir, SESSION_KEY_FILE), key);
@@ -327,7 +360,7 @@ function loadSessionState(stateDir, epoch = SESSION_EPOCH) {
327
360
  const signedOut = advance.advanced ? directory.revokeAll() : 0;
328
361
  return { key, directory, rotateKey: () => rotateSessionKey(stateDir), advance, signedOut };
329
362
  }
330
- var CODE_BYTES, SESSION_BYTES, SESSION_ID_BYTES, DEFAULT_CODE_TTL_MS, DEFAULT_SESSION_TTL_MS, SESSION_EPOCH, AccessStore, SESSION_KEY_FILE, SESSION_EPOCH_FILE;
363
+ var CODE_BYTES, SESSION_BYTES, SESSION_ID_BYTES, DEFAULT_CODE_TTL_MS, DEFAULT_SESSION_TTL_MS, SESSION_EPOCH, AccessStore, SESSION_KEY_FILE, SESSION_EPOCH_FILE, SESSION_DIRECTORY_FILE;
331
364
  var init_access_codes = __esm({
332
365
  "packages/server/src/access-codes.ts"() {
333
366
  "use strict";
@@ -504,6 +537,7 @@ var init_access_codes = __esm({
504
537
  };
505
538
  SESSION_KEY_FILE = "session-key";
506
539
  SESSION_EPOCH_FILE = "session-epoch";
540
+ SESSION_DIRECTORY_FILE = "sessions.json";
507
541
  }
508
542
  });
509
543
 
@@ -518,7 +552,7 @@ var init_local_path = __esm({
518
552
  });
519
553
 
520
554
  // packages/server/src/local-image.ts
521
- import { createReadStream, realpathSync, statSync } from "node:fs";
555
+ import { createReadStream, realpathSync, statSync as statSync2 } from "node:fs";
522
556
  import { extname, isAbsolute } from "node:path";
523
557
  function resolveLocalImage(rawPath, range) {
524
558
  if (rawPath) rawPath = normalizeLocalPath(rawPath);
@@ -533,7 +567,7 @@ function resolveLocalImage(rawPath, range) {
533
567
  }
534
568
  let size;
535
569
  try {
536
- const stat = statSync(real);
570
+ const stat = statSync2(real);
537
571
  if (!stat.isFile()) return { status: 404 };
538
572
  size = stat.size;
539
573
  } catch {
@@ -944,7 +978,7 @@ import {
944
978
  openSync,
945
979
  readdirSync,
946
980
  rmSync,
947
- statSync as statSync2,
981
+ statSync as statSync3,
948
982
  symlinkSync,
949
983
  writeFileSync as writeFileSync2,
950
984
  writeSync
@@ -989,7 +1023,7 @@ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Dat
989
1023
  }
990
1024
  const dated = entries.map((name) => {
991
1025
  try {
992
- return { name, at: statSync2(join4(dir, name)).mtimeMs };
1026
+ return { name, at: statSync3(join4(dir, name)).mtimeMs };
993
1027
  } catch {
994
1028
  return { name, at: 0 };
995
1029
  }
@@ -13111,6 +13145,15 @@ var init_interactions = __esm({
13111
13145
  }
13112
13146
  });
13113
13147
 
13148
+ // packages/shared/src/launch-prefix.ts
13149
+ var LAUNCH_PREFIX_MAX_LENGTH;
13150
+ var init_launch_prefix = __esm({
13151
+ "packages/shared/src/launch-prefix.ts"() {
13152
+ "use strict";
13153
+ LAUNCH_PREFIX_MAX_LENGTH = 2048;
13154
+ }
13155
+ });
13156
+
13114
13157
  // packages/shared/src/claim.ts
13115
13158
  function isAnonymousClaimName(name) {
13116
13159
  return ANONYMOUS_CLAIM_PATTERN.test(name);
@@ -13264,6 +13307,17 @@ var init_code_fences = __esm({
13264
13307
  }
13265
13308
  });
13266
13309
 
13310
+ // packages/shared/src/question-fence.ts
13311
+ var import_yaml;
13312
+ var init_question_fence = __esm({
13313
+ "packages/shared/src/question-fence.ts"() {
13314
+ "use strict";
13315
+ import_yaml = __toESM(require_dist(), 1);
13316
+ init_code_fences();
13317
+ init_src();
13318
+ }
13319
+ });
13320
+
13267
13321
  // packages/shared/src/delta.ts
13268
13322
  var init_delta = __esm({
13269
13323
  "packages/shared/src/delta.ts"() {
@@ -13468,16 +13522,18 @@ function signoffNudgeText(needsInput) {
13468
13522
  "the write-up.",
13469
13523
  "",
13470
13524
  "**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
13471
- "right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
13525
+ "right now: ask that one in a ```question fence. Every other open choice INSIDE the task \u2014 a name, a",
13472
13526
  "default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
13473
13527
  "what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
13474
13528
  "unanswered question is not permission to go build the answer.",
13475
13529
  "",
13476
- "Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
13530
+ "Otherwise, sign off \u2014 a question in your prose, or a fence at the END of your next message:",
13477
13531
  "",
13478
- "- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
13479
- " with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
13480
- " recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
13532
+ "- `` ```question `` \u2014 you need the human's decision. Write each fence right under the paragraph it",
13533
+ " decides, never stacked at the bottom: `id:` (a slug of your choosing), `kind:` (`choice`, `multi`,",
13534
+ " `text` or `secret`), a one-line `question:`, and `options:` with one-line trade-offs, the recommended",
13535
+ " one first. Frizz registers it when the message lands, and a question asked at this rest is its",
13536
+ " sign-off: rest normally.",
13481
13537
  "- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
13482
13538
  " if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
13483
13539
  " **bolded verb phrase**.",
@@ -13485,24 +13541,24 @@ function signoffNudgeText(needsInput) {
13485
13541
  "",
13486
13542
  " Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
13487
13543
  " A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
13488
- " awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
13489
- " `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
13544
+ " awaiting, you are done. Register a PR with `frizzctl watch-pr add` and a timer with",
13545
+ " `frizzctl timer set`; `frizzctl activity` reads back everything you have running, with its id.",
13490
13546
  "- `` ```awaiting `` with `steps:` \u2014 the human must PERFORM something you cannot: sign in, approve,",
13491
13547
  " merge, press a button you may not. List each step as a `- ` line under `steps:`, written to be",
13492
13548
  " followed cold: a real Markdown link to every page and file it names, every command in a code span,",
13493
13549
  " every value the act needs written in. Frizz reads them verbatim. Steps name the HUMAN as the wait,",
13494
13550
  " so the fence needs no other name and no `for:`, and the thread goes into their queue. Their Done",
13495
13551
  " comes back to you as their reply; anything else they need to say comes as a message of their own.",
13496
- "- `` ```awaiting `` with `questions:` \u2014 a question you registered at an EARLIER rest is still open and",
13497
- " you still need its answer. Name every such question (`questions: [qst_\u2026]`) and withdraw the rest with",
13498
- " `mcp__frizz__unask`: a fence that leaves an open question out is refused. A named card is drawn at",
13499
- " this rest; an unnamed one stays where you asked it.",
13552
+ "- A question you asked at an EARLIER rest, whose answer you still need: write a ```question fence",
13553
+ " carrying its `id:` alone. That draws its card in this message and keeps the thread in the human's",
13554
+ " queue. One you no longer need, withdraw with `frizzctl unask <id>`; one you leave unmentioned stays",
13555
+ " answerable where you asked it.",
13500
13556
  "",
13501
13557
  "**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
13502
13558
  "draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
13503
- "the part that is someone else's to do. Each ends on a question with your recommendation as option 1,",
13559
+ "the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
13504
13560
  "or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
13505
- "`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
13561
+ "`frizzctl spawn-thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
13506
13562
  "None of them is a `done`. And what is not",
13507
13563
  'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
13508
13564
  "in the handoff.",
@@ -13522,23 +13578,37 @@ function requireEffortOutsideAcp(profile, ctx) {
13522
13578
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
13523
13579
  }
13524
13580
  }
13581
+ function formatElapsed(ms) {
13582
+ if (!Number.isFinite(ms) || ms < 0) return "an unknown time";
13583
+ const s = Math.floor(ms / 1e3);
13584
+ if (s < 60) return `${s}s`;
13585
+ const m = Math.floor(s / 60);
13586
+ if (m < 60) return `${m}m`;
13587
+ const h = Math.floor(m / 60);
13588
+ if (h < 24) return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
13589
+ const d = Math.floor(h / 24);
13590
+ return h % 24 === 0 ? `${d}d` : `${d}d ${h % 24}h`;
13591
+ }
13525
13592
  function fallbackPort(base) {
13526
13593
  return base + 1e4;
13527
13594
  }
13528
- var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, 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, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, SteerThreadInput, SteerThreadResult, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13595
+ var import_yaml2, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, 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, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, CommandWatchRun, MonitorStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, COMMAND_WATCH_EVERY_MAX_MS, COMMAND_WATCH_FOR_MAX_MS, COMMAND_WATCH_COMMAND_MAX, COMMAND_WATCH_RUN_TIMEOUT_MS, CommandWatchState, CommandWatchView, AddOwnCommandWatchInput, AddOwnCommandWatchResult, DropOwnCommandWatchInput, DropOwnCommandWatchResult, ListOwnCommandWatchesInput, OwnCommandWatchesResult, MONITOR_FLOOD_MAX_EVENTS, MONITOR_FLOOD_WINDOW_MS, MONITOR_DESCRIPTION_MAX, AddOwnMonitorInput, AddOwnMonitorResult, MONITOR_FLOOD_REASON, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, QuestionFenceFaultView, ClosedQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, ANSWER_ROW_ID_PATTERN, ANSWER_ROW_ID, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, BoardState, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, SteerThreadInput, SteerThreadResult, SpawnThreadInput, SpawnThreadResult, SpawnChildInput, SpawnChildResult, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, ReorderPinnedThreadsInput, SetThreadConsolePinnedInput, ReorderConsolePinnedThreadsInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, ThreadBand, ThreadListing, ListThreadsInput, ListThreadsResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, McpTarget, McpServerState, McpServerReading, McpStatusResult, McpSignInInput, McpSignInState, McpSignInResult, McpSignInAttemptInput, McpSignInPollResult, McpSignInFinishInput, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubAccount, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, BoardDeltaEvent, ServerEvent, BoardDeltasInput, BoardDeltasResult, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketBoardClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
13529
13596
  var init_src = __esm({
13530
13597
  "packages/shared/src/index.ts"() {
13531
13598
  "use strict";
13532
- import_yaml = __toESM(require_dist(), 1);
13599
+ import_yaml2 = __toESM(require_dist(), 1);
13533
13600
  init_zod();
13534
13601
  init_interactions();
13602
+ init_launch_prefix();
13535
13603
  init_thread_slug();
13536
13604
  init_claim();
13537
13605
  init_claude_editions();
13538
13606
  init_code_fences();
13607
+ init_question_fence();
13539
13608
  init_delta();
13540
13609
  init_drainable_worker();
13541
13610
  init_interactions();
13611
+ init_launch_prefix();
13542
13612
  init_receipt_bus();
13543
13613
  init_relay_protocol();
13544
13614
  init_shell_writes();
@@ -13863,7 +13933,7 @@ var init_src = __esm({
13863
13933
  questions: external_exports.array(AskQuestion)
13864
13934
  });
13865
13935
  AwaitingHint = external_exports.object({
13866
- kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "status", "needs_input", "step", "question"]),
13936
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "command", "thread", "for", "title", "status", "needs_input", "step", "question"]),
13867
13937
  value: external_exports.string()
13868
13938
  });
13869
13939
  RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
@@ -13904,7 +13974,7 @@ var init_src = __esm({
13904
13974
  SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
13905
13975
  SIGNOFF_NUDGE_MESSAGE = signoffNudgeText(false);
13906
13976
  SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT = signoffNudgeText(true);
13907
- ThreadWatchKind = external_exports.enum(["shell", "github", "timer"]);
13977
+ ThreadWatchKind = external_exports.enum(["shell", "github", "timer", "command"]);
13908
13978
  GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
13909
13979
  GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
13910
13980
  GithubWatchStatus = external_exports.object({
@@ -13942,7 +14012,16 @@ var init_src = __esm({
13942
14012
  /** A digest of WHICH jobs are failing right now. The head commit alone cannot express a re-run of the
13943
14013
  * same job, or a slower job going red after the first — both of which are a second failure the worker
13944
14014
  * must hear about. Empty when nothing is failing. */
13945
- failureSig: external_exports.string().optional()
14015
+ failureSig: external_exports.string().optional(),
14016
+ /** WHO THE PR IS WAITING ON, the facts behind "will anything move here unless we act?"
14017
+ * (awaiting.prWaitsOnNoOne). Whether GitHub will merge it by itself (`queued` in a merge queue, `auto`
14018
+ * with auto-merge on, `none`), whether the signed-in `gh` account opened it and may merge it, and
14019
+ * whether a review is still requested of someone. Absent means unknown: the `gh` fallback and a
14020
+ * reading taken before 2026-10-27 carry none, and an unknown never counts as "nobody". */
14021
+ mergePath: external_exports.enum(["none", "queued", "auto"]).optional(),
14022
+ viewerAuthored: external_exports.boolean().optional(),
14023
+ viewerCanMerge: external_exports.boolean().optional(),
14024
+ reviewRequested: external_exports.boolean().optional()
13946
14025
  }).strict();
13947
14026
  GithubIssueStatus = external_exports.object({
13948
14027
  /** OPEN | CLOSED, lowercased. A closed issue ends the wait outright. */
@@ -13958,6 +14037,20 @@ var init_src = __esm({
13958
14037
  /** When frizz last heard from GitHub, for the same reason `GithubWatchStatus.polledAt` carries it. */
13959
14038
  polledAt: external_exports.string()
13960
14039
  }).strict();
14040
+ CommandWatchRun = external_exports.object({
14041
+ at: external_exports.string(),
14042
+ outcome: external_exports.enum(["exit", "timeout", "signal", "error"]),
14043
+ exitCode: external_exports.number().int().nullable(),
14044
+ output: external_exports.string()
14045
+ }).strict();
14046
+ MonitorStatus = external_exports.object({
14047
+ /** The worker's own words for what it watches ("errors in deploy.log"), when it gave any. */
14048
+ description: external_exports.string().optional(),
14049
+ /** Events printed so far: each batch of lines that woke (or will wake) the thread is one. */
14050
+ events: external_exports.number().int().nonnegative(),
14051
+ /** The newest event's text, trimmed, and when it was printed. */
14052
+ lastEvent: external_exports.object({ at: external_exports.string(), text: external_exports.string() }).strict().optional()
14053
+ }).strict();
13961
14054
  ThreadWatchView = external_exports.object({
13962
14055
  id: external_exports.string(),
13963
14056
  kind: ThreadWatchKind,
@@ -13979,7 +14072,22 @@ var init_src = __esm({
13979
14072
  * worker's prompt is the row's NAME (a `tmr_…` id names nothing to a human) and the fire instant is
13980
14073
  * its status. Unlike `github` there is no polled half to be absent: a timer that exists is fully
13981
14074
  * known, so a timer row always carries this. */
13982
- timer: external_exports.object({ fireAt: external_exports.string(), prompt: external_exports.string() }).strict().optional()
14075
+ timer: external_exports.object({ fireAt: external_exports.string(), prompt: external_exports.string() }).strict().optional(),
14076
+ /** `command` rows only: the armed command watch's registration and its last check — what the worker's
14077
+ * own read-back carries (CommandWatchView), minus the id and state the row already has. The command is
14078
+ * the row's NAME and the cadence its status. Until 2026-10-13 a command watch had no row at all, so a
14079
+ * thread parked on nothing else drew a card that said only "Waiting for an external update." */
14080
+ command: external_exports.object({
14081
+ command: external_exports.string(),
14082
+ /** The cadence in the house duration grammar (`2m`, `1h 30m`). */
14083
+ every: external_exports.string(),
14084
+ expiresAt: external_exports.string(),
14085
+ runs: external_exports.number().int().nonnegative(),
14086
+ lastRun: CommandWatchRun.optional(),
14087
+ /** A MONITOR's own reading (`frizzctl monitor`), and present on nothing else: the row is a running
14088
+ * script, so its status is how many events it has printed, not a cadence. */
14089
+ monitor: MonitorStatus.optional()
14090
+ }).strict().optional()
13983
14091
  }).strict();
13984
14092
  PR_WATCH_FOR_MAX_MS = 365 * 24 * 60 * 60 * 1e3;
13985
14093
  PR_WATCH_DEFAULT_FOR_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -14021,7 +14129,7 @@ var init_src = __esm({
14021
14129
  * still BOUNDS the poll; the point of the field is that a worker chooses, and one that cannot choose
14022
14130
  * is better bounded than broken. */
14023
14131
  for: external_exports.string().trim().min(1).max(16).optional(),
14024
- /** What `target` names. `issue` (from `mcp__frizz__watch_issue`, 2026-09-14) registers a watcher on a
14132
+ /** What `target` names. `issue` (from `frizzctl watch-issue`, 2026-09-14) registers a watcher on a
14025
14133
  * GitHub issue — same registry, same poll cadence, same drop verb, but a different probe (`gh issue
14026
14134
  * view`), a different GraphQL fragment and a report with no CI in it. ABSENT MEANS A PULL REQUEST,
14027
14135
  * for the same reason `for` is optional on the wire: every `watch_pr` binary dispatched before this
@@ -14109,6 +14217,98 @@ var init_src = __esm({
14109
14217
  dropped: external_exports.boolean(),
14110
14218
  watches: external_exports.array(OwnWatchView)
14111
14219
  }).strict();
14220
+ COMMAND_WATCH_EVERY_MAX_MS = 24 * 60 * 60 * 1e3;
14221
+ COMMAND_WATCH_FOR_MAX_MS = 7 * 24 * 60 * 60 * 1e3;
14222
+ COMMAND_WATCH_COMMAND_MAX = 4e3;
14223
+ COMMAND_WATCH_RUN_TIMEOUT_MS = 2 * 6e4;
14224
+ CommandWatchState = external_exports.enum(["armed", "dropped", "passed", "expired", "cancelled"]);
14225
+ CommandWatchView = external_exports.object({
14226
+ id: external_exports.string(),
14227
+ command: external_exports.string(),
14228
+ /** The cadence in the house duration grammar (`5m`, `1h 30m`). */
14229
+ every: external_exports.string(),
14230
+ state: CommandWatchState,
14231
+ createdAt: external_exports.string(),
14232
+ expiresAt: external_exports.string(),
14233
+ /** How many checks have run since it was armed, the registration's own first check included. */
14234
+ runs: external_exports.number().int().nonnegative(),
14235
+ lastRun: CommandWatchRun.optional(),
14236
+ /** Present on a MONITOR (`frizzctl monitor`) and nothing else: a script the server keeps RUNNING, whose
14237
+ * every printed line is a wake. A monitor has no cadence and no checks, so `every` reads `0s` and
14238
+ * `runs` 0 on one; read this instead. */
14239
+ monitor: MonitorStatus.optional()
14240
+ }).strict();
14241
+ AddOwnCommandWatchInput = external_exports.object({
14242
+ slug: ThreadSlug,
14243
+ /** Run with `/bin/sh -c` in the project root, with the server's environment. Exit status 0 is "passed". */
14244
+ command: external_exports.string().trim().min(1).max(COMMAND_WATCH_COMMAND_MAX),
14245
+ /** How often to check, as a DURATION (`5m`; parseAwaitingDurationRaw). Clamped to [1m, 24h] and reported. */
14246
+ every: external_exports.string().trim().min(1).max(16),
14247
+ /** How long to keep checking, as a DURATION. Clamped to COMMAND_WATCH_FOR_MAX_MS and reported. */
14248
+ for: external_exports.string().trim().min(1).max(16)
14249
+ }).strict();
14250
+ AddOwnCommandWatchResult = external_exports.object({
14251
+ /** Present when the call was REFUSED — the thread may not run commands outside the CLI's permission
14252
+ * system, or the command cannot run here at all. A refusal is an answer, not a fault: the text says
14253
+ * why and what to use instead, and nothing was stored. */
14254
+ refused: external_exports.string().optional(),
14255
+ /** The armed row's id. Absent when nothing was armed: a refusal, or a first check that already passed. */
14256
+ id: external_exports.string().optional(),
14257
+ /** This exact command was already watched on this thread, so the call registered nothing new. */
14258
+ alreadyArmed: external_exports.boolean().default(false),
14259
+ /** The FIRST CHECK, run before anything was stored. Exit 0 there means the condition already holds:
14260
+ * nothing is armed, and the worker proceeds now rather than waiting a cadence to be told so. */
14261
+ firstRun: CommandWatchRun.optional(),
14262
+ passed: external_exports.boolean().default(false),
14263
+ expiresAt: external_exports.string().optional(),
14264
+ every: external_exports.string().optional(),
14265
+ /** The `every` / `for` the worker wrote, present ONLY when it was clamped — a clamp nobody is told about
14266
+ * is a worker resting on coverage it does not have. */
14267
+ clamped: external_exports.object({ every: external_exports.string().optional(), for: external_exports.string().optional() }).strict().optional(),
14268
+ watches: external_exports.array(CommandWatchView)
14269
+ }).strict();
14270
+ DropOwnCommandWatchInput = external_exports.object({
14271
+ slug: ThreadSlug,
14272
+ id: external_exports.string().min(1).max(64)
14273
+ }).strict();
14274
+ DropOwnCommandWatchResult = external_exports.object({
14275
+ dropped: external_exports.boolean(),
14276
+ watches: external_exports.array(CommandWatchView)
14277
+ }).strict();
14278
+ ListOwnCommandWatchesInput = external_exports.object({ slug: ThreadSlug }).strict();
14279
+ OwnCommandWatchesResult = external_exports.object({ watches: external_exports.array(CommandWatchView) }).strict();
14280
+ MONITOR_FLOOD_MAX_EVENTS = 30;
14281
+ MONITOR_FLOOD_WINDOW_MS = 10 * 6e4;
14282
+ MONITOR_DESCRIPTION_MAX = 120;
14283
+ AddOwnMonitorInput = external_exports.object({
14284
+ slug: ThreadSlug,
14285
+ /** Run with `/bin/sh -c` in the project root, with the server's environment. Each stdout line is an event. */
14286
+ command: external_exports.string().trim().min(1).max(COMMAND_WATCH_COMMAND_MAX),
14287
+ /** What it watches, in the worker's words — the name on its row and drawer, and the label on every wake.
14288
+ * Required (2026-11-03), as a sub-agent's description is: without one the row could only show the script. */
14289
+ description: external_exports.string().trim().min(1).max(MONITOR_DESCRIPTION_MAX),
14290
+ /** How long to keep it running, as a DURATION. Clamped to COMMAND_WATCH_FOR_MAX_MS and reported. */
14291
+ for: external_exports.string().trim().min(1).max(16)
14292
+ }).strict();
14293
+ AddOwnMonitorResult = external_exports.object({
14294
+ /** Present when the call was REFUSED: the gate, the caps, or a script that could not run here. Nothing
14295
+ * is armed, and the text says why and what to use instead. */
14296
+ refused: external_exports.string().optional(),
14297
+ id: external_exports.string().optional(),
14298
+ /** This exact command already runs as a monitor on this thread; nothing new was registered. */
14299
+ alreadyArmed: external_exports.boolean().default(false),
14300
+ /** Still running when registration stopped watching it (MONITOR_REGISTRATION_WINDOW_MS). */
14301
+ running: external_exports.boolean().default(false),
14302
+ /** The events it printed while registration watched — handed back here rather than woken. */
14303
+ events: external_exports.array(external_exports.string()).default([]),
14304
+ /** It EXITED while registration watched: how, and the end of what it printed. Nothing stays armed. */
14305
+ exited: CommandWatchRun.optional(),
14306
+ expiresAt: external_exports.string().optional(),
14307
+ /** The `for` the worker wrote, present ONLY when it was clamped. */
14308
+ clamped: external_exports.object({ for: external_exports.string() }).strict().optional(),
14309
+ watches: external_exports.array(CommandWatchView)
14310
+ }).strict();
14311
+ MONITOR_FLOOD_REASON = `it printed ${MONITOR_FLOOD_MAX_EVENTS} events inside ${formatElapsed(MONITOR_FLOOD_WINDOW_MS)}, and every event is a turn of yours. Register it again with a tighter filter: print only the lines you would act on (\`grep --line-buffered\`), never raw logs.`;
14112
14312
  AskQuestionKind = external_exports.enum(["question", "multi"]);
14113
14313
  AskedOptionSchema = external_exports.lazy(() => external_exports.object({
14114
14314
  // 400, not the 120 it launched with (2026-09-03): a label is a chip line, but "Post the drafted
@@ -14127,32 +14327,55 @@ var init_src = __esm({
14127
14327
  // 4000, not the 600 it launched with (2026-09-03): this field is the whole card context — everything
14128
14328
  // the human needs to answer cold — and a question that carries its own evidence needs the room. The
14129
14329
  // answer restates it, so QuestionAnswerSchema's cap matches.
14330
+ handle: external_exports.string().trim().min(1).max(64).optional(),
14130
14331
  question: external_exports.string().trim().min(1).max(4e3),
14332
+ context: external_exports.string().trim().max(2e4).optional(),
14131
14333
  header: external_exports.string().trim().max(24).optional(),
14132
14334
  kind: AskQuestionKind,
14133
14335
  danger: external_exports.boolean().optional(),
14134
14336
  secret: external_exports.boolean().optional(),
14135
14337
  // UNBOUNDED, deliberately. This carried `.max(8)` from launch until 2026-09-03, when the maintainer
14136
14338
  // asked for the cap to go ("allow arbitrary numbers of options"): a `multi` over a long list — which
14137
- // gates to run, which of twenty findings to act on — is a real shape, and the card numbers past 9
14138
- // (only 1–9 are keys) already. The count is the worker's to choose; the answer's `chosen` is unbounded to match.
14339
+ // gates to run, which of twenty findings to act on — is a real shape, and the card letters past 26
14340
+ // (`AA.`) already. The count is the worker's to choose; the answer's `chosen` is unbounded to match.
14139
14341
  options: external_exports.array(AskedOptionSchema).optional()
14140
14342
  }).strict());
14141
14343
  AskInput = external_exports.object({
14142
14344
  slug: ThreadSlug,
14143
- // No cap on the count (four until 2026-09-03): the tool's own text says "several at once is one
14144
- // call — register them together", and a cap here told a worker with six to batch and then refused it.
14345
+ // No cap on the count (four until 2026-09-03): the command's own help says several at once is one
14346
+ // run — register them together — and a cap here told a worker with six to batch and then refused it.
14145
14347
  questions: external_exports.array(AskedQuestionSchema).min(1)
14146
14348
  }).strict();
14147
14349
  RegisteredQuestionView = external_exports.object({
14148
14350
  /** Minted by frizz. The worker never chose it, which is why an answer RESTATES the question text —
14149
14351
  * an id alone cannot be correlated back to what was asked. */
14150
14352
  id: external_exports.string(),
14353
+ /** The worker's own id for it, from its ```question fence — what `unask` and every correction name it
14354
+ * by. Absent on a question `ask` registered, which only ever had the minted id. */
14355
+ handle: external_exports.string().optional(),
14151
14356
  spec: AskedQuestionSchema,
14152
14357
  askedAt: external_exports.string(),
14153
14358
  /** A `secret` question only: the path its value will be served at. Known at registration, so the
14154
14359
  * worker can prepare the command that reads it before the human has answered. */
14155
- secretPath: external_exports.string().optional()
14360
+ secretPath: external_exports.string().optional(),
14361
+ /** ON THE BOARD ONLY: does this question STAND at the thread's current rest — asked at it, or
14362
+ * referenced by it (server question-fences.ts standingQuestions)? `false` is a question that has
14363
+ * FADED: still open and answerable where it was asked, but not what this rest is waiting on, so it
14364
+ * neither queues the thread nor marks its row. Absent — the worker's own read-back, a snapshot from
14365
+ * before 2026-10-13 — reads as standing (hasStandingQuestion). */
14366
+ standing: external_exports.boolean().optional()
14367
+ }).strict();
14368
+ QuestionFenceFaultView = external_exports.object({
14369
+ at: external_exports.string(),
14370
+ id: external_exports.string().optional(),
14371
+ faults: external_exports.array(external_exports.string())
14372
+ }).strict();
14373
+ ClosedQuestionView = external_exports.object({
14374
+ id: external_exports.string(),
14375
+ /** The worker's own id for it — what its fence in the transcript carries. */
14376
+ handle: external_exports.string(),
14377
+ state: external_exports.enum(["dismissed", "withdrawn"]),
14378
+ askedAt: external_exports.string()
14156
14379
  }).strict();
14157
14380
  SECRET_TTL_MS = 15 * 6e4;
14158
14381
  AskResult = external_exports.object({
@@ -14166,6 +14389,14 @@ var init_src = __esm({
14166
14389
  }).strict();
14167
14390
  UnaskResult = external_exports.object({
14168
14391
  withdrawn: external_exports.boolean(),
14392
+ /** The question the id resolved to, by the name the WORKER knows it by (its fence's `id:`, or the
14393
+ * `qst_…` an `ask` minted) — absent when the id names nothing on this thread. It is the ROOT's: a
14394
+ * follow-up rides inside its parent's row and cannot be withdrawn alone, so `unask sqlite-path`
14395
+ * withdraws nothing and answers `settings-store` here — the id that takes the pair back. */
14396
+ id: external_exports.string().optional(),
14397
+ /** What that question was when the call arrived. `open` beside `withdrawn: true` is the ordinary
14398
+ * withdrawal; anything else is why nothing was withdrawn — and `answered` means a wake is on its way. */
14399
+ state: external_exports.enum(["open", "answered", "withdrawn", "dismissed"]).optional(),
14169
14400
  open: external_exports.array(RegisteredQuestionView)
14170
14401
  }).strict();
14171
14402
  QuestionAnswerSchema = external_exports.lazy(() => external_exports.object({
@@ -14209,6 +14440,8 @@ var init_src = __esm({
14209
14440
  ThreadSettledQuestionsResult = external_exports.object({
14210
14441
  questions: external_exports.array(SettledQuestionView)
14211
14442
  }).strict();
14443
+ ANSWER_ROW_ID_PATTERN = "[a-z0-9][a-z0-9._-]{0,63}";
14444
+ ANSWER_ROW_ID = new RegExp(`^${ANSWER_ROW_ID_PATTERN}$`);
14212
14445
  MarkOwnDoneInput = external_exports.object({
14213
14446
  slug: ThreadSlug,
14214
14447
  body: external_exports.string().trim().min(1).max(2e4)
@@ -14235,7 +14468,7 @@ var init_src = __esm({
14235
14468
  body: external_exports.string(),
14236
14469
  // fence body minus hint lines, capped server-side; may be ""
14237
14470
  hints: external_exports.array(AwaitingHint).default([]),
14238
- // Present only on a completion the worker REGISTERED (`mcp__frizz__done`, board.registeredDoneFence)
14471
+ // Present only on a completion the worker REGISTERED (`frizzctl done`, board.registeredDoneFence)
14239
14472
  // rather than wrote as a ```done fence in its final message. The two are one fence to every predicate,
14240
14473
  // but the TRANSCRIPT draws a fence card from the message text it parses — so a registered done, which
14241
14474
  // is in no message, needs the client to know it must draw the card itself at the bottom of the thread
@@ -14379,6 +14612,20 @@ var init_src = __esm({
14379
14612
  // outlive the message that carried it. These rows can, and they carry the question itself rather than
14380
14613
  // merely asserting one exists — the card renders from this instead of re-parsing prose.
14381
14614
  questions: external_exports.array(RegisteredQuestionView).default([]),
14615
+ /** The ```question fences of this thread that could NOT register, from its latest sync — every one in
14616
+ * the server's ring of fenced messages, not only the current rest's. Present only when there is one.
14617
+ * A refused fence has no row, and so does a fence the registrar has not reached yet; this is what
14618
+ * tells them apart, so the transcript draws the first as the code block it is and only the second as
14619
+ * the question it is about to become (web lib/questionFaults). Its other job is the reused id: a fence
14620
+ * refused for naming a spent id must not move that id's card, which a name match alone would do. */
14621
+ questionFaults: external_exports.array(QuestionFenceFaultView).optional(),
14622
+ /** The questions this thread asked with a ```question fence that CLOSED WITHOUT AN ANSWER — dismissed
14623
+ * (the human's ×, or Frizz cancelling a question an autonomous thread wrote) or withdrawn by the
14624
+ * worker. Present only when there is one. Their fences are still in the transcript, and without this a
14625
+ * fence with no open and no answered row reads as a question about to become answerable; with it the
14626
+ * fence draws its question closed, saying which way (web ClosedQuestionCard). Compact on purpose — the
14627
+ * fence itself carries the question — because the board ships every thread on every keyframe. */
14628
+ closedQuestions: external_exports.array(ClosedQuestionView).optional(),
14382
14629
  /** The answer the human has already SENT that the worker has not received yet, as the exact message
14383
14630
  * the delivery will carry (board.answersInFlight → questionAnswerMessage). The chat parses it with
14384
14631
  * the same reader it uses on the delivered turn, so the in-flight card and the landed one are the
@@ -14418,6 +14665,16 @@ var init_src = __esm({
14418
14665
  foreign: external_exports.boolean().optional(),
14419
14666
  // ui.db lifecycle for session threads (open|archived) — written ONLY by explicit Archive/Reopen.
14420
14667
  state: external_exports.enum(["open", "archived"]).optional(),
14668
+ // The instant the thread was marked done (session.archived_at), present while it is archived. The
14669
+ // Done band's order: the one marked done most recently is at its top (maintainer 2026-10-23). Absent
14670
+ // on a thread archived before the column existed, which the band places by its rest time instead.
14671
+ archivedAt: external_exports.string().optional(),
14672
+ // The instant this thread last ENTERED the Running band (session.running_since), present while it is
14673
+ // in it: the board stamps it the first build that finds the thread there and clears it the first
14674
+ // build that does not. The Running band's order: the thread that most recently switched back to
14675
+ // running is at its top (maintainer 2026-10-23). A thread that stays in the band keeps its instant, so
14676
+ // a working rest, a sub-agent returning or a mid-turn message never moves it.
14677
+ runningSince: external_exports.string().optional(),
14421
14678
  // Exact durable user snooze. While this instant is in the future, an otherwise-resting thread is
14422
14679
  // suppressed from Queue and shown dimmed in Held. Hard interactive gates (question, permission,
14423
14680
  // native approval, crash) deliberately break through it. Expired values are cleared server-side.
@@ -14431,6 +14688,23 @@ var init_src = __esm({
14431
14688
  // this instant's order. Lifecycle metadata like the snooze: written only by the pin/unpin verb, and
14432
14689
  // it outranks every derived state (a pinned thread that finishes stays pinned).
14433
14690
  pinnedAt: external_exports.string().optional(),
14691
+ // The instant the human pinned this thread on the CONSOLE's "By status" view (absent = not pinned
14692
+ // there). A second shelf, independent of `pinnedAt`: pinning on the Console does not pin on the
14693
+ // project's board, nor the other way round. The instant is the Console's pinned band's order across
14694
+ // every project, and like the project pin it outranks every derived state.
14695
+ consolePinnedAt: external_exports.string().optional(),
14696
+ /** CHILD THREADS (Settings.childThreads): the thread that spawned this one with `frizzctl spawn-child`.
14697
+ * Present only while that thread still exists. A child is never a row of its own on the board — not in
14698
+ * any band, the queue, a count or a badge — and is drawn under its parent instead, unless it stands at a
14699
+ * gate only the human can settle (drawnUnderParent). */
14700
+ parentSlug: external_exports.string().optional(),
14701
+ /** This child's rest is one that would have put it in the human's queue, so its handoff goes to its
14702
+ * PARENT instead (board.ts deriveChildHandoff; the scheduler delivers it once per rest). A permission
14703
+ * prompt, a secret request and a usage-limit stop are not handoffs: they still queue for the human. */
14704
+ handoffToParent: external_exports.literal(true).optional(),
14705
+ /** The instant of the rest `handoffToParent` describes (the tailer's lastAssistantAt), so the scheduler
14706
+ * can tell a verdict about the rest it sees from one a cached board still carries about an older rest. */
14707
+ handoffRestAt: external_exports.string().optional(),
14434
14708
  /** The EVENT snooze on the resting card is armed for this exact rest — the human has said "hide this
14435
14709
  * until something reports". Distinct from `snoozedUntil`, which is a wall-clock park on the whole
14436
14710
  * thread: this one has no deadline and clears itself when the thread comes to a NEW rest.
@@ -14439,8 +14713,20 @@ var init_src = __esm({
14439
14713
  * the reasoning that the card states a FACT the drawer must keep showing or it blanks at rest and
14440
14714
  * reads as "the agent died". That reasoning holds for a thread nobody has parked; once the human has
14441
14715
  * explicitly parked THIS rest, showing them the same card with the same button one surface over is
14442
- * not information, and they said so. */
14716
+ * not information, and they said so.
14717
+ *
14718
+ * The header's "Until activity" (SnoozeMenu, 2026-10-19) arms the same snooze on any rested thread. */
14443
14719
  bgSnoozed: external_exports.boolean().optional(),
14720
+ /** Something of the thread's own can wake it without a person — a running sub-agent or shell, an
14721
+ * armed watch or timer, a Goal that fires on rest or on a clock (board.wakesOnItsOwn). The snooze
14722
+ * menu greys out "Until activity" without it, and the server stops honouring that snooze once it
14723
+ * goes false. Absent on a dead worker and on an archived thread. */
14724
+ wakesOnItsOwn: external_exports.boolean().optional(),
14725
+ /** A QUIET PARK THE BOARD REFUSED because an earlier `needs_input` handoff still stands: the human has
14726
+ * neither typed to the thread nor snoozed a rest since it (board handoffStands). The surfaces keep that
14727
+ * handoff's fence drawn (web lib/fenceBlocks heldHandoffIndex), since it is what the thread is in the
14728
+ * queue for. Set only while it is what queued the thread. */
14729
+ heldHandoff: external_exports.boolean().optional(),
14444
14730
  // Which Claude transport serves this thread. "broker" — a session-broker-owned Agent SDK session with
14445
14731
  // a typed control channel — is the only one there is; ABSENT means a row dispatched before the broker
14446
14732
  // became the sole transport, which frizz can no longer reach that way. Only the broker can be asked to
@@ -14594,6 +14880,11 @@ var init_src = __esm({
14594
14880
  // falls back to rendering the plain `errors` strings.
14595
14881
  errorItems: external_exports.array(BoardErrorItem).optional()
14596
14882
  });
14883
+ BoardState = external_exports.object({
14884
+ board: BoardSnapshot,
14885
+ seq: external_exports.number().int().min(0),
14886
+ bootId: external_exports.string()
14887
+ });
14597
14888
  QuotaWindow = external_exports.object({
14598
14889
  key: external_exports.string(),
14599
14890
  // stable id: "5h" | "weekly" (provider-neutral)
@@ -14609,8 +14900,20 @@ var init_src = __esm({
14609
14900
  planType: external_exports.string().optional(),
14610
14901
  // "pro" / "max" / etc. when the source reports it
14611
14902
  windows: external_exports.array(QuotaWindow),
14612
- detail: external_exports.string().optional()
14903
+ detail: external_exports.string().optional(),
14613
14904
  // why unavailable, or an extra note
14905
+ // Recovery evidence from a LIVE Codex read only. Rollout quota has no account identity or freshness
14906
+ // guarantee and must never wake a failed thread. accountKey is a hash, not a credential/account id.
14907
+ codexRecovery: external_exports.object({
14908
+ observedAt: external_exports.number(),
14909
+ // request start, unix milliseconds (not the later cache/read time)
14910
+ accountKey: external_exports.string().optional(),
14911
+ creditsAvailable: external_exports.boolean().optional(),
14912
+ ordinaryUsageAllowed: external_exports.boolean().nullable().optional(),
14913
+ // null is explicitly unavailable
14914
+ blocked: external_exports.boolean().optional()
14915
+ // explicit workspace/spend-control denial beats apparent headroom
14916
+ }).optional()
14614
14917
  });
14615
14918
  QuotaSnapshot = external_exports.object({
14616
14919
  claude: ProviderQuota,
@@ -14672,6 +14975,16 @@ var init_src = __esm({
14672
14975
  * Machine-level: which chrome you want is a property of the person, not the repo.
14673
14976
  */
14674
14977
  projectRail: external_exports.boolean(),
14978
+ /**
14979
+ * CHILD THREADS, an experiment (2026-11-04). On, a worker may run `frizzctl spawn-child`: a new thread
14980
+ * in this project, on either harness, linked to the thread that spawned it. Its rests that would
14981
+ * have queued for the human wake its parent instead, it can own its own PR watchers, and it sits
14982
+ * under its parent on the board rather than in the queue. Off (the default), the command refuses and
14983
+ * the worker contract never mentions it. A child spawned while it was on keeps its link either way.
14984
+ *
14985
+ * Machine-level, like the rail: whether to try an experiment is the person's call, not the repo's.
14986
+ */
14987
+ childThreads: external_exports.boolean(),
14675
14988
  // There is no `font` key any more. The interface rendered in one of two type families as a machine
14676
14989
  // setting until 2026-09-19 (maintainer: "let's drop monospace as an option"); every surface is sans
14677
14990
  // now, and index.html pins `data-font="sans"` on <html> directly. Settings is a non-strict object,
@@ -14711,6 +15024,19 @@ var init_src = __esm({
14711
15024
  command: external_exports.string().trim().min(1).max(1024),
14712
15025
  args: external_exports.array(external_exports.string().max(1024)).max(32).optional()
14713
15026
  })).max(32).optional(),
15027
+ // A command each runtime's workers are started THROUGH, per runtime: `op run --env-file .env --`
15028
+ // makes a new Claude worker start as `op run --env-file .env -- /path/to/claude …`. Kept as the text
15029
+ // the operator typed and split into words by parseLaunchPrefix (launch-prefix.ts), the one parser the
15030
+ // settings field and the spawn share; it is never run in a shell. Claude applies it at every fork of a
15031
+ // thread's worker; Codex to the app-server the project's Codex threads share, which is replaced at
15032
+ // the next moment no Codex turn is running once the prefix changes. ACP agents need no key here:
15033
+ // their `command` above is already the operator's own. A string rather than a refined argv so a
15034
+ // stored value the parser later stops accepting fails ITS launch, not the whole settings parse
15035
+ // (getSettings falls back to every default on a parse miss). Optional so an old blob parses.
15036
+ launchPrefix: external_exports.object({
15037
+ claude: external_exports.string().max(LAUNCH_PREFIX_MAX_LENGTH).optional(),
15038
+ codex: external_exports.string().max(LAUNCH_PREFIX_MAX_LENGTH).optional()
15039
+ }).optional(),
14714
15040
  // The GitHub batch-dispatch prompt template (the picker's per-item worker prompt). Optional: when
14715
15041
  // unset OR blank the server falls back to its exported DEFAULT_GITHUB_PROMPT. Substitution tokens
14716
15042
  // the server fills: {repo} {n} {title} {url} {labels} {body}. The leading `THREAD: <slug>` tag is
@@ -14805,7 +15131,21 @@ var init_src = __esm({
14805
15131
  // Binds the call to the session the tab is looking at, so a stale page cannot deliver a follow-up
14806
15132
  // into a thread that has since been re-dispatched (merged from origin/main, 2026-07-21).
14807
15133
  sessionId: external_exports.string().min(1),
14808
- message: external_exports.string().min(1),
15134
+ // EMPTY only when `answers` ride: the staged answers alone are a message. The server refuses a send
15135
+ // that carries neither.
15136
+ message: external_exports.string(),
15137
+ // REGISTERED-QUESTION ANSWERS SENT WITH THIS MESSAGE — the prompt box's one Send. The server settles
15138
+ // each question exactly as `answerQuestions` does, then delivers ONE turn: the answers in their wire
15139
+ // form (questionAnswerMessage) and `message` under them as the note (withAnswersNote). They ride the
15140
+ // follow-up rather than the scheduler's answer wake so the pair arrives together, in the order the
15141
+ // human wrote it, and as fast as a steer does.
15142
+ answers: external_exports.array(QuestionAnswerSchema).optional(),
15143
+ // SEND TO THE QUEUE: while the agent is mid-turn, Frizz keeps this message and delivers it when the
15144
+ // thread comes to rest, so it opens the next turn instead of steering this one. The queue goes out ONE
15145
+ // message per turn (2026-10-20), so a message also waits, even at rest, behind one already held or one
15146
+ // whose turn has not ended. With nothing ahead of it, it is an ordinary send. See `held` and
15147
+ // nextHeldRelease in the server's delivery-ledger.ts.
15148
+ hold: external_exports.boolean().optional(),
14809
15149
  // Generated once before the optimistic clear so a transport replay can be idempotent.
14810
15150
  deliveryId: external_exports.string().min(1).max(200).optional(),
14811
15151
  // Retire the worker's live process before delivering, so this message lands in a `claude` that has
@@ -14814,17 +15154,19 @@ var init_src = __esm({
14814
15154
  // newer frizz build in place (hooks are read once, at startup). Everything else that needs a fresh
14815
15155
  // process derives it server-side; see needsFreshProcessForLimit.
14816
15156
  freshProcess: external_exports.boolean().optional(),
14817
- // PREEMPT the operation the worker is running right now, so this message is read at once instead of
14818
- // when that operation finishes. The operator's "Interrupt and send" verb, and opt-in for the same
14819
- // reason `freshProcess` is: it costs the in-flight tool call's result. (It no longer costs the
14820
- // worker's sub-agents: since 2026-09-24 the broker's SDK query declares `perTaskStopAffordance`,
14821
- // so the interrupt aborts only the turn and background agents run on.)
15157
+ // SEND NOW: this message is read at once instead of when the operation the worker is running
15158
+ // finishes. The operator's forced send (⌘⏎), and opt-in because it can cost work in flight. Since
15159
+ // 2026-10-13 it usually does not: the runtime moves a running command or foreground sub-agent to the
15160
+ // background and takes the message into the same turn. Only when it has nothing to move — the model
15161
+ // is mid-answer, a wake Frizz wrote is waiting too — is the turn aborted, as it always was before,
15162
+ // and that costs the in-flight call its result. (An abort has not cost the worker's BACKGROUND
15163
+ // sub-agents since 2026-09-24: the broker's SDK query declares `perTaskStopAffordance`.)
14822
15164
  //
14823
15165
  // It exists because delivery is ALREADY as fast as queueing can be. Measured over 14 days of this
14824
15166
  // project's own transcripts, Claude Code drains its queue at the first sampling boundary that
14825
15167
  // exists; the wait an operator feels is the remaining time of whatever was in flight (a long `Bash`,
14826
15168
  // or one 73–133s reasoning+answer generation), which put mid-turn operator prose at p50 13.8s,
14827
- // p90 49s, p99 2.5m. Preempting is the only lever left.
15169
+ // p90 49s, p99 2.5m. Getting that operation out of the way is the only lever left.
14828
15170
  //
14829
15171
  // Broker-backed Claude only — that is every Claude thread dispatched since the broker cutover. On
14830
15172
  // any other runtime the message is delivered normally and this is ignored, never refused: a send
@@ -14839,16 +15181,38 @@ var init_src = __esm({
14839
15181
  SteerThreadInput = external_exports.object({
14840
15182
  from: ThreadSlug,
14841
15183
  to: ThreadSlug,
15184
+ project: external_exports.string().min(1).max(200).optional(),
14842
15185
  message: external_exports.string().min(1).max(1e5)
14843
15186
  }).strict();
14844
15187
  SteerThreadResult = external_exports.object({
14845
15188
  slug: ThreadSlug,
14846
15189
  title: external_exports.string(),
15190
+ // The target's project URL slug — present only when it is NOT the caller's project, so the receipt's
15191
+ // link can name it (`/project/<project>/thread/<slug>`) and a reply can be addressed back.
15192
+ project: external_exports.string().optional(),
14847
15193
  // What the target was doing when the message arrived: `running` ⇒ it reads it at its next tool
14848
15194
  // boundary; `resting` ⇒ the message started its next turn; `reopened` ⇒ it was marked done and this
14849
15195
  // message put it back on the board.
14850
15196
  delivery: external_exports.enum(["running", "resting", "reopened"])
14851
15197
  }).strict();
15198
+ SpawnThreadInput = DispatchInput.pick({ title: true, prompt: true, model: true, backend: true, effort: true }).extend({
15199
+ from: ThreadSlug,
15200
+ project: external_exports.string().min(1).max(200)
15201
+ }).strict();
15202
+ SpawnThreadResult = external_exports.object({
15203
+ slug: ThreadSlug,
15204
+ sessionId: external_exports.string(),
15205
+ // The new thread's project URL slug — present only when it is NOT the caller's project, as on a steer.
15206
+ project: external_exports.string().optional()
15207
+ }).strict();
15208
+ SpawnChildInput = DispatchInput.pick({ title: true, prompt: true, model: true, backend: true, effort: true }).extend({
15209
+ from: ThreadSlug
15210
+ }).strict();
15211
+ SpawnChildResult = external_exports.union([
15212
+ external_exports.object({ slug: ThreadSlug, sessionId: external_exports.string() }).strict(),
15213
+ // The experiment is off, or the caller is itself a child. Exit 2 at the command, like every refusal.
15214
+ external_exports.object({ refused: external_exports.string() }).strict()
15215
+ ]);
14852
15216
  DismissFailedFollowUpInput = external_exports.object({
14853
15217
  slug: ThreadSlug,
14854
15218
  deliveryId: external_exports.string().min(1).max(200)
@@ -14867,11 +15231,19 @@ var init_src = __esm({
14867
15231
  DeliverQueuedNowInput = external_exports.object({
14868
15232
  slug: ThreadSlug,
14869
15233
  // Same staleness guard as unqueueFollowUp: a stale tab must not preempt a re-dispatched session.
14870
- sessionId: external_exports.string().min(1)
15234
+ sessionId: external_exports.string().min(1),
15235
+ // Hand over what Frizz is HOLDING for the agent's rest ("Send to queue") and stop there — no
15236
+ // interrupt. The "Send now" under a held bubble: the operator changed their mind about waiting, not
15237
+ // about interrupting, so the message goes in as the steer it would have been.
15238
+ releaseOnly: external_exports.boolean().optional()
14871
15239
  }).strict();
14872
15240
  DeliverQueuedNowResult = external_exports.object({
14873
15241
  interrupted: external_exports.boolean(),
14874
- reason: external_exports.string().optional()
15242
+ reason: external_exports.string().optional(),
15243
+ // How many messages Frizz was HOLDING for the agent's rest ("Send to queue") and has now handed over.
15244
+ // Those were never in a provider queue, so on a runtime that cannot be interrupted this is the whole
15245
+ // of what "send now" did — and it is a success the surface must not report as a refusal.
15246
+ released: external_exports.number().int().nonnegative().optional()
14875
15247
  }).strict();
14876
15248
  CompactThreadInput = external_exports.object({
14877
15249
  slug: ThreadSlug,
@@ -14893,6 +15265,17 @@ var init_src = __esm({
14893
15265
  sessionId: external_exports.string().min(1),
14894
15266
  pinned: external_exports.boolean()
14895
15267
  }).strict();
15268
+ ReorderPinnedThreadsInput = external_exports.object({
15269
+ slugs: external_exports.array(ThreadSlug).max(500)
15270
+ }).strict();
15271
+ SetThreadConsolePinnedInput = external_exports.object({
15272
+ slug: ThreadSlug,
15273
+ sessionId: external_exports.string().min(1),
15274
+ pinned: external_exports.boolean()
15275
+ }).strict();
15276
+ ReorderConsolePinnedThreadsInput = external_exports.object({
15277
+ pins: external_exports.array(external_exports.object({ slug: ThreadSlug, at: SnoozeUntil }).strict()).max(500)
15278
+ }).strict();
14896
15279
  SetThreadRecurringPromptInput = external_exports.object({
14897
15280
  slug: ThreadSlug,
14898
15281
  sessionId: external_exports.string().min(1),
@@ -14949,7 +15332,7 @@ var init_src = __esm({
14949
15332
  timers: external_exports.array(ThreadTimerView)
14950
15333
  }).strict();
14951
15334
  ThreadActivityItem = external_exports.object({
14952
- kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue"]),
15335
+ kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "command", "thread"]),
14953
15336
  /** The string a `<kind>:` fence line must carry. For a shell that is the runtime task id the worker
14954
15337
  * was shown; for a PR, `owner/repo#N`; for a timer, its `tmr_…` row id. */
14955
15338
  id: external_exports.string(),
@@ -14978,6 +15361,62 @@ var init_src = __esm({
14978
15361
  * agent to read out the current set of watchers and questions?"). */
14979
15362
  questions: external_exports.array(RegisteredQuestionView).default([])
14980
15363
  }).strict();
15364
+ ThreadBand = external_exports.enum(["rested", "active", "snoozed", "done"]);
15365
+ ThreadListing = external_exports.object({
15366
+ slug: ThreadSlug,
15367
+ /** The title the sidebar shows (displayTitle). */
15368
+ title: external_exports.string(),
15369
+ band: ThreadBand,
15370
+ runtime: RuntimeState,
15371
+ backend: Backend.optional(),
15372
+ model: external_exports.string().optional(),
15373
+ /** The human pinned it to the top of this board's sidebar. */
15374
+ pinned: external_exports.literal(true).optional(),
15375
+ /** The caption of the tool call in flight, while a turn is running ("Running the focused tests"). */
15376
+ doing: external_exports.string().optional(),
15377
+ /** The first line of its newest message — the verdict line of a handoff. Capped. */
15378
+ lastLine: external_exports.string().optional(),
15379
+ /** The signal fence its last rest ended on, while the board still reads it. */
15380
+ fence: external_exports.enum(["done", "awaiting"]).optional(),
15381
+ /** Questions standing at its current rest (hasStandingQuestion's rule), plus a native ask or a
15382
+ * permission prompt the human has not answered. */
15383
+ questions: external_exports.number().int(),
15384
+ /** Its own sub-agents still running (direct children only, as the board counts them). */
15385
+ subAgents: external_exports.number().int(),
15386
+ /** Background shells still running. */
15387
+ shells: external_exports.number().int(),
15388
+ /** CHILD THREADS it started (`frizzctl spawn-child`) that are still open. A child is never a row of its
15389
+ * own here, as on the board (drawnUnderParent), so this is how another thread finds one to steer. */
15390
+ children: external_exports.array(external_exports.object({ slug: ThreadSlug, title: external_exports.string(), runtime: RuntimeState }).strict()).optional(),
15391
+ /** What its armed watches are on: `owner/repo#N` for a PR or issue, the prompt for a timer, the
15392
+ * command for a command watch. */
15393
+ watches: external_exports.array(external_exports.string()),
15394
+ /** Killed by a usage limit, waiting for the window to reset. */
15395
+ limited: external_exports.literal(true).optional(),
15396
+ /** Its process is gone with the turn unfinished. */
15397
+ crashed: external_exports.literal(true).optional(),
15398
+ spawnedAt: external_exports.string().optional(),
15399
+ /** When its own turn last came to rest (ThreadView.lastAssistantAt). */
15400
+ restedAt: external_exports.string().optional(),
15401
+ lastActivityAt: external_exports.string().optional(),
15402
+ /** A wall-clock snooze still in the future. */
15403
+ snoozedUntil: external_exports.string().optional(),
15404
+ archivedAt: external_exports.string().optional()
15405
+ }).strict();
15406
+ ListThreadsInput = external_exports.object({
15407
+ /** Another project's board — its slug, id or registered name — instead of the one the call lands on. */
15408
+ project: external_exports.string().trim().min(1).optional(),
15409
+ /** Include the Done band (archived threads), which on a long-lived board is nearly every thread. */
15410
+ all: external_exports.boolean().optional()
15411
+ }).strict();
15412
+ ListThreadsResult = external_exports.object({
15413
+ /** The board's project: its URL slug, its registry id, and the name the board shows. */
15414
+ project: external_exports.object({ slug: external_exports.string(), id: external_exports.string(), name: external_exports.string() }).strict(),
15415
+ /** Rested, then active, then snoozed, then done; newest activity first within each. */
15416
+ threads: external_exports.array(ThreadListing),
15417
+ /** Every band's size, the Done band's included when its threads are not listed. */
15418
+ counts: external_exports.object({ rested: external_exports.number().int(), active: external_exports.number().int(), snoozed: external_exports.number().int(), done: external_exports.number().int() }).strict()
15419
+ }).strict();
14981
15420
  SetOwnThreadTimerResult = external_exports.object({
14982
15421
  id: external_exports.string(),
14983
15422
  fireAt: external_exports.string(),
@@ -14998,13 +15437,6 @@ var init_src = __esm({
14998
15437
  intervalSeconds: RecurringIntervalSeconds.optional(),
14999
15438
  enabled: external_exports.boolean().optional()
15000
15439
  }).strict();
15001
- ThreadPluginReloadResult = external_exports.object({
15002
- plugins: external_exports.number().int().min(0),
15003
- commands: external_exports.number().int().min(0),
15004
- agents: external_exports.number().int().min(0),
15005
- mcpServers: external_exports.array(external_exports.string()),
15006
- errorCount: external_exports.number().int().min(0)
15007
- }).strict();
15008
15440
  RenameThreadInput = external_exports.object({
15009
15441
  slug: ThreadSlug,
15010
15442
  title: external_exports.string().trim().min(1).max(200)
@@ -15050,6 +15482,42 @@ var init_src = __esm({
15050
15482
  }).strict();
15051
15483
  ThreadSkillsInput = external_exports.object({ slug: ThreadSlug }).strict();
15052
15484
  ThreadSkillsResult = external_exports.object({ skills: external_exports.array(ThreadSkill).max(1024) }).strict();
15485
+ McpTarget = external_exports.union([
15486
+ external_exports.object({ slug: ThreadSlug }).strict(),
15487
+ external_exports.object({ backend: Backend, model: external_exports.string().trim().max(200).optional() }).strict()
15488
+ ]);
15489
+ McpServerState = external_exports.enum(["connected", "needs-auth", "failed", "pending", "disabled"]);
15490
+ McpServerReading = external_exports.object({
15491
+ name: external_exports.string().min(1).max(256),
15492
+ state: McpServerState,
15493
+ tools: external_exports.number().int().nonnegative().optional(),
15494
+ error: external_exports.string().max(2e3).optional(),
15495
+ remote: external_exports.boolean().optional()
15496
+ }).strict();
15497
+ McpStatusResult = external_exports.object({
15498
+ backend: Backend,
15499
+ /** The agent's display name, for the empty state ("Gemini CLI doesn't support …"). */
15500
+ agent: external_exports.string().max(200),
15501
+ supported: external_exports.boolean(),
15502
+ servers: external_exports.array(McpServerReading).max(256),
15503
+ /**
15504
+ * Why the servers can't be read right now (Claude Code isn't signed in on this machine), drawn as the
15505
+ * menu's note. An answer, not an error: every prompt box asks on page load, and a machine without the
15506
+ * agent's login is an ordinary machine, not a failing server.
15507
+ */
15508
+ note: external_exports.string().max(500).optional()
15509
+ }).strict();
15510
+ McpSignInInput = external_exports.object({ target: McpTarget, server: external_exports.string().min(1).max(256) }).strict();
15511
+ McpSignInState = external_exports.enum(["waiting", "done", "failed", "expired", "cancelled"]);
15512
+ McpSignInResult = external_exports.object({
15513
+ attemptId: external_exports.string().min(1).max(64),
15514
+ /** Absent when the agent finished without the human (a refresh token was still good). */
15515
+ authUrl: external_exports.string().url().max(8192).optional(),
15516
+ state: McpSignInState
15517
+ }).strict();
15518
+ McpSignInAttemptInput = external_exports.object({ attemptId: external_exports.string().min(1).max(64) }).strict();
15519
+ McpSignInPollResult = external_exports.object({ state: McpSignInState, error: external_exports.string().max(2e3).optional() }).strict();
15520
+ McpSignInFinishInput = external_exports.object({ attemptId: external_exports.string().min(1).max(64), callbackUrl: external_exports.string().trim().min(1).max(8192) }).strict();
15053
15521
  SetThreadProfileInput = external_exports.object({
15054
15522
  slug: ThreadSlug,
15055
15523
  model: external_exports.string().trim().min(1).max(200),
@@ -15110,6 +15578,21 @@ ${DISPATCH_TASK_BANNER}
15110
15578
  nameWithOwner: external_exports.string().nullable(),
15111
15579
  authed: external_exports.boolean()
15112
15580
  });
15581
+ GithubAccount = external_exports.object({
15582
+ signedIn: external_exports.boolean(),
15583
+ /** "app" — Frizz's GitHub App, signed in here; "token" — a stored token; "env" — FRIZZ_GITHUB_TOKEN. */
15584
+ source: external_exports.enum(["app", "token", "env"]).optional(),
15585
+ login: external_exports.string().nullable(),
15586
+ /** Why a stored sign-in stopped working. */
15587
+ problem: external_exports.string().optional(),
15588
+ /** Whether this build knows Frizz's GitHub App, i.e. whether "Sign in" can be offered at all. */
15589
+ appRegistered: external_exports.boolean(),
15590
+ signIn: external_exports.discriminatedUnion("phase", [
15591
+ external_exports.object({ phase: external_exports.literal("idle") }),
15592
+ external_exports.object({ phase: external_exports.literal("pending"), userCode: external_exports.string(), verificationUri: external_exports.string(), expiresAt: external_exports.number() }),
15593
+ external_exports.object({ phase: external_exports.literal("failed"), message: external_exports.string() })
15594
+ ])
15595
+ });
15113
15596
  GithubRefCard = external_exports.object({
15114
15597
  ref: external_exports.string(),
15115
15598
  kind: external_exports.enum(["issue", "pr", "commit"]),
@@ -15209,6 +15692,14 @@ ${DISPATCH_TASK_BANNER}
15209
15692
  // same pre-restart back-compat reason as on BoardSnapshot.
15210
15693
  errorItems: external_exports.array(BoardErrorItem).optional()
15211
15694
  });
15695
+ BoardDeltaEvent = external_exports.object({
15696
+ type: external_exports.literal("board-delta"),
15697
+ seq: external_exports.number(),
15698
+ bootId: external_exports.string(),
15699
+ upserts: external_exports.array(ThreadView),
15700
+ removed: external_exports.array(ThreadSlug),
15701
+ meta: BoardMeta.optional()
15702
+ });
15212
15703
  ServerEvent = external_exports.discriminatedUnion("type", [
15213
15704
  external_exports.object({
15214
15705
  type: external_exports.literal("board"),
@@ -15220,17 +15711,7 @@ ${DISPATCH_TASK_BANNER}
15220
15711
  seq: external_exports.number().optional(),
15221
15712
  bootId: external_exports.string().optional()
15222
15713
  }),
15223
- external_exports.object({
15224
- // Keyed per-thread delta. `upserts` are COMPLETE ThreadViews for threads whose serialization
15225
- // changed (or are new); `removed` are ids gone from the board; `meta` is present only when a
15226
- // board-level field changed. Emitted only by a post-restart server → seq/bootId are required here.
15227
- type: external_exports.literal("board-delta"),
15228
- seq: external_exports.number(),
15229
- bootId: external_exports.string(),
15230
- upserts: external_exports.array(ThreadView),
15231
- removed: external_exports.array(ThreadSlug),
15232
- meta: BoardMeta.optional()
15233
- }),
15714
+ BoardDeltaEvent,
15234
15715
  external_exports.object({
15235
15716
  type: external_exports.literal("notify"),
15236
15717
  slug: ThreadSlug,
@@ -15247,8 +15728,24 @@ ${DISPATCH_TASK_BANNER}
15247
15728
  interactionId: InteractionOpaqueId,
15248
15729
  lifecycle: InteractionLifecycle,
15249
15730
  recordRevision: InteractionRevision
15731
+ }).strict(),
15732
+ external_exports.object({
15733
+ // The machine's PROJECT LIST changed: a reorder, a rename, an icon, a project added or forgotten.
15734
+ // Payload-free for the same reason as above — the list is one cheap read, and a client that
15735
+ // re-reads it cannot apply a stale copy out of order. It is the one event that is not about this
15736
+ // stream's own project: the server publishes it on EVERY open project's stream, because the rail
15737
+ // and the grid draw the same list in every tab whichever board that tab is on.
15738
+ type: external_exports.literal("projects-changed")
15250
15739
  }).strict()
15251
15740
  ]);
15741
+ BoardDeltasInput = external_exports.object({
15742
+ bootId: external_exports.string().min(1).max(200),
15743
+ seq: external_exports.number().int().min(0)
15744
+ });
15745
+ BoardDeltasResult = external_exports.union([
15746
+ external_exports.object({ bootId: external_exports.string(), deltas: external_exports.array(BoardDeltaEvent) }),
15747
+ external_exports.object({ bootId: external_exports.string(), resync: external_exports.enum(["boot", "expired", "ahead"]) })
15748
+ ]);
15252
15749
  TranscriptEdit = external_exports.object({
15253
15750
  file: external_exports.string(),
15254
15751
  old: external_exports.string(),
@@ -15490,7 +15987,8 @@ ${DISPATCH_TASK_BANNER}
15490
15987
  // the server's from the instant it arrives. "failed": the transport threw, or never answered — the
15491
15988
  // text is kept until the operator retries, edits or dismisses it, and nothing retries it on its own.
15492
15989
  // Additive + optional: an older client renders both as a plain bubble (gray for "sending").
15493
- deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed"]).optional(),
15990
+ // "held": the operator sent it TO THE QUEUE — Frizz has it and no transport does, until the agent rests.
15991
+ deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed", "held"]).optional(),
15494
15992
  // Why a "failed" send failed, verbatim from the transport. Set only alongside deliveryState "failed".
15495
15993
  deliveryError: external_exports.string().optional(),
15496
15994
  // FRIZZ wrote this user turn, not the human: it is a scheduler wake delivery (isWakeDelivery). The
@@ -15615,7 +16113,12 @@ ${DISPATCH_TASK_BANNER}
15615
16113
  external_exports.object({ t: external_exports.literal("sub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict(),
15616
16114
  external_exports.object({ t: external_exports.literal("unsub"), topic: external_exports.literal("file"), path: SocketFilePath }).strict()
15617
16115
  ]);
15618
- SocketClientMsg = external_exports.union([SocketTranscriptClientMsg, SocketFileClientMsg]);
16116
+ SocketBoardClientMsg = external_exports.object({
16117
+ t: external_exports.literal("board"),
16118
+ bootId: external_exports.string().min(1).max(200),
16119
+ seq: external_exports.number().int().min(0)
16120
+ }).strict();
16121
+ SocketClientMsg = external_exports.union([SocketTranscriptClientMsg, SocketFileClientMsg, SocketBoardClientMsg]);
15619
16122
  ProjectCard = external_exports.object({
15620
16123
  id: external_exports.string(),
15621
16124
  slug: external_exports.string(),
@@ -15625,12 +16128,15 @@ ${DISPATCH_TASK_BANNER}
15625
16128
  /** The directory is gone — moved or deleted. The card stays so it can be reopened or forgotten. */
15626
16129
  stale: external_exports.boolean(),
15627
16130
  /**
15628
- * When this project's icon was last established, or absent if it never has been.
16131
+ * A hash of the icon's bytes; for a project with no icon, when it was last scanned. Absent if it
16132
+ * never has been.
15629
16133
  *
15630
16134
  * Carried purely so the client can hang it off the `/_frizz/project-icon` URL: the icon bytes are
15631
16135
  * cached hard (a rail of forty squares is forty requests, and none of them should recur), which
15632
16136
  * means a newly uploaded icon would otherwise stay invisible behind the cached old one. A changed
15633
16137
  * version is a changed URL, so the swap is immediate without weakening the caching for everyone.
16138
+ * It is the HASH, not the scan time, because a rescan restamps the time every ten minutes whether
16139
+ * or not the picture changed, and every restamp was a fresh download of every icon.
15634
16140
  *
15635
16141
  * Deliberately NOT "does this project have an icon". Answering that for a project nobody has
15636
16142
  * scanned yet would mean scanning it, and this list is one file read on purpose.
@@ -15642,8 +16148,9 @@ ${DISPATCH_TASK_BANNER}
15642
16148
  * Three states, not a boolean, because the two "no icon to draw right now" cases must behave
15643
16149
  * differently and a boolean collapses them:
15644
16150
  * · `icon` — one is stored; draw it.
15645
- * · `none` — scanned, nothing found. Draw the monogram and DO NOT request the icon route,
15646
- * which is what stops an iconless project flashing its initials and then swapping.
16151
+ * · `none` — scanned recently, nothing found. Draw the monogram and DO NOT request the icon
16152
+ * route, which is what stops an iconless project flashing its initials and then
16153
+ * swapping. An OLD miss reads `unknown` again, so an icon added since is found.
15647
16154
  * · `unknown` — never scanned. The monogram shows, but the request MUST still go out, because
15648
16155
  * that request is what triggers the (lazy, cached) scan in the first place.
15649
16156
  *
@@ -20120,7 +20627,7 @@ import {
20120
20627
  realpathSync as realpathSync2,
20121
20628
  renameSync,
20122
20629
  rmSync as rmSync3,
20123
- statSync as statSync3,
20630
+ statSync as statSync4,
20124
20631
  writeFileSync as writeFileSync5
20125
20632
  } from "node:fs";
20126
20633
  import { homedir as homedir7 } from "node:os";
@@ -20171,7 +20678,7 @@ function syncDirectory(path) {
20171
20678
  }
20172
20679
  function lockOwnerPath(lockPath) {
20173
20680
  try {
20174
- return statSync3(lockPath).isDirectory() ? join8(lockPath, GLOBAL_LOCK_OWNER) : lockPath;
20681
+ return statSync4(lockPath).isDirectory() ? join8(lockPath, GLOBAL_LOCK_OWNER) : lockPath;
20175
20682
  } catch {
20176
20683
  return lockPath;
20177
20684
  }
@@ -20202,10 +20709,10 @@ function observeLock(lockPath, adapter) {
20202
20709
  }
20203
20710
  try {
20204
20711
  const ownerPath = lockOwnerPath(lockPath);
20205
- let modified = statSync3(lockPath).mtimeMs;
20712
+ let modified = statSync4(lockPath).mtimeMs;
20206
20713
  if (ownerPath !== lockPath) {
20207
20714
  try {
20208
- modified = Math.max(modified, statSync3(ownerPath).mtimeMs);
20715
+ modified = Math.max(modified, statSync4(ownerPath).mtimeMs);
20209
20716
  } catch {
20210
20717
  }
20211
20718
  }
@@ -20536,7 +21043,7 @@ import {
20536
21043
  readFileSync as readFileSync6,
20537
21044
  renameSync as renameSync2,
20538
21045
  rmSync as rmSync4,
20539
- statSync as statSync4,
21046
+ statSync as statSync5,
20540
21047
  writeFileSync as writeFileSync6
20541
21048
  } from "node:fs";
20542
21049
  import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join9 } from "node:path";
@@ -20603,7 +21110,7 @@ function quarantine(path, suffix) {
20603
21110
  }
20604
21111
  function pathAge(path) {
20605
21112
  try {
20606
- return Date.now() - statSync4(path).mtimeMs;
21113
+ return Date.now() - statSync5(path).mtimeMs;
20607
21114
  } catch {
20608
21115
  return void 0;
20609
21116
  }
@@ -21009,7 +21516,7 @@ var init_boot_progress = __esm({
21009
21516
 
21010
21517
  // packages/server/src/project-root.ts
21011
21518
  import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
21012
- import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync8 } from "node:fs";
21519
+ import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
21013
21520
  import { homedir as homedir8 } from "node:os";
21014
21521
  import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
21015
21522
  function projectIdPath(root) {
@@ -21103,17 +21610,6 @@ function isHomeDirectory(dir, home = homedir8()) {
21103
21610
  return false;
21104
21611
  }
21105
21612
  }
21106
- function isWorldWritable(dir, platform = process.platform) {
21107
- if (platform === "win32") return false;
21108
- try {
21109
- return (statSync5(dir).mode & 2) !== 0;
21110
- } catch {
21111
- return false;
21112
- }
21113
- }
21114
- function refuseWorldWritableProject(dir) {
21115
- if (isWorldWritable(dir)) throw new Error(`A folder every account can write to cannot be a project: ${dir}`);
21116
- }
21117
21613
  function existingProjectId(dir) {
21118
21614
  const fromFile = readProjectIdFile(dir);
21119
21615
  if (fromFile) return fromFile;
@@ -21173,7 +21669,7 @@ var init_project_root = __esm({
21173
21669
  });
21174
21670
 
21175
21671
  // packages/server/src/project-registry.ts
21176
- import { closeSync as closeSync4, existsSync as existsSync5, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
21672
+ import { closeSync as closeSync4, existsSync as existsSync5, fsyncSync as fsyncSync4, lstatSync, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, unlinkSync, writeFileSync as writeFileSync9 } from "node:fs";
21177
21673
  import { homedir as homedir9 } from "node:os";
21178
21674
  import { basename as basename4, dirname as dirname9, join as join12 } from "node:path";
21179
21675
  function registryPath(home = homedir9()) {
@@ -21190,10 +21686,11 @@ function readRegistry(home = homedir9()) {
21190
21686
  const parsed = JSON.parse(raw);
21191
21687
  if (parsed?.version !== 1 || !Array.isArray(parsed.projects)) return { ...EMPTY, projects: [] };
21192
21688
  const forgotten = Array.isArray(parsed.forgotten) ? parsed.forgotten.filter((id) => typeof id === "string") : [];
21689
+ const host = canonicalPath(launchHostDir(home));
21193
21690
  return {
21194
21691
  version: 1,
21195
21692
  projects: parsed.projects.filter(
21196
- (p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string"
21693
+ (p) => p && typeof p.id === "string" && typeof p.path === "string" && typeof p.slug === "string" && p.path !== host
21197
21694
  ),
21198
21695
  ...forgotten.length > 0 ? { forgotten } : {}
21199
21696
  };
@@ -21227,6 +21724,12 @@ function writeRegistry(registry, home = homedir9()) {
21227
21724
  }
21228
21725
  throw error;
21229
21726
  }
21727
+ for (const listener of writeListeners) {
21728
+ try {
21729
+ listener(path);
21730
+ } catch {
21731
+ }
21732
+ }
21230
21733
  }
21231
21734
  function slugifyProject(value) {
21232
21735
  const s = slugify(value);
@@ -21297,20 +21800,11 @@ function registerProject(input, home = homedir9()) {
21297
21800
  writeRegistry(registry, home);
21298
21801
  return { entry, action: "created" };
21299
21802
  }
21300
- function listProjects(home = homedir9()) {
21301
- const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
21302
- return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync5(p.path) })).sort((a, b) => {
21303
- if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
21304
- if (a.order === void 0) return 1;
21305
- if (b.order === void 0) return -1;
21306
- return a.order - b.order;
21307
- });
21308
- }
21309
21803
  function findByPath(path, home = homedir9()) {
21310
21804
  const canonical = canonicalPath(path);
21311
21805
  return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
21312
21806
  }
21313
- var EMPTY, GENERIC, RESERVED;
21807
+ var EMPTY, GENERIC, RESERVED, writeListeners, ICON_RESCAN_AFTER_MS;
21314
21808
  var init_project_registry = __esm({
21315
21809
  "packages/server/src/project-registry.ts"() {
21316
21810
  "use strict";
@@ -21342,6 +21836,7 @@ var init_project_registry = __esm({
21342
21836
  "term",
21343
21837
  "attach",
21344
21838
  "local-image",
21839
+ "local-file",
21345
21840
  "local-visualization",
21346
21841
  // `control` answers `/control/stop` and `/control/status` — the supervisor channel the board polls
21347
21842
  // every few seconds. It was missing from this list, so a directory called `control` could have taken
@@ -21356,15 +21851,55 @@ var init_project_registry = __esm({
21356
21851
  "api",
21357
21852
  "health"
21358
21853
  ]);
21854
+ writeListeners = /* @__PURE__ */ new Set();
21855
+ ICON_RESCAN_AFTER_MS = 10 * 60 * 1e3;
21856
+ }
21857
+ });
21858
+
21859
+ // packages/server/src/launch-routing.ts
21860
+ import { realpathSync as realpathSync5 } from "node:fs";
21861
+ import { homedir as homedir10 } from "node:os";
21862
+ function classifyLaunch(dir, home = homedir10()) {
21863
+ const candidate = realpathSync5(discoverProjectRoot(dir, home));
21864
+ const refused = isHomeDirectory(candidate, home);
21865
+ if (!refused && (isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0)) {
21866
+ return { kind: "open", root: candidate };
21867
+ }
21868
+ return !refused && hasProjectMarker(candidate) ? { kind: "offer", directory: candidate } : { kind: "grid" };
21869
+ }
21870
+ function offerPath(directory) {
21871
+ return `/?add=${encodeURIComponent(directory)}`;
21872
+ }
21873
+ function parseLaunchRoute(body) {
21874
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
21875
+ const { protocol, path, project, notice } = body;
21876
+ if (protocol !== LAUNCH_ROUTE_PROTOCOL || typeof path !== "string") return void 0;
21877
+ if (!path.startsWith("/") || path.startsWith("//") || path.includes("\\") || /[\u0000-\u001f]/u.test(path)) return void 0;
21878
+ const named = project && typeof project === "object" ? project : void 0;
21879
+ return {
21880
+ path,
21881
+ ...named && typeof named.name === "string" && typeof named.dir === "string" ? { project: { name: named.name, dir: named.dir } } : {},
21882
+ ...typeof notice === "string" && notice.length > 0 ? { notice: notice.slice(0, 500) } : {}
21883
+ };
21884
+ }
21885
+ var LAUNCH_ROUTE_PROTOCOL, LAUNCH_ROUTE_PATH, LAUNCH_TOKEN_HEADER;
21886
+ var init_launch_routing = __esm({
21887
+ "packages/server/src/launch-routing.ts"() {
21888
+ "use strict";
21889
+ init_project_registry();
21890
+ init_project_root();
21891
+ LAUNCH_ROUTE_PROTOCOL = 1;
21892
+ LAUNCH_ROUTE_PATH = "/control/launch";
21893
+ LAUNCH_TOKEN_HEADER = "x-frizz-launch-token";
21359
21894
  }
21360
21895
  });
21361
21896
 
21362
21897
  // src/production.ts
21363
21898
  init_local_origin();
21364
- import { readFileSync as readFileSync14 } from "node:fs";
21365
- import { homedir as homedir11 } from "node:os";
21899
+ import { readdirSync as readdirSync4, readFileSync as readFileSync14 } from "node:fs";
21900
+ import { homedir as homedir12 } from "node:os";
21366
21901
  import { fileURLToPath as fileURLToPath2 } from "node:url";
21367
- import { join as join17 } from "node:path";
21902
+ import { dirname as dirname14, join as join17 } from "node:path";
21368
21903
 
21369
21904
  // src/browser.ts
21370
21905
  import { spawn, execFile } from "node:child_process";
@@ -23879,7 +24414,7 @@ async function runDeviceFlow(options2) {
23879
24414
  const clientId = options2.clientId === void 0 ? githubClientId() : options2.clientId;
23880
24415
  if (!clientId) {
23881
24416
  throw new GithubSignInError(
23882
- "custom frizz.sh names are not available in this version of Frizz yet: its GitHub sign-in is not set up. A private frizz.sh name needs no account and works today.",
24417
+ "custom frizz.sh subdomains are not available in this version of Frizz yet: its GitHub sign-in is not set up. A private frizz.sh subdomain needs no account and works today.",
23883
24418
  "not-configured"
23884
24419
  );
23885
24420
  }
@@ -24417,7 +24952,7 @@ var CUSTOM_NAMES_OFFERED = false;
24417
24952
  function customNamesOffered(env = process.env) {
24418
24953
  return CUSTOM_NAMES_OFFERED || env.FRIZZ_CUSTOM_NAMES === "1";
24419
24954
  }
24420
- var CUSTOM_NAMES_PAUSED = "custom frizz.sh names are paused for now \u2014 press enter for a private name (no account needed), or answer with the hostname of a tunnel you run";
24955
+ var CUSTOM_NAMES_PAUSED = "custom frizz.sh subdomains are paused for now \u2014 press enter for a private subdomain (no account needed), or answer with the hostname of a tunnel you run";
24421
24956
  function isClaimedConfig(config) {
24422
24957
  return typeof config.claim === "string" && config.claim.length > 0;
24423
24958
  }
@@ -24437,7 +24972,7 @@ async function reconcileCloudConfig(config, port, home = homedir5(), onNotice, o
24437
24972
  if (!label) return config;
24438
24973
  if (!isAnonymousClaimName(label) && !customNamesOffered()) {
24439
24974
  throw new Error(
24440
- `${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh names are paused for now \u2014 press R to pick a private name or another setup`
24975
+ `${config.hostname} needs moving onto the Frizz relay, which means claiming it again, and custom frizz.sh subdomains are paused for now \u2014 press R to pick a private subdomain or another setup`
24441
24976
  );
24442
24977
  }
24443
24978
  onNotice?.(
@@ -24519,7 +25054,7 @@ async function establishCloudConfig(answer, port, home = homedir5(), origin, git
24519
25054
  }
24520
25055
  if (!REGISTRAR_IS_LIVE && !origin && !process.env.FRIZZ_REGISTRAR) {
24521
25056
  throw new Error(
24522
- "claiming a name on frizz.sh is not available yet.\n Reach this board through a tunnel of your own instead \u2014 see docs/remote-access.md \u2014 and\n answer with its hostname rather than a bare name."
25057
+ "claiming a frizz.sh subdomain is not available yet.\n Reach this board through a tunnel of your own instead \u2014 see docs/remote-access.md \u2014 and\n answer with its hostname rather than a bare name."
24523
25058
  );
24524
25059
  }
24525
25060
  const anonymous = answer === "" || isAnonymousClaimName(answer);
@@ -24535,7 +25070,7 @@ async function establishCloudConfig(answer, port, home = homedir5(), origin, git
24535
25070
  token = authorization.token;
24536
25071
  if (authorization.login) console.log(` claiming ${name}.frizz.sh for GitHub user ${authorization.login}`);
24537
25072
  } else {
24538
- console.log(" claiming a private name on frizz.sh \u2014 no account needed");
25073
+ console.log(" claiming a private frizz.sh subdomain \u2014 no account needed");
24539
25074
  }
24540
25075
  let result;
24541
25076
  try {
@@ -24735,8 +25270,8 @@ async function probeTailscale() {
24735
25270
  // src/remote-pane.ts
24736
25271
  init_src();
24737
25272
  var CHOICES = [
24738
- { kind: "private", title: "Private name", blurb: "an unguessable name on frizz.sh \u2014 no account, nothing to install" },
24739
- { kind: "frizz", title: "Custom name", blurb: "<name>.frizz.sh of your choosing; needs a GitHub account" },
25273
+ { kind: "private", title: "frizz.sh subdomain", blurb: "a private, unguessable address; no account, nothing to install" },
25274
+ { kind: "frizz", title: "Custom subdomain", blurb: "<name>.frizz.sh of your choosing; needs a GitHub account" },
24740
25275
  { kind: "cloudflare", title: "Cloudflare Tunnel", blurb: "a domain you own on Cloudflare; cloudflared on this machine" },
24741
25276
  { kind: "tailscale", title: "Tailscale", blurb: "your tailnet; tailscale serve does the TLS" },
24742
25277
  { kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
@@ -24746,7 +25281,7 @@ function choicesFor(current) {
24746
25281
  if (customNamesOffered()) return CHOICES;
24747
25282
  if (kindOf(current) === "frizz") {
24748
25283
  return CHOICES.map(
24749
- (choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom names are paused` } : choice
25284
+ (choice) => choice.kind === "frizz" ? { ...choice, blurb: `${current.hostname} \u2014 kept and renewed; new custom subdomains are paused` } : choice
24750
25285
  );
24751
25286
  }
24752
25287
  return CHOICES.filter((choice) => choice.kind !== "frizz");
@@ -24807,7 +25342,7 @@ function createRemotePane(options2) {
24807
25342
  "",
24808
25343
  ...choices().map((choice, index) => {
24809
25344
  const marker = index === s.index ? "\u276F" : " ";
24810
- const title = choice.title.padEnd(18);
25345
+ const title = choice.title.padEnd(20);
24811
25346
  const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
24812
25347
  return `${marker} ${title}${DIM}${choice.blurb}${RESET2}${now}`;
24813
25348
  }),
@@ -24862,10 +25397,10 @@ function createRemotePane(options2) {
24862
25397
  const formHead = (kind) => {
24863
25398
  if (kind === "frizz") {
24864
25399
  return [
24865
- "Custom frizz.sh name",
25400
+ "Custom frizz.sh subdomain",
24866
25401
  "",
24867
25402
  ...wrap(
24868
- "Claims <name>.frizz.sh of your choosing, tied to your GitHub account so names cannot be squatted. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released. (A private name needs none of this.)"
25403
+ "Claims <name>.frizz.sh of your choosing, tied to your GitHub account so names cannot be squatted. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released. (A private subdomain needs none of this.)"
24869
25404
  ),
24870
25405
  ...options2.sandbox ? ["", ...wrap("This is a sandbox, but a claim is real: it binds this machine's one name to your account, and your real board keeps it. Only the setup saved here is thrown away.")] : [],
24871
25406
  "",
@@ -24889,7 +25424,7 @@ function createRemotePane(options2) {
24889
25424
  " cloudflared tunnel route dns my-board board.example.com",
24890
25425
  "",
24891
25426
  ...wrap(
24892
- `Then name them here. Frizz writes ~/.cloudflared/frizz.yml (ingress to http://127.0.0.1:${options2.port}) and runs the tunnel beside the board, so the two share a lifetime.`
25427
+ `Point it at this board in ~/.cloudflared/frizz.yml (ingress to http://127.0.0.1:${options2.port}; https://frizz.sh/docs/cloudflare-tunnel has the file), then name them here. Frizz runs the tunnel beside the board, so the two share a lifetime.`
24893
25428
  )
24894
25429
  ];
24895
25430
  }
@@ -24999,7 +25534,7 @@ function createRemotePane(options2) {
24999
25534
  };
25000
25535
  const claimPrivate = async () => {
25001
25536
  const back = { name: "menu", index: 0 };
25002
- screen = { name: "busy", message: "claiming a private name on frizz.sh\u2026" };
25537
+ screen = { name: "busy", message: "claiming a private frizz.sh subdomain\u2026" };
25003
25538
  paint();
25004
25539
  try {
25005
25540
  const next = await options2.claim("", { signal: new AbortController().signal, onDeviceCode: () => {
@@ -25459,20 +25994,20 @@ init_frizz_paths();
25459
25994
  import { execFileSync as execFileSync3 } from "node:child_process";
25460
25995
  import { createServer as createServer2 } from "node:net";
25461
25996
  import { randomUUID as randomUUID5 } from "node:crypto";
25462
- import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as realpathSync5, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
25463
- import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
25997
+ import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as realpathSync6, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
25998
+ import { homedir as homedir11, tmpdir as tmpdir2 } from "node:os";
25464
25999
  import { basename as basename5, dirname as dirname10, join as join13, resolve as resolve4 } from "node:path";
25465
26000
  import { setTimeout as delay2 } from "node:timers/promises";
25466
26001
  init_project_root();
26002
+ init_launch_routing();
25467
26003
  init_logging();
25468
26004
  init_src();
25469
- init_project_registry();
25470
26005
  var PORT_SCAN_COUNT = 100;
25471
26006
  var LAUNCH_TIMEOUT_MS = BOOT_STALL_TIMEOUT_MS;
25472
26007
  var LAUNCH_HARD_TIMEOUT_MS = BOOT_HARD_TIMEOUT_MS;
25473
26008
  var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
25474
26009
  function retiredFlagMessage(flag) {
25475
- return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a private frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
26010
+ return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a private frizz.sh subdomain, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
25476
26011
  }
25477
26012
  function parseCliArgs(argv) {
25478
26013
  const args2 = new Set(argv);
@@ -25583,7 +26118,7 @@ function boardAddress(url) {
25583
26118
  return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
25584
26119
  }
25585
26120
  var SANDBOX_DROPPED_XDG_ROOTS = ["XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"];
25586
- function prepareSandbox(env = process.env, realHome = homedir10()) {
26121
+ function prepareSandbox(env = process.env, realHome = homedir11()) {
25587
26122
  const home = mkdtempSync(join13(tmpdir2(), "frizz-sandbox-"));
25588
26123
  const inherited = { ...env };
25589
26124
  env.HOME = home;
@@ -25614,9 +26149,13 @@ function shareCredentials(realHome, home, realEnv, sandboxEnv) {
25614
26149
  mkdirSync10(dirname10(realKey), { recursive: true });
25615
26150
  mkdirSync10(dirname10(sandboxKey), { recursive: true });
25616
26151
  link(realKey, sandboxKey);
25617
- if (existsSync6(join13(realHome, ".config", "gh"))) {
25618
- mkdirSync10(join13(home, ".config"), { recursive: true });
25619
- link(join13(realHome, ".config", "gh"), join13(home, ".config", "gh"));
26152
+ const realGithub = join13(frizzPaths({ home: realHome, env: realEnv }).data, "github");
26153
+ if (existsSync6(realGithub)) {
26154
+ const sandboxGithub = join13(frizzPaths({ home, env: sandboxEnv }).data, "github");
26155
+ mkdirSync10(dirname10(sandboxGithub), { recursive: true });
26156
+ link(realGithub, sandboxGithub);
26157
+ const keychain = join13(realHome, "Library", "Keychains", "login.keychain-db");
26158
+ if (process.platform === "darwin" && existsSync6(keychain)) sandboxEnv.FRIZZ_GITHUB_KEYCHAIN = keychain;
25620
26159
  }
25621
26160
  const cloudflared = join13(realHome, ".cloudflared");
25622
26161
  if (existsSync6(cloudflared)) {
@@ -25639,34 +26178,46 @@ function cleanupSandbox(home) {
25639
26178
  } catch {
25640
26179
  }
25641
26180
  }
25642
- function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
25643
- const candidate = realpathSync5(discoverProjectRoot(cwd, home));
25644
- const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
25645
- const refused = isHomeDirectory(candidate, home) || isWorldWritable(candidate);
25646
- if (known && !refused) return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
25647
- const host = mostRecentProject(home, env) ?? hostWorkspace(home);
25648
- return !refused && hasProjectMarker(candidate) ? { kind: "offer", workspace: host, directory: candidate } : { kind: "grid", workspace: host };
26181
+ function resolveLaunchIntent(cwd = process.cwd(), home = homedir11(), env = process.env) {
26182
+ const launch = classifyLaunch(cwd, home);
26183
+ if (launch.kind === "open") return { kind: "open", workspace: resolveWorkspace(cwd, home, env) };
26184
+ const host = hostWorkspace(home);
26185
+ return launch.kind === "offer" ? { kind: "offer", workspace: host, directory: launch.directory } : { kind: "grid", workspace: host };
26186
+ }
26187
+ async function askLaunchRoute(port, ownerToken, dir, options2 = {}) {
26188
+ const fetcher = options2.fetcher ?? fetch;
26189
+ const deadline = Date.now() + (options2.patienceMs ?? 6e3);
26190
+ for (; ; ) {
26191
+ const controller = new AbortController();
26192
+ const timeout = setTimeout(() => controller.abort(), options2.timeoutMs ?? 5e3);
26193
+ timeout.unref?.();
26194
+ try {
26195
+ const response = await fetcher(`http://127.0.0.1:${port}${FRIZZ_ROUTE_PREFIX}${LAUNCH_ROUTE_PATH}`, {
26196
+ method: "POST",
26197
+ headers: { "content-type": "application/json", [LAUNCH_TOKEN_HEADER]: ownerToken },
26198
+ body: JSON.stringify({ protocol: LAUNCH_ROUTE_PROTOCOL, dir, ...options2.args?.length ? { args: options2.args } : {} }),
26199
+ signal: controller.signal
26200
+ });
26201
+ if (response.status === 200) return parseLaunchRoute(await response.json());
26202
+ if (response.status !== 503 || Date.now() >= deadline) return void 0;
26203
+ } catch {
26204
+ return void 0;
26205
+ } finally {
26206
+ clearTimeout(timeout);
26207
+ }
26208
+ await delay2(250);
26209
+ }
25649
26210
  }
25650
- function hostWorkspace(home = homedir10()) {
26211
+ function hostWorkspace(home = homedir11()) {
25651
26212
  const dir = launchHostDir(home);
25652
26213
  mkdirSync10(dir, { recursive: true });
25653
- const root = realpathSync5(dir);
26214
+ const root = realpathSync6(dir);
25654
26215
  const id = ensureProjectIdFile(root, home);
25655
26216
  const stateDir = projectStateDir(id, home);
25656
26217
  mkdirSync10(stateDir, { recursive: true });
25657
26218
  return { root, id, stateDir, name: basename5(root), identityScope: "repository" };
25658
26219
  }
25659
- function mostRecentProject(home, env) {
25660
- for (const entry of listProjects(home)) {
25661
- if (entry.stale || isHomeDirectory(entry.path, home) || isWorldWritable(entry.path)) continue;
25662
- try {
25663
- return resolveWorkspace(entry.path, home, env);
25664
- } catch {
25665
- }
25666
- }
25667
- return void 0;
25668
- }
25669
- function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process.env) {
26220
+ function resolveWorkspace(cwd = process.cwd(), home = homedir11(), env = process.env) {
25670
26221
  let gitRoot;
25671
26222
  try {
25672
26223
  gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
@@ -25680,8 +26231,7 @@ function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process
25680
26231
  if (!isNotAGitWorktree(error)) throw new Error("unable to resolve Git repository root");
25681
26232
  gitRoot = void 0;
25682
26233
  }
25683
- const root0 = realpathSync5(gitRoot ?? discoverProjectRoot(cwd, home));
25684
- refuseWorldWritableProject(root0);
26234
+ const root0 = realpathSync6(gitRoot ?? discoverProjectRoot(cwd, home));
25685
26235
  const identity = gitRoot ? resolveGitProjectIdentity(root0, home) : void 0;
25686
26236
  const root = identity?.root ?? root0;
25687
26237
  const id = ensureProjectIdFile(root, home, identity?.id);
@@ -25712,7 +26262,7 @@ function workspaceLaunchTarget(workspace2) {
25712
26262
  function workspaceFromLaunchTarget(target2, env = process.env) {
25713
26263
  let root;
25714
26264
  try {
25715
- root = realpathSync5(target2.projectDir);
26265
+ root = realpathSync6(target2.projectDir);
25716
26266
  } catch {
25717
26267
  throw new Error("pinned Frizz workspace is no longer available");
25718
26268
  }
@@ -26483,9 +27033,25 @@ var Supervisor = class {
26483
27033
  if (this.restartCompletion === completion) this.restartCompletion = null;
26484
27034
  }
26485
27035
  }
27036
+ /**
27037
+ * A restart or an update drains the running child and forks its replacement in the project the
27038
+ * server is hosted on. Once that folder is deleted the fork fails with ENOENT, and a child given
27039
+ * any other cwd refuses to boot without its project — and so does the one that would put the old
27040
+ * child back, so the click meant to refresh the board stops it. The running child is unharmed by
27041
+ * the missing folder, so refuse before draining and leave it serving.
27042
+ *
27043
+ * Measured 2026-10-11 on a board hosted on a folder deleted a day later: its Update failed one
27044
+ * step earlier, on npm dying with `uv_cwd ENOENT`, and that accident was all that kept it up.
27045
+ */
27046
+ missingLaunchFolder() {
27047
+ if (existsSync7(this.cwd)) return void 0;
27048
+ return `Frizz is running on ${this.cwd}, which no longer exists, so a new server cannot start there. Stop Frizz and start it again.`;
27049
+ }
26486
27050
  /** Called only by the public supervisor control endpoint, never by ordinary document reloads. */
26487
27051
  restartFromBrowser() {
26488
27052
  if (this.browserRestart) return this.browserRestart;
27053
+ const missing = this.missingLaunchFolder();
27054
+ if (missing) return Promise.resolve({ state: "failed", message: missing });
26489
27055
  const previousBoot = this.boot?.bootId;
26490
27056
  const work = (async () => {
26491
27057
  this.lastRestartFailure = void 0;
@@ -26511,6 +27077,8 @@ var Supervisor = class {
26511
27077
  }
26512
27078
  async updateFromBrowser() {
26513
27079
  if (!this.updateRestart) return { state: "failed", message: "Update & Restart is unavailable in this launcher mode" };
27080
+ const missing = this.missingLaunchFolder();
27081
+ if (missing) return { state: "failed", message: missing };
26514
27082
  if (this.updateMode === "child") return this.updateChildFromBrowser();
26515
27083
  this.emitActivity("updating", "preparing the update \u2014 the running Frizz is untouched until it is ready");
26516
27084
  const candidate = await this.updateRestart();
@@ -27172,7 +27740,7 @@ function createNpmRegistryReleaseAdapter(options2 = {}) {
27172
27740
  return settle({ code: null, signal: null, stdout: "", stderr: error instanceof Error ? error.message : String(error) });
27173
27741
  }
27174
27742
  const started = Date.now();
27175
- const child = spawn3(npm.command, [...npm.prefixArgs, "exec", "--yes", `--package=${packageSpec}`, "--", bin, ...args2], {
27743
+ const child = spawn3(npm.command, [...npm.prefixArgs, "exec", "--yes", "--min-release-age=0", `--package=${packageSpec}`, "--", bin, ...args2], {
27176
27744
  cwd,
27177
27745
  env,
27178
27746
  stdio: ["ignore", "pipe", "pipe"],
@@ -27220,12 +27788,13 @@ import {
27220
27788
  mkdirSync as mkdirSync12,
27221
27789
  openSync as openSync6,
27222
27790
  readFileSync as readFileSync12,
27223
- realpathSync as realpathSync6,
27791
+ realpathSync as realpathSync7,
27224
27792
  renameSync as renameSync6,
27225
27793
  rmSync as rmSync9,
27226
27794
  statSync as statSync6,
27227
27795
  writeFileSync as writeFileSync11
27228
27796
  } from "node:fs";
27797
+ import { tmpdir as tmpdir3 } from "node:os";
27229
27798
  import { delimiter as delimiter2, dirname as dirname13, isAbsolute as isAbsolute6, join as join15, relative as relative3, resolve as resolve6, sep as sep4 } from "node:path";
27230
27799
  var SERVER_PROTOCOL = 1;
27231
27800
  var SERVER_DATA_EPOCH = 1;
@@ -27262,9 +27831,18 @@ function serverReleaseSpec(manifest, packageOverride) {
27262
27831
  assertSpec(spec);
27263
27832
  return spec;
27264
27833
  }
27834
+ function serverReleaseMinimum(manifest) {
27835
+ const metadata = record(record(manifest).frizzServer);
27836
+ const minimum = metadata.minimum;
27837
+ if (minimum === void 0) return void 0;
27838
+ if (typeof minimum !== "string" || !VERSION.test(minimum)) throw new Error("invalid minimum Frizz server version");
27839
+ const order = typeof metadata.version === "string" ? compareReleaseVersions(metadata.version, minimum) : null;
27840
+ if (order === null || order < 0) throw new Error("the pinned Frizz server is older than the minimum this launcher declares");
27841
+ return minimum;
27842
+ }
27265
27843
  function containedFile(root, name, directory = false) {
27266
- const path = realpathSync6(join15(root, name));
27267
- const rel = relative3(realpathSync6(root), path);
27844
+ const path = realpathSync7(join15(root, name));
27845
+ const rel = relative3(realpathSync7(root), path);
27268
27846
  if (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute6(rel))
27269
27847
  throw new Error(`Frizz server artifact escapes its package: ${name}`);
27270
27848
  const stat = statSync6(path);
@@ -27286,7 +27864,7 @@ function validateServerGeneration(root, spec, id, compatibility = CURRENT_COMPAT
27286
27864
  containedFile(root, "web-dist/index.html");
27287
27865
  containedFile(root, "runtime/board/index.mjs");
27288
27866
  containedFile(root, "runtime/cc-worker/.claude-plugin/plugin.json");
27289
- return { ...spec, id, root: realpathSync6(root), entry };
27867
+ return { ...spec, id, root: realpathSync7(root), entry };
27290
27868
  }
27291
27869
  function serverGenerationLaunch(generation) {
27292
27870
  return {
@@ -27305,7 +27883,7 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
27305
27883
  for (const directory of [dirname13(executable), ...(env.PATH ?? env.Path ?? "").split(delimiter2).filter(Boolean)]) {
27306
27884
  candidates.push(join15(directory, "node_modules/npm/bin/npm-cli.js"), join15(directory, "../lib/node_modules/npm/bin/npm-cli.js"));
27307
27885
  try {
27308
- const path = realpathSync6(join15(directory, "npm"));
27886
+ const path = realpathSync7(join15(directory, "npm"));
27309
27887
  if (path.endsWith("npm-cli.js")) candidates.push(path);
27310
27888
  } catch {
27311
27889
  }
@@ -27314,7 +27892,15 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
27314
27892
  if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
27315
27893
  return resolve6(found);
27316
27894
  }
27317
- function npmServerPackageInstaller(env = process.env) {
27895
+ var NPM_OPERATION_TIMEOUT_MS = 18e4;
27896
+ function npmFailureSummary(stderr) {
27897
+ const lines = stderr.trim().split("\n");
27898
+ const tail = lines.slice(-8);
27899
+ const cause = lines.find((line) => /^(?:[A-Z]\w*)?Error(?: \[\w+\])?: /u.test(line));
27900
+ return (cause && !tail.includes(cause) ? [cause, ...tail] : tail).join("\n");
27901
+ }
27902
+ function npmServerPackageInstaller(env = process.env, options2 = {}) {
27903
+ const timeoutMs = options2.timeoutMs ?? NPM_OPERATION_TIMEOUT_MS;
27318
27904
  const run2 = (args2, cwd) => new Promise((resolveOutput, reject) => {
27319
27905
  let cli;
27320
27906
  try {
@@ -27328,14 +27914,17 @@ function npmServerPackageInstaller(env = process.env) {
27328
27914
  cwd,
27329
27915
  encoding: "utf8",
27330
27916
  windowsHide: true,
27331
- timeout: 18e4,
27917
+ timeout: timeoutMs,
27332
27918
  killSignal: "SIGKILL",
27333
27919
  maxBuffer: 4 * 1024 * 1024
27334
27920
  }, (error, stdout, stderr) => {
27335
- if (error) {
27336
- const tail = stderr.trim().split("\n").slice(-8).join("\n");
27337
- reject(new Error(`Frizz server npm operation failed: ${tail || error.message}`));
27338
- } else resolveOutput(stdout);
27921
+ if (!error) {
27922
+ resolveOutput(stdout);
27923
+ return;
27924
+ }
27925
+ const limit = timeoutMs >= 6e4 ? `${Math.ceil(timeoutMs / 6e4)}m` : `${Math.ceil(timeoutMs / 1e3)}s`;
27926
+ const why = error.killed ? `npm ${args2[0]} did not finish within ${limit} and was stopped` : npmFailureSummary(stderr) || error.message;
27927
+ reject(new Error(`Frizz server npm operation failed: ${why}`));
27339
27928
  });
27340
27929
  });
27341
27930
  return {
@@ -27347,6 +27936,7 @@ function npmServerPackageInstaller(env = process.env) {
27347
27936
  prefix,
27348
27937
  "--global=false",
27349
27938
  "--ignore-scripts",
27939
+ "--min-release-age=0",
27350
27940
  "--no-audit",
27351
27941
  "--no-fund",
27352
27942
  "--engine-strict",
@@ -27359,7 +27949,7 @@ function npmServerPackageInstaller(env = process.env) {
27359
27949
  },
27360
27950
  async latestVersion(packageName) {
27361
27951
  if (!PACKAGE.test(packageName)) throw new Error("invalid Frizz server package name");
27362
- const parsed = JSON.parse(await run2(["view", `${packageName}@latest`, "version", "--json"]));
27952
+ const parsed = JSON.parse(await run2(["view", `${packageName}@latest`, "version", "--json"], tmpdir3()));
27363
27953
  const version = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
27364
27954
  if (typeof version !== "string" || !VERSION.test(version)) throw new Error("npm returned an invalid Frizz server version");
27365
27955
  return version;
@@ -27393,11 +27983,12 @@ function atomicJson2(path, value) {
27393
27983
  }
27394
27984
  }
27395
27985
  var ServerReleaseStore = class {
27396
- constructor(spec, installer, roots = frizzPaths(), compatibility = CURRENT_COMPATIBILITY) {
27986
+ constructor(spec, installer, roots = frizzPaths(), compatibility = CURRENT_COMPATIBILITY, minimum) {
27397
27987
  this.spec = spec;
27398
27988
  this.installer = installer;
27399
27989
  this.roots = roots;
27400
27990
  this.compatibility = compatibility;
27991
+ this.minimum = minimum;
27401
27992
  assertSpec(spec, compatibility);
27402
27993
  const key = createHash4("sha256").update(spec.package).digest("hex").slice(0, 16);
27403
27994
  this.generations = join15(roots.cache, "server-releases", key);
@@ -27447,6 +28038,22 @@ var ServerReleaseStore = class {
27447
28038
  this.markCompatibilityBeforeLaunch();
27448
28039
  return generation;
27449
28040
  }
28041
+ /**
28042
+ * The selected release, when it is older than this shell works with: `load` will start the pinned
28043
+ * one instead. Undefined for a machine with no selection, an unreadable one (`load` reports that),
28044
+ * and any selection at or above the minimum.
28045
+ */
28046
+ belowMinimum() {
28047
+ if (this.minimum === void 0 || !existsSync9(this.selection)) return void 0;
28048
+ let selected;
28049
+ try {
28050
+ selected = record(readJson(this.selection)).version;
28051
+ } catch {
28052
+ return void 0;
28053
+ }
28054
+ if (typeof selected !== "string" || compareReleaseVersions(selected, this.minimum) !== -1) return void 0;
28055
+ return { selected, minimum: this.minimum, pinned: this.spec.version };
28056
+ }
27450
28057
  async load() {
27451
28058
  this.assertMarkerIsSupported();
27452
28059
  if (!existsSync9(this.selection)) return this.prepareInitial(this.spec.version);
@@ -27460,6 +28067,7 @@ var ServerReleaseStore = class {
27460
28067
  if (spec.dataEpoch < this.compatibility.dataEpoch) return this.prepareInitial(this.spec.version);
27461
28068
  if (spec.dataEpoch > this.compatibility.dataEpoch)
27462
28069
  throw new Error("this Frizz server requires a newer launcher or a data migration; restart with a compatible Frizz release");
28070
+ if (this.belowMinimum()) return this.prepareInitial(this.spec.version);
27463
28071
  const root = this.packageRoot(selected.id);
27464
28072
  const generation = !existsSync9(root) ? await this.prepare(spec.version) : validateServerGeneration(root, spec, selected.id, this.compatibility);
27465
28073
  this.markCompatibilityBeforeLaunch();
@@ -27484,7 +28092,7 @@ var ServerReleaseStore = class {
27484
28092
  }
27485
28093
  }
27486
28094
  commit(generation) {
27487
- if (generation.package !== this.spec.package || generation.root !== realpathSync6(this.packageRoot(generation.id)))
28095
+ if (generation.package !== this.spec.package || generation.root !== realpathSync7(this.packageRoot(generation.id)))
27488
28096
  throw new Error("cannot select a Frizz server outside this release store");
27489
28097
  validateServerGeneration(generation.root, generation, generation.id, this.compatibility);
27490
28098
  const { package: packageName, version, protocol, dataEpoch, id } = generation;
@@ -27496,7 +28104,7 @@ var ServerReleaseStore = class {
27496
28104
  init_frizz_paths();
27497
28105
  init_project_launch();
27498
28106
  import { randomUUID as randomUUID8 } from "node:crypto";
27499
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync7 } from "node:fs";
28107
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync8 } from "node:fs";
27500
28108
  import { join as join16 } from "node:path";
27501
28109
  var STABLE_SERVER_OWNER_NAME = "frizz-server";
27502
28110
  var STABLE_SERVER_OWNER_PROJECT_ID = "c1fd5810-0f8a-4c1d-91a0-6d7445d28e5a";
@@ -27510,7 +28118,7 @@ function stableServerOwnerTarget(roots) {
27510
28118
  mkdirSync13(resolved.data, { recursive: true, mode: 448 });
27511
28119
  return {
27512
28120
  projectId: STABLE_SERVER_OWNER_PROJECT_ID,
27513
- projectDir: realpathSync7(resolved.data),
28121
+ projectDir: realpathSync8(resolved.data),
27514
28122
  stateDir: join16(resolved.state, STABLE_SERVER_OWNER_NAME)
27515
28123
  };
27516
28124
  }
@@ -27587,6 +28195,9 @@ function releaseHeldLease(lease) {
27587
28195
  return true;
27588
28196
  }
27589
28197
 
28198
+ // src/production.ts
28199
+ init_launch_routing();
28200
+
27590
28201
  // src/preflight.ts
27591
28202
  import { spawnSync } from "node:child_process";
27592
28203
  var SUPPORTED_NODE_LINES = [
@@ -27681,8 +28292,9 @@ Options:
27681
28292
  --no-app print the URL without opening a browser
27682
28293
  --port <port> request a fixed port for a new workspace server
27683
28294
  --sandbox a disposable Frizz to try things in: throwaway home and project, its
27684
- own port, deleted when this terminal closes; credentials (gh,
27685
- cloudflared, Claude, Codex, the machine's frizz.sh key) are shared
28295
+ own port, deleted when this terminal closes; credentials (Frizz's
28296
+ GitHub sign-in, cloudflared, Claude, Codex, the machine's frizz.sh
28297
+ key) are shared
27686
28298
  --link print a fresh single-use access link for the running board
27687
28299
  --sessions list the devices signed in to the running board
27688
28300
  --sign-out <id|all> sign one device out, or every one of them
@@ -27693,10 +28305,10 @@ Options:
27693
28305
 
27694
28306
 
27695
28307
  To reach the board from a phone or another machine, press R in the terminal running it: a short
27696
- walkthrough sets up a private frizz.sh name (no account needed), a Cloudflare
27697
- Tunnel, Tailscale, or a proxy of your own, and
27698
- remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
27699
- shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
28308
+ walkthrough sets up a private frizz.sh subdomain (no account needed), a Cloudflare Tunnel,
28309
+ Tailscale, or a proxy of your own, and remembers the choice, so a plain launch serves it from then
28310
+ on. The board stays on loopback and shows a single-use sign-in link as a QR; press L for a fresh
28311
+ one, or run --link from another shell.`
27700
28312
  );
27701
28313
  process.exit(0);
27702
28314
  }
@@ -27713,7 +28325,13 @@ var serverOwner;
27713
28325
  process.on("exit", () => {
27714
28326
  if (serverOwner) releaseStableServerOwner(serverOwner);
27715
28327
  });
27716
- var launchIntent;
28328
+ var launchDir = (() => {
28329
+ try {
28330
+ return process.cwd();
28331
+ } catch {
28332
+ return homedir12();
28333
+ }
28334
+ })();
27717
28335
  var workspace = (() => {
27718
28336
  try {
27719
28337
  if (!reexec) {
@@ -27725,21 +28343,26 @@ var workspace = (() => {
27725
28343
  if (!pinned) throw new Error("registry successor is missing its pinned project identity");
27726
28344
  return workspaceFromLaunchTarget(pinned);
27727
28345
  }
27728
- const intent = resolveLaunchIntent();
27729
- launchIntent = intent;
27730
- return intent.workspace;
28346
+ return hostWorkspace();
27731
28347
  } catch (error) {
27732
28348
  return fail(error);
27733
28349
  }
27734
28350
  })();
27735
- process.chdir(workspace.root);
28351
+ function runningBoardPort() {
28352
+ const machine = readStableServerOwner();
28353
+ if (machine.kind === "running") return machine.port;
28354
+ return liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace))?.port;
28355
+ }
28356
+ function controlPorts() {
28357
+ const machine = readStableServerOwner();
28358
+ return [.../* @__PURE__ */ new Set([...machine.kind === "running" ? [machine.port] : [], DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])];
28359
+ }
27736
28360
  if (options.stop) {
27737
28361
  try {
27738
- const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
27739
- const where = control.host ? workspace.root : `${workspace.root} (the board was launched from ${control.target.projectDir})`;
28362
+ const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace), ports: controlPorts() });
27740
28363
  const result = await stopProjectLaunch({ stateDir: control.stateDir, target: control.target });
27741
28364
  console.log(
27742
- result.kind !== "stopped" ? `Frizz is not running for ${where}` : result.stale ? `Frizz was not running for ${where}; cleared the record a dead board left behind` : `stopped Frizz for ${where}; running agents keep going \u2014 they are detached daemons`
28365
+ result.kind !== "stopped" ? "Frizz is not running" : result.stale ? "Frizz was not running; cleared the record a dead board left behind" : "stopped Frizz; running agents keep going \u2014 they are detached daemons"
27743
28366
  );
27744
28367
  process.exit(0);
27745
28368
  } catch (error) {
@@ -27747,14 +28370,13 @@ if (options.stop) {
27747
28370
  }
27748
28371
  }
27749
28372
  if (options.status) {
27750
- const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace) });
28373
+ const control = await resolveLaunchControlTarget({ stateDir: workspace.stateDir, target: workspaceLaunchTarget(workspace), ports: controlPorts() });
27751
28374
  const running = await runningFrizzStatus({ stateDir: control.stateDir, target: control.target });
27752
28375
  if (!running) {
27753
- console.log(`Frizz is not running for ${workspace.root}`);
28376
+ console.log("Frizz is not running");
27754
28377
  process.exit(1);
27755
28378
  }
27756
28379
  console.log(`running: http://127.0.0.1:${running.port}`);
27757
- console.log(`workspace: ${workspace.root}`);
27758
28380
  if (!control.host) console.log(`launched from: ${control.target.projectDir} (one server serves every project)`);
27759
28381
  console.log(`supervisor pid: ${running.pid}`);
27760
28382
  console.log(`version: ${running.version ?? "unknown"}`);
@@ -27762,23 +28384,23 @@ if (options.status) {
27762
28384
  process.exit(0);
27763
28385
  }
27764
28386
  if (options.sessions || options.signOut !== void 0) {
27765
- const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
27766
- if (!running?.port) {
27767
- console.error("frizz: no board is running for this workspace");
28387
+ const port = runningBoardPort();
28388
+ if (!port) {
28389
+ console.error("frizz: Frizz is not running");
27768
28390
  process.exit(1);
27769
28391
  }
27770
- if (options.signOut !== void 0) await signOutSession(running.port, options.signOut);
27771
- await listSessions(running.port);
28392
+ if (options.signOut !== void 0) await signOutSession(port, options.signOut);
28393
+ await listSessions(port);
27772
28394
  }
27773
28395
  if (options.link) {
27774
- const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
27775
- if (!running?.port) {
27776
- console.error("frizz: no board is running for this workspace");
28396
+ const port = runningBoardPort();
28397
+ if (!port) {
28398
+ console.error("frizz: Frizz is not running");
27777
28399
  process.exit(1);
27778
28400
  }
27779
- const response = await fetch(`http://127.0.0.1:${running.port}${SUPERVISOR_ACCESS_CODE_PATH}`, {
28401
+ const response = await fetch(`http://127.0.0.1:${port}${SUPERVISOR_ACCESS_CODE_PATH}`, {
27780
28402
  method: "POST",
27781
- headers: { origin: `http://127.0.0.1:${running.port}` }
28403
+ headers: { origin: `http://127.0.0.1:${port}` }
27782
28404
  }).catch(() => void 0);
27783
28405
  if (!response?.ok) {
27784
28406
  const detail = response ? (await response.json().catch(() => void 0))?.error : void 0;
@@ -27802,7 +28424,7 @@ readout?.plan([
27802
28424
  { key: "browser", label: options.noApp ? "Address" : "Browser" }
27803
28425
  ]);
27804
28426
  readout?.begin("server", "starting");
27805
- logger.info("launcher", `Frizz launcher ${PACKAGE_VERSION} starting for ${workspace.root}`);
28427
+ logger.info("launcher", `Frizz launcher ${PACKAGE_VERSION} starting in ${launchDir}`);
27806
28428
  var target = workspaceLaunchTarget(workspace);
27807
28429
  var expected = expectedOwnerHealth(target, readProjectLaunchOwner(workspace.stateDir));
27808
28430
  async function existingPort() {
@@ -27811,39 +28433,54 @@ async function existingPort() {
27811
28433
  for (const port of new Set(ports)) if (await probeFrizz(port, expected)) return port;
27812
28434
  return void 0;
27813
28435
  }
27814
- function ownSlug() {
28436
+ var legacyIntentCache;
28437
+ function legacyIntent() {
28438
+ return legacyIntentCache ??= resolveLaunchIntent(launchDir);
28439
+ }
28440
+ function legacyProject() {
28441
+ const intent = legacyIntent();
28442
+ if (intent.kind !== "open") return void 0;
28443
+ const { id, root, name } = intent.workspace;
27815
28444
  try {
27816
- return registerProject(
27817
- { dir: workspace.root, id: target.projectId, remoteOwner: resolveProjectLabel(workspace.root)?.split("/")[0] },
27818
- homedir11()
27819
- ).entry?.slug;
28445
+ const slug = registerProject({ dir: root, id, remoteOwner: resolveProjectLabel(root)?.split("/")[0] }, homedir12()).entry?.slug;
28446
+ return slug ? { id, root, name, slug } : void 0;
27820
28447
  } catch {
27821
28448
  return void 0;
27822
28449
  }
27823
28450
  }
27824
- var cachedSlugPath;
27825
- function slugPath() {
27826
- if (cachedSlugPath === void 0) {
27827
- if (launchIntent?.kind === "grid") cachedSlugPath = "/";
27828
- else if (launchIntent?.kind === "offer") cachedSlugPath = `/?add=${encodeURIComponent(launchIntent.directory)}`;
27829
- else {
27830
- const slug = ownSlug();
27831
- cachedSlugPath = slug ? `/project/${slug}` : "";
27832
- }
28451
+ function legacyLanding() {
28452
+ try {
28453
+ const intent = legacyIntent();
28454
+ if (intent.kind === "grid") return { path: "/" };
28455
+ if (intent.kind === "offer") return { path: offerPath(intent.directory) };
28456
+ const project = legacyProject();
28457
+ return project ? { path: `/project/${project.slug}`, project: { name: project.name, dir: project.root } } : { path: "/" };
28458
+ } catch (error) {
28459
+ return { path: "/", notice: `Could not open ${launchDir}: ${error instanceof Error ? error.message : String(error)}` };
27833
28460
  }
27834
- return cachedSlugPath;
27835
28461
  }
27836
- async function joinRunningFrizz() {
27837
- const slug = ownSlug();
27838
- if (!slug) return void 0;
28462
+ async function resolveLanding(port, ownerToken) {
28463
+ const answered = ownerToken ? await askLaunchRoute(port, ownerToken, launchDir) : void 0;
28464
+ if (answered) return answered;
28465
+ logger.info("launcher", "the running server did not say where this launch lands; using this launcher's own rules");
28466
+ return legacyLanding();
28467
+ }
28468
+ async function joinPreSplitFrizz() {
27839
28469
  for (const port of /* @__PURE__ */ new Set([DEFAULT_PORT, fallbackPort(DEFAULT_PORT)])) {
27840
- if (await probeFrizz(port, { projectId: target.projectId, projectDir: target.projectDir, slug }))
27841
- return { port, slug };
28470
+ if (!await readFrizzHealth(port)) continue;
28471
+ let project;
28472
+ try {
28473
+ project = legacyProject();
28474
+ } catch {
28475
+ return void 0;
28476
+ }
28477
+ if (!project) return void 0;
28478
+ if (await probeFrizz(port, { projectId: project.id, projectDir: project.root, slug: project.slug })) return port;
27842
28479
  }
27843
28480
  return void 0;
27844
28481
  }
27845
- async function openOrPrint(port, reused, path = "") {
27846
- const url = `http://127.0.0.1:${port}${path}`;
28482
+ async function openOrPrint(port, reused, landing) {
28483
+ const url = `http://127.0.0.1:${port}${landing.path}`;
27847
28484
  logger.info("launcher", `${reused ? "reusing" : "started"} Frizz at ${url}`);
27848
28485
  readout?.settle("server", "done", reused ? `already running on port ${port}` : `port ${port}`);
27849
28486
  let browser;
@@ -27870,26 +28507,27 @@ async function openOrPrint(port, reused, path = "") {
27870
28507
  const warnings = [];
27871
28508
  if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
27872
28509
  if (sessionNotice && !reused) warnings.push(sessionNotice);
28510
+ if (landing.notice) warnings.push(landing.notice);
27873
28511
  if (!readout) {
27874
- console.log(`${reused ? "reusing" : "started"} Frizz${launchIntent && launchIntent.kind !== "open" ? "" : ` for ${workspace.root}`}`);
28512
+ console.log(`${reused ? "reusing" : "started"} Frizz${landing.project ? ` for ${landing.project.dir}` : ""}`);
27875
28513
  console.log(url);
27876
28514
  if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
27877
28515
  for (const warning of warnings) console.log(warning);
27878
28516
  return;
27879
28517
  }
27880
- const home = homedir11();
28518
+ const home = homedir12();
27881
28519
  readout.ready(
27882
28520
  [
27883
28521
  { label: "Local", value: boardAddress(url), accent: true },
27884
28522
  ...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
27885
- // Only an `open` launch is ABOUT its workspace; any other is hosted on a project the operator
27886
- // did not pick (launcher.ts resolveLaunchIntent), and naming it here reads as "you opened this".
27887
- ...launchIntent && launchIntent.kind !== "open" ? [] : [{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` }],
28523
+ // Only a launch that OPENED a project names one: the dashboard and an offer are about no
28524
+ // project, and the folder the server runs on is Frizz's own.
28525
+ ...landing.project ? [{ label: "Project", value: `${landing.project.name} \u2014 ${tildePath(landing.project.dir, home)}` }] : [],
27888
28526
  ...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
27889
28527
  ],
27890
28528
  reused ? (
27891
28529
  // This launch owns nothing and exits immediately, so ctrl-c would not stop what it reopened.
27892
- "reopened the server already running for this project \xB7 stop it from the terminal that started it"
28530
+ "reopened the Frizz already running on this machine \xB7 stop it from the terminal that started it"
27893
28531
  ) : options.debug ? void 0 : publicOrigin ? "press L for a fresh sign-in link \xB7 R to change how this board is reached \xB7 ctrl-c to stop" : "press R to reach this board from a phone or another machine \xB7 ctrl-c to stop",
27894
28532
  {
27895
28533
  ...reused ? { status: `already running on port ${port}` } : {},
@@ -27914,32 +28552,49 @@ async function runSupervisor(port, token, onPrepared = () => {
27914
28552
  target,
27915
28553
  owner.token
27916
28554
  );
27917
- const spec = serverReleaseSpec(
27918
- JSON.parse(readFileSync14(join17(import.meta.dirname, "..", "package.json"), "utf8")),
27919
- process.env.FRIZZ_SERVER_PACKAGE
27920
- );
28555
+ const manifest = JSON.parse(readFileSync14(join17(import.meta.dirname, "..", "package.json"), "utf8"));
28556
+ const spec = serverReleaseSpec(manifest, process.env.FRIZZ_SERVER_PACKAGE);
27921
28557
  const installer = npmServerPackageInstaller(env);
27922
- const store = new ServerReleaseStore(spec, installer);
27923
- activityReadout?.notice("progress", "Starting", "loading the selected Frizz server release");
28558
+ const store = new ServerReleaseStore(spec, installer, void 0, void 0, serverReleaseMinimum(manifest));
28559
+ const raise = store.belowMinimum();
28560
+ if (raise)
28561
+ logger.info("launcher", `the installed Frizz server ${raise.selected} is older than this launcher works with (${raise.minimum}); starting on ${raise.pinned}`);
28562
+ activityReadout?.notice(
28563
+ "progress",
28564
+ "Starting",
28565
+ raise ? `installing Frizz server ${raise.pinned}: ${raise.selected} is older than this launcher works with` : "loading the selected Frizz server release"
28566
+ );
27924
28567
  let active = await store.load();
27925
28568
  let pending;
27926
28569
  let previous = active;
27927
28570
  onPrepared();
27928
28571
  let updateAvailable = false;
27929
28572
  let updateVersion;
28573
+ let probeFailure;
27930
28574
  const refreshUpdateAvailable = async () => {
27931
28575
  try {
27932
28576
  const checkedVersion = active.version;
27933
28577
  const plan = await planRegistryUpdate(spec.package, checkedVersion, installer);
28578
+ probeFailure = void 0;
27934
28579
  if (active.version !== checkedVersion) return;
27935
28580
  updateAvailable = plan !== null;
27936
28581
  updateVersion = plan?.latestVersion;
27937
- } catch {
28582
+ } catch (error) {
28583
+ const message = error instanceof Error ? error.message : String(error);
28584
+ if (message !== probeFailure) logger.warn("launcher", `could not check npm for a newer Frizz server: ${message}`);
28585
+ probeFailure = message;
27938
28586
  }
27939
28587
  };
27940
28588
  void refreshUpdateAvailable();
27941
28589
  const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
27942
28590
  updateAvailablePoll.unref();
28591
+ try {
28592
+ const projects = dirname14(workspace.stateDir);
28593
+ const adopted = adoptSessionState(workspace.stateDir, readdirSync4(projects).map((name) => join17(projects, name)));
28594
+ if (adopted) logger.info("launcher", `carried the remote sign-in state over from ${adopted}`);
28595
+ } catch (error) {
28596
+ logger.warn("launcher", `could not carry the remote sign-in state over: ${error instanceof Error ? error.message : String(error)}`);
28597
+ }
27943
28598
  const sessions = loadSessionState(workspace.stateDir);
27944
28599
  const epochRecord = sessionEpochLogLine(sessions.advance, sessions.signedOut);
27945
28600
  if (epochRecord) logger.warn("launcher", epochRecord);
@@ -28043,7 +28698,7 @@ async function runSupervisor(port, token, onPrepared = () => {
28043
28698
  paneHost?.dispose();
28044
28699
  remote?.stop();
28045
28700
  const suffix = logger.file ? `
28046
- log: ${tildePath(logger.file, homedir11())}` : "";
28701
+ log: ${tildePath(logger.file, homedir12())}` : "";
28047
28702
  process.stdout.write(
28048
28703
  code === 0 ? `
28049
28704
  Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
@@ -28080,7 +28735,7 @@ try {
28080
28735
  if (globalClaim.kind === "running") {
28081
28736
  if (options.port && options.port !== globalClaim.port)
28082
28737
  throw new Error(`Frizz is already running on port ${globalClaim.port}; --port cannot change a running server`);
28083
- await openOrPrint(globalClaim.port, true, slugPath());
28738
+ await openOrPrint(globalClaim.port, true, await resolveLanding(globalClaim.port, globalClaim.owner.token));
28084
28739
  process.exit(0);
28085
28740
  }
28086
28741
  serverOwner = globalClaim.lease;
@@ -28092,16 +28747,16 @@ try {
28092
28747
  }
28093
28748
  const existing = await existingPort();
28094
28749
  if (existing) {
28095
- await openOrPrint(existing, true, slugPath());
28750
+ await openOrPrint(existing, true, await resolveLanding(existing, readProjectLaunchOwner(workspace.stateDir)?.token));
28096
28751
  process.exit(0);
28097
28752
  }
28098
- const joined = await joinRunningFrizz();
28099
- if (joined) {
28100
- await openOrPrint(joined.port, true, slugPath());
28753
+ const preSplit = await joinPreSplitFrizz();
28754
+ if (preSplit) {
28755
+ await openOrPrint(preSplit, true, legacyLanding());
28101
28756
  process.exit(0);
28102
28757
  }
28103
28758
  const claim = tryAcquireProjectLaunchOwner(target, "launcher");
28104
- if (claim.kind !== "acquired") throw new Error("Frizz is starting for this project; retry shortly");
28759
+ if (claim.kind !== "acquired") throw new Error("Frizz is starting on this machine; retry shortly");
28105
28760
  let release = await acquireGlobalLaunchLock();
28106
28761
  let portReservation;
28107
28762
  try {
@@ -28127,7 +28782,7 @@ try {
28127
28782
  ]);
28128
28783
  portReservation();
28129
28784
  portReservation = void 0;
28130
- await openOrPrint(port, false, slugPath());
28785
+ await openOrPrint(port, false, await resolveLanding(port, serverOwner.token));
28131
28786
  await running;
28132
28787
  } finally {
28133
28788
  release?.();