@lamemind/loom-deck 0.76.0 → 0.77.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/README.md +23 -1
- package/dist/actions.js +32 -5
- package/dist/config.js +1 -1
- package/dist/deck-run.js +7 -0
- package/dist/doc-tree.js +1 -1
- package/dist/inbox.js +1 -1
- package/dist/platform.js +1 -2
- package/dist/project-path.js +31 -0
- package/dist/sane-note.js +18 -24
- package/dist/spawn-catalog.js +4 -4
- package/dist/spawn-config.js +4 -4
- package/dist/spawn.js +51 -7
- package/dist/tab-args.js +141 -0
- package/dist/tab-compose.js +233 -0
- package/dist/tab-launch.js +169 -0
- package/dist/tab-wrapper.js +44 -0
- package/dist/tasks.js +1 -1
- package/dist/windows-terminal.js +153 -0
- package/dist/wrap-scan.js +1 -1
- package/package.json +2 -1
- package/scripts/deck-run +11 -678
- package/scripts/prompt-catalog +6 -11
package/scripts/deck-run
CHANGED
|
@@ -2,681 +2,14 @@
|
|
|
2
2
|
#
|
|
3
3
|
# deck-run <TaskID> | deck-run --no-task — spawn primitive (UI-agnostico) di loom-deck.
|
|
4
4
|
#
|
|
5
|
-
# Apre una tab
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
# Fuori dai quattro, e volutamente non un asse: --new-window, che cambia SOLO il
|
|
17
|
-
# verbo dell'exec finale (tab nella finestra attiva → finestra nuova).
|
|
18
|
-
# Il terzo asse (T56) prima non esisteva: il prompt era una CONSEGUENZA degli
|
|
19
|
-
# altri due (bound ⇒ recap, --no-task ⇒ niente, --resume ⇒ niente), quindi
|
|
20
|
-
# "task-bound SENZA prompt" era inesprimibile e l'unico modo di non avere un
|
|
21
|
-
# prompt era perdere la task. Il kind è un SIMBOLO e il suo testo sta nel
|
|
22
|
-
# catalogo condiviso `prompt-catalog` (sibling di questo script).
|
|
23
|
-
# T117 — accanto al kind c'è ora `--prompt <testo>`, che passa il prompt
|
|
24
|
-
# LETTERALE. I due sono mutuamente esclusivi: sono due modi di dire la stessa
|
|
25
|
-
# cosa, e accettarli insieme costringerebbe a stabilire quale vince. Serve a chi
|
|
26
|
-
# mostra il prompt all'utente prima dello spawn e glielo lascia modificare —
|
|
27
|
-
# dopo una modifica nessun kind descrive più quel testo.
|
|
28
|
-
# Il quarto asse (T108) è indipendente dagli altri tre: vale sul ramo bound come
|
|
29
|
-
# su --no-task, su una sessione nuova come su una ripresa.
|
|
30
|
-
#
|
|
31
|
-
# Con --no-task la sessione è NUDA: niente LOOM_TASK, niente --session-id.
|
|
32
|
-
# Serve al lavoro spot che non appartiene a nessuna task.
|
|
33
|
-
# T134 — un prompt iniziale ce l'ha se glielo si passa LETTERALE (`--prompt`):
|
|
34
|
-
# il lavoro sulla doc — drenare un file inbox, srotolare l'hard-wrap di un path
|
|
35
|
-
# — non appartiene a nessuna task e non deve ereditarne una. I `--prompt-kind`
|
|
36
|
-
# del catalogo restano invece rifiutati con --no-task: nominano tutti un TaskID.
|
|
37
|
-
#
|
|
38
|
-
# Con --resume <sid> riprende una conversazione esistente; aggiungendo --fork la
|
|
39
|
-
# riprende RAMANDOLA (`--fork-session`): id nuovo, transcript copiato, l'origine
|
|
40
|
-
# resta intatta e scrivibile.
|
|
41
|
-
#
|
|
42
|
-
# Su stdout, prima di aprire la tab, stampa una riga
|
|
43
|
-
# LOOM_DECK_INTAB <comando>
|
|
44
|
-
# col comando che girerà DENTRO la tab (la sessione `claude` con le sue env).
|
|
45
|
-
# Serve a chi spawna deck-run e vuole mostrare l'invocazione vera senza
|
|
46
|
-
# ricomporla per conto proprio; le diagnostiche restano tutte su stderr.
|
|
47
|
-
#
|
|
48
|
-
# Spawn: `ptyxis --tab -- bash -lc <cmd>`, comando inline sul profilo di default
|
|
49
|
-
# di Ptyxis — LOOM_TASK e chiave di stato viaggiano dentro il comando, nessuna
|
|
50
|
-
# scrittura dconf e nessun profilo dedicato.
|
|
51
|
-
#
|
|
52
|
-
# Env:
|
|
53
|
-
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
54
|
-
# LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
|
|
55
|
-
# T161: inerte sugli spawn che arrivano dal deck TUI —
|
|
56
|
-
# quelli passano SEMPRE `--prompt`, su cui non ha mai
|
|
57
|
-
# vinto. Resta per le invocazioni a mano e per i test.
|
|
58
|
-
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
59
|
-
# LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: `id` del file
|
|
60
|
-
# config del progetto; settata a vuoto = nessun annuncio)
|
|
61
|
-
# LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
|
|
62
|
-
# LOOM_DECK_MODEL modello quando --model non è passato (batte il catalogo, default: fable)
|
|
63
|
-
#
|
|
64
|
-
set -euo pipefail
|
|
65
|
-
|
|
66
|
-
# Il catalogo dei prompt è un file DATI sibling di questo script, non un `case`
|
|
67
|
-
# qui dentro: lo legge anche il deck TUI, che con la stessa risoluzione sibling
|
|
68
|
-
# arriva allo stesso file della stessa installazione.
|
|
69
|
-
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
70
|
-
PROMPT_CATALOG="${LOOM_DECK_PROMPT_CATALOG:-${_SCRIPT_DIR}/prompt-catalog}"
|
|
71
|
-
|
|
72
|
-
TASK=""
|
|
73
|
-
SESSION_ID=""
|
|
74
|
-
RESUME_ID=""
|
|
75
|
-
NO_TASK=0
|
|
76
|
-
FORK=0
|
|
77
|
-
# T162 — finestra NUOVA invece di una tab nella finestra attiva.
|
|
78
|
-
#
|
|
79
|
-
# `ptyxis --tab` va sempre nella finestra col focus, e va bene per il deck, che
|
|
80
|
-
# è già una tab della finestra del progetto. Un chiamante che vive FUORI da
|
|
81
|
-
# quella finestra — compass, che riapre una conversazione pinnata di un
|
|
82
|
-
# progetto chiuso — non ha una finestra attiva giusta: senza un verbo diverso
|
|
83
|
-
# la tab finisce nella finestra di un altro progetto, o in una a caso.
|
|
84
|
-
#
|
|
85
|
-
# Non è un quinto asse: non cambia cosa gira nella tab, solo dove la tab nasce.
|
|
86
|
-
NEW_WINDOW=0
|
|
87
|
-
TITLE_NOTE=""
|
|
88
|
-
# Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
|
|
89
|
-
# "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
|
|
90
|
-
# (nessun prompt, come sempre) e il secondo è un errore d'uso.
|
|
91
|
-
PROMPT_KIND=""
|
|
92
|
-
# T117 — prompt letterale. Vuoto NON basta a dire "non passato": `--prompt ''`
|
|
93
|
-
# è una richiesta legittima (nessun prompt, deciso da chi ha svuotato il campo),
|
|
94
|
-
# quindi la presenza del flag si tiene in un booleano a parte.
|
|
95
|
-
PROMPT_TEXT=""
|
|
96
|
-
PROMPT_GIVEN=0
|
|
97
|
-
# Vuoto = flag non passato: la cascata (env, poi default) si risolve più sotto,
|
|
98
|
-
# insieme alla validazione, così l'ingresso da argomento e quello da env passano
|
|
99
|
-
# per lo stesso enum.
|
|
100
|
-
MODEL=""
|
|
101
|
-
# Positional <TaskID> + flag opzionale --session-id <uuid> (T27): il deck genera
|
|
102
|
-
# l'UUID e lo pinna così il binding sidecar sessionId↔taskId è deterministico.
|
|
103
|
-
# --no-task (T42): modalità NUDA, senza TaskID — nessuna LOOM_TASK, nessun prompt
|
|
104
|
-
# iniziale, nessun sessionId pinnato. Flag esplicito e non "positional opzionale"
|
|
105
|
-
# perché un `deck-run` a mani vuote resta un errore d'uso, non una sessione spot.
|
|
106
|
-
# --resume <sid> (T49): riapre una sessione esistente con `claude --resume`.
|
|
107
|
-
# Componibile con entrambe le forme: `deck-run <TaskID> --resume <sid>` (scoped:
|
|
108
|
-
# la ripresa eredita LOOM_TASK + titolo · task) e `deck-run --no-task --resume
|
|
109
|
-
# <sid>` (spot: resume nudo). Su resume niente prompt iniziale (si continua la
|
|
110
|
-
# conversazione, non se ne inietta una nuova) e niente --session-id (l'id ce
|
|
111
|
-
# l'ha già la sessione ripresa).
|
|
112
|
-
# --fork (T28): MODIFICATORE di --resume, speculare al `--fork-session` del CLI
|
|
113
|
-
# (che è definito allo stesso modo: "when resuming, create a new session ID").
|
|
114
|
-
# Non è una terza forma di spawn ma una variante della ripresa → richiede
|
|
115
|
-
# --resume, e senza di esso è un errore d'uso, non un fork "dell'ultima".
|
|
116
|
-
# Sotto --fork il --session-id torna AMMESSO (anzi: è il punto) — la mutua
|
|
117
|
-
# esclusione con --resume esiste perché una ripresa nuda riscrive il transcript
|
|
118
|
-
# dell'id ripreso, mentre il fork ne apre uno NUOVO, che possiamo quindi pinnare.
|
|
119
|
-
# --prompt-kind <none|recap|recap-task|recap-epic|preflight|run|checkpoint>: sceglie il prompt iniziale fra
|
|
120
|
-
# quelli del catalogo qui sotto. Enum e non stringa libera: il prompt viaggia
|
|
121
|
-
# dentro apici singoli in `bash -lc`, quindi il testo va tenuto in un posto solo
|
|
122
|
-
# e verificato una volta, invece di spostare il rischio di quoting su ogni
|
|
123
|
-
# chiamante. Assente → `recap`, così ogni invocazione preesistente è invariata.
|
|
124
|
-
# --title-note <testo> (T64): appende la nota della conversazione al titolo della
|
|
125
|
-
# tab. È l'UNICO ingresso di testo libero nel titolo — gli altri componenti
|
|
126
|
-
# (emoji/name dal file committato, TaskID) sono controllati — quindi il testo non
|
|
127
|
-
# si quota, si RIDUCE a un alfabeto sicuro (vedi _sane_note): tutto ciò che non
|
|
128
|
-
# ci rientra sparisce, apici inclusi. L'alfabeto ammette quattro emoji (T156)
|
|
129
|
-
# perché il deck ci fa passare il prefisso del titolo di fallback (`🚀 slug`);
|
|
130
|
-
# ogni altra emoji cade ancora.
|
|
131
|
-
# --model <fable|opus|sonnet|haiku> (T108): modello della sessione. Enum e non
|
|
132
|
-
# stringa libera perché il valore finisce dentro `bash -lc` e un refuso
|
|
133
|
-
# produrrebbe una tab con un comando che il CLI rifiuta. Le voci sono ALIAS e
|
|
134
|
-
# restano tali: espanderle a id versionati (`claude-opus-5`) cablerebbe qui una
|
|
135
|
-
# generazione, che al primo bump diventa un modello inesistente.
|
|
136
|
-
while [[ $# -gt 0 ]]; do
|
|
137
|
-
case "$1" in
|
|
138
|
-
--model)
|
|
139
|
-
MODEL="${2:-}"; shift 2 ;;
|
|
140
|
-
--model=*)
|
|
141
|
-
MODEL="${1#*=}"; shift ;;
|
|
142
|
-
--title-note)
|
|
143
|
-
TITLE_NOTE="${2:-}"; shift 2 ;;
|
|
144
|
-
--title-note=*)
|
|
145
|
-
TITLE_NOTE="${1#*=}"; shift ;;
|
|
146
|
-
--prompt-kind)
|
|
147
|
-
PROMPT_KIND="${2:-}"; shift 2 ;;
|
|
148
|
-
--prompt-kind=*)
|
|
149
|
-
PROMPT_KIND="${1#*=}"; shift ;;
|
|
150
|
-
--prompt)
|
|
151
|
-
PROMPT_TEXT="${2:-}"; PROMPT_GIVEN=1; shift 2 ;;
|
|
152
|
-
--prompt=*)
|
|
153
|
-
PROMPT_TEXT="${1#*=}"; PROMPT_GIVEN=1; shift ;;
|
|
154
|
-
--session-id)
|
|
155
|
-
SESSION_ID="${2:-}"; shift 2 ;;
|
|
156
|
-
--session-id=*)
|
|
157
|
-
SESSION_ID="${1#*=}"; shift ;;
|
|
158
|
-
--resume)
|
|
159
|
-
RESUME_ID="${2:-}"; shift 2 ;;
|
|
160
|
-
--resume=*)
|
|
161
|
-
RESUME_ID="${1#*=}"; shift ;;
|
|
162
|
-
--fork)
|
|
163
|
-
FORK=1; shift ;;
|
|
164
|
-
--no-task)
|
|
165
|
-
NO_TASK=1; shift ;;
|
|
166
|
-
--new-window)
|
|
167
|
-
NEW_WINDOW=1; shift ;;
|
|
168
|
-
*)
|
|
169
|
-
if [[ -z "$TASK" ]]; then TASK="$1"; shift
|
|
170
|
-
else echo "argomento inatteso: '$1'" >&2; exit 2; fi ;;
|
|
171
|
-
esac
|
|
172
|
-
done
|
|
173
|
-
|
|
174
|
-
USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--session-id <uuid>]
|
|
175
|
-
(es. deck-run T18)
|
|
176
|
-
| deck-run --no-task (sessione nuda, senza task)
|
|
177
|
-
| deck-run <TaskID> --resume <uuid> (riprende sessione scoped)
|
|
178
|
-
| deck-run --no-task --resume <uuid> (riprende sessione spot)
|
|
179
|
-
| deck-run <TaskID> --resume <uuid> --fork [--session-id <nuovo-uuid>]
|
|
180
|
-
(forka: ramo con id NUOVO)
|
|
181
|
-
|
|
182
|
-
--prompt-kind prompt iniziale della sessione bound (default: recap)
|
|
183
|
-
none nessun prompt — sessione aperta sulla task, a mani nude
|
|
184
|
-
recap /loom-works:recap-status <TaskID> (dispatcher)
|
|
185
|
-
recap-task /loom-works:recap-status-task <TaskID> (task non cappello)
|
|
186
|
-
recap-epic /loom-works:recap-status-epic <TaskID> (Size: Epic)
|
|
187
|
-
preflight /loom-works:preflight-task <TaskID>
|
|
188
|
-
run /loom-works:run-task <TaskID>
|
|
189
|
-
checkpoint /loom-works:checkpoint-task <TaskID>
|
|
190
|
-
|
|
191
|
-
--prompt prompt LETTERALE, per chi lo ha già composto (o fatto
|
|
192
|
-
modificare). Esclusivo con --prompt-kind; vuoto = nessun
|
|
193
|
-
prompt. Ammesso anche con --no-task: un testo già scritto non
|
|
194
|
-
ha una task da nominare, a differenza dei kind del catalogo
|
|
195
|
-
|
|
196
|
-
--model modello della sessione: fable|opus|sonnet|haiku
|
|
197
|
-
(default: il modello del kind in catalogo, o 'fable' fuori catalogo)
|
|
198
|
-
valore ignoto → fallback sul default, con avviso su stderr
|
|
199
|
-
|
|
200
|
-
--title-note nota della conversazione, appesa al titolo tab
|
|
201
|
-
(ridotta a lettere/cifre/spazi/-/_, cap 60 char)
|
|
202
|
-
|
|
203
|
-
--new-window apre una FINESTRA nuova invece di una tab nella finestra
|
|
204
|
-
attiva. Per chi chiama da fuori dalla finestra del progetto"
|
|
205
|
-
|
|
206
|
-
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
207
|
-
echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
|
|
208
|
-
echo "$USAGE" >&2
|
|
209
|
-
exit 2
|
|
210
|
-
fi
|
|
211
|
-
if [[ $NO_TASK -eq 0 && -z "$TASK" ]]; then
|
|
212
|
-
echo "$USAGE" >&2
|
|
213
|
-
exit 2
|
|
214
|
-
fi
|
|
215
|
-
if [[ $FORK -eq 1 && -z "$RESUME_ID" ]]; then
|
|
216
|
-
echo "--fork richiede --resume <uuid>: si forka una conversazione esistente, non il nulla" >&2
|
|
217
|
-
echo "$USAGE" >&2
|
|
218
|
-
exit 2
|
|
219
|
-
fi
|
|
220
|
-
if [[ -n "$RESUME_ID" && -n "$SESSION_ID" && $FORK -eq 0 ]]; then
|
|
221
|
-
echo "--resume e --session-id sono mutuamente esclusivi (la sessione ripresa ha già il suo id)" >&2
|
|
222
|
-
echo "usa --fork se vuoi un ramo con id nuovo" >&2
|
|
223
|
-
echo "$USAGE" >&2
|
|
224
|
-
exit 2
|
|
225
|
-
fi
|
|
226
|
-
# Un kind fuori catalogo è un errore d'uso del CHIAMANTE (simbolo interno, non
|
|
227
|
-
# configurazione utente) → exit, non fallback silenzioso come permissionMode:
|
|
228
|
-
# lì il valore arriva da un file editabile a mano, qui da un argomento nostro.
|
|
229
|
-
if [[ -n "$PROMPT_KIND" ]]; then
|
|
230
|
-
case "$PROMPT_KIND" in
|
|
231
|
-
none|recap|recap-task|recap-epic|preflight|run|checkpoint) ;;
|
|
232
|
-
*) echo "--prompt-kind ignoto: '${PROMPT_KIND}' (usa none|recap|recap-task|recap-epic|preflight|run|checkpoint)" >&2
|
|
233
|
-
echo "$USAGE" >&2
|
|
234
|
-
exit 2 ;;
|
|
235
|
-
esac
|
|
236
|
-
fi
|
|
237
|
-
# Ogni prompt del catalogo nomina il TaskID: senza task non c'è nulla da
|
|
238
|
-
# chiedere, e un `--no-task --prompt-kind run` è una richiesta contraddittoria,
|
|
239
|
-
# non una da degradare in silenzio.
|
|
240
|
-
if [[ $NO_TASK -eq 1 && -n "$PROMPT_KIND" ]]; then
|
|
241
|
-
echo "--prompt-kind richiede una task: una sessione --no-task non ha prompt iniziale" >&2
|
|
242
|
-
echo "$USAGE" >&2
|
|
243
|
-
exit 2
|
|
244
|
-
fi
|
|
245
|
-
# T134/D9 — `--prompt` con `--no-task` è AMMESSO, e il divieto che c'era qui è
|
|
246
|
-
# caduto. Il razionale del divieto valeva per il catalogo, non per il testo
|
|
247
|
-
# letterale: i sette kind nominano tutti il TaskID, quindi senza task non c'è
|
|
248
|
-
# nulla da chiedere loro; un prompt già composto dal chiamante non ha invece
|
|
249
|
-
# nessuna task da nominare — e ce ne sono di legittimi che non ne hanno una.
|
|
250
|
-
# Il caso che ha fatto cadere il divieto: il deck offre il drain di un file
|
|
251
|
-
# inbox e lo srotolamento dei `.md` di un path, due lavori che stanno sulla doc
|
|
252
|
-
# e non su una task; legarli a un cappello esporterebbe `LOOM_TASK`, farebbe
|
|
253
|
-
# iniettare in contesto il task file di un lavoro chiuso e metterebbe `· T<n>`
|
|
254
|
-
# nel titolo della tab (D10 preflight).
|
|
255
|
-
#
|
|
256
|
-
# Il divieto gemello su `--prompt-kind` RESTA. Toglierlo non è simmetrico:
|
|
257
|
-
# ogni template del catalogo interpola `{TASK}`, e senza task produrrebbe un
|
|
258
|
-
# prompt come `/loom-works:run-task ` con l'id vuoto — cioè una sessione che
|
|
259
|
-
# parte su un comando monco, che è il guasto che il divieto esiste per evitare.
|
|
260
|
-
# Due modi di dire la stessa cosa: accettarli insieme obbligherebbe a stabilire
|
|
261
|
-
# quale vince, cioè a scrivere una precedenza che nessun chiamante ha chiesto.
|
|
262
|
-
if [[ $PROMPT_GIVEN -eq 1 && -n "$PROMPT_KIND" ]]; then
|
|
263
|
-
echo "--prompt e --prompt-kind sono mutuamente esclusivi (il testo o il simbolo, non entrambi)" >&2
|
|
264
|
-
echo "$USAGE" >&2
|
|
265
|
-
exit 2
|
|
266
|
-
fi
|
|
267
|
-
|
|
268
|
-
WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
|
|
269
|
-
|
|
270
|
-
# ── Titolo tab = label standard loom (matchabile da compass) ──────────────────
|
|
271
|
-
# compass matcha le finestre cercando nel titolo un prefisso `{emoji} {name}`,
|
|
272
|
-
# dove i due token sono DERIVATI dal contratto committato .claude/loom-works.json
|
|
273
|
-
# → es. "🧵 loom-works". Un titolo "cc <task>" non contiene la label → finestra
|
|
274
|
-
# ORFANA (progetto appare spento anche mentre ci lavori dentro). Deriviamo la
|
|
275
|
-
# label dal file e la usiamo come titolo, con
|
|
276
|
-
# suffisso "· <task>" per restare distinguibili fra più tab claude (il suffisso
|
|
277
|
-
# non rompe il match: è .includes, non equals — stesso pattern del "· deck").
|
|
278
|
-
# Fallback su "cc <task>" se manca il file o jq (progetto non loom-registered).
|
|
279
|
-
|
|
280
|
-
# Riduzione della nota a un alfabeto sicuro (T64). Whitelist, non blacklist: il
|
|
281
|
-
# titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
|
|
282
|
-
# nota è testo digitato a mano — enumerare i caratteri pericolosi significa
|
|
283
|
-
# sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
|
|
284
|
-
# italiane, spazio, trattino, underscore e sei emoji (sotto); tutto il resto
|
|
285
|
-
# (apici, backslash, `$`, backtick, `;`, quadre, ogni altra emoji…) cade.
|
|
286
|
-
#
|
|
287
|
-
# T156 — le emoji entrano per il PREFISSO del titolo che il deck compone
|
|
288
|
-
# (`🚀 slug`). Senza di loro il prefisso spariva qui e restava nella lista del
|
|
289
|
-
# deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la stessa
|
|
290
|
-
# conversazione portava due titoli diversi, senza errore. Le quadre che
|
|
291
|
-
# delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
|
|
292
|
-
# stacca già dallo slug.
|
|
293
|
-
# T161/T160 — le sette stanno tutte nei TEMPLATE di titolo del catalogo delle
|
|
294
|
-
# azioni (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può
|
|
295
|
-
# riscrivere: quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox
|
|
296
|
-
# che si drena, 📏 il path che si srotola, 🧭 il bersaglio doc che si
|
|
297
|
-
# riorganizza.
|
|
298
|
-
# Questa whitelist ha un GEMELLO TypeScript (`saneNote` in `src/sane-note.ts`),
|
|
299
|
-
# che riduce il template al salvataggio perché quello che sta nel file di
|
|
300
|
-
# progetto sia quello che la tab mostrerà. I due lati non si leggono a vicenda e
|
|
301
|
-
# vanno cambiati insieme: il custode è il gate di `test/deck-run.test.ts`, che
|
|
302
|
-
# passa la stessa batteria di input a questa funzione e al gemello e pretende
|
|
303
|
-
# l'uguaglianza.
|
|
304
|
-
# La whitelist le nomina una per una e NON apre un intervallo di code point. Due
|
|
305
|
-
# ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
|
|
306
|
-
# cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
|
|
307
|
-
# quel verdetto si verifica un glifo alla volta (🗺️ è astrale e finisce comunque
|
|
308
|
-
# in `·`); e l'ordine di un intervallo dentro un bracket expression segue la
|
|
309
|
-
# collazione della locale, non i code point.
|
|
310
|
-
#
|
|
311
|
-
# Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
|
|
312
|
-
# da sola, quindi il limite serve solo a non spingere fuori vista la parte del
|
|
313
|
-
# titolo che compass matcha, non a far stare la nota nella tab.
|
|
314
|
-
_sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
|
|
315
|
-
# LC_ALL locale alla funzione: serve a bash per tagliare a 60 CARATTERI e non
|
|
316
|
-
# a 60 byte (a byte, un cap che cade a metà di una `à` lascia UTF-8 rotto nel
|
|
317
|
-
# titolo). L'assegnazione rifà setlocale, l'uscita dalla funzione lo ripristina.
|
|
318
|
-
local LC_ALL=C.UTF-8
|
|
319
|
-
local s
|
|
320
|
-
# La locale va rimessa SUL `sed`, non ereditata da qui: `local LC_ALL` non
|
|
321
|
-
# porta l'attributo export quando `LC_ALL` non era già esportata nell'ambiente
|
|
322
|
-
# (misurato: il figlio la vede vuota), quindi `sed` ricade su `LANG` — sotto
|
|
323
|
-
# una locale `C` lavora a byte e un carattere multibyte fuori whitelist lascia
|
|
324
|
-
# nel titolo i byte che coincidono con quelli ammessi. Le emoji astrali, che
|
|
325
|
-
# condividono i primi due byte `f0 9f`, ne lasciano sempre due: `🔥 fuoco`
|
|
326
|
-
# diventa `\xf0\x9f fuoco`, cioè UTF-8 rotto dentro il titolo della tab.
|
|
327
|
-
# `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
|
|
328
|
-
# come byte di continuazione di una sequenza UTF-8.
|
|
329
|
-
s="$(printf '%s' "$1" \
|
|
330
|
-
| LC_ALL=C.UTF-8 sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏🧭-]//g' \
|
|
331
|
-
| tr -s ' ')"
|
|
332
|
-
s="${s# }"; s="${s% }"
|
|
333
|
-
printf '%s' "${s:0:60}"
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
_find_project_root() { # <start-dir> → dir con .claude/loom-works.json, o vuoto
|
|
337
|
-
local d="${1%/}"
|
|
338
|
-
while [[ -n "$d" && "$d" != "/" ]]; do
|
|
339
|
-
[[ -f "$d/.claude/loom-works.json" ]] && { printf '%s\n' "$d"; return 0; }
|
|
340
|
-
d="$(dirname "$d")"
|
|
341
|
-
done
|
|
342
|
-
[[ -f "/.claude/loom-works.json" ]] && { printf '/\n'; return 0; }
|
|
343
|
-
return 1
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
# Project root risolta UNA volta: la usano sia la label sia permissionMode (T45).
|
|
347
|
-
_proot=""
|
|
348
|
-
if command -v jq >/dev/null 2>&1; then
|
|
349
|
-
_proot="$(_find_project_root "$WORKDIR" || true)"
|
|
350
|
-
fi
|
|
351
|
-
_cfg=""
|
|
352
|
-
[[ -n "$_proot" ]] && _cfg="${_proot}/.claude/loom-works.json"
|
|
353
|
-
|
|
354
|
-
# La label vale per OGNI surface claude, task o no (T42): il match compass è
|
|
355
|
-
# window-level e non sa nulla di task → legare la titolazione alla presenza di
|
|
356
|
-
# una task sarebbe un errore di layer. Cambia solo il suffisso: `· <task>` quando
|
|
357
|
-
# la sessione è bound, nessun suffisso quando è nuda.
|
|
358
|
-
if [[ $NO_TASK -eq 1 ]]; then TITLE="cc"; else TITLE="cc ${TASK}"; fi
|
|
359
|
-
if [[ -n "$_cfg" ]]; then
|
|
360
|
-
_label="$(jq -r 'if (.emoji and .name)
|
|
361
|
-
then "\(.emoji) \(.name)" else empty end' \
|
|
362
|
-
"$_cfg" 2>/dev/null || true)"
|
|
363
|
-
if [[ -n "${_label:-}" ]]; then
|
|
364
|
-
if [[ $NO_TASK -eq 1 ]]; then TITLE="${_label}"; else TITLE="${_label} · ${TASK}"; fi
|
|
365
|
-
fi
|
|
366
|
-
fi
|
|
367
|
-
# Suffisso nota (T64): distingue fra loro le tab di più conversazioni sulla
|
|
368
|
-
# STESSA task, che altrimenti condividono un titolo identico (`label · T81`) —
|
|
369
|
-
# la nota è già la maniglia con cui l'utente le distingue in lista. In coda per
|
|
370
|
-
# la solita ragione degli altri suffissi: il match compass è `.includes(label)`,
|
|
371
|
-
# la label deve restare intatta in TESTA.
|
|
372
|
-
# T150 — nudo, senza i caporali `«…»` che lo decoravano: la nota di fallback
|
|
373
|
-
# generata dal deck quando il campo è vuoto passa dallo stesso alfabeto ridotto
|
|
374
|
-
# e non ha bisogno di una cornice per distinguersi da un titolo scritto a mano.
|
|
375
|
-
# Nota ridotta a vuoto (era tutta emoji/punteggiatura) → nessun suffisso.
|
|
376
|
-
if [[ -n "$TITLE_NOTE" ]]; then
|
|
377
|
-
_note="$(_sane_note "$TITLE_NOTE")"
|
|
378
|
-
[[ -n "$_note" ]] && TITLE="${TITLE} ${_note}"
|
|
379
|
-
fi
|
|
380
|
-
# Suffisso fork (T28): un ramo eredita task e label dell'origine, quindi senza
|
|
381
|
-
# marcatore le due tab risulterebbero omonime nella stessa window. Suffisso e
|
|
382
|
-
# non prefisso perché il match compass è `.includes(label)` e la label deve
|
|
383
|
-
# restare intatta in testa — stesso schema di `· <task>` e `· deck`.
|
|
384
|
-
[[ $FORK -eq 1 ]] && TITLE="${TITLE} · fork"
|
|
385
|
-
|
|
386
|
-
# ── permissionMode (T45) ─────────────────────────────────────────────────────
|
|
387
|
-
# Precedenza: env LOOM_DECK_PERMISSION_MODE > campo del file config > 'manual'.
|
|
388
|
-
# Il flag è passato SEMPRE, anche per 'manual': lo spawn resta deterministico e
|
|
389
|
-
# leggibile nel process tree, invece di dipendere dal default del CLI (che può
|
|
390
|
-
# cambiare fra versioni). Un valore fuori enum NON viene passato al CLI (che
|
|
391
|
-
# rifiuterebbe, lasciando una tab con un comando rotto): fallback su 'manual'
|
|
392
|
-
# con segnalazione su stderr.
|
|
393
|
-
PERM_MODE="${LOOM_DECK_PERMISSION_MODE:-}"
|
|
394
|
-
if [[ -z "$PERM_MODE" && -n "$_cfg" ]]; then
|
|
395
|
-
PERM_MODE="$(jq -r '.permissionMode // empty' "$_cfg" 2>/dev/null || true)"
|
|
396
|
-
fi
|
|
397
|
-
case "${PERM_MODE:-}" in
|
|
398
|
-
acceptEdits|auto|bypassPermissions|manual|dontAsk|plan) ;;
|
|
399
|
-
'') PERM_MODE="manual" ;;
|
|
400
|
-
*) echo "permissionMode ignoto: '${PERM_MODE}' → fallback 'manual'" >&2
|
|
401
|
-
PERM_MODE="manual" ;;
|
|
402
|
-
esac
|
|
403
|
-
MODE_FLAG="--permission-mode ${PERM_MODE} "
|
|
404
|
-
|
|
405
|
-
# ── modello (T108 + T152) ────────────────────────────────────────────────────
|
|
406
|
-
# Precedenza: --model > env LOOM_DECK_MODEL > modello del kind nel catalogo >
|
|
407
|
-
# 'fable'.
|
|
408
|
-
#
|
|
409
|
-
# T161 — questa cascata vale per chi invoca deck-run A MANO. Il deck TUI passa
|
|
410
|
-
# SEMPRE `--model` già risolto (default del deck ← override di progetto nel
|
|
411
|
-
# blocco `spawn` di `.claude/loom-works.json`), quindi dal deck si ferma al primo
|
|
412
|
-
# gradino e i tre sotto non intervengono mai. Il blocco di progetto qui non si
|
|
413
|
-
# legge di proposito: `jq` è dipendenza opzionale di questo script, e dargli un
|
|
414
|
-
# JSON da consultare la renderebbe obbligatoria su un percorso che oggi degrada
|
|
415
|
-
# senza. L'ultimo gradino è il gemello bash di `MODEL_DEFAULT` in spawn.ts e
|
|
416
|
-
# va tenuto allineato a mano: i due lati non si leggono a vicenda (il deck deve
|
|
417
|
-
# poter MOSTRARE la selezione iniziale prima che deck-run esista come processo),
|
|
418
|
-
# quindi un cambio qui senza l'altro fa partire dalla lista un modello diverso
|
|
419
|
-
# da quello che il detail aveva scritto a schermo.
|
|
420
|
-
# Fino a T152 il default era un valore unico per tutta la famiglia,
|
|
421
|
-
# perché il frontmatter `model:` delle skill riscriveva comunque il modello di
|
|
422
|
-
# sessione — un default configurabile qui non sarebbe servito a nulla. Da
|
|
423
|
-
# quando le skill non dichiarano più `model:`, il default DEVE viaggiare col
|
|
424
|
-
# kind: `run` costa quanto `preflight` solo se «vuole sonnet» è scritto da
|
|
425
|
-
# qualche parte che deck-run consulta, e quella parte è il catalogo che il
|
|
426
|
-
# deck TUI legge già per il testo del prompt (T117) — stessa fonte, stesso
|
|
427
|
-
# `kind`, niente seconda copia.
|
|
428
|
-
# Il flag è passato SEMPRE, anche sul default, per la stessa ragione di
|
|
429
|
-
# permissionMode: lo spawn resta leggibile nel process tree invece di dipendere
|
|
430
|
-
# dal default del CLI, che cambia fra versioni.
|
|
431
|
-
# Valore fuori enum → fallback con avviso, non exit. Regime opposto a
|
|
432
|
-
# --prompt-kind, e il discriminante è dove finisce il valore: un kind ignoto si
|
|
433
|
-
# ferma DENTRO lo script (nessun template da scegliere, l'errore è del deck),
|
|
434
|
-
# un modello ignoto arriverebbe al CLI e produrrebbe una tab con un comando che
|
|
435
|
-
# fallisce all'avvio — cioè il guasto che permissionMode evita degradando.
|
|
436
|
-
_model_for_kind() { # <kind> → modello dichiarato in catalogo (3ª colonna), o niente
|
|
437
|
-
local want="$1" k t m
|
|
438
|
-
[[ -n "$want" && -f "$PROMPT_CATALOG" ]] || return 1
|
|
439
|
-
while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
|
|
440
|
-
[[ -z "$k" || "$k" == \#* ]] && continue
|
|
441
|
-
[[ "$k" == "$want" && -n "$m" ]] && { printf '%s' "$m"; return 0; }
|
|
442
|
-
done < "$PROMPT_CATALOG"
|
|
443
|
-
return 1
|
|
444
|
-
}
|
|
445
|
-
# Kind di catalogo effettivo: SOLO quando la sessione è bound e senza prompt
|
|
446
|
-
# letterale — le due strade (--no-task, --prompt) non hanno una riga di
|
|
447
|
-
# catalogo da consultare, né per il prompt (sotto) né per il modello (qui).
|
|
448
|
-
# Riusato tal quale nella composizione del prompt: stesso guard, stesso valore.
|
|
449
|
-
CATALOG_KIND=""
|
|
450
|
-
if [[ $PROMPT_GIVEN -eq 0 && $NO_TASK -eq 0 ]]; then
|
|
451
|
-
CATALOG_KIND="${PROMPT_KIND:-recap}"
|
|
452
|
-
fi
|
|
453
|
-
MODEL="${MODEL:-${LOOM_DECK_MODEL:-}}"
|
|
454
|
-
if [[ -z "$MODEL" && -n "$CATALOG_KIND" && "$CATALOG_KIND" != "none" ]]; then
|
|
455
|
-
MODEL="$(_model_for_kind "$CATALOG_KIND" || true)"
|
|
456
|
-
fi
|
|
457
|
-
case "${MODEL:-}" in
|
|
458
|
-
fable|opus|sonnet|haiku) ;;
|
|
459
|
-
'') MODEL="fable" ;;
|
|
460
|
-
*) echo "modello ignoto: '${MODEL}' → fallback 'fable' (usa fable|opus|sonnet|haiku)" >&2
|
|
461
|
-
MODEL="fable" ;;
|
|
462
|
-
esac
|
|
463
|
-
MODEL_FLAG="--model ${MODEL} "
|
|
464
|
-
|
|
465
|
-
# ── Chiave di stato per compass ──────────────────────────────────────────────
|
|
466
|
-
# Lo stato di una sessione (running/ask/done) viaggia verso compass via D-Bus
|
|
467
|
-
# keyed su $PTYXIS_PROFILE (hook Claude → `compass <stato>`), e compass lo mappa
|
|
468
|
-
# a un progetto SOLO se quella chiave è l'`id` di un progetto del registry. La
|
|
469
|
-
# tab nasce da `ptyxis --tab` nudo, e Ptyxis esporta da sé PTYXIS_PROFILE col
|
|
470
|
-
# valore del profilo DEFAULT: senza un prefisso l'annuncio arriverebbe keyed su
|
|
471
|
-
# un UUID che nessun progetto dichiara → stato scartato, badge fermo per sempre.
|
|
472
|
-
# Il match della FINESTRA non se ne accorge (passa dal titolo, non dalla
|
|
473
|
-
# chiave), quindi il progetto risulta presente ma senza stato — un guasto che
|
|
474
|
-
# non si manifesta come errore, solo come badge che non cambia mai.
|
|
475
|
-
# Rimedio: forzare in-tab PTYXIS_PROFILE=<id del progetto>. Il nome della
|
|
476
|
-
# variabile è storico: non contiene l'UUID di un profilo Ptyxis, e nessuna
|
|
477
|
-
# chiamata a Ptyxis la usa. È la stessa chiave che mette il bottone 🤖 di compass.
|
|
478
|
-
#
|
|
479
|
-
# `${VAR+set}`, non `${VAR:-}`: env settata anche a VUOTO = "nessun annuncio"
|
|
480
|
-
# (deterministico per i test). Non settata = `id` del file config del progetto.
|
|
481
|
-
# Assente/non risolvibile → nessun prefisso: si degrada a stato orfano invece di
|
|
482
|
-
# rompere lo spawn.
|
|
483
|
-
if [[ -n "${LOOM_DECK_STATE_PROFILE+set}" ]]; then
|
|
484
|
-
STATE_PROFILE="${LOOM_DECK_STATE_PROFILE}"
|
|
485
|
-
else
|
|
486
|
-
STATE_PROFILE=""
|
|
487
|
-
if [[ -n "$_cfg" ]]; then
|
|
488
|
-
STATE_PROFILE="$(jq -r '.id // empty' "$_cfg" 2>/dev/null || true)"
|
|
489
|
-
fi
|
|
490
|
-
fi
|
|
491
|
-
# Whitelist come per la nota del titolo: il valore entra in `bash -lc "…"`, e il
|
|
492
|
-
# file config è editabile a mano. Lo schema non vincola la forma dell'`id`:
|
|
493
|
-
# accetto l'alfabeto id-safe e scarto il resto invece di quotare.
|
|
494
|
-
if [[ -n "$STATE_PROFILE" && ! "$STATE_PROFILE" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
|
495
|
-
echo "profilo di stato ignorato (caratteri non ammessi): '${STATE_PROFILE}'" >&2
|
|
496
|
-
STATE_PROFILE=""
|
|
497
|
-
fi
|
|
498
|
-
PROFILE_ENV=""
|
|
499
|
-
[[ -n "$STATE_PROFILE" ]] && PROFILE_ENV="PTYXIS_PROFILE=${STATE_PROFILE} "
|
|
500
|
-
|
|
501
|
-
# --session-id per la tab: pinna il sessionId della sessione CC (vuoto → CC ne
|
|
502
|
-
# genera uno). Lo spazio finale tiene la concatenazione pulita quando è assente.
|
|
503
|
-
SID_FLAG=""
|
|
504
|
-
[[ -n "$SESSION_ID" ]] && SID_FLAG="--session-id ${SESSION_ID} "
|
|
505
|
-
|
|
506
|
-
# --resume per la tab (T49): riprende la sessione esistente. Stesso pattern
|
|
507
|
-
# flag-opzionale di SID_FLAG — la variante resta dentro l'unico IN_TAB_CMD.
|
|
508
|
-
RES_FLAG=""
|
|
509
|
-
[[ -n "$RESUME_ID" ]] && RES_FLAG="--resume ${RESUME_ID} "
|
|
510
|
-
|
|
511
|
-
# --fork-session per la tab (T28): CC apre un id NUOVO invece di riscrivere
|
|
512
|
-
# quello ripreso → mai due writer sullo stesso JSONL. Terzo gemello del pattern
|
|
513
|
-
# flag-opzionale, per la stessa ragione degli altri due: la variante non deve
|
|
514
|
-
# duplicare IN_TAB_CMD.
|
|
515
|
-
FORK_FLAG=""
|
|
516
|
-
[[ $FORK -eq 1 ]] && FORK_FLAG="--fork-session "
|
|
517
|
-
|
|
518
|
-
# ── Catalogo dei prompt iniziali (T56 · T117) ────────────────────────────────
|
|
519
|
-
# Il prompt iniziale è la scelta di CHI APRE la sessione, non una proprietà della
|
|
520
|
-
# task: lo stesso task file si apre per leggerne lo stato, per un preflight, per
|
|
521
|
-
# eseguirlo o a mani nude. Il kind è quell'intento.
|
|
522
|
-
# none → nessun prompt (≠ stringa vuota: PROMPT_ARG sparisce del tutto,
|
|
523
|
-
# come sul ramo --resume, altrimenti CC riceve un posizionale vuoto)
|
|
524
|
-
# recap → skill di recap sulla task. È un DISPATCHER: `recap-status`
|
|
525
|
-
# risolve la task, classifica (progetto / task / epica) e passa a
|
|
526
|
-
# una sotto-skill. Fino a v0.45 era invece un prompt diretto
|
|
527
|
-
# ("recap stato task <id>"), scelto perché non esisteva una skill
|
|
528
|
-
# di recap tarata sulla singola task e quella di progetto avrebbe
|
|
529
|
-
# risposto largo — vincolo caduto col dispatcher.
|
|
530
|
-
# recap-task → la sotto-skill diretta, senza il giro del dispatcher
|
|
531
|
-
# recap-epic → idem per un cappello (`Size: Epic`)
|
|
532
|
-
# I due specializzati li sceglie chi SA già come è fatta la task:
|
|
533
|
-
# il detail del deck, che il task file l'ha aperto. Chi non lo sa
|
|
534
|
-
# — gli acceleratori della lista, che leggono solo `tasks.md`, e
|
|
535
|
-
# quindi non vedono il `Size` — resta su `recap` e lascia
|
|
536
|
-
# classificare al dispatcher. Il criterio non è duplicato: sta in
|
|
537
|
-
# `taskIsEpic` (src/tasks.ts) e legge lo stesso campo che leggerebbe
|
|
538
|
-
# il dispatcher; quello che cambia è solo QUANDO viene letto.
|
|
539
|
-
# preflight → skill di preflight sulla task
|
|
540
|
-
# run → skill di esecuzione sulla task
|
|
541
|
-
# checkpoint → skill di checkpoint sulla task
|
|
542
|
-
#
|
|
543
|
-
# T117 — i testi non stanno più qui ma nel file dati `prompt-catalog`, sibling di
|
|
544
|
-
# questo script. Il deck TUI li mostra nel campo prompt del detail prima dello
|
|
545
|
-
# spawn, e per farlo deve leggerli: come `case` bash sarebbe una seconda scrittura
|
|
546
|
-
# delle stesse regole. Il file resta l'unica copia, letta da entrambi i lati.
|
|
547
|
-
# Formato a righe e non JSON perché `jq` è dipendenza OPZIONALE di questo script
|
|
548
|
-
# (con fallback) e leggere il catalogo lo renderebbe dura.
|
|
549
|
-
#
|
|
550
|
-
# Precedenza con l'env: LOOM_DECK_ENTER_PROMPT VINCE sul kind (retro-compat: chi
|
|
551
|
-
# lo usa oggi non si accorge del cambio), tranne su `none` — chiedere
|
|
552
|
-
# esplicitamente nessun prompt non è una richiesta che un default d'ambiente
|
|
553
|
-
# possa scavalcare. Non vince invece su `--prompt`, che è una richiesta esplicita
|
|
554
|
-
# e già risolta: un override d'ambiente che scavalca un testo composto a mano
|
|
555
|
-
# butterebbe via proprio la modifica che il flag esiste per portare.
|
|
556
|
-
# Ogni template nomina il TaskID via `{TASK}`: la lista del deck è monofamiglia
|
|
557
|
-
# (`T`, unico prefisso del contratto loom), quindi nessun prompt discrimina sulla
|
|
558
|
-
# forma dell'id.
|
|
559
|
-
_prompt_template() { # <kind> → template col placeholder {TASK}, o niente
|
|
560
|
-
local want="$1" k t m
|
|
561
|
-
[[ -f "$PROMPT_CATALOG" ]] || return 1
|
|
562
|
-
# 3 variabili, non 2 (T152): da quando il catalogo porta una 3ª colonna
|
|
563
|
-
# modello, un `read -r k t` a due si mangerebbe il tab separatore e il
|
|
564
|
-
# modello dentro `t`, che è esattamente il template passato al CLI.
|
|
565
|
-
while IFS=$'\t' read -r k t m || [[ -n "$k" ]]; do
|
|
566
|
-
[[ -z "$k" || "$k" == \#* ]] && continue
|
|
567
|
-
[[ "$k" == "$want" ]] && { printf '%s' "$t"; return 0; }
|
|
568
|
-
done < "$PROMPT_CATALOG"
|
|
569
|
-
return 1
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if [[ $PROMPT_GIVEN -eq 1 ]]; then
|
|
573
|
-
PROMPT="$PROMPT_TEXT"
|
|
574
|
-
elif [[ $NO_TASK -eq 1 ]]; then
|
|
575
|
-
# Sessione nuda senza `--prompt`: nessun prompt iniziale, punto.
|
|
576
|
-
#
|
|
577
|
-
# Il default `recap` del catalogo NON si applica qui, e la guardia deve stare
|
|
578
|
-
# a monte invece che a valle. Finché il ramo `--no-task` scartava il prompt
|
|
579
|
-
# al momento di comporre il comando in-tab, un `PROMPT` risolto e mai usato
|
|
580
|
-
# era innocuo; da quando il PROMPT_ARG entra anche in quel ramo (T134/D9,
|
|
581
|
-
# per il drain e lo srotolamento) il testo del catalogo ci arriva davvero —
|
|
582
|
-
# e siccome ogni template interpola `{TASK}`, con la task vuota diventa
|
|
583
|
-
# `/loom-works:recap-status ` con l'id monco: la sessione vuota del deck (`c`)
|
|
584
|
-
# nasceva su un recap che nessuno ha chiesto.
|
|
585
|
-
#
|
|
586
|
-
# Stessa ragione per `LOOM_DECK_ENTER_PROMPT`, che interpola `{TASK}` a sua
|
|
587
|
-
# volta e qui resta quindi inerte: senza task non ha nulla da nominare.
|
|
588
|
-
PROMPT=""
|
|
589
|
-
else
|
|
590
|
-
_kind="$CATALOG_KIND"
|
|
591
|
-
if [[ "$_kind" == "none" ]]; then
|
|
592
|
-
PROMPT=""
|
|
593
|
-
else
|
|
594
|
-
# Catalogo assente o voce mancante: il kind è già passato dall'enum sopra,
|
|
595
|
-
# quindi qui manca il FILE — un'installazione mutila, non un errore d'uso.
|
|
596
|
-
# Si degrada a nessun prompt dicendolo, invece di aprire una sessione con un
|
|
597
|
-
# prompt inventato al volo (che sarebbe la copia del catalogo).
|
|
598
|
-
if ! PROMPT="$(_prompt_template "$_kind")"; then
|
|
599
|
-
echo "catalogo prompt non leggibile ('${PROMPT_CATALOG}'): sessione senza prompt iniziale" >&2
|
|
600
|
-
PROMPT=""
|
|
601
|
-
fi
|
|
602
|
-
PROMPT="${PROMPT//\{TASK\}/${TASK}}"
|
|
603
|
-
fi
|
|
604
|
-
if [[ "$_kind" != "none" && -n "${LOOM_DECK_ENTER_PROMPT:-}" ]]; then
|
|
605
|
-
PROMPT="${LOOM_DECK_ENTER_PROMPT//\{TASK\}/${TASK}}"
|
|
606
|
-
fi
|
|
607
|
-
fi
|
|
608
|
-
|
|
609
|
-
# Comando eseguito dentro la tab: bind LOOM_TASK per la sessione, avvia CC sul PROMPT.
|
|
610
|
-
# `--name '<TITLE>'` = canale AUTORITATIVO del terminal-title (Ptyxis -T è solo il
|
|
611
|
-
# titolo iniziale, prima che CC parta): CC lo setta e nomina anche la sessione nel
|
|
612
|
-
# picker. Override-abile via LOOM_DECK_INTAB_CMD per i test empirici (probe non-CC).
|
|
613
|
-
# Ramo --no-task (T42): saltano i TRE elementi che legano la sessione alla task —
|
|
614
|
-
# iniezione LOOM_TASK, prompt iniziale, --session-id pinnato. Resta claude nudo,
|
|
615
|
-
# titolato e con il permission mode del progetto.
|
|
616
|
-
# Su resume (T49) il prompt iniziale salta anche nel ramo task: riprendere una
|
|
617
|
-
# conversazione significa continuarla, non iniettarle un nuovo messaggio. Il
|
|
618
|
-
# fork (T28) ricade nello stesso ramo — è una ripresa, con id nuovo.
|
|
619
|
-
# Il kind `none` (T56) atterra sulla stessa assenza per una ragione diversa: lì
|
|
620
|
-
# la sessione è nuova e bound, ma l'utente ha chiesto di entrarci a mani nude.
|
|
621
|
-
#
|
|
622
|
-
# QUOTING DEL PROMPT — il testo entra in `bash -lc "$IN_TAB_CMD"`, cioè in una
|
|
623
|
-
# riga che una shell PARSA. Finché i template erano scritti qui dentro bastava il
|
|
624
|
-
# vincolo "nessun apice singolo nel catalogo"; da T117 il testo può arrivare da
|
|
625
|
-
# `--prompt`, cioè digitato a mano dall'utente nel campo del detail — e lì un
|
|
626
|
-
# apice singolo chiuderebbe la stringa, consegnando alla shell tutto ciò che
|
|
627
|
-
# segue come comando. Si passa quindi per la stessa forma canonica di
|
|
628
|
-
# `shellQuote` in spawn.ts: apice chiuso, apice letterale escapato, apice
|
|
629
|
-
# riaperto (`'\''`). Dentro apici singoli nessun altro carattere resta speciale,
|
|
630
|
-
# quindi non c'è nient'altro da enumerare — newline compresi.
|
|
631
|
-
_shell_quote() { # <testo> → il testo come argomento singolo per una shell POSIX
|
|
632
|
-
printf "'%s'" "${1//\'/\'\\\'\'}"
|
|
633
|
-
}
|
|
634
|
-
PROMPT_ARG=""
|
|
635
|
-
[[ -n "$PROMPT" && -z "$RESUME_ID" ]] && PROMPT_ARG="$(_shell_quote "$PROMPT")"
|
|
636
|
-
# Il SID_FLAG entra anche nel ramo --no-task, che altrimenti lo scarta insieme
|
|
637
|
-
# agli altri elementi task-bound: nel fork di una sessione SPOT il sessionId
|
|
638
|
-
# pinnato non serve a legare una task (non c'è) ma a rendere noto in anticipo
|
|
639
|
-
# l'id del ramo, senza il quale il lineage non sarebbe registrabile.
|
|
640
|
-
# PROFILE_ENV in testa a entrambi i rami: lo stato è una proprietà della SESSIONE
|
|
641
|
-
# (esiste anche senza task), esattamente come la label del titolo — legarlo al
|
|
642
|
-
# ramo task-bound ripeterebbe l'errore di layer già evitato per la titolazione.
|
|
643
|
-
# MODEL_FLAG in entrambi i rami e prima dei flag di continuità: il modello è la
|
|
644
|
-
# scelta di chi apre la sessione, non una proprietà della task né della ripresa
|
|
645
|
-
# (T108) — stesso layer di MODE_FLAG, che infatti gli sta accanto.
|
|
646
|
-
if [[ $NO_TASK -eq 1 ]]; then
|
|
647
|
-
# T134/D9 — il PROMPT_ARG entra anche qui: dal 134 una sessione nuda può
|
|
648
|
-
# nascere su un prompt letterale (drain di un file inbox, srotolamento di un
|
|
649
|
-
# path). Resta vuoto su ogni altro percorso `--no-task`, perché senza
|
|
650
|
-
# `--prompt` il ramo del catalogo non gira — `PROMPT_KIND` con `--no-task`
|
|
651
|
-
# è ancora rifiutato a monte.
|
|
652
|
-
_default_intab="${PROFILE_ENV}claude --name '${TITLE}' ${MODE_FLAG}${MODEL_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
|
|
653
|
-
else
|
|
654
|
-
_default_intab="${PROFILE_ENV}LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${MODEL_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
|
|
655
|
-
fi
|
|
656
|
-
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
657
|
-
|
|
658
|
-
# ANNUNCIO del comando in-tab, su stdout, prima dell'exec.
|
|
659
|
-
#
|
|
660
|
-
# È l'unico modo che ha un chiamante di sapere cosa gira davvero dentro la tab:
|
|
661
|
-
# il comando si compone QUI — catalogo dei prompt, quoting, permission mode,
|
|
662
|
-
# profilo di stato, titolo — e ricomporlo dall'altro lato sarebbe una seconda
|
|
663
|
-
# scrittura delle stesse regole, divergente al primo flag aggiunto. Il deck lo
|
|
664
|
-
# legge da questa riga e la mostra nella propria riga di stato.
|
|
665
|
-
#
|
|
666
|
-
# Deve stare DOPO l'ultima assegnazione di IN_TAB_CMD e PRIMA dell'exec (che
|
|
667
|
-
# sostituisce il processo): annunciare un valore ancora modificabile
|
|
668
|
-
# annuncerebbe qualcosa di diverso da quello eseguito.
|
|
669
|
-
#
|
|
670
|
-
# stdout e non stderr: qui non c'è nessuna anomalia da segnalare, è l'esito
|
|
671
|
-
# normale del lavoro dello script. Le diagnostiche restano su stderr, e i
|
|
672
|
-
# chiamanti che leggono l'ULTIMA riga di stdout (lo shim `ptyxis` dei test)
|
|
673
|
-
# vedono comunque quello che vedevano prima.
|
|
674
|
-
printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
|
|
675
|
-
|
|
676
|
-
# Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova —
|
|
677
|
-
# `--new-window` è l'unica differenza fra i due casi.
|
|
678
|
-
if [[ $NEW_WINDOW -eq 1 ]]; then WIN_FLAG="--new-window"; else WIN_FLAG="--tab"; fi
|
|
679
|
-
|
|
680
|
-
# -- <cmd>: comando inline nella tab. LOOM_TASK e chiave di stato viaggiano
|
|
681
|
-
# dentro il comando → nessuna scrittura dconf, nessun profilo dedicato.
|
|
682
|
-
exec ptyxis "$WIN_FLAG" -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
|
|
5
|
+
# Apre una tab di terminale nella finestra attiva e vi avvia una sessione Claude
|
|
6
|
+
# Code: Ptyxis su Linux, Windows Terminal su Windows. Argomenti, regole e
|
|
7
|
+
# messaggi: `deck-run` senza argomenti stampa l'uso.
|
|
8
|
+
#
|
|
9
|
+
# T176 — la composizione è TypeScript (`src/tab-args.ts`, `src/tab-compose.ts`,
|
|
10
|
+
# `src/tab-launch.ts`), una sola per le due piattaforme. Questo file resta allo
|
|
11
|
+
# stesso path, eseguibile, come shim: compass lo risolve come
|
|
12
|
+
# `<cli>/../scripts/deck-run` e ne controlla l'eseguibilità prima di lanciarlo, e
|
|
13
|
+
# `test/deck-run.test.ts` lo esegue a scatola chiusa come gate di parità.
|
|
14
|
+
# Esegue il BUILD (`dist/`), come il bin del pacchetto.
|
|
15
|
+
exec node "$(dirname "${BASH_SOURCE[0]}")/../dist/deck-run.js" "$@"
|