@lamemind/loom-deck 0.61.1 → 0.62.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.
package/dist/actions.js CHANGED
@@ -15,8 +15,9 @@
15
15
  // `test/modes-smoke.test.ts` (`seleziona una task`, `terminale su`, `deck-run`,
16
16
  // `nessun push`, `eliminare N task?`, `scartate`): si copiano verbatim, non si
17
17
  // migliorano di passaggio.
18
- import { useRef } from 'react';
18
+ import { useMemo, useRef } from 'react';
19
19
  import { randomUUID } from 'node:crypto';
20
+ import { loadPromptCatalog, modelFor } from './prompt-catalog.js';
20
21
  import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
21
22
  import { neighborId } from './session-list.js';
22
23
  import { cut, cutMiddle } from './width.js';
@@ -57,6 +58,9 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
57
58
  // prima dell'exec), ed è anche il comando da ripetere a mano per vedere
58
59
  // l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
59
60
  // comporre.
61
+ // T152 — stessa fonte del detail (sheet.ts): un file di sei righe accanto al
62
+ // codice, letto una volta per vita del deck.
63
+ const catalog = useMemo(() => loadPromptCatalog(), []);
60
64
  const spawnSeq = useRef(0);
61
65
  function noteSpawn(spawned) {
62
66
  noteCommand(spawned.cmd);
@@ -132,8 +136,11 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
132
136
  // invece deve restare vivo.
133
137
  function spawnTaskSession(kind, keyLabel) {
134
138
  const task = selectedTaskOr(keyLabel, 'spawnare');
139
+ // T152 — stesso catalogo del detail: senza, `^R` partirebbe sul default
140
+ // fisso mentre l'azione `run` del detail parte sul modello del kind, e le
141
+ // due superfici divergerebbero sullo stesso comando.
135
142
  if (task)
136
- spawnForTask(task.id, kind, MODEL_DEFAULT);
143
+ spawnForTask(task.id, kind, modelFor(catalog, kind) ?? MODEL_DEFAULT);
137
144
  }
138
145
  // T49 — resume di una conversazione in una nuova tab. Unico punto: lo chiamano
139
146
  // il `⏎` della lista sessioni e quello sulla riga-sessione della ricerca, che
@@ -21,7 +21,7 @@ import { sanitizeTyped } from '../glyphs.js';
21
21
  import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap, } from '../spawn.js';
22
22
  import { taskIsEpic } from '../tasks.js';
23
23
  import { fieldsKey } from '../fields.js';
24
- import { loadPromptCatalog, promptFor } from '../prompt-catalog.js';
24
+ import { loadPromptCatalog, modelFor, promptFor } from '../prompt-catalog.js';
25
25
  // T117 — le quattro righe dell'area di compilazione del detail, nell'ordine in
26
26
  // cui si leggono dall'alto. Gli indici sono nominati perché li usano insieme
27
27
  // l'handler dei tasti e la resa, e un `2` nudo in due file diversi è la
@@ -113,32 +113,39 @@ export function useSheetOverlay(deps) {
113
113
  // Calcolato qui e usato subito: `setEpic` non ha ancora aggiornato lo stato
114
114
  // quando `setPrompt` gira, quindi il valore locale è l'unico leggibile ora.
115
115
  const isEpic = taskIsEpic(next.id, next.text);
116
+ const kind = specializeRecap(DETAIL_ACTIONS[0].kind, isEpic);
116
117
  setSheet(next);
117
118
  setTop(0);
118
119
  setAction(0);
119
120
  setEpic(isEpic);
120
- setModel(MODEL_DEFAULT);
121
+ // T152 — il modello segue l'azione iniziale, come già il prompt: su una
122
+ // task Epic la prima azione specializza a `recap-epic`, che nel catalogo
123
+ // può portare un modello diverso da `recap`. Nessun kind fuori catalogo →
124
+ // MODEL_DEFAULT, lo stesso fallback che vale ovunque nel deck.
125
+ setModel(modelFor(catalog, kind) ?? MODEL_DEFAULT);
121
126
  setSpawnNote('');
122
- setPrompt(promptFor(catalog, specializeRecap(DETAIL_ACTIONS[0].kind, isEpic), next.id));
127
+ setPrompt(promptFor(catalog, kind, next.id));
123
128
  setCursor({ row: DROW.action, caret: 0 });
124
129
  setFind(null);
125
130
  setOccIdx(0);
126
131
  setNote('');
127
132
  setMode('detail');
128
133
  }
