@lamemind/loom-deck 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,8 @@ import { scanText, topForOffset } from '../text-search.js';
18
18
  import { parseMarkdown } from '../markdown.js';
19
19
  import { cpLen, insertAt, removeAt } from '../layout.js';
20
20
  import { sanitizeTyped } from '../glyphs.js';
21
- import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, } from '../spawn.js';
21
+ import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap, } from '../spawn.js';
22
+ import { taskIsEpic } from '../tasks.js';
22
23
  import { fieldsKey } from '../fields.js';
23
24
  import { loadPromptCatalog, promptFor } from '../prompt-catalog.js';
24
25
  // T117 — le quattro righe dell'area di compilazione del detail, nell'ordine in
@@ -50,6 +51,10 @@ export function useSheetOverlay(deps) {
50
51
  // T117 — il prompt iniziale, EDITABILE. Quello che si legge nel campo è quello
51
52
  // che parte: non un'anteprima di qualcos'altro.
52
53
  const [prompt, setPrompt] = useState('');
54
+ /** La task aperta è un cappello (`Size: Epic`)? Deciso UNA VOLTA all'apertura,
55
+ * dal testo che il detail ha già in mano: rifarlo a ogni cambio di azione
56
+ * riparserebbe l'intero task file per un campo dell'header. */
57
+ const [epic, setEpic] = useState(false);
53
58
  const [cursor, setCursor] = useState({ row: DROW.action, caret: 0 });
54
59
  // Il catalogo si legge una volta per vita del deck: è un file di quattro righe
55
60
  // accanto al codice, non un dato che cambia sotto i piedi.
@@ -105,12 +110,16 @@ export function useSheetOverlay(deps) {
105
110
  }, [findRes, occCur, lines, capacity]);
106
111
  /** Apre il detail su una task, azzerando scroll, area di compilazione e ricerca. */
107
112
  function open(next) {
113
+ // Calcolato qui e usato subito: `setEpic` non ha ancora aggiornato lo stato
114
+ // quando `setPrompt` gira, quindi il valore locale è l'unico leggibile ora.
115
+ const isEpic = taskIsEpic(next.id, next.text);
108
116
  setSheet(next);
109
117
  setTop(0);
110
118
  setAction(0);
119
+ setEpic(isEpic);
111
120
  setModel(MODEL_DEFAULT);
112
121
  setSpawnNote('');
113
- setPrompt(promptFor(catalog, DETAIL_ACTIONS[0].kind, next.id));
122
+ setPrompt(promptFor(catalog, specializeRecap(DETAIL_ACTIONS[0].kind, isEpic), next.id));
114
123
  setCursor({ row: DROW.action, caret: 0 });
115
124
  setFind(null);
116
125
  setOccIdx(0);
@@ -129,7 +138,7 @@ export function useSheetOverlay(deps) {
129
138
  setAction(index);
130
139
  const id = sheet?.id;
131
140
  if (id)
132
- setPrompt(promptFor(catalog, DETAIL_ACTIONS[index].kind, id));
141
+ setPrompt(promptFor(catalog, specializeRecap(DETAIL_ACTIONS[index].kind, epic), id));
133
142
  }
134
143
  // Il ponte fra le quattro righe e i quattro stati. Le righe restano
135
144
  // TIPIZZATE dove vivono (`ModelKind`, indice dell'azione) invece di finire in
@@ -234,13 +243,16 @@ export function useSheetOverlay(deps) {
234
243
  if (key.return) {
235
244
  // `⏎` esegue SEMPRE l'azione selezionata, da qualunque riga: i campi sono
236
245
  // di una riga sola, quindi nessuno di loro ha da farci un a-capo.
246
+ // Il kind viaggia specializzato quanto il prompt che lo accompagna: se il
247
+ // campo è stato svuotato a mano il testo non parte e resta lui a dire cosa
248
+ // ricevera' la sessione, quindi i due non possono divergere.
237
249
  const act = DETAIL_ACTIONS[action];
238
250
  const id = sheet?.id;
239
251
  const note = spawnNote.trim();
240
252
  const text = prompt.trim();
241
253
  close();
242
254
  if (id)
243
- onAction(id, act.kind, model, note, text);
255
+ onAction(id, specializeRecap(act.kind, epic), model, note, text);
244
256
  return;
245
257
  }
246
258
  if (key.pageUp) {
package/dist/spawn.js CHANGED
@@ -117,6 +117,27 @@ export function onInTabCommand(child, cb) {
117
117
  }
118
118
  });
119
119
  }
