Formularz ram polecenia na start (/brief): repo/folder, stawka, tryb, „gotowe, gdy”, recall, granice → składa polecenie po polsku i wkłada je do pola promptu…

/brief, v0.1)Mod Claude Code (API „function hooks”, early access; pisany pod 2.1.299, testy zielone na desktop 2.1.299 i CLI 2.1.289). Plan: claude-kanon/claude-code/mods/PLAN-mody-2026-10-16-z-logow.md §3.2. Z logów 42 transkryptów: 23 z 74 poleceń poniżej 80 znaków, 6 z 17 startów nazywa repo, 9 dopytań AskUserQuestion i 7 odpowiedzi „numerami” w osobnych turach, ops-recall 3 razy na 17 startów. Odpowiednik modu „Prompt Upgrade” z filmu O'Briena, ale bez modelu: formularz jest deterministyczny i darmowy.
/brief [tekst]: panel z formularzem — Polecenie (tekst z argumentu komendy), Repo/folder (domyślnie cwd sesji, pod nim przyciski podkatalogów pierwszego poziomu: bez ukrytych i node_modules, posortowane, 12 przycisków, reszta jako „+N więcej — wpisz ścieżkę”; dowiązanie symboliczne ma kind: 'other' i przycisku nie dostaje), Stawka niska / istotna / wysoka (wysoka dopisuje „analizę oddziel od mutacji, zgoda na każdy zapis”), Tryb sam / subagent / workflow, „Gotowe, gdy”, Recall ([ ] najpierw /ops-recall), Granice (bez zapisu na produkcję, bez PII do gita, bez pusha). Drugie naciśnięcie radia zdejmuje wybór.$.prompt.fill, tryb replace — kasuje szkic w polu) — Enter naciska właściciel. Odmowa no_composer (powierzchnia bez pola promptu: headless albo rysująca własny composer) → od razu schowek i toast „wklej”; dialog albo przyczyna nieznana → panel wraca wypełniony, toast kieruje do „Kopiuj”. Błąd w trakcie (np. $.ui.close odrzucone) → toast z błędem, panel wraca, jeśli zdążył się zamknąć.$.ui.copy); pole promptu nietknięte, panel zostaje.$.session.turns() = 0), krótszym niż 200 znaków, wpisanym przez osobę: „Krótkie polecenie na start — /brief doda repo, stawkę i kryterium „gotowe””. Raz na sesję (flaga w $.state); polecenie przechodzi niezmienione — prompt.submit tylko obserwuje (decyzja właściciela 2026-10-16; { drop } i przechwycenie Enter dopiero w v0.2, po dwóch tygodniach użycia). Bez toastu: komenda slash, isMeta, sesja wznowiona (turns > 0), polecenie pluginu, peera, notyfikacji zadania, harmonogramu, kanału (lista wykluczeń NIE_OSOBA), a także po /brief w tej sesji — więc i dla polecenia, które mod sam złożył (krótkie z ramami bywa poniżej 200 znaków).$.store klucz brief:<cwd> → { repo, stawka, tryb, recall, granice }; następny /brief w tym folderze wypełnia formularz tymi polami (puste repo z pamięci → cwd). /brief wyczysc (też wyczyść) kasuje wpis bieżącego folderu i zamyka formularz (otwarty zapisałby wpis z powrotem przy „Uruchom”); /brief zamknij zamyka panel. Wpis spoza wzorca (ręczna edycja pliku store) — pola nieznane pominięte, formularz działa.$.state i odświeża podgląd ramy:; przyciski czytają szkic nad stanem, więc tekst bez Entera też trafia do polecenia. Przeładowanie modu gubi tylko szkice.napraw test brief.test.ts
Ramy:
- Repo/folder: /Users/ksdevit/Developer/claude-mods/kanon-brief
- Stawka: wysoka — analizę oddziel od mutacji, zgoda na każdy zapis
- Tryb: sam
- Gotowe, gdy: claude plugin test . zielone na obu binarkach
- Recall: najpierw /ops-recall
- Granice: bez zapisu na produkcję, bez pusha
Sam tekst bez zaznaczeń → polecenie bez bloku „Ramy:”; same ramy bez tekstu → sam blok.
NIE_OSOBA) zamiast listy osób. Typy 2.1.299 wymieniają composer, bridge, sdk, unclassified…, a transkrypty desktopu (2.1.299) stemplują polecenie osoby jako "kind":"human", którego w typach nie ma; nowy albo niestemplowany origin liczy się jak osoba (toast u nikogo nie szkodzi, brak toastu u osoby psuje sens modu).Input → zamiast formularza jedna linia z wyjaśnieniem (desktop albo terminal).--plugin-dir, 2026-10-16/17)/brief napraw test → panel jako kolumna boczna ~50 znaków (zrzut niżej). Pierwszy rysunek ucinał wiersz „Granice:” do „G” i gubił trzeci przełącznik, a gap dokładał pusty wiersz między zawiniętymi rzędami podkatalogów → wiersze mają flexWrap="wrap" i columnGap. Etykietę Gotowe, gdy wąski panel łamie na dwie linie (rysuje to Input silnika).$.ui.log przy prompt.submit): pierwsze polecenie sesji → turns=0, origin={"kind":"composer"}, wait=false, command i isMeta undefined — warunki toastu zachodzą. Toast narysowany (zrzut niżej).kanon-brief: reloaded (4 hooks: session.start, prompt.submit, command.run, ui.render), otwarty panel przerysowany nowym kodem; silnik położył .claude-plugin/types/ (z własnym .gitignore), więc tsc -p kanon-brief działa.Zrzut panelu (tekst z panelu terminala aplikacji):
│Brief — ramy polecenia na start
│Polecenie: napraw test
│Repo/folder: /Users/ksdevit/Developer
│podkatalogi: _kopie BrandSignal
│chase-ai-materialy claude-canon claude-figma
│claude-kanon claude-masterclass-ola claude-mods
│claude-mods-stabilne claude-remotion clients
│cv-2026
│Stawka: ( ) niska ( ) istotna ( ) wysoka
│Tryb: ( ) sam ( ) subagent ( ) workflow
│Gotowe, kryterium sprawdzalne komendą albo w …
│gdy:
│[ ] najpierw /ops-recall
│Granice: [ ] bez zapisu na produkcję
│[ ] bez PII do gita [ ] bez pusha
│[ Uruchom ] [ Kopiuj ] [ Pomiń ]
│ramy: Repo/folder: /Users/ksdevit/Developer
Zrzut toastu (po claude … "odpowiedz jednym slowem: tak" jako pierwszym poleceniu sesji):
╭──────────────────────────────────────────╮
│ kanon-brief │
│ Krótkie polecenie na start — /brief doda │
│ repo, stawkę i kryterium „gotowe” │
╰──────────────────────────────────────────╯
~/.claude/dev-mods/<session_id>/kanon-brief/ czeka na zgodę właściciela na hot reload (pytanie przy końcu tury). Do zobaczenia tam: rysunek Pane na desktopie, $.prompt.fill po zamknięciu panelu z fokusem (w kicie: tak), schowek $.ui.copy, naciśnięcia przycisków i zapis $.store po „Uruchom” (w kicie: tak; w terminalu panelu model nie wciska klawiszy).human z transkryptów) — lista wykluczeń przepuszcza go bez sprawdzenia na żywo; sonda sonda-brief w dev-mods tej sesji zapisuje do swojego $.store, co hook widzi przy kolejnych poleceniach właściciela.$.prompt.fill działa na desktopie: typy opisują no_composer jako „a surface that draws its own composer” — jeśli to desktop, „Uruchom” kończy się schowkiem (toast mówi, że trzeba wkleić), a głównym przyciskiem desktopu jest w praktyce „Kopiuj”.Zero $.model, $.http, $.process, $.fs.write, bez tool.check; $.fs.list czyta tylko nazwy wpisów cwd. Jedyny stan między sesjami: $.store brief:<cwd> bez tekstu polecenia i bez „gotowe”. Hooki session.start i prompt.submit: try/catch w środku i .catch → next(e) (fail-open — błąd modu nie zatrzymuje polecenia ani startu sesji); command.run odpowiada za własną komendę bez next i ma try/catch (błąd = tekst Brief: błąd modu — … w odpowiedzi komendy; walidator i tak wypisuje go jako gating hook without .catch — fakt, nie ostrzeżenie). Mod nie pisze plików i nie wysyła nic poza sesję.
Linie walidatora (claude plugin validate, 2.1.299):
hooks: session.start, prompt.submit, command.run{command=brief}, ui.render{component=Pane, requestId=brief}
gating hook with .catch: prompt.submit
gating hook without .catch: command.run{command=brief}
calls: $.command.register, $.fs.list (via otworz), $.prompt.fill (via uruchom), $.session.cwd, $.session.turns, $.state.get, $.state.set, $.store.delete, $.store.get (via otworz), $.store.set (via zapamietaj), $.ui.close, $.ui.copy (via kopiuj, uruchom), $.ui.open (via otworzPanel), $.ui.resolve, $.ui.toast
state writes: kanon-brief.cwd, kanon-brief.podkatalogi, kanon-brief.polecenie, kanon-brief.ramy, kanon-brief.toast
claude plugin test . — 25 testów, zielone na desktop 2.1.299 i CLI 2.1.289 (silnik odegrany hookami testu: store jako Mapa, cwd/turns, fs.list, ui.*, prompt.fill; brzmienia ze spec jako literały w asercjach): zloz() (tylko wypełnione pola, sam tekst, sam blok, nic, białe znaki); toast raz w turze 1 i cisza przy drugim krótkim, wejście dochodzi do dna bez dopisków; bez toastu przy ≥ 200 znakach (granica 199/200), komendzie slash i command, isMeta, pustym tekście, turns = 3, originach nie-osoby (task-notification, plugin, peer, peer-send-message, scheduled-trigger, auto-continuation, coordinator, channel); toast dla human (spoza typów), undefined, bridge, sdk; brak toastu na poleceniu złożonym przez mod i po /brief; formularz na terminal i desktop (podkatalogi, złożenie, prompt.fill replace, panel zamknięty, dokładna treść wpisu store); Enter w polu → stan i podgląd, szkic bez Entera wygrywa, wyczyszczenie Enterem zeruje; Kopiuj (schowek z surface, prompt nietknięty, pamięć zapisana, odmowa schowka → toast z powodem); Pomiń (bez fill, bez schowka, bez zapisu); Uruchom bez tekstu → toast, Kopiuj bez niczego → toast; pamięć per folder, inny folder pusty, puste repo → cwd, /brief wyczysc kasuje i zamyka, alias wyczyść, /brief zamknij; wpis store spoza wzorca; odmowa prompt.fill (panel wraca, toast do „Kopiuj”); $.ui.close rzuca (toast z błędem, bez fill, bez ponownego otwarcia); fail-open ($.session.turns rzuca → polecenie przechodzi; $.session.cwd rzuca → /brief odpowiada tekstem błędu); $.fs.list pada (formularz bez podkatalogów) i panel bez miejsca (isPlaced: false → odpowiedź z powodem);
12 podkatalogów („+3 więcej”,
node_modulespominięte); aplikacja mobilna. Type-check:tsc6.0.3 strict (tsc -p kanon-brief
po pierwszym załadowaniu modu; wcześniej tsconfig z nagłówka pliku typów skilla, trzymany poza folderem modu).
Luki kitu: $.prompt.submit z $ testu bierze pełne wejście zdarzenia (wait, origin), które w sesji stempluje silnik — helper wpisz w teście; refusal z odpowiedzi hooka prompt.fill silnik zdejmuje (typy: „a hook's own refusal carries none”) — gałęzie dialog i no_composer poza zasięgiem kitu (testowana gałąź „przyczyna nieznana”); rzucające dno kit zgłasza jako „no implementation for <noun.method>”, nie treścią błędu.
Naprawione (🟡): toast odpalał się na poleceniu złożonym przez sam mod → flaga toastu ustawiana przy /brief; uruchom/kopiuj bez try/catch (odrzucone $.ui.close/$.prompt.fill/$.ui.copy = cisza) → try/catch z toastem, panel wraca; strażnik „nic do złożenia” martwy przy domyślnym repo = cwd → „Uruchom” wymaga tekstu; no_composer → od razu schowek; .slice(0, 12) ucinało listę bez śladu → „+N więcej”, node_modules pominięte; testy importowały brzmienia z kodu → literały; brak granicy 199/200, isMeta, command, pozostałych originów, fail-open, Entera w polu, pamięci przy Kopiuj, odmowy schowka, zamknij, aliasu → dodane (15 → 25). 🟢: surowy kod odmowy w toaście → po polsku; puste repo z pamięci nadpisywało cwd → zapis.repo || folder; wyczysc przy otwartym panelu → zamyka panel; mock refusal mylił → usunięty.
Zostawione — decyzje właściciela:
$.store rośnie o wpis (~150 B) na folder bez limitu; limit sklepu 4 MiB ≈ 25 000 folderów. Bez sprzątania w v0.1 (/brief wyczysc per folder). Jeśli chcieć limitu: liczba kluczy brief:* + usuwanie najstarszych (wpis musiałby dostać znacznik czasu)./brief <tekst> gubi tekst z linii komendy (komenda zjadła prompt). Plan §3.2: „Pomiń → oryginał z powrotem”; zakres v0.1 z briefu: „Pomiń bez dotykania promptu” — zostaje v0.1. Alternatywa: Pomiń wkłada sam tekst (bez ram) do pola promptu.unclassified (turn bez rozpoznanego pochodzenia, w tym harmonogram i /loop) liczy się jak osoba — toast w nieobserwowanej sesji nie szkodzi; do zmiany, gdy sonda pokaże, że desktop stempluje polecenie osoby jako composer/sdk.kind: 'other') bez przycisku — ścieżkę wpisuje się ręcznie.Odrzucone: szkice jako zmienna modułu nie przeciekają między sesjami — każda sesja to osobny proces claude (zmienna modułu = per proces); append zamiast replace — tryb replace jest w zakresie v0.1, skutek opisany wyżej.
Próba w jednej sesji: claude --plugin-dir ~/Developer/claude-mods/kanon-brief (terminal) albo kopia w ~/.claude/dev-mods/<session_id>/ (desktop, hot reload za zgodą). Na stałe: ~/Developer/claude-mods-stabilne/kanon-brief w env.CLAUDE_CODE_PLUGIN_DIRS (~/.claude/settings.json, z backupem), od NOWEJ sesji. Rollback: usunięcie ścieżki z env (od nowej sesji); dane modu: rm -f ~/.claude/plugins/store/kanon-brief*.
hooks/register.tsx 356 lines1// kanon-brief — formularz ram polecenia na start (mod Claude Code, API function hooks 2.1.299; early access). v0.1.
2// Z logów 42 transkryptów (plan modów 2026-10-16, §1 i §3.2): 23 z 74 poleceń poniżej 80 znaków, 6 z 17 startów nazywa repo,
3// 9 dopytań AskUserQuestion i 7 odpowiedzi „numerami” w osobnych turach, `ops-recall` 3 razy na 17 startów. Odpowiednik modu
4// „Prompt Upgrade” z filmu O'Briena, ale BEZ modelu: formularz jest deterministyczny i darmowy — składa polecenie po polsku
5// z pól, które właściciel wypełnił (tekst + blok „Ramy:”), i wkłada je do pola promptu (`$.prompt.fill`, tryb `replace`);
6// Enter naciska właściciel. „Kopiuj” → schowek (`$.ui.copy`), „Pomiń” → zamyka formularz bez dotykania pola promptu.
7// v0.1 (decyzja właściciela 2026-10-16): `prompt.submit` tylko OBSERWUJE — jeden toast na sesję, w turze 1, gdy polecenie z klawiatury
8// ma mniej niż 200 znaków; polecenie przechodzi niezmienione. `{ drop }` i przechwycenie Enter dopiero w v0.2 po dwóch tygodniach użycia.
9// Pamięć ram per folder w `$.store` (klucz `brief:<cwd>`): repo, stawka, tryb, recall, granice — BEZ tekstu polecenia i bez
10// „gotowe, gdy” (to treść zadania, nie folderu; dane modów bez treści rozmów). `/brief wyczysc` kasuje wpis bieżącego folderu.
11// Pola tekstowe: każde naciśnięcie klawisza idzie do szkicu w zmiennej modułu (bez przerysowania panelu), Enter w polu zapisuje do
12// `$.state` (przerysowanie, podgląd ram); przyciski czytają szkic nad stanem, więc tekst bez Entera też trafia do polecenia.
13// Bramki: zero `$.model`, zero `$.http`, bez `tool.check`, bez zapisu plików (`$.fs.list` tylko czyta cwd); jedyny stan między
14// sesjami to `$.store`. Hooki zdarzeń mają try/catch i `.catch` → `next(e)`: błąd modu nie zatrzymuje polecenia ani startu sesji.
15import { atom, read, update } from 'claude-code'
16import type { EngineInterface, PromptOrigin, Register, RenderSurface } from 'claude-code'
17
18import type { Granice, Ramy, Stawka, Tryb } from '../types'
19
20const PANE = 'brief'
21const PROG_TOASTU = 200 // znaków; poniżej — toast w turze 1 (plan §3.2)
22const MAX_PODKATALOGOW = 12 // przycisków; dłuższa lista = więcej rzędów niż wart jest formularz (reszta: „+N więcej”)
23const POMIJANE_KATALOGI: ReadonlySet<string> = new Set(['node_modules']) // nigdy nie jest „repo”
24const WIERSZE_PANELU = 14 // prośba o wysokość inline (silnik i właściciel mogą dać inną)
25/** Originy, które NIE są poleceniem wpisanym przez osobę (typy 2.1.299: PromptOrigin). Lista wykluczeń, nie dopuszczeń: nowy albo
26 * niestemplowany origin (starsza binarka, zestaw testowy, kanał bez stempla) liczy się jak osoba — toast u nikogo nie szkodzi,
27 * a brak toastu u osoby psuje sens moda. `sdk` zostaje dopuszczony: tak może przychodzić prompt z aplikacji desktop. */
28const NIE_OSOBA: ReadonlySet<string> = new Set([
29 'plugin', 'task-notification', 'scheduled-trigger', 'peer', 'peer-send-message', 'projects-relay', 'channel',
30 'coordinator', 'observer', 'observer-activity', 'auto-continuation', 'slack-ping',
31])
32
33// Skaner walidatora wymaga literałów plugin/key w każdym atomie (nie stałej) — stąd powtórzona nazwa.
34const PUSTE_RAMY: Ramy = { repo: '', stawka: null, tryb: null, gotowe: '', recall: false, granice: { produkcja: false, pii: false, push: false } }
35const ramy = atom({ plugin: 'kanon-brief', key: 'ramy' } as const, PUSTE_RAMY)
36const polecenie = atom({ plugin: 'kanon-brief', key: 'polecenie' } as const, '')
37const cwd = atom({ plugin: 'kanon-brief', key: 'cwd' } as const, '')
38const podkatalogi = atom({ plugin: 'kanon-brief', key: 'podkatalogi' } as const, [] as string[])
39const toast = atom({ plugin: 'kanon-brief', key: 'toast' } as const, false)
40
41const STAWKI: readonly Stawka[] = ['niska', 'istotna', 'wysoka']
42const TRYBY: readonly Tryb[] = ['sam', 'subagent', 'workflow']
43const GRANICE: readonly { klucz: keyof Granice; opis: string }[] = [
44 { klucz: 'produkcja', opis: 'bez zapisu na produkcję' },
45 { klucz: 'pii', opis: 'bez PII do gita' },
46 { klucz: 'push', opis: 'bez pusha' },
47]
48export const DOPISEK_WYSOKIEJ = 'analizę oddziel od mutacji, zgoda na każdy zapis'
49export const DOPISEK_RECALL = 'najpierw /ops-recall'
50export const TOAST_STARTU = 'Krótkie polecenie na start — /brief doda repo, stawkę i kryterium „gotowe”'
51
52export const kluczPamieci = (folder: string) => `brief:${folder}`
53
54/** Linie bloku „Ramy:” — tylko pola wypełnione, w stałej kolejności formularza. */
55export const linieRam = (r: Ramy): string[] => {
56 const l: string[] = []
57 if (r.repo.trim()) l.push(`Repo/folder: ${r.repo.trim()}`)
58 if (r.stawka) l.push(`Stawka: ${r.stawka}${r.stawka === 'wysoka' ? ` — ${DOPISEK_WYSOKIEJ}` : ''}`)
59 if (r.tryb) l.push(`Tryb: ${r.tryb}`)
60 if (r.gotowe.trim()) l.push(`Gotowe, gdy: ${r.gotowe.trim()}`)
61 if (r.recall) l.push(`Recall: ${DOPISEK_RECALL}`)
62 const g = GRANICE.filter(x => r.granice[x.klucz]).map(x => x.opis)
63 if (g.length) l.push(`Granice: ${g.join(', ')}`)
64 return l
65}
66
67/** Złożone polecenie: tekst, pusta linia, blok ram (tylko wypełnione pola). Sam tekst bez ram, sam blok bez tekstu; '' gdy nic nie ma. */
68export const zloz = (tekst: string, r: Ramy): string => {
69 const t = tekst.trim()
70 const l = linieRam(r)
71 if (!l.length) return t
72 const blok = ['Ramy:', ...l.map(x => `- ${x}`)].join('\n')
73 return t ? `${t}\n\n${blok}` : blok
74}
75
76/** Wpis $.store → pola ram; wartości spoza wzorca (ręczna edycja, starsza wersja modu) są pomijane, nie psują formularza. */
77const zPamieci = (v: unknown): Partial<Ramy> => {
78 if (!v || typeof v !== 'object') return {}
79 const o = v as Record<string, unknown>
80 const r: Partial<Ramy> = {}
81 if (typeof o.repo === 'string') r.repo = o.repo
82 if (STAWKI.includes(o.stawka as Stawka)) r.stawka = o.stawka as Stawka
83 if (TRYBY.includes(o.tryb as Tryb)) r.tryb = o.tryb as Tryb
84 if (typeof o.recall === 'boolean') r.recall = o.recall
85 const g = o.granice
86 if (g && typeof g === 'object') {
87 const gg = g as Record<string, unknown>
88 r.granice = { produkcja: gg.produkcja === true, pii: gg.pii === true, push: gg.push === true }
89 }
90 return r
91}
92/** Co idzie do $.store: ramy folderu bez tekstu polecenia i bez „gotowe, gdy”. */
93const doPamieci = (r: Ramy) => ({ repo: r.repo, stawka: r.stawka, tryb: r.tryb, recall: r.recall, granice: r.granice })
94
95// Szkice pól tekstowych między naciśnięciem klawisza a Enterem/przyciskiem (zmienna modułu: reload modu je gubi, stan nie).
96type Pole = 'polecenie' | 'repo' | 'gotowe'
97// Zmienna modułu = per proces; każda sesja (także desktop) to osobny proces `claude`, więc szkice nie przeciekają między sesjami.
98const szkice = new Map<Pole, string>()
99
100/** Wartości pól do złożenia: szkic (tekst wpisany bez Entera) nad stanem. */
101async function zebrane($: EngineInterface): Promise<{ tekst: string; r: Ramy }> {
102 const [r, p] = await Promise.all([read($, ramy), read($, polecenie)])
103 return { tekst: szkice.get('polecenie') ?? p, r: { ...r, repo: szkice.get('repo') ?? r.repo, gotowe: szkice.get('gotowe') ?? r.gotowe } }
104}
105
106const otworzPanel = ($: EngineInterface) => $.ui.open({ id: PANE, title: 'Brief', focus: true, closeOnEscape: true, rows: WIERSZE_PANELU })
107
108/** `/brief [tekst]`: pola z pamięci folderu (gdy jest) nad pustymi, repo = cwd, podkatalogi z `$.fs.list`; otwiera panel. */
109async function otworz($: EngineInterface, tekst: string): Promise<string> {
110 const folder = await $.session.cwd()
111 let lista: string[] = []
112 try {
113 // `kind: 'dir'` — dowiązanie symboliczne do katalogu ma `kind: 'other'` i przycisku nie dostaje (ścieżkę wpisuje się ręcznie).
114 lista = (await $.fs.list(folder))
115 .filter(e => e.kind === 'dir' && !e.name.startsWith('.') && !POMIJANE_KATALOGI.has(e.name))
116 .map(e => e.name)
117 .sort((a, b) => a.localeCompare(b))
118 } catch {
119 lista = [] // folder nieczytelny albo zdalny — formularz bez przycisków, repo wpisuje się ręcznie
120 }
121 const zapis = zPamieci(await $.store.get(kluczPamieci(folder)).catch(() => undefined))
122 szkice.clear()
123 await update($, cwd, () => folder)
124 await update($, podkatalogi, () => lista)
125 await update($, ramy, () => ({ ...PUSTE_RAMY, ...zapis, repo: zapis.repo || folder })) // puste repo z pamięci → cwd
126 await update($, polecenie, () => tekst)
127 await update($, toast, () => true) // kto otworzył /brief, podpowiedzi o /brief nie potrzebuje (także dla polecenia, które mod złoży)
128 const o = await otworzPanel($)
129 const skad = Object.keys(zapis).length ? ', pola z pamięci folderu' : ''
130 return o.isPlaced ? `Formularz ram otwarty (${folder}${skad}).` : `Formularz ram czeka na szersze okno: ${o.reason}`
131}
132
133async function zapamietaj($: EngineInterface, r: Ramy): Promise<void> {
134 const folder = await read($, cwd)
135 if (folder) await $.store.set(kluczPamieci(folder), doPamieci(r))
136}
137
138const opisBledu = (err: unknown) => (err instanceof Error ? err.message : String(err))
139
140/** „Uruchom”: złożone polecenie do pola promptu (replace); panel zamknięty PRZED fill, bo otwarty z `focus` trzyma klawiaturę.
141 * Wymaga tekstu polecenia — same ramy (repo domyślnie = cwd) to nie zadanie. `no_composer` (powierzchnia bez pola promptu:
142 * headless albo rysująca własny composer) → od razu schowek; `dialog` albo przyczyna nieznana → panel wraca, „Kopiuj”. */
143async function uruchom($: EngineInterface, surface: RenderSurface): Promise<void> {
144 let zamkniety = false
145 try {
146 const { tekst, r } = await zebrane($)
147 if (!tekst.trim()) {
148 $.ui.toast('Brief: wpisz polecenie — same ramy to nie zadanie')
149 return
150 }
151 const calosc = zloz(tekst, r)
152 await zapamietaj($, r)
153 await $.ui.close({ id: PANE })
154 zamkniety = true
155 const f = await $.prompt.fill({ text: calosc, mode: 'replace' })
156 if (f.isFilled) {
157 $.ui.toast('Brief: polecenie w polu promptu — Enter wysyła')
158 return
159 }
160 if (f.refusal === 'no_composer') {
161 const c = await $.ui.copy({ text: calosc, surface })
162 $.ui.toast(
163 c.isCopied
164 ? 'Brief: ta powierzchnia nie ma pola promptu — polecenie w schowku, wklej je'
165 : `Brief: ta powierzchnia nie ma pola promptu, a schowek odmówił (${c.reason})`,
166 { timeoutMs: 8000 },
167 )
168 return
169 }
170 await otworzPanel($) // stan został, formularz wraca wypełniony
171 $.ui.toast(
172 f.refusal === 'dialog'
173 ? 'Brief: okno dialogowe trzyma klawiaturę — zamknij je albo użyj „Kopiuj”'
174 : 'Brief: pole promptu nie przyjęło tekstu — użyj „Kopiuj”',
175 { timeoutMs: 8000 },
176 )
177 } catch (err) {
178 $.ui.toast(`Brief: błąd — ${opisBledu(err)}; użyj „Kopiuj”`, { timeoutMs: 8000 })
179 if (zamkniety) await otworzPanel($).catch(() => undefined)
180 }
181}
182
183async function kopiuj($: EngineInterface, surface: RenderSurface): Promise<void> {
184 try {
185 const { tekst, r } = await zebrane($)
186 const calosc = zloz(tekst, r)
187 if (!calosc) {
188 $.ui.toast('Brief: nic do skopiowania — wpisz polecenie albo zaznacz pola')
189 return
190 }
191 await zapamietaj($, r)
192 const c = await $.ui.copy({ text: calosc, surface })
193 $.ui.toast(c.isCopied ? 'Brief: polecenie w schowku' : `Brief: schowek nie przyjął tekstu (${c.reason})`)
194 } catch (err) {
195 $.ui.toast(`Brief: błąd kopiowania — ${opisBledu(err)}`, { timeoutMs: 8000 })
196 }
197}
198
199export const register: Register = on => {
200 on('session.start', async ($, e, next) => {
201 await $.command.register({
202 name: 'brief',
203 description: 'Formularz ram polecenia: repo/folder, stawka, tryb, „gotowe, gdy”, recall, granice → składa polecenie i wkłada do pola promptu; „wyczysc” kasuje pamięć folderu',
204 argumentHint: '[tekst polecenia|wyczysc|zamknij]',
205 })
206 return next(e)
207 }).catch(($, e, next) => next(e))
208
209 // v0.1: obserwacja. Toast raz na sesję, gdy pierwsze polecenie osoby (origin spoza NIE_OSOBA) jest krótkie; komendy slash,
210 // prompty meta, pluginów, peerów i sesja wznowiona (turns > 0) — cisza. `next(e)` zawsze z niezmienionym `e`.
211 on('prompt.submit', async ($, e, next) => {
212 try {
213 // Czyje polecenie: plugin, peer, notyfikacja zadania, harmonogram, kanał — nie osoba (NIE_OSOBA); reszta tak.
214 const kind = (e.origin as PromptOrigin | undefined)?.kind
215 const zKlawiatury = kind === undefined || !NIE_OSOBA.has(kind)
216 const t = e.text.trim()
217 if (zKlawiatury && !e.command && !e.isMeta && t.length > 0 && t.length < PROG_TOASTU && !t.startsWith('/')) {
218 if (!(await read($, toast)) && (await $.session.turns()) === 0) {
219 await update($, toast, () => true)
220 $.ui.toast(TOAST_STARTU, { timeoutMs: 8000 })
221 }
222 }
223 } catch {
224 // obserwator: błąd odczytu nie dotyka polecenia
225 }
226 return next(e)
227 }).catch(($, e, next) => next(e))
228
229 // Hook odpowiada za własną komendę; try/catch, bo hook, który się wysypie, silnik pomija (komenda bez odpowiedzi).
230 on('command.run', { command: 'brief' }, async ($, e) => {
231 try {
232 const arg = e.args.trim()
233 const a = arg.toLowerCase()
234 if (a === 'wyczysc' || a === 'wyczyść') {
235 const folder = await $.session.cwd()
236 await $.store.delete(kluczPamieci(folder))
237 await $.ui.close({ id: PANE }) // otwarty formularz zapisałby wpis z powrotem przy „Uruchom”
238 return { text: `Pamięć ram folderu ${folder} wyczyszczona.` }
239 }
240 if (a === 'zamknij') {
241 await $.ui.close({ id: PANE })
242 return { text: 'Formularz ram zamknięty.' }
243 }
244 return { text: await otworz($, arg) }
245 } catch (err) {
246 return { text: `Brief: błąd modu — ${err instanceof Error ? err.message : String(err)}` }
247 }
248 })
249
250 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
251 if (e.surface === 'mobile') {
252 // Tabela elementów aplikacji mobilnej nie ma `Input` (typy 2.1.299) — formularz bez pól tekstowych nie ma sensu.
253 const { Box, Text } = $.ui.resolve(e)
254 return (
255 <Box>
256 <Text dimColor>Formularz /brief wymaga pól tekstowych, których aplikacja mobilna nie rysuje — użyj go na desktopie albo w terminalu.</Text>
257 </Box>
258 )
259 }
260 const { Box, Button, Input, Text } = $.ui.resolve(e)
261 const [r, p, folder, lista] = await Promise.all([read($, ramy), read($, polecenie), read($, cwd), read($, podkatalogi)])
262 const tekst = szkice.get('polecenie') ?? p
263 const repo = szkice.get('repo') ?? r.repo
264 const gotowe = szkice.get('gotowe') ?? r.gotowe
265 const podglad = linieRam({ ...r, repo, gotowe })
266 const radio = (wl: boolean) => (wl ? '(•)' : '( )')
267 const krzyzyk = (wl: boolean) => (wl ? '[x]' : '[ ]')
268 const zapiszTekst = (v: string) => {
269 szkice.delete('polecenie')
270 void update($, polecenie, () => v)
271 }
272 const zapiszPole = (pole: 'repo' | 'gotowe') => (v: string) => {
273 szkice.delete(pole)
274 void update($, ramy, x => ({ ...x, [pole]: v }))
275 }
276
277 // Wiersze zawijają się (`flexWrap`), bo w terminalu panel bywa wąską kolumną (~50 znaków, próba 2026-10-16); `columnGap`
278 // zamiast `gap`, bo `gap` dokłada pusty wiersz między zawiniętymi rzędami.
279 return (
280 <Box flexDirection="column">
281 <Text bold>Brief — ramy polecenia na start</Text>
282 <Input
283 key="polecenie"
284 label="Polecenie"
285 placeholder="co ma być zrobione"
286 value={tekst}
287 submitLabel="zapisz"
288 onInput={v => szkice.set('polecenie', v)}
289 onSubmit={zapiszTekst}
290 />
291 <Input key="repo" label="Repo/folder" value={repo} submitLabel="zapisz" onInput={v => szkice.set('repo', v)} onSubmit={zapiszPole('repo')} />
292 {lista.length > 0 && (
293 <Box columnGap={1} flexWrap="wrap">
294 <Text dimColor>podkatalogi:</Text>
295 {lista.slice(0, MAX_PODKATALOGOW).map(n => (
296 <Button
297 key={`kat-${n}`}
298 plain
299 dimColor
300 label={n}
301 onPress={() => {
302 szkice.delete('repo')
303 void update($, ramy, x => ({ ...x, repo: `${folder}/${n}` }))
304 }}
305 />
306 ))}
307 {lista.length > MAX_PODKATALOGOW && <Text dimColor>{`+${lista.length - MAX_PODKATALOGOW} więcej — wpisz ścieżkę`}</Text>}
308 </Box>
309 )}
310 <Box columnGap={1} flexWrap="wrap">
311 <Text>Stawka:</Text>
312 {STAWKI.map(s => (
313 <Button key={`stawka-${s}`} plain label={`${radio(r.stawka === s)} ${s}`} onPress={() => update($, ramy, x => ({ ...x, stawka: x.stawka === s ? null : s }))} />
314 ))}
315 {r.stawka === 'wysoka' && <Text dimColor>{`→ ${DOPISEK_WYSOKIEJ}`}</Text>}
316 </Box>
317 <Box columnGap={1} flexWrap="wrap">
318 <Text>Tryb:</Text>
319 {TRYBY.map(t => (
320 <Button key={`tryb-${t}`} plain label={`${radio(r.tryb === t)} ${t}`} onPress={() => update($, ramy, x => ({ ...x, tryb: x.tryb === t ? null : t }))} />
321 ))}
322 </Box>
323 <Input
324 key="gotowe"
325 label="Gotowe, gdy"
326 placeholder="kryterium sprawdzalne komendą albo w aplikacji"
327 value={gotowe}
328 submitLabel="zapisz"
329 onInput={v => szkice.set('gotowe', v)}
330 onSubmit={zapiszPole('gotowe')}
331 />
332 <Box columnGap={1} flexWrap="wrap">
333 <Button key="recall" plain label={`${krzyzyk(r.recall)} ${DOPISEK_RECALL}`} onPress={() => update($, ramy, x => ({ ...x, recall: !x.recall }))} />
334 </Box>
335 <Box columnGap={1} flexWrap="wrap">
336 <Text>Granice:</Text>
337 {GRANICE.map(g => (
338 <Button
339 key={`granica-${g.klucz}`}
340 plain
341 label={`${krzyzyk(r.granice[g.klucz])} ${g.opis}`}
342 onPress={() => update($, ramy, x => ({ ...x, granice: { ...x.granice, [g.klucz]: !x.granice[g.klucz] } }))}
343 />
344 ))}
345 </Box>
346 <Box columnGap={1} flexWrap="wrap">
347 <Button key="uruchom" variant="primary" label="Uruchom" onPress={press => uruchom($, press.surface)} />
348 <Button key="kopiuj" label="Kopiuj" onPress={press => kopiuj($, press.surface)} />
349 <Button key="pomin" role="dismiss" label="Pomiń" onPress={() => $.ui.close({ id: PANE })} />
350 </Box>
351 <Text dimColor>{podglad.length ? `ramy: ${podglad.join(' · ')}` : 'ramy: nic nie zaznaczono — Enter w polu tekstowym odświeża podgląd'}</Text>
352 </Box>
353 )
354 })
355}
356types/index.d.ts 36 lines1// Kontrakt stanu modu kanon-brief ($.state sesji; po reloadzie modułu wartości zostają, zmienne modułu startują od nowa).
2// Między sesjami żyje tylko $.store (klucz `brief:<cwd>`): ramy folderu bez tekstu polecenia i bez „gotowe, gdy”.
3export type Stawka = 'niska' | 'istotna' | 'wysoka'
4export type Tryb = 'sam' | 'subagent' | 'workflow'
5/** Granice zadania — każda to jedna linia bloku ram, gdy zaznaczona. */
6export type Granice = { produkcja: boolean; pii: boolean; push: boolean }
7/** Pola formularza poza tekstem polecenia. Pole puste (''/null/false) nie trafia do bloku ram. */
8export type Ramy = {
9 /** Repo albo folder zadania; domyślnie `$.session.cwd()`, przyciski podkatalogów podstawiają `<cwd>/<nazwa>`. */
10 repo: string
11 stawka: Stawka | null
12 tryb: Tryb | null
13 /** Kryterium „gotowe, gdy” — treść zadania, nie folderu: nie idzie do $.store. */
14 gotowe: string
15 /** Dopisek „najpierw /ops-recall”. */
16 recall: boolean
17 granice: Granice
18}
19
20declare module 'claude-code' {
21 interface PluginState {
22 'kanon-brief': {
23 /** Pola formularza; przy `/brief` start z $.store[`brief:<cwd>`] (gdy jest) nad pustymi. */
24 ramy: Ramy
25 /** Tekst polecenia z pola formularza albo z argumentu `/brief <tekst>`; nigdy do $.store. */
26 polecenie: string
27 /** Folder sesji przy ostatnim `/brief` — klucz pamięci i baza dla podkatalogów. */
28 cwd: string
29 /** Podkatalogi pierwszego poziomu cwd (`$.fs.list`, bez ukrytych, najwyżej 12) — przyciski „Repo/folder”. */
30 podkatalogi: string[]
31 /** Toast o krótkim poleceniu startowym już pokazany w tej sesji (raz na sesję, także po reloadzie modu). */
32 toast: boolean
33 }
34 }
35}
36