frizz 0.13.5 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frizz.js +367 -109
- package/package.json +2 -2
package/dist/frizz.js
CHANGED
|
@@ -12485,6 +12485,106 @@ var init_shell_writes = __esm({
|
|
|
12485
12485
|
});
|
|
12486
12486
|
|
|
12487
12487
|
// packages/shared/src/index.ts
|
|
12488
|
+
function signoffNudgeAwaitingLines(needsInput) {
|
|
12489
|
+
if (!needsInput) {
|
|
12490
|
+
return [
|
|
12491
|
+
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. FRONTMATTER, THEN MARKDOWN:",
|
|
12492
|
+
" one YAML list per kind of thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
|
|
12493
|
+
" as much prose as you want. The prose is OPTIONAL; the lines above it are not.",
|
|
12494
|
+
"",
|
|
12495
|
+
" ```awaiting",
|
|
12496
|
+
" shells: [<the id your runtime gave you>]",
|
|
12497
|
+
" prs: [owner/repo#123]",
|
|
12498
|
+
" for: 2h",
|
|
12499
|
+
" ---",
|
|
12500
|
+
" What you are waiting for, in your own words \u2014 this is what the human reads on your card.",
|
|
12501
|
+
" ```"
|
|
12502
|
+
];
|
|
12503
|
+
}
|
|
12504
|
+
return [
|
|
12505
|
+
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running: a background shell, a",
|
|
12506
|
+
" sub-agent, a timer or a registered PR. FRONTMATTER, THEN MARKDOWN: one YAML list per kind of thing",
|
|
12507
|
+
" you are waiting on, a REQUIRED `for:` duration, a REQUIRED `needs_input:` answer, then optionally a",
|
|
12508
|
+
" `---` line and prose.",
|
|
12509
|
+
"",
|
|
12510
|
+
" ```awaiting",
|
|
12511
|
+
" agents: [<the id your runtime gave you>]",
|
|
12512
|
+
" needs_input: false",
|
|
12513
|
+
" for: 2h",
|
|
12514
|
+
" ```",
|
|
12515
|
+
"",
|
|
12516
|
+
" `needs_input: false` \u2014 nothing for the human yet. The thread stays out of their queue until the work",
|
|
12517
|
+
" wakes you, and you owe NO write-up: the fence alone is the whole message. `needs_input: true` \u2014 the",
|
|
12518
|
+
" human can read, try or act on something NOW while the work runs (a partial result, a file you wrote,",
|
|
12519
|
+
" a server to try), even if you need nothing back from them; the thread goes into their queue, and the",
|
|
12520
|
+
" prose under `---` says what to look at. If you wrote ANY words for the human at this rest, it is",
|
|
12521
|
+
" `true` \u2014 a `false` rest is never put in front of them. A rest on running work with NO fence is a",
|
|
12522
|
+
" bare rest, and it lands in the human's queue."
|
|
12523
|
+
];
|
|
12524
|
+
}
|
|
12525
|
+
function signoffNudgeText(needsInput) {
|
|
12526
|
+
return [
|
|
12527
|
+
`${SIGNOFF_NUDGE_MARKER} Nothing about your task has changed, and no new work is being asked of you.`,
|
|
12528
|
+
"",
|
|
12529
|
+
"You rested without a fence, so this thread cannot be triaged.",
|
|
12530
|
+
"",
|
|
12531
|
+
"**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
|
|
12532
|
+
"original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
|
|
12533
|
+
"is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
|
|
12534
|
+
"neither is naming the next step or writing it into a scratch file.",
|
|
12535
|
+
"",
|
|
12536
|
+
"**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
|
|
12537
|
+
"FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
|
|
12538
|
+
"the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
|
|
12539
|
+
"thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
|
|
12540
|
+
"changes they now have to review. If it should be done, name it in one line and let the human dispatch",
|
|
12541
|
+
"it.",
|
|
12542
|
+
"",
|
|
12543
|
+
"**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
|
|
12544
|
+
"investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
|
|
12545
|
+
"work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
|
|
12546
|
+
"the write-up.",
|
|
12547
|
+
"",
|
|
12548
|
+
"**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
|
|
12549
|
+
"right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
|
|
12550
|
+
"default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
|
|
12551
|
+
"what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
|
|
12552
|
+
"unanswered question is not permission to go build the answer.",
|
|
12553
|
+
"",
|
|
12554
|
+
"Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
|
|
12555
|
+
"",
|
|
12556
|
+
"- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
|
|
12557
|
+
" with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
|
|
12558
|
+
" recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
|
|
12559
|
+
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
12560
|
+
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
12561
|
+
" **bolded verb phrase**.",
|
|
12562
|
+
...signoffNudgeAwaitingLines(needsInput),
|
|
12563
|
+
"",
|
|
12564
|
+
" Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
|
|
12565
|
+
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
12566
|
+
" awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
|
|
12567
|
+
" `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
|
|
12568
|
+
"",
|
|
12569
|
+
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
12570
|
+
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
12571
|
+
"the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
|
|
12572
|
+
"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",
|
|
12573
|
+
"`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
12574
|
+
"None of them is a `done`. And what is not",
|
|
12575
|
+
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
12576
|
+
"in the handoff.",
|
|
12577
|
+
"",
|
|
12578
|
+
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
12579
|
+
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
12580
|
+
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
12581
|
+
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
12582
|
+
"",
|
|
12583
|
+
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
12584
|
+
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
12585
|
+
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
12586
|
+
].join("\n");
|
|
12587
|
+
}
|
|
12488
12588
|
function requireEffortOutsideAcp(profile, ctx) {
|
|
12489
12589
|
if (profile.backend !== "acp" && profile.effort === void 0) {
|
|
12490
12590
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["effort"], message: `effort is required for a ${profile.backend} profile` });
|
|
@@ -12493,7 +12593,7 @@ function requireEffortOutsideAcp(profile, ctx) {
|
|
|
12493
12593
|
function fallbackPort(base) {
|
|
12494
12594
|
return base + 1e4;
|
|
12495
12595
|
}
|
|
12496
|
-
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, 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, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, 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, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, 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;
|
|
12596
|
+
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, 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, 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;
|
|
12497
12597
|
var init_src = __esm({
|
|
12498
12598
|
"packages/shared/src/index.ts"() {
|
|
12499
12599
|
"use strict";
|
|
@@ -12831,7 +12931,7 @@ var init_src = __esm({
|
|
|
12831
12931
|
questions: external_exports.array(AskQuestion)
|
|
12832
12932
|
});
|
|
12833
12933
|
AwaitingHint = external_exports.object({
|
|
12834
|
-
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title"]),
|
|
12934
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "needs_input"]),
|
|
12835
12935
|
value: external_exports.string()
|
|
12836
12936
|
});
|
|
12837
12937
|
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
|
|
@@ -12870,77 +12970,8 @@ var init_src = __esm({
|
|
|
12870
12970
|
TIMER_MAX_DELAY_SECONDS = 30 * 24 * 60 * 60;
|
|
12871
12971
|
TimerPromptText = external_exports.string().trim().min(1).max(TIMER_PROMPT_MAX);
|
|
12872
12972
|
SIGNOFF_NUDGE_MARKER = "**This message is from frizz, not from the human.**";
|
|
12873
|
-
SIGNOFF_NUDGE_MESSAGE =
|
|
12874
|
-
|
|
12875
|
-
"",
|
|
12876
|
-
"You rested without a fence, so this thread cannot be triaged.",
|
|
12877
|
-
"",
|
|
12878
|
-
"**IF THE TASK STILL HAS PARTS LEFT, THE FENCE IS NOT WHAT YOU OWE \u2014 THE WORK IS.** If ANY part of the",
|
|
12879
|
-
"original task is unfinished, unverified, or deferred, resume it NOW, in THIS turn, and sign off once it",
|
|
12880
|
-
"is genuinely finished. A milestone, a green test run and a long turn are none of them endings, and",
|
|
12881
|
-
"neither is naming the next step or writing it into a scratch file.",
|
|
12882
|
-
"",
|
|
12883
|
-
"**AND THAT TASK IS ALSO THE CEILING \u2014 finish it, and nothing else.** Work you notice on the way is a",
|
|
12884
|
-
"FINDING TO REPORT in your sign-off, never work to take on: the bug beside the one you were sent for,",
|
|
12885
|
-
"the refactor the code obviously wants, the second issue the first one touches. Widening the job is not",
|
|
12886
|
-
"thoroughness \u2014 it is a different job nobody asked for, and it buries the answer they did ask for under",
|
|
12887
|
-
"changes they now have to review. If it should be done, name it in one line and let the human dispatch",
|
|
12888
|
-
"it.",
|
|
12889
|
-
"",
|
|
12890
|
-
"**IF WHAT YOU WERE ASKED FOR IS A DOCUMENT, THE DOCUMENT IS THE ENDING.** A triage, a review, an",
|
|
12891
|
-
"investigation, a recommendation, a plan \u2014 when that is the deliverable, the finished write-up IS the",
|
|
12892
|
-
"work. Implementing what it proposes is the NEXT job, and not yours unless you were asked. Sign off with",
|
|
12893
|
-
"the write-up.",
|
|
12894
|
-
"",
|
|
12895
|
-
"**DECIDE RATHER THAN ASK.** Stop only for a decision that is genuinely the human's AND that blocks you",
|
|
12896
|
-
"right now: register that one with `mcp__frizz__ask`. Every other open choice INSIDE the task \u2014 a name, a",
|
|
12897
|
-
"default, a reversible design call \u2014 is yours to make: decide it, say in one line which way you went and",
|
|
12898
|
-
"what would reverse it, and carry on. A choice that would ENLARGE the task is not one of those: an",
|
|
12899
|
-
"unanswered question is not permission to go build the answer.",
|
|
12900
|
-
"",
|
|
12901
|
-
"Otherwise, sign off \u2014 a registration, or a fence at the END of your next message:",
|
|
12902
|
-
"",
|
|
12903
|
-
"- `mcp__frizz__ask` \u2014 you need the human. NOT a fence: the ```question fence is retired, and a fence",
|
|
12904
|
-
" with a question in its body is plain prose. Register it (options with one-line trade-offs, the",
|
|
12905
|
-
" recommended one first), then rest normally \u2014 an open registered question is the sign-off.",
|
|
12906
|
-
"- `` ```done `` \u2014 genuinely FINISHED. A DISMISSAL: the card is filed away and nobody looks again, so",
|
|
12907
|
-
" if anything is still owed, it is not done. Body: 1-3 sentences, then bullets, each opening with a",
|
|
12908
|
-
" **bolded verb phrase**.",
|
|
12909
|
-
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running. FRONTMATTER, THEN MARKDOWN:",
|
|
12910
|
-
" one structural line per thing you are waiting on, a REQUIRED `for:` duration, then a `---` line and",
|
|
12911
|
-
" as much prose as you want. The prose is OPTIONAL; the lines above it are not.",
|
|
12912
|
-
"",
|
|
12913
|
-
" ```awaiting",
|
|
12914
|
-
" shell: <the id your runtime gave you>",
|
|
12915
|
-
" pr: owner/repo#123",
|
|
12916
|
-
" for: 2h",
|
|
12917
|
-
" ---",
|
|
12918
|
-
" What you are waiting for, in your own words \u2014 this is what the human reads on your card.",
|
|
12919
|
-
" ```",
|
|
12920
|
-
"",
|
|
12921
|
-
" Frizz CHECKS every line: name something that is not running and you are bumped rather than parked.",
|
|
12922
|
-
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
12923
|
-
" awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
|
|
12924
|
-
" `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
|
|
12925
|
-
"",
|
|
12926
|
-
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
12927
|
-
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
12928
|
-
"the part that is someone else's to do. Each ends on a question with your recommendation as option A,",
|
|
12929
|
-
"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",
|
|
12930
|
-
"`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
12931
|
-
"None of them is a `done`. And what is not",
|
|
12932
|
-
'worth a card is not worth a SENTENCE: delete the dangling "one thing to carry forward", never park it',
|
|
12933
|
-
"in the handoff.",
|
|
12934
|
-
"",
|
|
12935
|
-
"**DO NOT REPEAT YOURSELF.** If the message you just wrote already stands on its own, reply with the",
|
|
12936
|
-
"fence ALONE \u2014 the human reads both together, so restating it costs them the second read for nothing.",
|
|
12937
|
-
"The same holds inside one message: the card is the ledger of what shipped, the prose is only what a",
|
|
12938
|
-
"ledger cannot hold, and a sentence that reads the same in either belongs in exactly one of them.",
|
|
12939
|
-
"",
|
|
12940
|
-
"Only if it does NOT stand alone, fix that first, briefly. It has to be readable cold: the human has",
|
|
12941
|
-
"seen nothing since their own last message \u2014 the Goal, this reminder, a watcher wake all came from",
|
|
12942
|
-
"frizz \u2014 so anything you assumed they had followed, they have not."
|
|
12943
|
-
].join("\n");
|
|
12973
|
+
SIGNOFF_NUDGE_MESSAGE = signoffNudgeText(false);
|
|
12974
|
+
SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT = signoffNudgeText(true);
|
|
12944
12975
|
ThreadWatchKind = external_exports.enum(["shell", "github", "timer"]);
|
|
12945
12976
|
GithubChecksState = external_exports.enum(["none", "running", "passing", "failing"]);
|
|
12946
12977
|
GithubMergeState = external_exports.enum(["mergeable", "blocked", "conflicting", "unknown"]);
|
|
@@ -13307,6 +13338,11 @@ var init_src = __esm({
|
|
|
13307
13338
|
UpsertOwnLinkResult = external_exports.object({ link: ThreadLinkView }).strict();
|
|
13308
13339
|
DropOwnLinkInput = external_exports.object({ slug: ThreadSlug, id: external_exports.string().min(1) }).strict();
|
|
13309
13340
|
DropOwnLinkResult = external_exports.object({ dropped: external_exports.boolean() }).strict();
|
|
13341
|
+
LiveTool = external_exports.object({
|
|
13342
|
+
name: external_exports.string(),
|
|
13343
|
+
desc: external_exports.string().optional(),
|
|
13344
|
+
detail: external_exports.string().optional()
|
|
13345
|
+
});
|
|
13310
13346
|
ThreadView = external_exports.object({
|
|
13311
13347
|
id: ThreadSlug,
|
|
13312
13348
|
// slug; filename is <slug>.md
|
|
@@ -13345,6 +13381,15 @@ var init_src = __esm({
|
|
|
13345
13381
|
// user hid the row from the nav; respawn/resume un-archives
|
|
13346
13382
|
lastAssistant: external_exports.string().optional(),
|
|
13347
13383
|
// trimmed preview of last assistant text
|
|
13384
|
+
// The FIRST non-empty line of that same text, markdown intact and newlines honoured (capped) — the
|
|
13385
|
+
// handoff's verdict line ("**Fixed** — …"), which `lastAssistant` cannot give back because its preview
|
|
13386
|
+
// collapses every newline to a space. The phone board's rested row reads it. Optional so old
|
|
13387
|
+
// snapshots parse.
|
|
13388
|
+
lastAssistantLine: external_exports.string().optional(),
|
|
13389
|
+
// The newest tool call the agent has issued and not yet had a result for (Claude session threads),
|
|
13390
|
+
// in the shape `toolActivityLabel` reads, so a list row can say "Running the focused tests" with the
|
|
13391
|
+
// gerund the chat's working indicator shows. Absent between calls, at rest, and for other backends.
|
|
13392
|
+
liveTool: LiveTool.optional(),
|
|
13348
13393
|
spawnedAt: external_exports.string().optional(),
|
|
13349
13394
|
// ISO8601
|
|
13350
13395
|
lastActivityAt: external_exports.string().optional(),
|
|
@@ -13403,6 +13448,11 @@ var init_src = __esm({
|
|
|
13403
13448
|
* same card and the swap is invisible. Absent whenever there is nothing in flight, which is almost
|
|
13404
13449
|
* always — it exists for the seconds between the human sending and the worker being handed it. */
|
|
13405
13450
|
answersInFlight: external_exports.string().optional(),
|
|
13451
|
+
/** The human's message — a follow-up or a registered answer — is on its way to the worker and the turn
|
|
13452
|
+
* has not started yet (board.deriveDeliveryInFlight). The queue has already let the thread go; this
|
|
13453
|
+
* tells the rail to draw it as working in every tab, not only the one that sent it (web
|
|
13454
|
+
* lib/steering.ts). Absent otherwise. */
|
|
13455
|
+
deliveryInFlight: external_exports.boolean().optional(),
|
|
13406
13456
|
// ISO8601 of the newest REAL user interaction (answer/steer/dispatch) — the chronological listing
|
|
13407
13457
|
// sort key. Optional; the listing falls back to spawnedAt when absent (a dispatch IS an interaction).
|
|
13408
13458
|
lastUserAt: external_exports.string().optional(),
|
|
@@ -13835,8 +13885,18 @@ var init_src = __esm({
|
|
|
13835
13885
|
// Broker-backed Claude only — that is every Claude thread dispatched since the broker cutover. On
|
|
13836
13886
|
// any other runtime the message is delivered normally and this is ignored, never refused: a send
|
|
13837
13887
|
// that arrives is always better than a send that errors.
|
|
13838
|
-
interrupt: external_exports.boolean().optional()
|
|
13888
|
+
interrupt: external_exports.boolean().optional(),
|
|
13889
|
+
// The deliveryId of a FAILED send this one re-sends — the failed bubble's Retry. The server drops that
|
|
13890
|
+
// failed entry in the same write that opens this send's own, so the retry replaces the failed bubble
|
|
13891
|
+
// rather than standing beside it. A new deliveryId, never the old one: the old send's failure may be
|
|
13892
|
+
// ambiguous, and only the operator decides to risk a second copy.
|
|
13893
|
+
supersedes: external_exports.string().min(1).max(200).optional()
|
|
13839
13894
|
});
|
|
13895
|
+
DismissFailedFollowUpInput = external_exports.object({
|
|
13896
|
+
slug: ThreadSlug,
|
|
13897
|
+
deliveryId: external_exports.string().min(1).max(200)
|
|
13898
|
+
}).strict();
|
|
13899
|
+
DismissFailedFollowUpResult = external_exports.object({ dismissed: external_exports.boolean() }).strict();
|
|
13840
13900
|
UnqueueFollowUpInput = external_exports.object({
|
|
13841
13901
|
slug: ThreadSlug,
|
|
13842
13902
|
// Same staleness guard as followUp: a stale tab must not unqueue against a re-dispatched session.
|
|
@@ -13856,6 +13916,11 @@ var init_src = __esm({
|
|
|
13856
13916
|
interrupted: external_exports.boolean(),
|
|
13857
13917
|
reason: external_exports.string().optional()
|
|
13858
13918
|
}).strict();
|
|
13919
|
+
CompactThreadInput = external_exports.object({
|
|
13920
|
+
slug: ThreadSlug,
|
|
13921
|
+
// Same staleness guard as followUp: a stale tab must not compact a re-dispatched session.
|
|
13922
|
+
sessionId: external_exports.string().min(1)
|
|
13923
|
+
}).strict();
|
|
13859
13924
|
SetThreadSnoozeInput = external_exports.object({
|
|
13860
13925
|
slug: ThreadSlug,
|
|
13861
13926
|
sessionId: external_exports.string().min(1),
|
|
@@ -14464,8 +14529,13 @@ ${DISPATCH_TASK_BANNER}
|
|
|
14464
14529
|
// yet — renders as an ordinary (un-grayed) user bubble. "unconfirmed": no evidence appeared within
|
|
14465
14530
|
// the timeout — the injection likely mutated/never landed; the client renders a quiet warning.
|
|
14466
14531
|
// Once the real transcript record lands the ledger drops the item and this field goes with it.
|
|
14467
|
-
//
|
|
14468
|
-
|
|
14532
|
+
// "sending": the server's WRITE-AHEAD entry, opened before any transport is touched, so the text is
|
|
14533
|
+
// the server's from the instant it arrives. "failed": the transport threw, or never answered — the
|
|
14534
|
+
// text is kept until the operator retries, edits or dismisses it, and nothing retries it on its own.
|
|
14535
|
+
// Additive + optional: an older client renders both as a plain bubble (gray for "sending").
|
|
14536
|
+
deliveryState: external_exports.enum(["sending", "pending", "enqueued", "delivered", "unconfirmed", "failed"]).optional(),
|
|
14537
|
+
// Why a "failed" send failed, verbatim from the transport. Set only alongside deliveryState "failed".
|
|
14538
|
+
deliveryError: external_exports.string().optional(),
|
|
14469
14539
|
// FRIZZ wrote this user turn, not the human: it is a scheduler wake delivery (isWakeDelivery). The
|
|
14470
14540
|
// client renders it as a first-party card rather than the human's off-white right-justified bubble,
|
|
14471
14541
|
// which was claiming the operator had typed a message the watcher composed. Additive + optional: an
|
|
@@ -14617,7 +14687,8 @@ ${DISPATCH_TASK_BANNER}
|
|
|
14617
14687
|
});
|
|
14618
14688
|
ProjectRailCounts = external_exports.object({
|
|
14619
14689
|
queued: external_exports.number().int().nonnegative(),
|
|
14620
|
-
running: external_exports.number().int().nonnegative()
|
|
14690
|
+
running: external_exports.number().int().nonnegative(),
|
|
14691
|
+
asks: external_exports.number().int().nonnegative()
|
|
14621
14692
|
});
|
|
14622
14693
|
PROJECT_ICON_MAX_BASE64_CHARS = 4 * 1024 * 1024;
|
|
14623
14694
|
DirectoryPickResult = external_exports.discriminatedUnion("kind", [
|
|
@@ -20558,6 +20629,29 @@ var AccessStore = class _AccessStore {
|
|
|
20558
20629
|
const id = _AccessStore.idOf(payload);
|
|
20559
20630
|
return !(id && this.sessions.isRevoked(id));
|
|
20560
20631
|
}
|
|
20632
|
+
/**
|
|
20633
|
+
* Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
|
|
20634
|
+
*
|
|
20635
|
+
* The id comes from the session itself, and only after its signature verifies, so the one thing this
|
|
20636
|
+
* can ever revoke is the credential the caller just proved it holds. There is deliberately no
|
|
20637
|
+
* parameter naming any other id: signing out SOMEONE ELSE stays loopback-only, because a stolen
|
|
20638
|
+
* session must not be able to evict the owner and keep the board.
|
|
20639
|
+
*
|
|
20640
|
+
* - `revoked`: the id is on the denylist now, so a surviving copy of this cookie is refused too.
|
|
20641
|
+
* - `legacy`: a session minted before ids existed verifies but has no id to revoke on its own. The
|
|
20642
|
+
* caller can still clear the cookie; rotating the key remains the way to kill a copy of it.
|
|
20643
|
+
* - `no-session`: nothing valid was presented — absent, forged, expired, or already signed out.
|
|
20644
|
+
*/
|
|
20645
|
+
signOutSession(session) {
|
|
20646
|
+
if (!session || !this.verifySession(session)) return { result: "no-session" };
|
|
20647
|
+
const id = _AccessStore.idOf(session.slice(0, session.lastIndexOf(".")));
|
|
20648
|
+
if (!id) return { result: "legacy" };
|
|
20649
|
+
if (!this.sessions.revoke(id)) {
|
|
20650
|
+
this.sessions.record({ id, label: "unknown device", createdAt: this.options.now() });
|
|
20651
|
+
this.sessions.revoke(id);
|
|
20652
|
+
}
|
|
20653
|
+
return this.sessions.isRevoked(id) ? { result: "revoked", id } : { result: "no-session" };
|
|
20654
|
+
}
|
|
20561
20655
|
/**
|
|
20562
20656
|
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
20563
20657
|
*
|
|
@@ -22270,13 +22364,26 @@ var qrcode_default = qrcode;
|
|
|
22270
22364
|
var stringToBytes = qrcode.stringToBytes;
|
|
22271
22365
|
|
|
22272
22366
|
// packages/server/src/qr.ts
|
|
22273
|
-
var
|
|
22274
|
-
var
|
|
22275
|
-
var
|
|
22367
|
+
var DARK = 232;
|
|
22368
|
+
var LIGHT = 231;
|
|
22369
|
+
var fg = (dark) => `\x1B[38;5;${dark ? DARK : LIGHT}m`;
|
|
22370
|
+
var bg = (dark) => `\x1B[48;5;${dark ? DARK : LIGHT}m`;
|
|
22276
22371
|
var RESET = "\x1B[0m";
|
|
22277
22372
|
var UPPER_HALF = "\u2580";
|
|
22278
22373
|
var LOWER_HALF = "\u2584";
|
|
22279
22374
|
var QUIET_ZONE = 4;
|
|
22375
|
+
var BLOCK_DRAWING_PROGRAMS = /* @__PURE__ */ new Set(["iTerm.app", "WezTerm", "ghostty", "WarpTerminal", "vscode"]);
|
|
22376
|
+
var BLOCK_DRAWING_TERMS = /* @__PURE__ */ new Set(["xterm-kitty", "xterm-ghostty", "alacritty", "wezterm"]);
|
|
22377
|
+
function drawsBlockElements(env = process.env) {
|
|
22378
|
+
const term = env.TERM ?? "";
|
|
22379
|
+
if (env.TMUX || env.STY || env.TERM_PROGRAM === "tmux" || term.startsWith("screen") || term.startsWith("tmux")) return false;
|
|
22380
|
+
if (BLOCK_DRAWING_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return true;
|
|
22381
|
+
if (BLOCK_DRAWING_TERMS.has(term)) return true;
|
|
22382
|
+
if (env.LC_TERMINAL === "iTerm2") return true;
|
|
22383
|
+
return Boolean(
|
|
22384
|
+
env.KITTY_WINDOW_ID || env.ALACRITTY_WINDOW_ID || env.WEZTERM_PANE || env.GHOSTTY_RESOURCES_DIR || env.WT_SESSION || env.VTE_VERSION
|
|
22385
|
+
);
|
|
22386
|
+
}
|
|
22280
22387
|
function encode(value, errorCorrection) {
|
|
22281
22388
|
const code = qrcode_default(0, errorCorrection);
|
|
22282
22389
|
code.addData(value);
|
|
@@ -22293,26 +22400,70 @@ function encode(value, errorCorrection) {
|
|
|
22293
22400
|
}
|
|
22294
22401
|
};
|
|
22295
22402
|
}
|
|
22296
|
-
function
|
|
22297
|
-
|
|
22298
|
-
|
|
22403
|
+
function sizeOf(size, style) {
|
|
22404
|
+
return style === "paint" ? { columns: size * 2, rows: size } : { columns: size, rows: Math.ceil(size / 2) };
|
|
22405
|
+
}
|
|
22406
|
+
function qrSize(value, style = "half", errorCorrection = "M") {
|
|
22407
|
+
return sizeOf(encode(value, errorCorrection).size, style);
|
|
22408
|
+
}
|
|
22409
|
+
function qrStyleFor(value, options2 = {}) {
|
|
22410
|
+
const { area } = options2;
|
|
22411
|
+
if (!area || drawsBlockElements(options2.env)) return "half";
|
|
22412
|
+
const paint = qrSize(value, "paint", options2.errorCorrection);
|
|
22413
|
+
return paint.columns <= area.columns && paint.rows <= area.rows ? "paint" : "half";
|
|
22414
|
+
}
|
|
22415
|
+
function qrAreaOf(stream, frame) {
|
|
22416
|
+
if (!stream.columns || !stream.rows) return void 0;
|
|
22417
|
+
return { columns: stream.columns - frame.indent, rows: stream.rows - frame.rows };
|
|
22418
|
+
}
|
|
22419
|
+
function halfLines(size, at, plain) {
|
|
22299
22420
|
const lines = [];
|
|
22300
22421
|
for (let y = 0; y < size; y += 2) {
|
|
22301
22422
|
let line = "";
|
|
22302
22423
|
for (let x = 0; x < size; x++) {
|
|
22303
22424
|
const top = at(x, y);
|
|
22304
22425
|
const bottom = y + 1 < size ? at(x, y + 1) : false;
|
|
22305
|
-
if (
|
|
22426
|
+
if (plain) {
|
|
22306
22427
|
line += top && bottom ? "#" : top ? "^" : bottom ? "v" : " ";
|
|
22307
22428
|
continue;
|
|
22308
22429
|
}
|
|
22309
|
-
if (top === bottom)
|
|
22310
|
-
|
|
22430
|
+
if (top === bottom) {
|
|
22431
|
+
line += `${bg(top)} `;
|
|
22432
|
+
continue;
|
|
22433
|
+
}
|
|
22434
|
+
const above = y > 0 ? at(x, y - 1) : false;
|
|
22435
|
+
const below = y + 2 < size ? at(x, y + 2) : false;
|
|
22436
|
+
line += below === bottom && above !== top ? `${fg(top)}${bg(bottom)}${UPPER_HALF}` : `${fg(bottom)}${bg(top)}${LOWER_HALF}`;
|
|
22437
|
+
}
|
|
22438
|
+
lines.push(plain ? line : `${line}${RESET}`);
|
|
22439
|
+
}
|
|
22440
|
+
return lines;
|
|
22441
|
+
}
|
|
22442
|
+
function paintLines(size, at, plain) {
|
|
22443
|
+
const lines = [];
|
|
22444
|
+
for (let y = 0; y < size; y++) {
|
|
22445
|
+
let line = "";
|
|
22446
|
+
let last;
|
|
22447
|
+
for (let x = 0; x < size; x++) {
|
|
22448
|
+
const dark = at(x, y);
|
|
22449
|
+
if (plain) line += dark ? "##" : " ";
|
|
22450
|
+
else {
|
|
22451
|
+
if (dark !== last) line += bg(dark);
|
|
22452
|
+
line += " ";
|
|
22453
|
+
}
|
|
22454
|
+
last = dark;
|
|
22311
22455
|
}
|
|
22312
|
-
lines.push(
|
|
22456
|
+
lines.push(plain ? line : `${line}${RESET}`);
|
|
22313
22457
|
}
|
|
22314
22458
|
return lines;
|
|
22315
22459
|
}
|
|
22460
|
+
function renderQrLines(value, options2 = {}) {
|
|
22461
|
+
if (!value) throw new Error("renderQr requires a value");
|
|
22462
|
+
const errorCorrection = options2.errorCorrection ?? "M";
|
|
22463
|
+
const { size, at } = encode(value, errorCorrection);
|
|
22464
|
+
const style = options2.style ?? qrStyleFor(value, { area: options2.area, env: options2.env, errorCorrection });
|
|
22465
|
+
return style === "paint" ? paintLines(size, at, options2.plain ?? false) : halfLines(size, at, options2.plain ?? false);
|
|
22466
|
+
}
|
|
22316
22467
|
|
|
22317
22468
|
// packages/server/src/restart-supervisor.ts
|
|
22318
22469
|
import { request as requestHttp, createServer } from "node:http";
|
|
@@ -22583,6 +22734,7 @@ var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restar
|
|
|
22583
22734
|
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
22584
22735
|
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
22585
22736
|
var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
|
|
22737
|
+
var SUPERVISOR_SIGN_OUT_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sign-out`;
|
|
22586
22738
|
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
22587
22739
|
function responseJson(res, status, value) {
|
|
22588
22740
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -22611,7 +22763,7 @@ function readCookie(header, name) {
|
|
|
22611
22763
|
}
|
|
22612
22764
|
function isControlRequest(req) {
|
|
22613
22765
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
22614
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH;
|
|
22766
|
+
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
|
|
22615
22767
|
}
|
|
22616
22768
|
function isLocalImageRequest(req) {
|
|
22617
22769
|
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
@@ -22817,6 +22969,11 @@ var RestartSupervisorProxy = class {
|
|
|
22817
22969
|
}
|
|
22818
22970
|
async handleControl(req, res) {
|
|
22819
22971
|
const pathname = new URL(req.url ?? "/", "http://frizz.invalid").pathname;
|
|
22972
|
+
if (pathname !== SUPERVISOR_SIGN_OUT_PATH && this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
22973
|
+
res.writeHead(401, { "cache-control": "no-store" });
|
|
22974
|
+
res.end();
|
|
22975
|
+
return;
|
|
22976
|
+
}
|
|
22820
22977
|
const sameOrigin = req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req);
|
|
22821
22978
|
const allowMissingOrigin = pathname === SUPERVISOR_STATUS_PATH && req.method === "GET" && sameOrigin;
|
|
22822
22979
|
if (!this.authorityAccepted(req, allowMissingOrigin)) {
|
|
@@ -22880,8 +23037,24 @@ var RestartSupervisorProxy = class {
|
|
|
22880
23037
|
responseJson(res, 200, { signedOut: 1 });
|
|
22881
23038
|
return;
|
|
22882
23039
|
}
|
|
23040
|
+
if (pathname === SUPERVISOR_SIGN_OUT_PATH) {
|
|
23041
|
+
if (req.method !== "POST") {
|
|
23042
|
+
res.writeHead(405, { allow: "POST" });
|
|
23043
|
+
res.end();
|
|
23044
|
+
return;
|
|
23045
|
+
}
|
|
23046
|
+
this.signOutThisDevice(req, res);
|
|
23047
|
+
return;
|
|
23048
|
+
}
|
|
22883
23049
|
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
22884
|
-
responseJson(res, 200, {
|
|
23050
|
+
responseJson(res, 200, {
|
|
23051
|
+
protocol: SUPERVISOR_CONTROL_PROTOCOL,
|
|
23052
|
+
...this.status(),
|
|
23053
|
+
// Sent ONLY when true, so a loopback tab's payload is byte-identical to what it was. The web
|
|
23054
|
+
// app cannot tell on its own: the session cookie is HttpOnly, and the page is the same bundle
|
|
23055
|
+
// whichever way it was reached. It is a fact about THIS request, never about the board.
|
|
23056
|
+
...this.carriesRemoteSession(req) ? { remoteSession: true } : {}
|
|
23057
|
+
});
|
|
22885
23058
|
return;
|
|
22886
23059
|
}
|
|
22887
23060
|
if (pathname !== SUPERVISOR_RESTART_PATH && pathname !== SUPERVISOR_UPDATE_RESTART_PATH || req.method !== "POST") {
|
|
@@ -22959,6 +23132,42 @@ var RestartSupervisorProxy = class {
|
|
|
22959
23132
|
if (!this.publicOrigin) return true;
|
|
22960
23133
|
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
22961
23134
|
}
|
|
23135
|
+
/** Did this request arrive through the public origin carrying a live session? Loopback never does. */
|
|
23136
|
+
carriesRemoteSession(req) {
|
|
23137
|
+
return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
23138
|
+
}
|
|
23139
|
+
/**
|
|
23140
|
+
* End the session this request carries, and nothing else — see SUPERVISOR_SIGN_OUT_PATH in
|
|
23141
|
+
* handleControl for why that makes it safe to reach from the tunnel.
|
|
23142
|
+
*
|
|
23143
|
+
* A loopback request is a no-op even if it somehow carries a cookie: the operator's own tab has no
|
|
23144
|
+
* remote session to end, and loopback is the place `--sign-out` already covers.
|
|
23145
|
+
*/
|
|
23146
|
+
signOutThisDevice(req, res) {
|
|
23147
|
+
const reply = (status, body, clearCookie) => {
|
|
23148
|
+
res.writeHead(status, {
|
|
23149
|
+
"content-type": "application/json; charset=utf-8",
|
|
23150
|
+
"cache-control": "no-store",
|
|
23151
|
+
// Same attributes the exchange set, so the browser matches and drops that exact cookie.
|
|
23152
|
+
...clearCookie ? { "set-cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0` } : {}
|
|
23153
|
+
});
|
|
23154
|
+
res.end(JSON.stringify(body));
|
|
23155
|
+
};
|
|
23156
|
+
if (!this.arrivedPublicly(req)) {
|
|
23157
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, false);
|
|
23158
|
+
return;
|
|
23159
|
+
}
|
|
23160
|
+
const outcome = this.access.signOutSession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
23161
|
+
if (outcome.result === "revoked") {
|
|
23162
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "signed-out", id: outcome.id }, true);
|
|
23163
|
+
return;
|
|
23164
|
+
}
|
|
23165
|
+
if (outcome.result === "legacy") {
|
|
23166
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "cookie-cleared" }, true);
|
|
23167
|
+
return;
|
|
23168
|
+
}
|
|
23169
|
+
reply(401, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, true);
|
|
23170
|
+
}
|
|
22962
23171
|
/**
|
|
22963
23172
|
* Trade `?frizz_code=…` for a cookie, then bounce to the same URL
|
|
22964
23173
|
* without the secret, so it never lands in history, a Referer header, or a screenshot.
|
|
@@ -23177,12 +23386,20 @@ function installPaneHost(options2) {
|
|
|
23177
23386
|
input.pause();
|
|
23178
23387
|
process.off("exit", restore);
|
|
23179
23388
|
};
|
|
23389
|
+
const reclaim = () => {
|
|
23390
|
+
if (disposed) return;
|
|
23391
|
+
try {
|
|
23392
|
+
input.setRawMode(false);
|
|
23393
|
+
input.setRawMode(true);
|
|
23394
|
+
} catch {
|
|
23395
|
+
}
|
|
23396
|
+
};
|
|
23180
23397
|
input.setRawMode(true);
|
|
23181
23398
|
input.resume();
|
|
23182
23399
|
input.setEncoding("utf8");
|
|
23183
23400
|
input.on("data", onKey);
|
|
23184
23401
|
process.on("exit", restore);
|
|
23185
|
-
return { dispose: restore, isOpen: () => active !== null };
|
|
23402
|
+
return { dispose: restore, isOpen: () => active !== null, reclaim };
|
|
23186
23403
|
}
|
|
23187
23404
|
|
|
23188
23405
|
// src/access-pane.ts
|
|
@@ -23196,6 +23413,18 @@ var RESET2 = "\x1B[0m";
|
|
|
23196
23413
|
function secondsUntil(expiresAt, now) {
|
|
23197
23414
|
return Math.max(0, Math.ceil((expiresAt - now) / 1e3));
|
|
23198
23415
|
}
|
|
23416
|
+
function fitScreen(entries, rows, columns) {
|
|
23417
|
+
const width = columns ?? 80;
|
|
23418
|
+
const height = (text) => Math.max(1, Math.ceil([...text.replace(/\x1b\[[0-9;]*m/g, "")].length / width));
|
|
23419
|
+
const kept = entries.map((entry) => typeof entry === "string" ? { text: entry, drop: Infinity } : entry);
|
|
23420
|
+
const total = () => kept.reduce((sum, entry) => sum + height(entry.text), 0);
|
|
23421
|
+
while (rows && total() > rows) {
|
|
23422
|
+
const next = kept.reduce((lowest, entry, index) => entry.drop < (kept[lowest]?.drop ?? Infinity) ? index : lowest, -1);
|
|
23423
|
+
if (next < 0) break;
|
|
23424
|
+
kept.splice(next, 1);
|
|
23425
|
+
}
|
|
23426
|
+
return kept.map((entry) => entry.text).join("\n");
|
|
23427
|
+
}
|
|
23199
23428
|
function createAccessPane(options2) {
|
|
23200
23429
|
const output = options2.output ?? process.stdout;
|
|
23201
23430
|
const now = options2.now ?? Date.now;
|
|
@@ -23207,16 +23436,25 @@ function createAccessPane(options2) {
|
|
|
23207
23436
|
if (!shown) return;
|
|
23208
23437
|
const remaining = secondsUntil(shown.expiresAt, now());
|
|
23209
23438
|
const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
|
|
23439
|
+
const footer = `${status} Press any other key to close.`;
|
|
23440
|
+
const width = output.columns ?? 80;
|
|
23441
|
+
const wrapped = (text) => Math.max(1, Math.ceil((text.length + 2) / width));
|
|
23442
|
+
const qr = renderQrLines(shown.url, { area: qrAreaOf(output, { indent: 2, rows: wrapped(shown.url) + wrapped(footer) + 1 }) });
|
|
23210
23443
|
output.write(CLEAR);
|
|
23211
|
-
output.write(
|
|
23212
|
-
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
${
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
`
|
|
23444
|
+
output.write(
|
|
23445
|
+
fitScreen(
|
|
23446
|
+
[
|
|
23447
|
+
{ text: "", drop: 1 },
|
|
23448
|
+
...qr.map((row) => ` ${row}`),
|
|
23449
|
+
{ text: "", drop: 3 },
|
|
23450
|
+
` ${shown.url}`,
|
|
23451
|
+
{ text: "", drop: 2 },
|
|
23452
|
+
` ${DIM}${footer}${RESET2}`
|
|
23453
|
+
],
|
|
23454
|
+
output.rows,
|
|
23455
|
+
output.columns
|
|
23456
|
+
)
|
|
23457
|
+
);
|
|
23220
23458
|
};
|
|
23221
23459
|
return {
|
|
23222
23460
|
open() {
|
|
@@ -24163,9 +24401,8 @@ function createRemotePane(options2) {
|
|
|
24163
24401
|
const write = (lines) => {
|
|
24164
24402
|
if (!open) return;
|
|
24165
24403
|
output.write(CLEAR);
|
|
24166
|
-
|
|
24167
|
-
|
|
24168
|
-
`);
|
|
24404
|
+
const indented = lines.map((line) => typeof line === "string" ? ` ${line}` : { ...line, text: ` ${line.text}` });
|
|
24405
|
+
output.write(fitScreen([{ text: "", drop: 0 }, ...indented], output.rows, output.columns));
|
|
24169
24406
|
};
|
|
24170
24407
|
const check = (ok, text) => ok ? `${text} \u2713` : text;
|
|
24171
24408
|
const paint = () => {
|
|
@@ -24207,13 +24444,25 @@ function createRemotePane(options2) {
|
|
|
24207
24444
|
return;
|
|
24208
24445
|
}
|
|
24209
24446
|
if (s.name === "done") {
|
|
24210
|
-
|
|
24211
|
-
|
|
24212
|
-
|
|
24213
|
-
lines.push("", s.link.url, "", `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`);
|
|
24447
|
+
if (!s.link) {
|
|
24448
|
+
write([s.message, "", `${DIM}press any key to return${RESET2}`]);
|
|
24449
|
+
return;
|
|
24214
24450
|
}
|
|
24215
|
-
|
|
24216
|
-
|
|
24451
|
+
const note = `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`;
|
|
24452
|
+
const width = output.columns ?? 80;
|
|
24453
|
+
const wrapped = (text) => Math.max(1, Math.ceil((text.length + 2) / width));
|
|
24454
|
+
const qr = renderQrLines(s.link.url, { area: qrAreaOf(output, { indent: 2, rows: wrapped(s.message) + 1 + wrapped(s.link.url) + 1 }) });
|
|
24455
|
+
write([
|
|
24456
|
+
s.message,
|
|
24457
|
+
{ text: "", drop: 4 },
|
|
24458
|
+
...qr,
|
|
24459
|
+
{ text: "", drop: 6 },
|
|
24460
|
+
s.link.url,
|
|
24461
|
+
{ text: "", drop: 2 },
|
|
24462
|
+
{ text: note, drop: 5 },
|
|
24463
|
+
{ text: "", drop: 3 },
|
|
24464
|
+
`${DIM}press any key to return${RESET2}`
|
|
24465
|
+
]);
|
|
24217
24466
|
return;
|
|
24218
24467
|
}
|
|
24219
24468
|
write([`Could not apply that: ${s.message}`, "", `${DIM}press any key to go back${RESET2}`]);
|
|
@@ -25578,6 +25827,7 @@ var Supervisor = class {
|
|
|
25578
25827
|
logLine;
|
|
25579
25828
|
errorLine;
|
|
25580
25829
|
onActivity;
|
|
25830
|
+
onChildExit;
|
|
25581
25831
|
/** Suppresses activity beats until the first boot has settled; until then the readout owns the terminal. */
|
|
25582
25832
|
booted = false;
|
|
25583
25833
|
/** When the restart currently in flight began, so its "ready" beat can report a duration. */
|
|
@@ -25630,6 +25880,7 @@ var Supervisor = class {
|
|
|
25630
25880
|
this.logLine = opts.log ?? ((line) => log.info("supervisor", stripPrefix(line)));
|
|
25631
25881
|
this.errorLine = opts.error ?? ((line) => log.error("supervisor", stripPrefix(line)));
|
|
25632
25882
|
this.onActivity = opts.onActivity;
|
|
25883
|
+
this.onChildExit = opts.onChildExit;
|
|
25633
25884
|
this.updateRestart = opts.updateRestart;
|
|
25634
25885
|
this.updateMode = opts.updateMode ?? "durableReexec";
|
|
25635
25886
|
this.commitUpdate = opts.commitUpdate;
|
|
@@ -26128,6 +26379,10 @@ var Supervisor = class {
|
|
|
26128
26379
|
settleSpawn(false);
|
|
26129
26380
|
});
|
|
26130
26381
|
child.once("exit", (code, signal) => {
|
|
26382
|
+
try {
|
|
26383
|
+
this.onChildExit?.();
|
|
26384
|
+
} catch {
|
|
26385
|
+
}
|
|
26131
26386
|
if (this.child === child) {
|
|
26132
26387
|
this.child = null;
|
|
26133
26388
|
this.childPort = void 0;
|
|
@@ -27096,7 +27351,7 @@ if (options.link) {
|
|
|
27096
27351
|
process.exit(1);
|
|
27097
27352
|
}
|
|
27098
27353
|
const { url } = await response.json();
|
|
27099
|
-
if (process.stdout.isTTY) for (const row of renderQrLines(url)) console.log(` ${row}`);
|
|
27354
|
+
if (process.stdout.isTTY) for (const row of renderQrLines(url, { area: qrAreaOf(process.stdout, { indent: 2, rows: 3 }) })) console.log(` ${row}`);
|
|
27100
27355
|
console.log(process.stdout.isTTY ? `
|
|
27101
27356
|
${url}` : url);
|
|
27102
27357
|
process.exit(0);
|
|
@@ -27274,6 +27529,9 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
27274
27529
|
// here is triggered from a browser tab or by a crash, so without this the foreground process is
|
|
27275
27530
|
// the last place to learn what happened to it.
|
|
27276
27531
|
onActivity: (event) => renderSupervisorActivity(activityReadout, event),
|
|
27532
|
+
// Every child leaves the terminal as it found it at its own fork, and the first one was forked
|
|
27533
|
+
// before the keyboard went raw — so without this R stopped working after the first update.
|
|
27534
|
+
onChildExit: () => paneHost?.reclaim(),
|
|
27277
27535
|
updateRestart: async () => {
|
|
27278
27536
|
previous = active;
|
|
27279
27537
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frizz",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A local web UI for running many coding agents at once",
|
|
6
6
|
"keywords": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"frizzServer": {
|
|
68
68
|
"package": "frizz-server",
|
|
69
|
-
"version": "0.15.
|
|
69
|
+
"version": "0.15.8",
|
|
70
70
|
"protocol": 1,
|
|
71
71
|
"dataEpoch": 1
|
|
72
72
|
}
|