129
- /** Cambia l'azione e RISCRIVE il prompt col default del nuovo kind (D2).
134
+ /** Cambia l'azione e RISCRIVE prompt e modello col default del nuovo kind (D2).
130
135
  *
131
- * Nessuna preservazione del testo modificato a mano, e non è una svista: la
132
- * regola `initialDetail` del modale edit protegge da un cambio di sorgente
133
- * ACCIDENTALE, e col fuoco per riga quel caso non esiste `←→` cambiano
134
- * azione solo dalla riga azione, mentre sul campo prompt muovono il caret.
135
- * Senza il cambio accidentale la preservazione difenderebbe da nulla, e
136
- * costerebbe un campo che non torna più al default. */
136
+ * Nessuna preservazione della scelta manuale di modello, per la stessa
137
+ * ragione già scritta sul prompt: col fuoco per riga un cambio di azione
138
+ * ACCIDENTALE non esiste `←→` cambiano azione solo dalla riga azione
139
+ * quindi la preservazione difenderebbe da nulla e costerebbe un campo che
140
+ * non torna più al default della nuova azione (T152). */
137
141
  function selectAction(index) {
138
142
  setAction(index);
139
143
  const id = sheet?.id;
140
- if (id)
141
- setPrompt(promptFor(catalog, specializeRecap(DETAIL_ACTIONS[index].kind, epic), id));
144
+ if (!id)
145
+ return;
146
+ const kind = specializeRecap(DETAIL_ACTIONS[index].kind, epic);
147
+ setPrompt(promptFor(catalog, kind, id));
148
+ setModel(modelFor(catalog, kind) ?? MODEL_DEFAULT);
142
149
  }
143
150
  // Il ponte fra le quattro righe e i quattro stati. Le righe restano
144
151
  // TIPIZZATE dove vivono (`ModelKind`, indice dell'azione) invece di finire in
@@ -16,17 +16,25 @@
16
16
  import { readFileSync } from 'node:fs';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { dirname, join } from 'node:path';
19
+ import { MODELS } from './spawn.js';
19
20
  /** Stessa risalita di `DECK_RUN`: src/ (dev) e dist/ (build) sono entrambi
20
21
  * figli della package root, quindi il sibling è un livello sopra. */
21
22
  export const PROMPT_CATALOG = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'prompt-catalog');
22
23
  /**
23
- * Il catalogo come mappa kind → template, col placeholder `{TASK}` ancora
24
- * dentro.
24
+ * Il catalogo come mappa kind → entry, col placeholder `{TASK}` del template
25
+ * ancora dentro.
25
26
  *
26
27
  * File illeggibile → mappa VUOTA, non un fallback cablato: un default scritto
27
28
  * qui sarebbe esattamente la copia del catalogo che questo modulo esiste per non
28
29
  * avere, e divergerebbe in silenzio. L'assenza si vede — il campo prompt resta
29
30
  * vuoto — e uno spawn senza prompt è un esito benigno.
31
+ *
32
+ * Il taglio è su DUE tab, non uno: il primo separa il kind, l'ultimo separa un
33
+ * modello opzionale in coda — così una riga a due colonne (nessun secondo tab)
34
+ * lascia il template intatto. Un valore in terza colonna che non è un
35
+ * `ModelKind` viene scartato come se la colonna non ci fosse (P2 preflight
36
+ * T152): il chiamante cade sul proprio default, stesso esito bash del `case`
37
+ * di `deck-run` che degrada un modello ignoto.
30
38
  */
31
39
  export function loadPromptCatalog(path = PROMPT_CATALOG) {
32
40
  const out = new Map();
@@ -43,13 +51,27 @@ export function loadPromptCatalog(path = PROMPT_CATALOG) {
43
51
  const tab = line.indexOf('\t');
44
52
  if (tab < 0)
45
53
  continue;
46
- out.set(line.slice(0, tab).trim(), line.slice(tab + 1).trim());
54
+ const kind = line.slice(0, tab).trim();
55
+ const rest = line.slice(tab + 1);
56
+ const lastTab = rest.lastIndexOf('\t');
57
+ const template = (lastTab >= 0 ? rest.slice(0, lastTab) : rest).trim();
58
+ const rawModel = lastTab >= 0 ? rest.slice(lastTab + 1).trim() : '';
59
+ const model = MODELS.includes(rawModel)
60
+ ? rawModel
61
+ : undefined;
62
+ out.set(kind, model ? { template, model } : { template });
47
63
  }
48
64
  return out;
49
65
  }