120
+ /**
121
+ * `recap` → la sotto-skill giusta, quando chi spawna SA se la task è un cappello.
122
+ *
123
+ * `recap` resta il kind onesto per chi non lo sa: punta al dispatcher, che
124
+ * risolve la task e classifica da sé. È il caso degli acceleratori della lista,
125
+ * dove il deck ha in mano solo `tasks.md` — e lì il `Size` non c'è. Il DETAIL
126
+ * invece il task file l'ha già letto, quindi può saltare il giro e pagare un
127
+ * turno di modello in meno.
128
+ *
129
+ * Non è una classificazione duplicata: il criterio (`Size: Epic`) resta uno solo
130
+ * e sta in `taskIsEpic`, che legge lo stesso campo che leggerebbe il dispatcher.
131
+ * Quello che si evita è il RITARDO, non il giudizio.
132
+ *
133
+ * Ogni kind diverso da `recap` passa intatto: la specializzazione è un caso, non
134
+ * una trasformazione da applicare a tutti.
135
+ */
136
+ export function specializeRecap(kind, epic) {
137
+ if (kind !== 'recap')
138
+ return kind;
139
+ return epic ? 'recap-epic' : 'recap-task';
140
+ }
120
141
  // L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
121
142
  // cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
122
143
  // anche il binding delle cifre `1`-`4`, passate poi al campo nota.
package/dist/tasks.js CHANGED
@@ -153,6 +153,30 @@ export function parseTaskDetail(id, content) {
153
153
  description: sanitize(descLines.join('\n').trim()),
154
154
  };
155
155
  }
