@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.
- package/dist/overlays/sheet.js +16 -4
- package/dist/spawn.js +21 -0
- package/dist/tasks.js +24 -0
- package/package.json +1 -1
- package/scripts/deck-run +23 -7
- package/scripts/prompt-catalog +8 -1
package/dist/overlays/sheet.js
CHANGED
|
@@ -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
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`,
|
|
8
|
-
#
|
|
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
|
|
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
|
|
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 →
|
|
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
|
package/scripts/prompt-catalog
CHANGED
|
@@ -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
|
|
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}
|