50
66
  /** Il prompt di un kind su una task. Kind fuori catalogo (`none`, o un file
51
67
  * mutilo) → stringa vuota, che è la stessa cosa che `deck-run` fa con `none`. */
52
68
  export function promptFor(catalog, kind, taskId) {
53
- const template = catalog.get(kind);
69
+ const template = catalog.get(kind)?.template;
54
70
  return template ? template.replaceAll('{TASK}', taskId) : '';
55
71
  }
72
+ /** Il modello di un kind, gemello di `promptFor`. Kind fuori catalogo o riga
73
+ * priva del campo → `undefined`: il chiamante cade sul proprio default
74
+ * (`MODEL_DEFAULT`), mai su un valore inventato qui. */
75
+ export function modelFor(catalog, kind) {
76
+ return catalog.get(kind)?.model;
77
+ }
package/dist/spawn.js CHANGED
@@ -143,12 +143,24 @@ export function specializeRecap(kind, epic) {
143
143
  // cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
144
144
  // anche il binding delle cifre `1`-`4`, passate poi al campo nota.
145
145
  export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
146
- // Default del selettore e di ogni percorso di spawn che non passa da lui
147
- // (`^K`/`^P`/`^R` dalla lista, resume, fork). Duplicato del default di deck-run
148
- // e non letto da lì: il deck deve poter MOSTRARE la selezione iniziale prima di
149
- // spawnare alcunché, e un valore che si conosce solo a spawn avvenuto non è
150
- // mostrabile.
146
+ // Default del selettore e FALLBACK per ogni kind fuori catalogo (kind `none`,
147
+ // o un catalogo mutilo). Duplicato del default di deck-run e non letto da lì:
148
+ // il deck deve poter MOSTRARE la selezione iniziale prima di spawnare
149
+ // alcunché, e un valore che si conosce solo a spawn avvenuto non è mostrabile.
150
+ //
151
+ // T152 — non è più IL default di `^K`/`^P`/`^R` dalla lista: quei percorsi
152
+ // leggono il catalogo come il detail (`modelFor`), e cadono qui solo quando il
153
+ // kind non ha una riga. Resta l'unico default che resume e fork usano per
154
+ // intero, perché lì il modello non è mai una funzione del kind — è quello
155
+ // della conversazione d'origine (T148).
151
156
  export const MODEL_DEFAULT = 'opus';
157
+ // T152 — `recap-status-project` non è una chiave del catalogo (§Doc Impact,
158
+ // "le chiavi del catalogo sono i valori di --prompt-kind"): lo spawn headless
159
+ // non passa mai --prompt-kind, quindi una riga lì sarebbe irraggiungibile da
160
+ // ogni chiamante. Il default vive qui, accanto a MODEL_DEFAULT, e non nel
161
+ // catalogo. Valore = copia del frontmatter `model: opus` che la skill portava
162
+ // prima di T152 (P4 preflight), nessuna ri-taratura.
163
+ export const PROJECT_STATUS_MODEL = 'opus';
152
164
  // T66 — le azioni del detail. Non sono un catalogo nuovo: ognuna è un
153
165
  // `--prompt-kind` già esistente più `checkpoint`, e tutte passano dallo stesso
154
166
  // `spawnForTask` dei CTRL della lista — una superficie in più, zero percorsi di
@@ -502,6 +514,8 @@ export function spawnProjectStatus(cwd, sessionId, onResult) {
502
514
  return spawnSkill('/loom-works:recap-status-project', cwd, sessionId, onResult, [
503
515
  '--permission-mode',
504
516
  'auto',
517
+ '--model',
518
+ PROJECT_STATUS_MODEL,
505
519
  ]);
506
520
  }
507
521
  // Comando della notifica desktop. L'override esiste per il collaudo del ramo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.61.1",
3
+ "version": "0.62.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
@@ -60,7 +60,7 @@
60
60
  # LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: bindings/claude
61
61
  # del progetto, letto da dconf; settata a vuoto = nessun annuncio)
62
62
  # LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
63
- # LOOM_DECK_MODEL modello quando --model non è passato (default: opus)
63
+ # LOOM_DECK_MODEL modello quando --model non è passato (batte il catalogo, default: opus)
64
64
  #
65
65
  set -euo pipefail
66
66
 
@@ -180,7 +180,8 @@ USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--sessio
180
180
  prompt. Ammesso anche con --no-task: un testo già scritto non
181
181
  ha una task da nominare, a differenza dei kind del catalogo
182
182
 
183
- --model modello della sessione: fable|opus|sonnet|haiku (default: opus)
183
+ --model modello della sessione: fable|opus|sonnet|haiku
184
+ (default: il modello del kind in catalogo, o 'opus' fuori catalogo)
184
185
  valore ignoto → fallback sul default, con avviso su stderr
185
186
 
186
187
  --title-note nota della conversazione, appesa al titolo tab
@@ -353,11 +354,16 @@ case "${PERM_MODE:-}" in
353
354
  esac
354
355
  MODE_FLAG="--permission-mode ${PERM_MODE} "
355
356
 
356
- # ── modello (T108) ───────────────────────────────────────────────────────────
357
- # Precedenza: --model > env LOOM_DECK_MODEL > 'opus'. Nessun campo nel file
358
- # config: il default è unico per tutta la famiglia, e uno scalare per-progetto
359
- # aggiungerebbe una chiave allo schema del plugin per un valore che il selettore
360
- # del deck sovrascrive a ogni spawn.
357
+ # ── modello (T108 + T152) ────────────────────────────────────────────────────
358
+ # Precedenza: --model > env LOOM_DECK_MODEL > modello del kind nel catalogo >
359
+ # 'opus'. Fino a T152 il default era un valore unico per tutta la famiglia,
360
+ # perché il frontmatter `model:` delle skill riscriveva comunque il modello di
361
+ # sessione un default configurabile qui non sarebbe servito a nulla. Da
362
+ # quando le skill non dichiarano più `model:`, il default DEVE viaggiare col
363
+ # kind: `run` costa quanto `preflight` solo se «vuole sonnet» è scritto da
364
+ # qualche parte che deck-run consulta, e quella parte è il catalogo che il
365
+ # deck TUI legge già per il testo del prompt (T117) — stessa fonte, stesso
366
+ # `kind`, niente seconda copia.
361
367
  # Il flag è passato SEMPRE, anche sul default, per la stessa ragione di
362
368
  # permissionMode: lo spawn resta leggibile nel process tree invece di dipendere
363
369
  # dal default del CLI, che cambia fra versioni.
@@ -366,7 +372,27 @@ MODE_FLAG="--permission-mode ${PERM_MODE} "
366
372
  # ferma DENTRO lo script (nessun template da scegliere, l'errore è del deck),
367
373
  # un modello ignoto arriverebbe al CLI e produrrebbe una tab con un comando che
368
374
  # fallisce all'avvio — cioè il guasto che permissionMode evita degradando.
375
+ _model_for_kind() { # <kind> → modello dichiarato in catalogo (3ª colonna), o niente
376
+ local want="$1" k t m
377
+ [[ -n "$want" && -f "$PROMPT_CATALOG" ]] || return 1
378
+ while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
379
+ [[ -z "$k" || "$k" == \#* ]] && continue
380
+ [[ "$k" == "$want" && -n "$m" ]] && { printf '%s' "$m"; return 0; }
381
+ done < "$PROMPT_CATALOG"
382
+ return 1
383
+ }
384
+ # Kind di catalogo effettivo: SOLO quando la sessione è bound e senza prompt
385
+ # letterale — le due strade (--no-task, --prompt) non hanno una riga di
386
+ # catalogo da consultare, né per il prompt (sotto) né per il modello (qui).
387
+ # Riusato tal quale nella composizione del prompt: stesso guard, stesso valore.
388
+ CATALOG_KIND=""
389
+ if [[ $PROMPT_GIVEN -eq 0 && $NO_TASK -eq 0 ]]; then
390
+ CATALOG_KIND="${PROMPT_KIND:-recap}"
391
+ fi
369
392
  MODEL="${MODEL:-${LOOM_DECK_MODEL:-}}"
393
+ if [[ -z "$MODEL" && -n "$CATALOG_KIND" && "$CATALOG_KIND" != "none" ]]; then
394
+ MODEL="$(_model_for_kind "$CATALOG_KIND" || true)"
395
+ fi
370
396
  case "${MODEL:-}" in
371
397
  fable|opus|sonnet|haiku) ;;
372
398
  '') MODEL="opus" ;;
@@ -476,9 +502,12 @@ FORK_FLAG=""
476
502
  # (`T`, unico prefisso del contratto loom), quindi nessun prompt discrimina sulla
477
503
  # forma dell'id.
478
504
  _prompt_template() { # <kind> → template col placeholder {TASK}, o niente
479
- local want="$1" k t
505
+ local want="$1" k t m
480
506
  [[ -f "$PROMPT_CATALOG" ]] || return 1
481
- while IFS=$'\t' read -r k t || [[ -n "$k" ]]; do
507
+ # 3 variabili, non 2 (T152): da quando il catalogo porta una 3ª colonna
508
+ # modello, un `read -r k t` a due si mangerebbe il tab separatore e il
509
+ # modello dentro `t`, che è esattamente il template passato al CLI.
510
+ while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
482
511
  [[ -z "$k" || "$k" == \#* ]] && continue
483
512
  [[ "$k" == "$want" ]] && { printf '%s' "$t"; return 0; }
484
513
  done < "$PROMPT_CATALOG"
@@ -503,7 +532,7 @@ elif [[ $NO_TASK -eq 1 ]]; then
503
532
  # volta e qui resta quindi inerte: senza task non ha nulla da nominare.
504
533
  PROMPT=""
505
534
  else
506
- _kind="${PROMPT_KIND:-recap}"
535
+ _kind="$CATALOG_KIND"
507
536
  if [[ "$_kind" == "none" ]]; then
508
537
  PROMPT=""
509
538
  else
@@ -1,28 +1,37 @@
1
1
  # Catalogo dei prompt iniziali di una sessione Claude aperta dal deck.
2
2
  #
3
- # Formato a righe: <kind><TAB><template>, con `{TASK}` interpolato col TaskID.
4
- # Righe vuote e righe che iniziano con `#` sono commenti.
3
+ # Formato a righe: <kind><TAB><template><TAB><modello>, con `{TASK}`
4
+ # interpolato col TaskID. Righe vuote e righe che iniziano con `#` sono
5
+ # commenti. Il modello è OPZIONALE: una riga a due colonne resta leggibile e
6
+ # chi la consulta cade sul proprio default (`MODEL_DEFAULT`/`opus`) — il campo
7
+ # esiste per i kind bound a una skill (T152), non per ogni riga del file.
5
8
  #
6
9
  # Perché un file dati e non un `case` dentro deck-run: due lati hanno bisogno
7
10
  # dello stesso testo — deck-run per comporre il comando in-tab, il deck TUI per
8
- # pre-riempire il campo prompt del detail con ciò che sta per essere eseguito.
9
- # Come codice, il secondo lato dovrebbe riscriverlo, e le due copie divergono
10
- # alla prima voce aggiunta da una parte sola.
11
+ # pre-riempire il campo prompt (e ora anche il modello) del detail con ciò che
12
+ # sta per essere eseguito. Come codice, il secondo lato dovrebbe riscriverlo, e
13
+ # le due copie divergono alla prima voce aggiunta da una parte sola.
11
14
  #
12
15
  # `none` non compare: nessun prompt non è un template vuoto, è l'assenza della
13
- # voce. Chi chiede un kind fuori da questo file riceve un prompt vuoto.
16
+ # voce. Chi chiede un kind fuori da questo file riceve un prompt vuoto e nessun
17
+ # modello (fallback sul default del chiamante).
14
18
  #
15
19
  # Vincolo sui template: nessun apice singolo. Il testo finisce dentro `'...'`
16
20
  # nel comando passato a `bash -lc`, e questo file è committato — un apice qui
17
21
  # sarebbe un errore di scrittura, non un input da quotare.
22
+ #
23
+ # Il modello è la copia esatta del `model:` che il frontmatter delle sei skill
24
+ # portava prima di T152 — non una ri-taratura: nasce da un difetto di canale
25
+ # (il frontmatter vinceva sempre su `--model`, rendendo il selettore del detail
26
+ # inerte), non da un giudizio su quale modello convenga a quale skill.
18
27
  # `recap` è il DISPATCHER: risolve la task, ne legge il Size e passa da sé alla
19
28
  # sotto-skill. Lo usa chi non sa se la task è un cappello — gli acceleratori
20
29
  # della lista, dove il deck ha in mano solo tasks.md, che il Size non ce l'ha.
21
30
  # Le due voci `recap-task`/`recap-epic` saltano quel giro: le sceglie il DETAIL,
22
31
  # 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}
26
- preflight /loom-works:preflight-task {TASK}
27
- run /loom-works:run-task {TASK}
28
- checkpoint /loom-works:checkpoint-task {TASK}
32
+ recap /loom-works:recap-status {TASK} opus
33
+ recap-task /loom-works:recap-status-task {TASK} opus
34
+ recap-epic /loom-works:recap-status-epic {TASK} opus
35
+ preflight /loom-works:preflight-task {TASK} opus
36
+ run /loom-works:run-task {TASK} sonnet
37
+ checkpoint /loom-works:checkpoint-task {TASK} sonnet