156
+ /**
157
+ * La task è un cappello (epica)?
158
+ *
159
+ * Il marker è `Size: Epic`, e sta sul CAPPELLO perché la parentela la dichiara
160
+ * la figlia (`**Parent Task**`): un padre non sa di averne senza scandagliare
161
+ * tutti gli altri task file, cosa che il deck non può fare nel loop di poll.
162
+ *
163
+ * Legge dal testo INTEGRALE del task file, l'unico posto dove il `Size` esiste:
164
+ * in `tasks.md` quella colonna non c'è, quindi la lista non può rispondere e la
165
+ * domanda ha senso solo dove il file è già stato aperto (il detail).
166
+ *
167
+ * Riusa `parseTaskDetail` invece di una regex propria: la grammatica dei bullet
168
+ * header (`- **Campo**: valore`) è già scritta lì, e una seconda copia
169
+ * divergerebbe al primo campo che cambia forma. Confronto case-insensitive — il
170
+ * valore lo scrive un umano nel file, non uno script.
171
+ *
172
+ * Testo assente (`null`, task file non ancora letto) → `false`: la mancanza di
173
+ * prova non è prova di cappello, e degradare sul caso comune è l'esito benigno.
174
+ */
175
+ export function taskIsEpic(id, text) {
176
+ if (!text)
177
+ return false;
178
+ return (parseTaskDetail(id, text).fields['Size'] ?? '').trim().toLowerCase() === 'epic';
179
+ }
156
180
  /**
157
181
  * Testo INTEGRALE del task file (T66 · detail).
158
182
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/deck-run CHANGED
@@ -4,8 +4,8 @@
4
4
  #
5
5
  # Apre una tab Ptyxis nella window ATTIVA (quella col focus = il deck) e vi
6
6
  # avvia una sessione Claude Code già bound alla task via LOOM_TASK, dritta sul
7
- # prompt iniziale scelto con --prompt-kind (default: `recap`, prompt diretto
8
- # "recap stato task <TaskID>" — no skill, solo recap → più controllo).
7
+ # prompt iniziale scelto con --prompt-kind (default: `recap`, cioè la skill
8
+ # `/loom-works:recap-status` sulla task).
9
9
  # Riusabile identico da TUI (Ink) e da web.
10
10
  #
11
11
  # Quattro assi ORTOGONALI, da non confondere fra loro:
@@ -103,7 +103,7 @@ MODEL=""
103
103
  # Sotto --fork il --session-id torna AMMESSO (anzi: è il punto) — la mutua
104
104
  # esclusione con --resume esiste perché una ripresa nuda riscrive il transcript
105
105
  # dell'id ripreso, mentre il fork ne apre uno NUOVO, che possiamo quindi pinnare.
106
- # --prompt-kind <none|recap|preflight|run|checkpoint> (T56, T66): sceglie il prompt iniziale fra
106
+ # --prompt-kind <none|recap|recap-task|recap-epic|preflight|run|checkpoint>: sceglie il prompt iniziale fra
107
107
  # quelli del catalogo qui sotto. Enum e non stringa libera: il prompt viaggia
108
108
  # dentro apici singoli in `bash -lc`, quindi il testo va tenuto in un posto solo
109
109
  # e verificato una volta, invece di spostare il rischio di quoting su ogni
@@ -164,7 +164,9 @@ USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--sessio
164
164
 
165
165
  --prompt-kind prompt iniziale della sessione bound (default: recap)
166
166
  none nessun prompt — sessione aperta sulla task, a mani nude
167
- recap recap stato task <TaskID>
167
+ recap /loom-works:recap-status <TaskID> (dispatcher)
168
+ recap-task /loom-works:recap-status-task <TaskID> (task non cappello)
169
+ recap-epic /loom-works:recap-status-epic <TaskID> (Size: Epic)
168
170
  preflight /loom-works:preflight-task <TaskID>
169
171
  run /loom-works:run-task <TaskID>
170
172
  checkpoint /loom-works:checkpoint-task <TaskID>
@@ -203,8 +205,8 @@ fi
203
205
  # lì il valore arriva da un file editabile a mano, qui da un argomento nostro.
204
206
  if [[ -n "$PROMPT_KIND" ]]; then
205
207
  case "$PROMPT_KIND" in
206
- none|recap|preflight|run|checkpoint) ;;
207
- *) echo "--prompt-kind ignoto: '${PROMPT_KIND}' (usa none|recap|preflight|run|checkpoint)" >&2
208
+ none|recap|recap-task|recap-epic|preflight|run|checkpoint) ;;
209
+ *) echo "--prompt-kind ignoto: '${PROMPT_KIND}' (usa none|recap|recap-task|recap-epic|preflight|run|checkpoint)" >&2
208
210
  echo "$USAGE" >&2
209
211
  exit 2 ;;
210
212
  esac
@@ -421,7 +423,21 @@ FORK_FLAG=""
421
423
  # eseguirlo o a mani nude. Il kind è quell'intento.
422
424
  # none → nessun prompt (≠ stringa vuota: PROMPT_ARG sparisce del tutto,
423
425
  # come sul ramo --resume, altrimenti CC riceve un posizionale vuoto)
424
- # recap → prompt DIRETTO, no skill: l'utente rivede/steera prima di lanciare
426
+ # recap → skill di recap sulla task. È un DISPATCHER: `recap-status`
427
+ # risolve la task, classifica (progetto / task / epica) e passa a
428
+ # una sotto-skill. Fino a v0.45 era invece un prompt diretto
429
+ # ("recap stato task <id>"), scelto perché non esisteva una skill
430
+ # di recap tarata sulla singola task e quella di progetto avrebbe
431
+ # risposto largo — vincolo caduto col dispatcher.
432
+ # recap-task → la sotto-skill diretta, senza il giro del dispatcher
433
+ # recap-epic → idem per un cappello (`Size: Epic`)
434
+ # I due specializzati li sceglie chi SA già come è fatta la task:
435
+ # il detail del deck, che il task file l'ha aperto. Chi non lo sa
436
+ # — gli acceleratori della lista, che leggono solo `tasks.md`, e
437
+ # quindi non vedono il `Size` — resta su `recap` e lascia
438
+ # classificare al dispatcher. Il criterio non è duplicato: sta in
439
+ # `taskIsEpic` (src/tasks.ts) e legge lo stesso campo che leggerebbe
440
+ # il dispatcher; quello che cambia è solo QUANDO viene letto.
425
441
  # preflight → skill di preflight sulla task
426
442
  # run → skill di esecuzione sulla task
427
443
  # checkpoint → skill di checkpoint sulla task
@@ -15,7 +15,14 @@
15
15
  # Vincolo sui template: nessun apice singolo. Il testo finisce dentro `'...'`
16
16
  # nel comando passato a `bash -lc`, e questo file è committato — un apice qui
17
17
  # sarebbe un errore di scrittura, non un input da quotare.
18
- recap recap stato task {TASK}
18
+ # `recap` è il DISPATCHER: risolve la task, ne legge il Size e passa da sé alla
19
+ # sotto-skill. Lo usa chi non sa se la task è un cappello — gli acceleratori
20
+ # della lista, dove il deck ha in mano solo tasks.md, che il Size non ce l'ha.
21
+ # Le due voci `recap-task`/`recap-epic` saltano quel giro: le sceglie il DETAIL,
22
+ # che il task file l'ha già aperto e quindi il Size lo conosce.
23
+ recap /loom-works:recap-status {TASK}
24
+ recap-task /loom-works:recap-status-task {TASK}
25
+ recap-epic /loom-works:recap-status-epic {TASK}
19
26
  preflight /loom-works:preflight-task {TASK}
20
27
  run /loom-works:run-task {TASK}
21
28
  checkpoint /loom-works:checkpoint-task {TASK}