@lamemind/loom-deck 0.61.0 → 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 +9 -2
- package/dist/commit-times.js +48 -2
- package/dist/epic-hierarchy.js +43 -11
- package/dist/hooks.js +21 -28
- package/dist/overlays/sheet.js +19 -12
- package/dist/prompt-catalog.js +26 -4
- package/dist/spawn.js +19 -5
- package/package.json +1 -1
- package/scripts/deck-run +39 -10
- package/scripts/prompt-catalog +21 -12
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
|
package/dist/commit-times.js
CHANGED
|
@@ -18,6 +18,32 @@ const FORMAT_RE = /^#(\d+)$/;
|
|
|
18
18
|
// Il basename del task file, non il path intero (che cambia con la docs-root)
|
|
19
19
|
// e non `TASK_ID_RE` di tasks.ts (ancorata all'id nudo, non matcha `T136-x.md`).
|
|
20
20
|
const TASK_FILE_RE = /^(T\d+)-.*\.md$/;
|
|
21
|
+
const EMPTY_MAP = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* T153 — gate module-level: `git log` gira solo se lo sha di HEAD è cambiato
|
|
24
|
+
* dall'ultima chiamata su questa stessa coppia (tasksDir, projectRoot). Le
|
|
25
|
+
* date di ultimo commit cambiano SOLO se HEAD si muove (§Implementation
|
|
26
|
+
* Notes T153): un rev-parse fallito degrada come degradava il log, mai un
|
|
27
|
+
* throw. `key` tiene le due coppie separate — un test che passa tmpdir
|
|
28
|
+
* diversi a chiamate successive non deve leggere la cache dell'altro.
|
|
29
|
+
*/
|
|
30
|
+
let cache = null;
|
|
31
|
+
let logSpawns = 0;
|
|
32
|
+
/** T153/DLV6 — quante volte è partito lo spawn COSTOSO (`git log`), non il
|
|
33
|
+
* gate. Il gate (`rev-parse`) gira a ogni chiamata per costruzione: contarlo
|
|
34
|
+
* renderebbe il numero inutile a dimostrare il no-op. */
|
|
35
|
+
export function commitLogSpawnCount() {
|
|
36
|
+
return logSpawns;
|
|
37
|
+
}
|
|
38
|
+
async function headSha(projectRoot) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: projectRoot });
|
|
41
|
+
return stdout.trim();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
21
47
|
/**
|
|
22
48
|
* Parsing puro dell'output `git log --format=#%ct --name-only`. Isolato da
|
|
23
49
|
* `commitTimes` perché è l'unica parte che vale la pena collaudare: invocare
|
|
@@ -54,13 +80,33 @@ export function parseCommitLog(output) {
|
|
|
54
80
|
* incompleta proprio per le task vecchie mai più toccate, quelle che sotto
|
|
55
81
|
* `desc` finiscono in coda — dove un timestamp assente e uno vecchio si
|
|
56
82
|
* confonderebbero.
|
|
83
|
+
*
|
|
84
|
+
* T153 — gate su `rev-parse HEAD` (un ordine di grandezza più economico
|
|
85
|
+
* dello spawn del log che sostituisce, cifre misurate nel Progress Log): a
|
|
86
|
+
* sha invariato torna la STESSA istanza di mappa, non una ricostruita — è
|
|
87
|
+
* ciò che rende inutile il confronto per firma in `useCommitTimes` (P1
|
|
88
|
+
* preflight).
|
|
57
89
|
*/
|
|
58
90
|
export async function commitTimes(tasksDir, projectRoot) {
|
|
91
|
+
const key = `${projectRoot}\u0000${tasksDir}`;
|
|
92
|
+
const sha = await headSha(projectRoot);
|
|
93
|
+
if (sha !== null && cache && cache.key === key && cache.sha === sha) {
|
|
94
|
+
return cache.map;
|
|
95
|
+
}
|
|
96
|
+
let map;
|
|
59
97
|
try {
|
|
98
|
+
// Il contatore sale PRIMA dell'await: conta gli spawn partiti, e uno
|
|
99
|
+
// spawn fallito costa comunque la fork del processo.
|
|
100
|
+
logSpawns++;
|
|
60
101
|
const { stdout } = await execFileAsync('git', ['log', '--format=#%ct', '--name-only', '--', tasksDir], { cwd: projectRoot });
|
|
61
|
-
|
|
102
|
+
map = parseCommitLog(stdout);
|
|
62
103
|
}
|
|
63
104
|
catch {
|
|
64
|
-
|
|
105
|
+
map = EMPTY_MAP;
|
|
65
106
|
}
|
|
107
|
+
// sha === null (git muto): non si cachea uno stato che il prossimo giro
|
|
108
|
+
// potrebbe smentire in silenzio (repo che ricompare) senza mai vedersi
|
|
109
|
+
// ricontrollato.
|
|
110
|
+
cache = sha !== null ? { key, sha, map } : null;
|
|
111
|
+
return map;
|
|
66
112
|
}
|
package/dist/epic-hierarchy.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// parentela si edita anche a mano dentro un task file già in tabella, non solo
|
|
9
9
|
// alla nascita di una riga: un trigger sulla sola firma degli id (come
|
|
10
10
|
// `useArchivable`) lascerebbe quell'edit stale fino al prossimo giro largo.
|
|
11
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { taskEpicOf, taskIsEpic } from './tasks.js';
|
|
14
14
|
// Stesso pattern di `TASK_FILE_RE` in commit-times.ts: il basename del task
|
|
@@ -17,10 +17,23 @@ import { taskEpicOf, taskIsEpic } from './tasks.js';
|
|
|
17
17
|
const TASK_FILE_RE = /^(T\d+)-.*\.md$/;
|
|
18
18
|
export const EMPTY_EPIC_HIERARCHY = { epicOf: new Map(), epics: new Set() };
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* T153 — gate module-level: il read+parse dei task file gira solo se la mtime
|
|
21
|
+
* MASSIMA fra i file `T<N>-*.md` di `tasksDir` è cambiata da ultima chiamata
|
|
22
|
+
* (un ordine di grandezza più economico del parse pieno che sostituisce,
|
|
23
|
+
* cifre misurate nel Progress Log). Sulla SOLA mtime e non su una firma
|
|
24
|
+
* degli id: deve intercettare un edit a mano di
|
|
25
|
+
* `**Parent Task**` DENTRO un file già in tabella, che non aggiunge né toglie
|
|
26
|
+
* nessun id (Testing Notes T153). A gate scattato torna la STESSA istanza —
|
|
27
|
+
* `useEpicHierarchy` non ha più bisogno di una firma propria per lo stesso
|
|
28
|
+
* motivo di `useCommitTimes` (P1 preflight).
|
|
29
|
+
*/
|
|
30
|
+
let cache = null;
|
|
31
|
+
/**
|
|
32
|
+
* Lettura SINCRONA di tutti i task file sotto `tasksDir`: costo del gate
|
|
33
|
+
* accettabile sul poll da 1,5s anche a scan pieno (mai il caso in regime).
|
|
34
|
+
* Cartella illeggibile o singolo file non apribile → entry saltata, mai un
|
|
35
|
+
* throw — un dato di rendering non può rompere il deck, stessa regola di
|
|
36
|
+
* `archivableIds`/`commitTimes`.
|
|
24
37
|
*/
|
|
25
38
|
export function scanEpicHierarchy(tasksDir) {
|
|
26
39
|
let entries;
|
|
@@ -28,15 +41,32 @@ export function scanEpicHierarchy(tasksDir) {
|
|
|
28
41
|
entries = readdirSync(tasksDir);
|
|
29
42
|
}
|
|
30
43
|
catch {
|
|
44
|
+
cache = null;
|
|
31
45
|
return EMPTY_EPIC_HIERARCHY;
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
const
|
|
47
|
+
let maxMtime = 0;
|
|
48
|
+
const files = [];
|
|
35
49
|
for (const file of entries) {
|
|
36
|
-
|
|
37
|
-
if (!m)
|
|
50
|
+
if (!TASK_FILE_RE.test(file))
|
|
38
51
|
continue;
|
|
39
|
-
|
|
52
|
+
let mtime;
|
|
53
|
+
try {
|
|
54
|
+
mtime = statSync(join(tasksDir, file)).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
files.push(file);
|
|
60
|
+
if (mtime > maxMtime)
|
|
61
|
+
maxMtime = mtime;
|
|
62
|
+
}
|
|
63
|
+
if (cache && cache.key === tasksDir && cache.maxMtime === maxMtime) {
|
|
64
|
+
return cache.hierarchy;
|
|
65
|
+
}
|
|
66
|
+
const epicOf = new Map();
|
|
67
|
+
const epics = new Set();
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const id = TASK_FILE_RE.exec(file)[1];
|
|
40
70
|
let content;
|
|
41
71
|
try {
|
|
42
72
|
content = readFileSync(join(tasksDir, file), 'utf8');
|
|
@@ -50,5 +80,7 @@ export function scanEpicHierarchy(tasksDir) {
|
|
|
50
80
|
if (parent)
|
|
51
81
|
epicOf.set(id, parent);
|
|
52
82
|
}
|
|
53
|
-
|
|
83
|
+
const hierarchy = { epicOf, epics };
|
|
84
|
+
cache = { key: tasksDir, maxMtime, hierarchy };
|
|
85
|
+
return hierarchy;
|
|
54
86
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -72,28 +72,26 @@ export function useTasks(tasksPath) {
|
|
|
72
72
|
/**
|
|
73
73
|
* T136 — data dell'ultimo commit di ogni task file, per la chiave di sort
|
|
74
74
|
* `commit`. D1 (preflight): sullo STESSO poll di `tasks.md` (`POLL_MS`), non
|
|
75
|
-
* su una scala propria come `useArchivable
|
|
76
|
-
* ~33ms ed è UNA invocazione per tick, indipendentemente dal numero di task.
|
|
75
|
+
* su una scala propria come `useArchivable`.
|
|
77
76
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* `
|
|
77
|
+
* T153 — `commitTimes` gatea da sé su `rev-parse HEAD` (davanti allo spawn
|
|
78
|
+
* caro del log, vedi `commit-times.ts`) e a sha invariato torna la STESSA
|
|
79
|
+
* istanza di mappa. L'hook confronta quindi per IDENTITÀ, non più per firma
|
|
80
|
+
* ricostruita: la firma evitava solo il re-render, il gate a monte evita il
|
|
81
|
+
* lavoro. `lastMtime` in `useTasks` resta il gemello sullo stesso schema.
|
|
83
82
|
*/
|
|
84
83
|
export function useCommitTimes(tasksDir, projectRoot) {
|
|
85
84
|
const [commitAt, setCommitAt] = useState(() => new Map());
|
|
86
85
|
useEffect(() => {
|
|
87
|
-
let
|
|
86
|
+
let last = null;
|
|
88
87
|
let alive = true;
|
|
89
88
|
const reload = () => {
|
|
90
89
|
commitTimes(tasksDir, projectRoot).then((next) => {
|
|
91
90
|
if (!alive)
|
|
92
91
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
lastSig = sig;
|
|
92
|
+
if (next === last)
|
|
93
|
+
return; // stessa istanza → il gate non è scattato
|
|
94
|
+
last = next;
|
|
97
95
|
setCommitAt(next);
|
|
98
96
|
});
|
|
99
97
|
};
|
|
@@ -108,29 +106,24 @@ export function useCommitTimes(tasksDir, projectRoot) {
|
|
|
108
106
|
}
|
|
109
107
|
/**
|
|
110
108
|
* T67 — mappa cappello/figlie (`**Parent Task**`, `Size: Epic`), sullo STESSO
|
|
111
|
-
* poll di `tasks.md` e non su una scala propria
|
|
112
|
-
* per il read+parse di 106 task file, meno dei ~33 ms di `git log` che
|
|
113
|
-
* `useCommitTimes` paga già ogni tick. La parentela si edita anche a mano su
|
|
114
|
-
* un task file già in tabella — un trigger sulla sola firma degli id, come
|
|
115
|
-
* `useArchivable`, non vedrebbe quell'edit e lo lascerebbe stale fino al
|
|
116
|
-
* prossimo giro largo.
|
|
109
|
+
* poll di `tasks.md` e non su una scala propria.
|
|
117
110
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
111
|
+
* T153 — `scanEpicHierarchy` gatea da sé sulla mtime massima dei task file
|
|
112
|
+
* (davanti al read+parse caro, vedi `epic-hierarchy.ts`) e a mtime invariata
|
|
113
|
+
* torna la STESSA istanza: la parentela editata a mano dentro un file già in
|
|
114
|
+
* tabella resta vista, perché il gate è sulla mtime e non sugli id. L'hook
|
|
115
|
+
* confronta quindi per IDENTITÀ, non più per firma ricostruita, gemello di
|
|
116
|
+
* `useCommitTimes` per lo stesso motivo (P1 preflight).
|
|
121
117
|
*/
|
|
122
118
|
export function useEpicHierarchy(tasksDir) {
|
|
123
119
|
const [hierarchy, setHierarchy] = useState(EMPTY_EPIC_HIERARCHY);
|
|
124
120
|
useEffect(() => {
|
|
125
|
-
let
|
|
121
|
+
let last = null;
|
|
126
122
|
const reload = () => {
|
|
127
123
|
const next = scanEpicHierarchy(tasksDir);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (sig === lastSig)
|
|
132
|
-
return;
|
|
133
|
-
lastSig = sig;
|
|
124
|
+
if (next === last)
|
|
125
|
+
return; // stessa istanza → il gate non è scattato
|
|
126
|
+
last = next;
|
|
134
127
|
setHierarchy(next);
|
|
135
128
|
};
|
|
136
129
|
reload();
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
134
|
+
/** Cambia l'azione e RISCRIVE prompt e modello col default del nuovo kind (D2).
|
|
130
135
|
*
|
|
131
|
-
* Nessuna preservazione
|
|
132
|
-
*
|
|
133
|
-
* ACCIDENTALE
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
-
|
|
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
|
package/dist/prompt-catalog.js
CHANGED
|
@@ -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 →
|
|
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
|
-
|
|
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
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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
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
|
|
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 >
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
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
|
-
|
|
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="$
|
|
535
|
+
_kind="$CATALOG_KIND"
|
|
507
536
|
if [[ "$_kind" == "none" ]]; then
|
|
508
537
|
PROMPT=""
|
|
509
538
|
else
|
package/scripts/prompt-catalog
CHANGED
|
@@ -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}`
|
|
4
|
-
# Righe vuote e righe che iniziano con `#` sono
|
|
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
|
|
9
|
-
# Come codice, il secondo lato dovrebbe riscriverlo, e
|
|
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
|