frizz-server 0.15.25 → 0.15.27
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/claude-agent-broker.js +14743 -13917
- package/dist/dev-child.js +19221 -16124
- package/package.json +1 -1
- package/runtime/cc-worker/bin/frizzctl.mjs +18 -5
- package/runtime/cc-worker/hooks/deny-ask.mjs +5 -5
- package/runtime/cc-worker/hooks/deny-plan.mjs +2 -2
- package/runtime/cc-worker/hooks/perm-policy.mjs +3 -2
- package/runtime/cc-worker/hooks/session-seed.mjs +1 -1
- package/runtime/cc-worker/lib/frizz-tools.mjs +203 -308
- package/runtime/cc-worker/skills/gh/SKILL.md +2 -2
- package/web-dist/assets/{abnfDiagram-VRR7QNED-BUGGhGcL.js → abnfDiagram-VRR7QNED-C1yPcG1z.js} +1 -1
- package/web-dist/assets/{arc-DlmiB5Kw.js → arc-DCZhAMyd.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-B1iuHL_Y.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-m4afIjbT.js → architectureDiagram-ZJ3FMSHR-BEVy-3f-.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-Kz2humn5.js → blockDiagram-677ZJIJ3-C1XryTx5.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-SeHCCIRv.js → c4Diagram-LMCZKHZV-BxzRPmz9.js} +1 -1
- package/web-dist/assets/channel-DmzWEsVb.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-Cs4BaciL.js → chunk-32BRIVSS-BOhxnrmS.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-Hrl54hxN.js → chunk-52WLFC77-DHFFeOOt.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-C_LwXwMT.js → chunk-C7G6YPKG-Bf7iTkYg.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-s0-pq_ja.js → chunk-EX3LRPZG-Tj-fNey7.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-Ci_L6bRR.js → chunk-FWX5IMBZ-B_587R9r.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-Cn43tXaT.js → chunk-HOUHSVGY-CWYb4IRw.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-DaehSUDb.js → chunk-ICXQ74PX-Jy4FGOLb.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-BOLvHrOY.js → chunk-MOJQB5TN-Doyk0ws3.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-B7N0H72R.js → chunk-OGEWGWER-X6cWpyUd.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-D9Tpv5xu.js → chunk-PUDLZKDR-CNCT6yjC.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-CZxzEfvh.js → chunk-Q4XR5HBZ-BcAtfHf9.js} +1 -1
- package/web-dist/assets/{chunk-RYQCIY6F-CH35JHMA.js → chunk-RYQCIY6F-8CUiRz24.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-28qGatq3.js → chunk-V7JOEXUC-C7UqD_CN.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-CkO9u5CM.js → chunk-VAUOI2AC-Dg3Nzy4F.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-BFszczwO.js → chunk-VR4S4FIN-BrkonRYO.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-CuBoterc.js → chunk-WYO6CB5R-CtzxQkcZ.js} +1 -1
- package/web-dist/assets/{chunk-XXDRQBXY-8nlDi-YO.js → chunk-XXDRQBXY-0TbCJaHg.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-B5WfIiLa.js → chunk-ZGVPDNZ5-CRkGfJ8p.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-BzUHMuoR.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-BzUHMuoR.js +1 -0
- package/web-dist/assets/{cose-bilkent-JH36ORCC-Csxg0M3P.js → cose-bilkent-JH36ORCC-CjL8X4g9.js} +1 -1
- package/web-dist/assets/{cynefin-VYW2F7L2-DY-oaly-.js → cynefin-VYW2F7L2-2pg1obUS.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-Baev5OIz.js → cynefinDiagram-TSTJHNR4-CXst9IDq.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-VV2KSHL4.js → dagre-VKFMJZFB-DayLnbRm.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY-rLuhqzIC.js → diagram-FQU43EPY-6yRM--yJ.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-BBRVU5Kr.js → diagram-G47NLZAW-eNczVIx_.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-BMSjTTHf.js → diagram-NH7WQ7WH-B_b4Cb9a.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-7NO_QsSx.js → diagram-OA4YK3LP-DHW8gg6j.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-evQWfr1h.js → diagram-WEI45ONY-CaBjDDII.js} +1 -1
- package/web-dist/assets/{dist-BoiR_HZa.js → dist-CTtNsFkL.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-BGCXILB-.js → ebnfDiagram-CCIWWBDH-CtmnW6EM.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-CJSi0YB2.js → erDiagram-Q63AITRT-N214SxuY.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-C3Xuk6Lf.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-DdLuCY6R.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-DfKGoGJe.js → ganttDiagram-NO4QXBWP-CzuKeYZd.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-DDOEbCVa.js → gitGraph-TEB2WS4Q-CNQpTl8i.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-tKMl1T5T.js → gitGraphDiagram-IHSO6WYX-Bo3w9_BM.js} +1 -1
- package/web-dist/assets/index-BJngxAUp.js +372 -0
- package/web-dist/assets/index-CWk594R-.css +2 -0
- package/web-dist/assets/{info-DKCQHKI2-BqMXJJEF.js → info-DKCQHKI2-CQ5sLj3R.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-DXpmORxt.js → infoDiagram-FWYZ7A6U-H6Ct-2Yz.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-D9yCb_iG.js → ishikawaDiagram-FXEZZL3T-rUTDcs-s.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-DwyXsO1X.js → journeyDiagram-5HDEW3XC-pET45cR6.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-C-BupP2L.js → kanban-definition-HUTT4EX6-CMEmImiX.js} +1 -1
- package/web-dist/assets/{line-CVohBkYV.js → line-DmbABxm7.js} +1 -1
- package/web-dist/assets/{linear-CUS0dsDf.js → linear-BV686rjE.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-BaUI30hf.js → mermaid-parser.core-BhfkgmJj.js} +3 -3
- package/web-dist/assets/{mermaid.core-DRwFVBM9.js → mermaid.core-CjApJjX5.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-Dwz7wXjl.js → mindmap-definition-LN4V7U3C-C-1ivbvY.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-Cc8S-JIW.js → packet-7NZHBO7P-BWeKd3YB.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-DEvuejFX.js → pegDiagram-2B236MQR-CMt6u9H9.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-lDSko9Ox.js → pie-RZYD4A2V-CT1Y-9Bd.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-DoIHahh_.js → pieDiagram-ENE6RG2P-WctsTMEX.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-ttYUswX_.js → quadrantDiagram-ABIIQ3AL-detmDaKK.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-Dlai2VWL.js → radar-I7S5WNFK-DNFNnoP7.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-CfOvvQCL.js → railroad-3IZDKUUU-Bzhtn1VK.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-D4qAcwJF.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-C9BHZsDg.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-CnYxGTOV.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-BRpMN8zD.js → railroadDiagram-RFXS5EU6-C0ehB7_S.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-C1f5KNrx.js → requirementDiagram-TGXJPOKE-g7iubHbK.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-CjXPLZ2A.js → sankeyDiagram-HTMAVEWB-DIYvCsv4.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-BxVxwb4e.js → sequenceDiagram-DBY2YBRQ-7GQEb58j.js} +1 -1
- package/web-dist/assets/{src-DjcREoQq.js → src-BwwZrn6Z.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-CCx9rSmu.js → stateDiagram-2N3HPSRC-DRgMko33.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-BytUD2US.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-L3Legvfa.js → swimlanes-5IMT3BWC-BleEENNi.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-BT_dw9UO.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-0p22547Z.js → timeline-definition-FHXFAJF6-CnadbNaJ.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-Xv4-kZ7I.js → treeView-QDETBFTQ-By5b6e2R.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-MhycNmte.js → treemap-6X3UGDF4-K3mpMY_i.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-P33JCPMu.js → vennDiagram-L72KCM5P-B-0Xnf8Y.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-CS41M7I-.js → wardley-OPB4EBWU-DtS5at8O.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-CbQaI_DG.js → wardleyDiagram-EHGQE667-Rs7Ad73f.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-tg1lQQ6H.js → xychartDiagram-FW5EYKEG-BSoig2A4.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-DvYaN-Hu.js +0 -1
- package/web-dist/assets/channel-Og7O24Xl.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-DBua7nWT.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-DBua7nWT.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-CMWWvyUk.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-CFOr0Vr_.js +0 -1
- package/web-dist/assets/index-DYhjAs_a.js +0 -354
- package/web-dist/assets/index-htGvcLfn.css +0 -2
- package/web-dist/assets/railroad-abnf-AHOZXSZD-Br3ohVd5.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-DFaY0AnY.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-Cy2FBxXb.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-bSyE7VSt.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-n0v1Z0Xu.js +0 -8
|
@@ -463,184 +463,97 @@ const UNWATCH = {
|
|
|
463
463
|
},
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
// ---- `ask` / `
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
properties: {
|
|
529
|
-
question: {
|
|
530
|
-
type: "string",
|
|
531
|
-
description:
|
|
532
|
-
"THE QUESTION, on one line, in the human's own vocabulary. They have their original prompt " +
|
|
533
|
-
"and nothing else — not your plan, not your notes, not the names you coined while working. " +
|
|
534
|
-
"Lead with the behaviour, not the identifier. NO \"I\" AND NO \"you\": clicking an option is " +
|
|
535
|
-
"the HUMAN speaking, so first and second person flip between writer and reader. Name the " +
|
|
536
|
-
"actor outright instead.",
|
|
537
|
-
},
|
|
538
|
-
header: { type: "string", description: "A very short chip label for the card, 12 characters or so — \"Auth method\", \"Storage\"." },
|
|
539
|
-
kind: {
|
|
540
|
-
type: "string",
|
|
541
|
-
enum: ["question", "multi"],
|
|
542
|
-
description:
|
|
543
|
-
"`question` = pick ONE. `multi` = pick SEVERAL, for choices that are not mutually exclusive. " +
|
|
544
|
-
"A question with NO options at all is a free-text box, which is the right shape when you need " +
|
|
545
|
-
"a name, a value or a sentence rather than a decision between things you have enumerated.",
|
|
546
|
-
},
|
|
547
|
-
danger: {
|
|
548
|
-
type: "boolean",
|
|
549
|
-
description:
|
|
550
|
-
"The DESTRUCTIVE gate, and nothing softer: a force-push, a deletion, a history rewrite, a " +
|
|
551
|
-
"production rollback. It changes two things — the card wears the risk tone, and the human's " +
|
|
552
|
-
"x cannot dismiss it, because a generic close icon is not consent for something irreversible. " +
|
|
553
|
-
"Declining must therefore be one of your own options.",
|
|
554
|
-
},
|
|
555
|
-
// No `maxItems`: the count is the worker's to choose (maintainer 2026-09-03 — "allow arbitrary
|
|
556
|
-
// numbers of options"). A `multi` over a long list is a real shape; the card numbers every row (1–9 are its keys).
|
|
557
|
-
options: {
|
|
558
|
-
type: "array",
|
|
559
|
-
description:
|
|
560
|
-
"As many as the choice actually has — a fork of two, or a `multi` over twenty findings. " +
|
|
561
|
-
"Omit entirely for a free-text question.",
|
|
562
|
-
items: option,
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
required: ["question", "kind"],
|
|
566
|
-
}
|
|
567
|
-
}
|
|
466
|
+
// ---- `ask` / `secret`: RETIRED, and `unask` -------------------------------------------------------------
|
|
467
|
+
//
|
|
468
|
+
// A QUESTION IS A ```question FENCE IN THE WORKER'S OWN MESSAGE (2026-10-13). The server reads every
|
|
469
|
+
// assistant message, registers each new `id:` as a `thread_question` row, and reads an `id:`-only fence as
|
|
470
|
+
// a reference to one that exists (packages/shared/src/question-fence.ts). `ask` worked — about 4 failures
|
|
471
|
+
// in 220 calls — but 61% of those calls were the last tool call before the rest: a whole request spent
|
|
472
|
+
// re-reading the context to register, and then a handoff that copied the minted id back into a placement
|
|
473
|
+
// marker. See plans/rest-by-registration.md.
|
|
474
|
+
//
|
|
475
|
+
// BOTH COMMANDS STAY, AND REFUSE. A worker whose contract predates the fence still reaches for them, and
|
|
476
|
+
// so does one that read `frizzctl ask` in a project's own docs; refusing at that moment, with the fence in
|
|
477
|
+
// the refusal, teaches better than a command that is simply gone ("no such command" teaches nothing). The
|
|
478
|
+
// refusal makes no RPC: the `ask` RPC itself stays for the server's own registrar, its seeds and its tests.
|
|
479
|
+
// Neither is listed by `frizzctl --help`, and each MCP description is one line.
|
|
480
|
+
|
|
481
|
+
/** What a refused `ask` prints, and what `frizzctl ask --help` prints: the fence, whole enough to write one
|
|
482
|
+
* without the contract that taught it, because the worker reading it may have a contract that never did. */
|
|
483
|
+
const ASK_RETIRED =
|
|
484
|
+
"NOT registered — `frizzctl ask` is retired (2026-10-13), and nothing was sent. Ask the human by writing " +
|
|
485
|
+
"a ```question fence in your own message instead: Frizz registers it when the message lands and draws " +
|
|
486
|
+
"its card exactly where you wrote it.\n\n" +
|
|
487
|
+
"```question\n" +
|
|
488
|
+
"id: settings-store\n" +
|
|
489
|
+
"kind: choice\n" +
|
|
490
|
+
"question: Should the settings store use SQLite or a JSON file?\n" +
|
|
491
|
+
"options:\n" +
|
|
492
|
+
" - label: SQLite\n" +
|
|
493
|
+
" description: transactional, and one store to back up\n" +
|
|
494
|
+
" recommended: true\n" +
|
|
495
|
+
" - label: JSON file\n" +
|
|
496
|
+
" description: no dependency, hand-editable\n" +
|
|
497
|
+
"---\n" +
|
|
498
|
+
"Optional context, in any Markdown: a table, a diff, a callout, a <details> section.\n" +
|
|
499
|
+
"```\n\n" +
|
|
500
|
+
"- `id:` is yours to choose: a lowercase slug, unique in this thread for good.\n" +
|
|
501
|
+
"- `kind:` is `choice` (pick one), `multi` (pick several), `text` (free text) or `secret` (one masked " +
|
|
502
|
+
"box, for a token or a one-time code).\n" +
|
|
503
|
+
"- `question:` is one line. Each option takes `label:`, `description:` (one line), `recommended: true` " +
|
|
504
|
+
"and `detail: |` (an indented Markdown block). The question may also carry `danger: true`.\n" +
|
|
505
|
+
"- Every value is read verbatim to the end of its line. A code block inside the fence needs a longer " +
|
|
506
|
+
"outer fence (````question).\n" +
|
|
507
|
+
"- Several questions are several fences in one message. A follow-up is its own fence in the same message " +
|
|
508
|
+
"with `when: <parent id> = <option label>`.\n" +
|
|
509
|
+
"- At a later rest that still needs an answer, write a fence carrying that question's `id:` alone. " +
|
|
510
|
+
"Withdraw one you no longer need with `frizzctl unask <id>`.\n\n" +
|
|
511
|
+
"The question is your rest's sign-off and it does not end your turn: keep doing what does not depend " +
|
|
512
|
+
"on the answer, then rest normally. A fence Frizz cannot register comes back to you as a correction " +
|
|
513
|
+
"naming the fault."
|
|
514
|
+
|
|
515
|
+
const SECRET_RETIRED =
|
|
516
|
+
"NOT registered — `frizzctl secret` is retired (2026-10-13), and nothing was sent. Ask for a value only " +
|
|
517
|
+
"the human holds with a `kind: secret` question fence in your own message:\n\n" +
|
|
518
|
+
"```question\n" +
|
|
519
|
+
"id: npm-otp\n" +
|
|
520
|
+
"kind: secret\n" +
|
|
521
|
+
"question: The npm one-time code for the maintainer account, to publish frizz-server\n" +
|
|
522
|
+
"```\n\n" +
|
|
523
|
+
"The card is one masked box, and what the human pastes never enters the transcript or your context. The " +
|
|
524
|
+
"answer's wake gives you the path of a private pipe that reads ONCE, within 15m: read it inside the " +
|
|
525
|
+
"command that uses it — `npm publish --otp \"$(cat '<path>')\"` — and never cat, echo or copy it on its " +
|
|
526
|
+
"own. A one-time code expires within a minute, so have the command ready before you rest. " +
|
|
527
|
+
"`frizzctl ask --help` shows the rest of the question fence."
|
|
568
528
|
|
|
569
529
|
const ASK = {
|
|
570
530
|
name: "ask",
|
|
571
|
-
description:
|
|
572
|
-
|
|
573
|
-
"WAY TO ASK: the free-form ```question fence is retired (2026-09-11), and a fence with a question " +
|
|
574
|
-
"written in its body is plain prose — no card, no answer, no sign-off. The row renders as an " +
|
|
575
|
-
"answerable card on the board and in the thread, and it STAYS there: it survives your turn ending, " +
|
|
576
|
-
"a compaction, a restart, and the transcript scrolling past. A fence has the lifetime of the " +
|
|
577
|
-
"message carrying it, which is why a question written into one was unanswerable an hour later.\n\n" +
|
|
578
|
-
"YOUR DEFAULT IS TO DECIDE, AND THIS COMMAND DOES NOT CHANGE THAT. A reversible call costs minutes to " +
|
|
579
|
-
"redo; a round-trip to the human costs hours with the whole effort idle. Anything derivable from " +
|
|
580
|
-
"the code, the conventions or ordinary engineering judgement is yours: make it, say which way you " +
|
|
581
|
-
"went, and keep moving. THE TEST THAT CATCHES ALMOST EVERY BAD QUESTION: if you are about to mark " +
|
|
582
|
-
"one option `recommended`, you already know the answer — so implement it instead of asking.\n\n" +
|
|
583
|
-
"ASK WHEN A WRONG GUESS WOULD BE BOTH COSTLY AND HARD TO UNDO — something destructive or " +
|
|
584
|
-
"irreversible, an external-facing commitment, a security posture with real exposure, product or UX " +
|
|
585
|
-
"direction that is genuinely the human's taste to set. And ask when you KNOW the answer but cannot " +
|
|
586
|
-
"ACT on it: a merge, a publish, a spend, a comment that goes out under their name. Then the " +
|
|
587
|
-
"recommendation is the point, and it goes first.\n\n" +
|
|
588
|
-
"ASKING DOES NOT END YOUR TURN. A question waits on a person, so it carries no timeout and expires " +
|
|
589
|
-
"never — but you keep working. Do everything that does NOT depend on the answer first, and register " +
|
|
590
|
-
"the question at the moment you find it rather than saving it for the end.\n\n" +
|
|
591
|
-
"AND WHEN YOU DO STOP, THE QUESTION IS THAT REST'S SIGN-OFF — rest normally. Frizz draws the card " +
|
|
592
|
-
"at the rest that asked it whether you mention it or not, so nothing you write can hide one. The " +
|
|
593
|
-
"card draws itself at the tail of the rest the question was asked — never write the question into " +
|
|
594
|
-
"your handoff (one question, one card). AT EVERY LATER REST it is no longer your sign-off and frizz " +
|
|
595
|
-
"does not redraw it under your newer handoff: name it under `questions:` in an ```awaiting fence " +
|
|
596
|
-
"while you still need the answer (its card is drawn there), or withdraw it with `frizzctl unask <id>`. TO PLACE IT " +
|
|
597
|
-
"INSIDE YOUR PROSE instead, write " +
|
|
598
|
-
"an EMPTY fence naming the id this command printed — ```question qst_ab12cd34 on one line, ``` on " +
|
|
599
|
-
"the next — and the card renders there. One marker per question; nothing in the body; placement is " +
|
|
600
|
-
"optional, and every answer of the rest still sends together.\n\n" +
|
|
601
|
-
"SEVERAL AT ONCE IS ONE COMMAND. The card sends every answer as a unit, so a second `frizzctl ask` for a second " +
|
|
602
|
-
"question just makes the human send twice. Register them together.\n\n" +
|
|
603
|
-
"The answer comes back to you as its own wake, restating what was asked. Withdraw one you no longer " +
|
|
604
|
-
"need with `frizzctl unask <id>` — a question you have since answered yourself, still sitting on the human's " +
|
|
605
|
-
"board, is worse than never having asked it.\n\n" +
|
|
606
|
-
"ON AN AUTONOMOUS THREAD THIS REFUSES, and tells you the standing instruction you are working " +
|
|
607
|
-
"under. A thread carrying a rest Goal has already been told to keep going and decide for itself, " +
|
|
608
|
-
"so the refusal is that instruction arriving at the moment it matters. Decide, and say which way " +
|
|
609
|
-
"you went in your write-up. If the call is genuinely the human's — destructive or irreversible — " +
|
|
610
|
-
"put it in your FINAL MESSAGE instead of here; autonomous does not mean nobody is reading.\n\n" +
|
|
611
|
-
"AN ACT IS NOT A QUESTION. When the human must PERFORM something you cannot — sign in, approve, " +
|
|
612
|
-
"merge, press a button you may not — do not ask whether they will: list the steps under `steps:` " +
|
|
613
|
-
"in your ```awaiting fence. That works on an autonomous thread too, and their reply wakes you.",
|
|
614
|
-
inputSchema: {
|
|
615
|
-
type: "object",
|
|
616
|
-
properties: {
|
|
617
|
-
// No `maxItems` (four until 2026-09-03): "several at once is one command" above, and a cap here told
|
|
618
|
-
// a worker with six to batch them and then refused the batch.
|
|
619
|
-
questions: {
|
|
620
|
-
type: "array",
|
|
621
|
-
minItems: 1,
|
|
622
|
-
description: "The questions to register, together. Each becomes its own card and its own row.",
|
|
623
|
-
items: questionSchema(1),
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
required: ["questions"],
|
|
627
|
-
},
|
|
531
|
+
description: "RETIRED (2026-10-13): ask the human by writing a ```question fence in your own message. This only refuses, and the refusal shows the fence.",
|
|
532
|
+
inputSchema: { type: "object", properties: {} },
|
|
628
533
|
}
|
|
629
534
|
|
|
630
535
|
const UNASK = {
|
|
631
536
|
name: "unask",
|
|
632
537
|
description:
|
|
633
|
-
"WITHDRAW A QUESTION you
|
|
634
|
-
"never asked.\n\n" +
|
|
538
|
+
"WITHDRAW A QUESTION you asked, by its id — the `id:` you wrote in its ```question fence. Its card " +
|
|
539
|
+
"disappears and the human is never asked.\n\n" +
|
|
635
540
|
"Use it the moment the question stops mattering: you worked out the answer yourself, the code moved " +
|
|
636
541
|
"and the fork is gone, or you are about to finish. A stale question on someone's board is worse " +
|
|
637
|
-
"than no question — they answer it, and the answer is about a decision that no longer exists
|
|
638
|
-
"
|
|
639
|
-
"
|
|
542
|
+
"than no question — they answer it, and the answer is about a decision that no longer exists. An " +
|
|
543
|
+
"open question also blocks `frizzctl done` until it is answered or withdrawn.\n\n" +
|
|
544
|
+
"You do NOT need this for a question that gets answered; that settles itself and wakes you. Nor for " +
|
|
545
|
+
"one a later rest simply does not mention: an older question stays answerable where it was asked. " +
|
|
546
|
+
"Withdrawing is YOUR move and is never reported back to you as news. A withdrawn id is never asked " +
|
|
547
|
+
"again — a new question takes a new id.",
|
|
640
548
|
inputSchema: {
|
|
641
549
|
type: "object",
|
|
642
550
|
properties: {
|
|
643
|
-
id: {
|
|
551
|
+
id: {
|
|
552
|
+
type: "string",
|
|
553
|
+
description:
|
|
554
|
+
"The question's id: the `id:` of its ```question fence, as `frizzctl activity` lists it — or a " +
|
|
555
|
+
"`qst_…` id an older `frizzctl ask` printed. Only your own thread's.",
|
|
556
|
+
},
|
|
644
557
|
},
|
|
645
558
|
required: ["id"],
|
|
646
559
|
},
|
|
@@ -648,39 +561,8 @@ const UNASK = {
|
|
|
648
561
|
|
|
649
562
|
const SECRET = {
|
|
650
563
|
name: "secret",
|
|
651
|
-
description:
|
|
652
|
-
|
|
653
|
-
"needs and you must never see. The human may be on a phone, far from this machine: this is how a " +
|
|
654
|
-
"code reaches a command without anyone typing into a terminal here.\n\n" +
|
|
655
|
-
"It registers a question like `frizzctl ask`, drawn as a card with ONE MASKED BOX. What the human pastes is " +
|
|
656
|
-
"held in Frizz's memory — never on disk — and served through a private named pipe whose path this " +
|
|
657
|
-
"command prints NOW. It never enters the transcript, the database or your context: the answer you are " +
|
|
658
|
-
"woken with names the path, not the value.\n\n" +
|
|
659
|
-
"THE PATH CAN BE READ ONCE, within 15 minutes of the answer; then it is gone. So read it INSIDE the " +
|
|
660
|
-
"command that uses it: `npm publish --otp \"$(cat '<path>')\"`, `<cmd> < '<path>'`. A value several " +
|
|
661
|
-
"commands need goes into a variable at the start of ONE Bash call: " +
|
|
662
|
-
"`T=\"$(cat '<path>')\"; cmd1 --token \"$T\"; cmd2 --token \"$T\"`. Never `cat` it bare, echo it, " +
|
|
663
|
-
"copy it into a file, or paste it into a message — any of those spends the one read and puts the " +
|
|
664
|
-
"value in the transcript. To discard an unread value, `rm` the path.\n\n" +
|
|
665
|
-
"A one-time code expires in about thirty seconds, so have the command ready BEFORE you rest: when the " +
|
|
666
|
-
"answer wakes you, run it at once.\n\n" +
|
|
667
|
-
"Like a question, it is your rest's sign-off and it gates `done` until answered; name it under " +
|
|
668
|
-
"`questions:` at later rests, withdraw it with `frizzctl unask <id>`. It is allowed on an autonomous thread — a " +
|
|
669
|
-
"credential is not something you can decide.",
|
|
670
|
-
inputSchema: {
|
|
671
|
-
type: "object",
|
|
672
|
-
properties: {
|
|
673
|
-
question: {
|
|
674
|
-
type: "string",
|
|
675
|
-
description:
|
|
676
|
-
"What to paste, said so the human can answer cold: which account or service, which kind of " +
|
|
677
|
-
"value, and what it unlocks — \"The npm one-time code for the maintainer account, to publish " +
|
|
678
|
-
"frizz 0.16.0.\" NO \"I\" AND NO \"you\": name the actor outright.",
|
|
679
|
-
},
|
|
680
|
-
header: { type: "string", description: "A very short chip label for the card, 12 characters or so — \"npm OTP\", \"API token\"." },
|
|
681
|
-
},
|
|
682
|
-
required: ["question"],
|
|
683
|
-
},
|
|
564
|
+
description: "RETIRED (2026-10-13): ask for a value only the human holds with a `kind: secret` ```question fence in your own message. This only refuses, and the refusal shows the fence.",
|
|
565
|
+
inputSchema: { type: "object", properties: {} },
|
|
684
566
|
}
|
|
685
567
|
|
|
686
568
|
// STEER (2026-10-09): one thread messaging ANOTHER thread of the same project. Claude Code's own
|
|
@@ -754,7 +636,8 @@ const DONE = {
|
|
|
754
636
|
"plan, doc or commissioned report written INTO A FILE. An open pull request is not done — the " +
|
|
755
637
|
"merge is. An investigation headed for a fix is not done — the fix is. And a verdict that ends in " +
|
|
756
638
|
"SOMEBODY SHOULD NOW DO SOMETHING (merge it, post this, pick one of these) is not done either: " +
|
|
757
|
-
"that is a
|
|
639
|
+
"that is a question — a ```question fence in your message — carrying your recommendation as the " +
|
|
640
|
+
"first option.\n\n" +
|
|
758
641
|
"THE TEST IS NEVER \"HAVE I STOPPED WORKING\". It is: WHAT IS LOST IF NOBODY EVER OPENS THIS " +
|
|
759
642
|
"THREAD AGAIN? Name one thing and you are not done. Uncertain is not done.",
|
|
760
643
|
inputSchema: {
|
|
@@ -770,6 +653,14 @@ const DONE = {
|
|
|
770
653
|
"sentence that would read the same in both places belongs in exactly one of them. Nothing " +
|
|
771
654
|
"here may point vaguely forward — no \"a follow-up could…\". Do it, ask about it, or drop it.",
|
|
772
655
|
},
|
|
656
|
+
title: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description:
|
|
659
|
+
"OPTIONAL. The card's heading: a short phrase in sentence case naming what LANDED (\"Resolver " +
|
|
660
|
+
"cache keyed on the normalized id\"), not the handoff. It wraps, so name the work fully; past " +
|
|
661
|
+
"120 characters it is trimmed on a word boundary. Without it the card is headed \"Done\", which " +
|
|
662
|
+
"is true of every completion and specific to none. It wins over a `title:` line the body opens with.",
|
|
663
|
+
},
|
|
773
664
|
},
|
|
774
665
|
required: ["body"],
|
|
775
666
|
},
|
|
@@ -824,12 +715,11 @@ const ACTIVITY = {
|
|
|
824
715
|
"one of them, every QUESTION still owed an answer, and saved links/files with their lnk_ ids.\n\n" +
|
|
825
716
|
"WHY YOU NEED IT: an ```awaiting fence names what you are waiting on BY ID, and frizz checks every " +
|
|
826
717
|
"one against what is actually live. A name that matches nothing is not a park — you are bumped and " +
|
|
827
|
-
"your thread queues. The same goes for the ids `unwatch` and `unask` take, and for the id
|
|
828
|
-
"
|
|
829
|
-
"
|
|
830
|
-
"
|
|
831
|
-
"
|
|
832
|
-
"WITHOUT registering or withdrawing one.\n\n" +
|
|
718
|
+
"your thread queues. The same goes for the ids `unwatch` and `unask` take, and for the `id:` a " +
|
|
719
|
+
"```question fence references when a later rest still needs that question's answer. So if you have " +
|
|
720
|
+
"lost one (a compaction, a long turn, a wake you did not expect), run this rather than guessing. " +
|
|
721
|
+
"Guessing is the failure this command exists to remove — and it is how you read back your open " +
|
|
722
|
+
"questions, by the id you gave each one.\n\n" +
|
|
833
723
|
"It takes nothing and changes nothing. You can only ever read your OWN thread.",
|
|
834
724
|
inputSchema: { type: "object", properties: {}, required: [] },
|
|
835
725
|
}
|
|
@@ -1029,24 +919,24 @@ async function activity() {
|
|
|
1029
919
|
const linksBlock = links.length === 0 ? "" : "\n\nSaved links and files (not running work; remove with `frizzctl unlink <id>`):\n" +
|
|
1030
920
|
links.map((link) => ` ${link.id} ${link.kind}: ${link.label}\n ${link.target}`).join("\n")
|
|
1031
921
|
// THE QUESTIONS GET THEIR OWN SECTION, because they are not running work: a question waits on a
|
|
1032
|
-
// person.
|
|
1033
|
-
//
|
|
922
|
+
// person. Each is listed by the worker's own id (its fence's `id:`), the one `unask` takes and a
|
|
923
|
+
// reference fence names; a `qst_…` row `frizzctl ask` registered has no other. Since 2026-10-13 no
|
|
924
|
+
// fence has to name them — `questions:` is still read for older contracts, and taught nowhere — so the
|
|
925
|
+
// ready-to-paste fence below no longer carries them.
|
|
926
|
+
const questionId = (q) => String(q?.handle ?? q?.spec?.handle ?? q?.id ?? "")
|
|
1034
927
|
const askedBlock = questions.length === 0 ? "" : (
|
|
1035
928
|
`\n\n${questions.length} question${questions.length === 1 ? "" : "s"} still owed an answer:\n\n` +
|
|
1036
|
-
questions.map((q) => ` question: ${q
|
|
1037
|
-
"\n\nEach one blocks `done` until it is answered or withdrawn
|
|
1038
|
-
"
|
|
1039
|
-
"
|
|
1040
|
-
"under `questions:` or is refused, and at a rest after the one that asked, a question you neither " +
|
|
1041
|
-
"name nor withdraw gets you bumped. In a fence: `questions: [" +
|
|
1042
|
-
questions.map((q) => q.id).join(", ") + "]`"
|
|
929
|
+
questions.map((q) => ` question: ${questionId(q)}\n ${String(q?.spec?.question ?? "").replace(/\s+/g, " ").slice(0, 160)}`).join("\n") +
|
|
930
|
+
"\n\nEach one blocks `done` until it is answered or withdrawn — `frizzctl unask <id>` the ones since " +
|
|
931
|
+
"decided. Its card stays where you asked it, and a later rest owes it nothing; a rest that still needs " +
|
|
932
|
+
"the answer writes a ```question fence carrying its `id:` alone, which draws the card there."
|
|
1043
933
|
)
|
|
1044
934
|
if (!items.length) {
|
|
1045
935
|
if (questions.length > 0) {
|
|
1046
936
|
return (
|
|
1047
937
|
"Nothing is RUNNING on this thread — no background shells, no sub-agents, no armed timers, no " +
|
|
1048
|
-
"registered PRs.
|
|
1049
|
-
"
|
|
938
|
+
"registered PRs. Your open questions are not running work: they need no ```awaiting fence, and a " +
|
|
939
|
+
"rest that still needs an answer references its question instead." +
|
|
1050
940
|
askedBlock + linksBlock
|
|
1051
941
|
)
|
|
1052
942
|
}
|
|
@@ -1054,7 +944,7 @@ async function activity() {
|
|
|
1054
944
|
"Nothing is running on this thread — no background shells, no sub-agents, no armed timers, no " +
|
|
1055
945
|
"registered PRs, and no open questions.\n\nSo there is nothing to wait on: an ```awaiting fence " +
|
|
1056
946
|
"would have nothing to name, and a fence naming nothing is not a park. End with ```done, or " +
|
|
1057
|
-
"
|
|
947
|
+
"ask a ```question if you need the human." + linksBlock
|
|
1058
948
|
)
|
|
1059
949
|
}
|
|
1060
950
|
const lines = items.map((i) => {
|
|
@@ -1073,13 +963,9 @@ async function activity() {
|
|
|
1073
963
|
const block = Object.entries({ shells: byKind.shell, agents: byKind.agent, timers: byKind.timer, prs: byKind.pr, issues: byKind.issue, commands: byKind.command })
|
|
1074
964
|
.filter(([, ids]) => ids.length > 0)
|
|
1075
965
|
.map(([key, ids]) => ` ${key}: [${ids.join(", ")}]`)
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
// a PR, an issue or a timer a watch — and the prose tells the worker to correct it.
|
|
1080
|
-
const questionIds = questions.map((q) => q.id).filter(Boolean)
|
|
1081
|
-
if (questionIds.length > 0) block.push(` questions: [${questionIds.join(", ")}]`)
|
|
1082
|
-
const status = questionIds.length > 0 ? "needs_input" : byKind.shell.length + byKind.agent.length > 0 ? "working" : "watching"
|
|
966
|
+
// The template guesses `status:` from the kinds — a shell or a sub-agent is usually work that finishes
|
|
967
|
+
// by itself, a PR, an issue or a timer a watch — and the prose tells the worker to correct it.
|
|
968
|
+
const status = byKind.shell.length + byKind.agent.length > 0 ? "working" : "watching"
|
|
1083
969
|
return (
|
|
1084
970
|
`${items.length} thing${items.length === 1 ? "" : "s"} running on this thread:\n\n${lines.join("\n")}\n\n` +
|
|
1085
971
|
"Name the ones you are ACTUALLY waiting on in your ```awaiting fence. The frontmatter is YAML — one " +
|
|
@@ -1781,58 +1667,20 @@ async function unwatch(args) {
|
|
|
1781
1667
|
function openQuestionList(result) {
|
|
1782
1668
|
const open = Array.isArray(result?.open) ? result.open : []
|
|
1783
1669
|
if (!open.length) return "Nothing else is open on this thread — the human owes you no answer."
|
|
1784
|
-
const lines = open.map((q) => ` ${q.id} ${(q.spec?.question ?? "").split("\n")[0]}`)
|
|
1670
|
+
const lines = open.map((q) => ` ${q.handle ?? q.spec?.handle ?? q.id} ${(q.spec?.question ?? "").split("\n")[0]}`)
|
|
1785
1671
|
return `Open on this thread now:\n${lines.join("\n")}`
|
|
1786
1672
|
}
|
|
1787
1673
|
|
|
1788
|
-
/** The `ask` handler:
|
|
1789
|
-
* @
|
|
1790
|
-
async function ask(
|
|
1791
|
-
|
|
1792
|
-
const questions = Array.isArray(args.questions) ? args.questions : []
|
|
1793
|
-
if (!questions.length) throw new Error("`questions` is required — at least one question to register")
|
|
1794
|
-
const result = (await callRpc("ask", { slug, questions }))?.result
|
|
1795
|
-
const registered = Array.isArray(result?.registered) ? result.registered : []
|
|
1796
|
-
const lines = registered.map((q) => ` ${q.id} ${(q.spec?.question ?? "").split("\n")[0]}`)
|
|
1797
|
-
const head = registered.length === 1
|
|
1798
|
-
? `Registered 1 question. It is on the human's board now and it will stay there until they answer it.`
|
|
1799
|
-
: `Registered ${registered.length} questions. They are on the human's board now and they will stay ` +
|
|
1800
|
-
"there until answered — the card sends every answer as one batch."
|
|
1801
|
-
return (
|
|
1802
|
-
`${head}\n${lines.join("\n")}\n\n` +
|
|
1803
|
-
"KEEP WORKING. A question waits on a person, carries no timeout and does not end your turn — do " +
|
|
1804
|
-
"everything that does not depend on the answer while it sits there. The answer arrives as its own " +
|
|
1805
|
-
"wake, restating what was asked.\n\n" +
|
|
1806
|
-
"WITHDRAW ONE THE MOMENT IT STOPS MATTERING (`frizzctl unask <id>`), above all if you work the answer out " +
|
|
1807
|
-
`yourself.\n\n${openQuestionList(result)}`
|
|
1808
|
-
)
|
|
1674
|
+
/** The `ask` handler: RETIRED (2026-10-13). It refuses without calling Frizz, and the refusal is the fence.
|
|
1675
|
+
* @returns {Promise<Refused>} */
|
|
1676
|
+
async function ask() {
|
|
1677
|
+
return new Refused(ASK_RETIRED)
|
|
1809
1678
|
}
|
|
1810
1679
|
|
|
1811
|
-
/** The `secret` handler:
|
|
1812
|
-
*
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
async function secret(args) {
|
|
1816
|
-
const slug = threadSlug()
|
|
1817
|
-
const question = typeof args.question === "string" ? args.question.trim() : ""
|
|
1818
|
-
if (!question) throw new Error("`question` is required — say what the human should paste, and for what")
|
|
1819
|
-
const header = typeof args.header === "string" && args.header.trim() ? args.header.trim() : undefined
|
|
1820
|
-
const result = (await callRpc("ask", { slug, questions: [{ question, kind: "question", secret: true, ...(header ? { header } : {}) }] }))?.result
|
|
1821
|
-
const registered = Array.isArray(result?.registered) ? result.registered[0] : undefined
|
|
1822
|
-
const path = typeof registered?.secretPath === "string" ? registered.secretPath : ""
|
|
1823
|
-
if (!registered?.id || !path) throw new Error("Frizz did not return the secret's file — this server predates `secret`; restart Frizz and retry")
|
|
1824
|
-
// Quoted, because the macOS state dir is `~/Library/Application Support/…` and an unquoted path
|
|
1825
|
-
// splits at the space.
|
|
1826
|
-
const quoted = `'${path.replace(/'/g, `'\\''`)}'`
|
|
1827
|
-
return (
|
|
1828
|
-
`Registered secret request ${registered.id}. The human sees a masked box on the board.\n\n` +
|
|
1829
|
-
`Once answered, the value can be read ONCE, within 15 minutes, at:\n ${path}\n\n` +
|
|
1830
|
-
`Read it INSIDE the command that uses it — \`--otp "$(cat ${quoted})"\` — and never cat, echo or copy ` +
|
|
1831
|
-
"it on its own: that spends the one read. Several commands share it through a variable set at the " +
|
|
1832
|
-
"start of ONE Bash call. Get the command ready now: the answer wakes you, and a one-time code expires " +
|
|
1833
|
-
"within a minute.\n\n" +
|
|
1834
|
-
`${openQuestionList(result)}`
|
|
1835
|
-
)
|
|
1680
|
+
/** The `secret` handler: RETIRED with `ask` — a `kind: secret` question fence is how a value is asked for.
|
|
1681
|
+
* @returns {Promise<Refused>} */
|
|
1682
|
+
async function secret() {
|
|
1683
|
+
return new Refused(SECRET_RETIRED)
|
|
1836
1684
|
}
|
|
1837
1685
|
|
|
1838
1686
|
/** The `unask` handler: withdraw one registered question by id.
|
|
@@ -1840,23 +1688,86 @@ async function secret(args) {
|
|
|
1840
1688
|
async function unask(args) {
|
|
1841
1689
|
const slug = threadSlug()
|
|
1842
1690
|
const id = typeof args.id === "string" ? args.id.trim() : ""
|
|
1843
|
-
if (!id) throw new Error("`id` is required —
|
|
1691
|
+
if (!id) throw new Error("`id` is required — the `id:` of the question's fence, or what `frizzctl activity` lists")
|
|
1844
1692
|
const result = (await callRpc("unask", { slug, id }))?.result
|
|
1845
1693
|
// A withdrawal that matched nothing is reported rather than swallowed: the id was wrong, the human
|
|
1846
1694
|
// already answered it, or it is another thread's — and a worker that believes it withdrew a question
|
|
1847
1695
|
// the human is still looking at will get an answer it has stopped expecting.
|
|
1696
|
+
//
|
|
1697
|
+
// A FOLLOW-UP IS NOT WITHDRAWN ON ITS OWN. It rides inside its parent's row, so the server answers
|
|
1698
|
+
// `unask sqlite-path` with the `settings-store` it follows and withdraws nothing: taking the parent
|
|
1699
|
+
// back for an id the worker did not type cost it a question it may still have wanted answered.
|
|
1700
|
+
const root = typeof result?.id === "string" && result.id ? result.id : id
|
|
1701
|
+
const viaFollowUp = root.toLowerCase() !== id.toLowerCase()
|
|
1702
|
+
const settled = {
|
|
1703
|
+
answered: `Question ${root} was already answered, so nothing was withdrawn — the answer reaches you as its own message.`,
|
|
1704
|
+
dismissed: `Question ${root} was already dismissed by the human, so nothing was withdrawn — decide it yourself.`,
|
|
1705
|
+
withdrawn: `Question ${root} was already withdrawn.`,
|
|
1706
|
+
}
|
|
1848
1707
|
const head = result?.withdrawn
|
|
1849
|
-
? `Question ${
|
|
1850
|
-
:
|
|
1708
|
+
? `Question ${root} withdrawn. Its card is gone and the human will not be asked.`
|
|
1709
|
+
: viaFollowUp && result?.state === "open"
|
|
1710
|
+
? `Nothing was withdrawn: ${id} is a follow-up of ${root}, and a follow-up cannot be withdrawn on its own. ` +
|
|
1711
|
+
`The human is only asked it after picking the option it hangs on. To take the whole question back, ` +
|
|
1712
|
+
`follow-ups and all, run \`frizzctl unask ${root}\`.`
|
|
1713
|
+
: settled[result?.state] ??
|
|
1714
|
+
`No OPEN question ${id} on this thread — it was already answered or dismissed, or the id is not one of yours.`
|
|
1851
1715
|
return `${head}\n\n${openQuestionList(result)}`
|
|
1852
1716
|
}
|
|
1853
1717
|
|
|
1718
|
+
/** A body's own leading `title:` block — the key line and any indented lines under it, then a `---`
|
|
1719
|
+
* if one follows — cut off, so `--title` can replace it rather than stack a second heading above it.
|
|
1720
|
+
* The SAME rule as `splitDoneFrontmatter` in @frizz/shared, which this plain .mjs cannot import (it
|
|
1721
|
+
* ships loose beside the server with no build step): only a FIRST non-blank line counts, and a body
|
|
1722
|
+
* that does not open on one comes back untouched.
|
|
1723
|
+
* @param {string} body @returns {string} */
|
|
1724
|
+
function withoutLeadingTitle(body) {
|
|
1725
|
+
const lines = body.split("\n").map((l) => l.replace(/\r$/, ""))
|
|
1726
|
+
let i = 0
|
|
1727
|
+
while (i < lines.length && lines[i].trim() === "") i++
|
|
1728
|
+
if (i >= lines.length || !/^title:/i.test(lines[i])) return body
|
|
1729
|
+
i++
|
|
1730
|
+
while (i < lines.length && /^\s+\S/.test(lines[i])) i++
|
|
1731
|
+
let next = i
|
|
1732
|
+
while (next < lines.length && lines[next].trim() === "") next++
|
|
1733
|
+
if (next < lines.length && /^\s*---+\s*$/.test(lines[next])) i = next + 1
|
|
1734
|
+
return lines.slice(i).join("\n").trim()
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* What an open question does to a sign-off as done: how many are open, each by the worker's own id with
|
|
1739
|
+
* its first line, and the two ways out.
|
|
1740
|
+
*
|
|
1741
|
+
* A COPY, AND IT MUST STAY ONE. The same paragraph is sent by the server when a worker signs off with a
|
|
1742
|
+
* ```done FENCE over an open question (scheduler SOURCE 15), built by the function of this name in
|
|
1743
|
+
* `packages/shared/src/index.ts`. This file imports nothing but node, because it ships to a worker as
|
|
1744
|
+
* plain .mjs, so the words live in both places; `packages/server/src/question-done.test.ts` fails when
|
|
1745
|
+
* the two outputs differ. Change one, change the other.
|
|
1746
|
+
* @param {{ id: string, question?: string }[]} questions @returns {string} */
|
|
1747
|
+
function doneOpenQuestionsRefusal(questions) {
|
|
1748
|
+
const lines = questions.map((q) => ` ${q.id} ${(q.question ?? "").split("\n")[0]}`)
|
|
1749
|
+
return (
|
|
1750
|
+
`${lines.length} question${lines.length === 1 ? "" : "s"} the human has not answered:\n${lines.join("\n")}\n` +
|
|
1751
|
+
"Each one dies unread with a done card. Decide it yourself and withdraw it (`frizzctl unask <id>`), or leave it " +
|
|
1752
|
+
"open and keep working until it is answered."
|
|
1753
|
+
)
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1854
1756
|
/** The `done` handler: declare the effort finished, or report exactly what refuses to let it.
|
|
1757
|
+
*
|
|
1758
|
+
* A `--title` is sent INSIDE the body, in the ```done fence's own grammar (`title: …`, `---`, then the
|
|
1759
|
+
* ledger), rather than as a field of its own: the server stores one body and splits it with the same
|
|
1760
|
+
* function that splits a fence, so a registered done and a fenced one can never head their cards two
|
|
1761
|
+
* different ways (2026-10-13).
|
|
1855
1762
|
* @param {Record<string, unknown>} args @returns {Promise<string>} */
|
|
1856
1763
|
async function done(args) {
|
|
1857
1764
|
const slug = threadSlug()
|
|
1858
|
-
|
|
1765
|
+
let body = typeof args.body === "string" ? args.body.trim() : ""
|
|
1859
1766
|
if (!body) throw new Error("`body` is required — the write-up the human reads on the card")
|
|
1767
|
+
// One line, because a heading is one: a title with a newline in it would end the `title:` line and
|
|
1768
|
+
// spill the rest into the ledger.
|
|
1769
|
+
const title = typeof args.title === "string" ? args.title.replace(/\s+/g, " ").trim() : ""
|
|
1770
|
+
if (title) body = `title: ${title}\n---\n${withoutLeadingTitle(body)}`
|
|
1860
1771
|
const result = (await callRpc("markOwnDone", { slug, body }))?.result
|
|
1861
1772
|
if (result?.done) {
|
|
1862
1773
|
return (
|
|
@@ -1867,16 +1778,10 @@ async function done(args) {
|
|
|
1867
1778
|
}
|
|
1868
1779
|
// REFUSED, with everything that refuses it named by id, so the next move is a command and not a
|
|
1869
1780
|
// guess. Reported as an ordinary result rather than thrown: this is a gate doing its job, not a fault.
|
|
1870
|
-
const questions = (result?.blockingQuestions ?? []).map((q) =>
|
|
1781
|
+
const questions = (result?.blockingQuestions ?? []).map((q) => ({ id: q.handle ?? q.id, question: q.question }))
|
|
1871
1782
|
const watches = (result?.blockingWatches ?? []).map((w) => ` ${w.id} ${w.what}`)
|
|
1872
1783
|
const parts = ["NOT marked done. This thread still holds work open."]
|
|
1873
|
-
if (questions.length)
|
|
1874
|
-
parts.push(
|
|
1875
|
-
`${questions.length} question${questions.length === 1 ? "" : "s"} the human has not answered:\n${questions.join("\n")}\n` +
|
|
1876
|
-
"Each one dies unread with a done card. Decide it yourself and withdraw it (`frizzctl unask <id>`), or leave it " +
|
|
1877
|
-
"open and keep working until it is answered.",
|
|
1878
|
-
)
|
|
1879
|
-
}
|
|
1784
|
+
if (questions.length) parts.push(doneOpenQuestionsRefusal(questions))
|
|
1880
1785
|
if (watches.length) {
|
|
1881
1786
|
parts.push(
|
|
1882
1787
|
`${watches.length} registration${watches.length === 1 ? "" : "s"} still armed:\n${watches.join("\n")}\n` +
|
|
@@ -2091,42 +1996,31 @@ async function watchCommand(args) {
|
|
|
2091
1996
|
* Every argument is ALSO a `--flag`, and the whole set a JSON object through `--input`.
|
|
2092
1997
|
* then — further positionals that depend on the first one's value, for the commands that
|
|
2093
1998
|
* switch on an `action` (`frizzctl timer cancel tmr_ab12`).
|
|
2094
|
-
* wrap — the one array argument a bare JSON array, or a single item, is taken to be
|
|
2095
|
-
*
|
|
1999
|
+
* wrap — the one array argument a bare JSON array, or a single item, is taken to be.
|
|
2000
|
+
* retired — the text a retired command prints instead of running: its refusal, and its `--help`.
|
|
2096
2001
|
* examples — real invocations, shown under the command's own help.
|
|
2097
2002
|
*
|
|
2098
2003
|
* `group` only orders the top-level listing.
|
|
2099
2004
|
*/
|
|
2100
2005
|
const CLI = {
|
|
2006
|
+
// RETIRED (2026-10-13): a question is a ```question fence. `retired` is the text the command prints in
|
|
2007
|
+
// place of running — as its refusal (exit 2) and as its `--help` — and it keeps the command out of the
|
|
2008
|
+
// top-level listing.
|
|
2101
2009
|
ask: {
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
examples: [
|
|
2106
|
-
"frizzctl ask --input - <<'JSON'\n" +
|
|
2107
|
-
'{"questions": [{\n' +
|
|
2108
|
-
' "question": "Should the settings store use SQLite or a JSON file?",\n' +
|
|
2109
|
-
' "kind": "question",\n' +
|
|
2110
|
-
' "options": [\n' +
|
|
2111
|
-
' {"label": "SQLite", "description": "transactional, matches how sessions are stored", "recommended": true},\n' +
|
|
2112
|
-
' {"label": "JSON file", "description": "zero deps, human-editable, racy under concurrent writes"}\n' +
|
|
2113
|
-
" ]\n" +
|
|
2114
|
-
"}]}\n" +
|
|
2115
|
-
"JSON",
|
|
2116
|
-
"frizzctl ask --input questions.json",
|
|
2117
|
-
],
|
|
2010
|
+
summary: "Retired: a question is a ```question fence in your own message",
|
|
2011
|
+
retired: ASK_RETIRED,
|
|
2012
|
+
examples: [],
|
|
2118
2013
|
},
|
|
2119
2014
|
unask: {
|
|
2120
2015
|
group: "Sign off",
|
|
2121
|
-
summary: "Withdraw a question you asked",
|
|
2016
|
+
summary: "Withdraw a question you asked, by its id",
|
|
2122
2017
|
positional: ["id"],
|
|
2123
|
-
examples: ["frizzctl unask
|
|
2018
|
+
examples: ["frizzctl unask settings-store"],
|
|
2124
2019
|
},
|
|
2125
2020
|
secret: {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
examples: ['frizzctl secret "The npm one-time code for publishing frizz 0.14.0" --header "npm OTP"'],
|
|
2021
|
+
summary: "Retired: a value only the human holds is a `kind: secret` question fence",
|
|
2022
|
+
retired: SECRET_RETIRED,
|
|
2023
|
+
examples: [],
|
|
2130
2024
|
},
|
|
2131
2025
|
done: {
|
|
2132
2026
|
group: "Sign off",
|
|
@@ -2138,6 +2032,7 @@ const CLI = {
|
|
|
2138
2032
|
"- **Added a regression test**; `npm test` green.\n" +
|
|
2139
2033
|
"EOF",
|
|
2140
2034
|
"frizzctl done --body-file handoff.md",
|
|
2035
|
+
"frizzctl done --title \"Resolver cache keyed on the normalized id\" --body-file handoff.md",
|
|
2141
2036
|
],
|
|
2142
2037
|
},
|
|
2143
2038
|
activity: {
|
|
@@ -2238,4 +2133,4 @@ const CLI = {
|
|
|
2238
2133
|
},
|
|
2239
2134
|
}
|
|
2240
2135
|
|
|
2241
|
-
export { TOOLS, HANDLERS, CLI, Refused }
|
|
2136
|
+
export { TOOLS, HANDLERS, CLI, Refused, doneOpenQuestionsRefusal }
|