@lamemind/loom-deck 0.72.2 → 0.73.1
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 +2 -2
- package/dist/task-edit.js +16 -21
- package/dist/tasks.js +46 -17
- package/package.json +1 -1
- package/scripts/deck-run +29 -67
package/README.md
CHANGED
|
@@ -405,8 +405,8 @@ Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato t
|
|
|
405
405
|
|
|
406
406
|
I testi del catalogo stanno in `scripts/prompt-catalog` (formato `kind<TAB>template`, `{TASK}` interpolato), non dentro lo script: li legge anche il deck, per mostrare nel detail il prompt prima dello spawn. `--prompt` passa invece un testo **letterale** ed è esclusivo con `--prompt-kind` — sono due modi di dire la stessa cosa, e accettarli insieme costringerebbe a stabilire quale vince. Il testo viene quotato per la shell in-tab, apici singoli compresi: è l'unico ingresso di testo libero in una riga che una shell parsa.
|
|
407
407
|
|
|
408
|
-
La tab porta anche `PTYXIS_PROFILE` forzata
|
|
409
|
-
(settata a vuoto → nessun annuncio); progetto
|
|
408
|
+
La tab porta anche `PTYXIS_PROFILE` forzata all'`id` del progetto, letto da `.claude/loom-works.json`: è la chiave con cui loom-compass associa a un progetto lo stato annunciato dagli hook (running/ask/done). Il nome della variabile è storico — non contiene l'UUID di un profilo Ptyxis. Una tab `ptyxis --tab` nuda porterebbe l'UUID del profilo di default, e l'annuncio finirebbe keyed su una chiave che nessun progetto dichiara — badge fermo, senza alcun errore visibile. Un `id` fuori da `^[A-Za-z0-9_-]+$` viene scartato con un avviso su stderr, perché il valore entra in una riga che una shell parsa. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
|
|
409
|
+
(settata a vuoto → nessun annuncio); progetto senza file config → nessun prefisso.
|
|
410
410
|
|
|
411
411
|
## Sviluppo (TUI Ink)
|
|
412
412
|
|
package/dist/task-edit.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// isolata in `writeTaskEdit`, così la grammatica delle sostituzioni è testabile
|
|
4
4
|
// senza toccare il filesystem.
|
|
5
5
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
-
import { TASK_ID_RE, findTaskFile,
|
|
6
|
+
import { TASK_ID_RE, findTaskFile, overviewRows } from './tasks.js';
|
|
7
7
|
// Il task file scrive la priorità per NOME (`- **Priority**: Med`), tasks.md la
|
|
8
8
|
// scrive come GLIFO (colonna Pri). Due rappresentazioni dello stesso valore →
|
|
9
9
|
// due mappe, entrambe keyed sul nome canonico di view.ts (unica fonte del rango).
|
|
@@ -83,10 +83,13 @@ export function initialDetail(current, prog, date = today()) {
|
|
|
83
83
|
* toccate restano i token grezzi originali: nessun re-flow della tabella, il
|
|
84
84
|
* diff resta di una riga. `ok:false` = id assente → il chiamante non scrive nulla.
|
|
85
85
|
*
|
|
86
|
-
* Le
|
|
87
|
-
* `parseTasks
|
|
88
|
-
*
|
|
89
|
-
*
|
|
86
|
+
* Le righe candidate vengono da `overviewRows`, lo stesso confine di
|
|
87
|
+
* `parseTasks`: solo dentro la Tasks Overview, con le colonne del suo HEADER e
|
|
88
|
+
* mai un indice cablato. Qui si SCRIVE su disco, quindi senza confine una riga
|
|
89
|
+
* cross-deps con lo stesso id (`| T07 | — | ✔️T08 |`) sopra la tabella verrebbe
|
|
90
|
+
* riscritta al posto della riga task, con Pri/Prog/titolo sopra le celle Parent e
|
|
91
|
+
* Cross Deps; e una colonna in più o in meno darebbe un titolo sovrascritto col
|
|
92
|
+
* glifo. Header assente → nessuna riga → `ok:false`, non si tira a indovinare.
|
|
90
93
|
*
|
|
91
94
|
* `desc` undefined ≠ `desc` vuota: la prima non tocca la cella (chiamata
|
|
92
95
|
* legacy, solo pri/prog), la seconda la svuota davvero.
|
|
@@ -104,23 +107,14 @@ export function updateTasksMdRow(content, id, priGlyph, progGlyph, desc) {
|
|
|
104
107
|
if (!TASK_ID_RE.test(id))
|
|
105
108
|
return { content, ok: false };
|
|
106
109
|
const lines = content.split('\n');
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
let ok = false;
|
|
111
|
-
const out = lines.map((line) => {
|
|
112
|
-
if (ok)
|
|
113
|
-
return line;
|
|
114
|
-
if (!line.trim().startsWith('|'))
|
|
115
|
-
return line;
|
|
110
|
+
for (const { index, line, id: rowId, cols } of overviewRows(lines)) {
|
|
111
|
+
if (rowId !== id)
|
|
112
|
+
continue;
|
|
116
113
|
const cells = line.split('|');
|
|
117
|
-
if ((cells[1] ?? '').trim() !== id)
|
|
118
|
-
return line;
|
|
119
114
|
// Serve la cella desc PIÙ la coda dopo il `|` finale: senza la coda lo
|
|
120
115
|
// splice inserirebbe invece di sostituire.
|
|
121
116
|
if (cells.length < cols.desc + 2)
|
|
122
|
-
|
|
123
|
-
ok = true;
|
|
117
|
+
continue; // riga malformata → non toccarla
|
|
124
118
|
cells[cols.pri] = ` ${priGlyph} `;
|
|
125
119
|
cells[cols.prog] = ` ${progGlyph} `;
|
|
126
120
|
if (desc !== undefined) {
|
|
@@ -128,9 +122,10 @@ export function updateTasksMdRow(content, id, priGlyph, progGlyph, desc) {
|
|
|
128
122
|
// ben formata) e va conservata, o la riga perderebbe il bordo destro.
|
|
129
123
|
cells.splice(cols.desc, cells.length - cols.desc - 1, ` ${sanitizeCell(desc)} `);
|
|
130
124
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
lines[index] = cells.join('|');
|
|
126
|
+
return { content: lines.join('\n'), ok: true };
|
|
127
|
+
}
|
|
128
|
+
return { content, ok: false };
|
|
134
129
|
}
|
|
135
130
|
/**
|
|
136
131
|
* Rende un testo scrivibile dentro una CELLA di tabella markdown. Due caratteri
|
package/dist/tasks.js
CHANGED
|
@@ -59,31 +59,60 @@ export function headerColumns(line) {
|
|
|
59
59
|
return null;
|
|
60
60
|
return { pri, prog, desc: prog + 1 };
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Le righe task della Tasks Overview: FONTE UNICA del confine della tabella,
|
|
64
|
+
* condivisa da `parseTasks` (lettura) e `updateTasksMdRow` (scrittura su disco),
|
|
65
|
+
* come `TASK_ID_RE` e `headerColumns` — due confini scritti in due posti
|
|
66
|
+
* divergerebbero, e quello dello scrittore è quello che fa danno.
|
|
67
|
+
*
|
|
68
|
+
* La tabella apre all'header (`headerColumns`) e chiude alla PRIMA riga che non
|
|
69
|
+
* inizia con `|`; un secondo header ne riapre una, con le proprie colonne. Il
|
|
70
|
+
* confine serve perché la prima cella `^T\d+$` non basta a riconoscere una riga
|
|
71
|
+
* task: le righe della tabella cross-deps dell'Execution Plan
|
|
72
|
+
* (`| T07 | — | ✔️T08 |`) hanno la stessa forma appena perdono il 🔒 davanti
|
|
73
|
+
* all'id, e senza confine diventavano task doppie con Pri = cella Parent e
|
|
74
|
+
* Prog = cella Cross Deps.
|
|
75
|
+
*
|
|
76
|
+
* Il titolo `## Tasks Overview` non entra nel criterio: il gate è l'header, così
|
|
77
|
+
* il deck non dipende dal nome della sezione. Il prezzo: una riga vuota o un
|
|
78
|
+
* commento DENTRO la tabella la chiude, e le righe sotto spariscono dal deck
|
|
79
|
+
* senza errore.
|
|
80
|
+
*/
|
|
81
|
+
export function overviewRows(lines) {
|
|
82
|
+
const rows = [];
|
|
70
83
|
let cols = null;
|
|
71
|
-
|
|
84
|
+
lines.forEach((line, index) => {
|
|
72
85
|
const header = headerColumns(line);
|
|
73
86
|
if (header) {
|
|
74
87
|
cols = header;
|
|
75
|
-
|
|
88
|
+
return;
|
|
76
89
|
}
|
|
77
90
|
if (!cols)
|
|
78
|
-
|
|
91
|
+
return;
|
|
79
92
|
const t = line.trim();
|
|
80
|
-
if (!t.startsWith('|'))
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
if (!t.startsWith('|')) {
|
|
94
|
+
cols = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// cells[0] = '' (prima del primo |): l'id è la seconda cella. Header e
|
|
98
|
+
// separatore non matchano `^T\d+$` → scartati.
|
|
99
|
+
const id = (t.split('|')[1] ?? '').trim();
|
|
100
|
+
if (TASK_ID_RE.test(id))
|
|
101
|
+
rows.push({ index, line, id, cols });
|
|
102
|
+
});
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
// Estrae le righe `| Tnn | Pri | Prog | Task |` della Tasks Overview — il
|
|
106
|
+
// confine della tabella sta in `overviewRows`.
|
|
107
|
+
//
|
|
108
|
+
// Le righe si parsano solo DOPO aver incontrato l'header: senza indici derivati
|
|
109
|
+
// non si tira a indovinare, la lista resta vuota. Un fallimento visibile invece
|
|
110
|
+
// di una cella letta al posto sbagliato.
|
|
111
|
+
export function parseTasks(content) {
|
|
112
|
+
const tasks = [];
|
|
113
|
+
for (const { line, id, cols } of overviewRows(content.split('\n'))) {
|
|
83
114
|
// cells[0] = '' (prima del primo |), cells[last] = '' (dopo l'ultimo |)
|
|
84
|
-
const
|
|
85
|
-
if (!TASK_ID_RE.test(id))
|
|
86
|
-
continue;
|
|
115
|
+
const cells = line.trim().split('|').map((c) => c.trim());
|
|
87
116
|
const pri = cells[cols.pri] ?? '';
|
|
88
117
|
const prog = cells[cols.prog] ?? '';
|
|
89
118
|
// desc = colonna finale; join per resistere a eventuali `|` nella descrizione.
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -45,25 +45,19 @@
|
|
|
45
45
|
# Serve a chi spawna deck-run e vuole mostrare l'invocazione vera senza
|
|
46
46
|
# ricomporla per conto proprio; le diagnostiche restano tutte su stderr.
|
|
47
47
|
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# La scelta fra le due è la Decisione aperta §9 della proposta: la modalità
|
|
53
|
-
# "inline" è il default perché non muta stato dconf condiviso. Vedi la nota
|
|
54
|
-
# di decisione nella task folder di T18.
|
|
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.
|
|
55
51
|
#
|
|
56
52
|
# Env:
|
|
57
|
-
# LOOM_DECK_SPAWN_MODE inline|profile (default: inline)
|
|
58
|
-
# LOOM_DECK_PROFILE_UUID UUID profilo Ptyxis (default: cc-host)
|
|
59
53
|
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
60
54
|
# LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
|
|
61
55
|
# T161: inerte sugli spawn che arrivano dal deck TUI —
|
|
62
56
|
# quelli passano SEMPRE `--prompt`, su cui non ha mai
|
|
63
57
|
# vinto. Resta per le invocazioni a mano e per i test.
|
|
64
58
|
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
65
|
-
# LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default:
|
|
66
|
-
# del progetto
|
|
59
|
+
# LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: `id` del file
|
|
60
|
+
# config del progetto; settata a vuoto = nessun annuncio)
|
|
67
61
|
# LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
|
|
68
62
|
# LOOM_DECK_MODEL modello quando --model non è passato (batte il catalogo, default: fable)
|
|
69
63
|
#
|
|
@@ -89,7 +83,6 @@ FORK=0
|
|
|
89
83
|
# la tab finisce nella finestra di un altro progetto, o in una a caso.
|
|
90
84
|
#
|
|
91
85
|
# Non è un quinto asse: non cambia cosa gira nella tab, solo dove la tab nasce.
|
|
92
|
-
# Vale identico nei due rami di spawn (inline e profile).
|
|
93
86
|
NEW_WINDOW=0
|
|
94
87
|
TITLE_NOTE=""
|
|
95
88
|
# Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
|
|
@@ -272,8 +265,6 @@ if [[ $PROMPT_GIVEN -eq 1 && -n "$PROMPT_KIND" ]]; then
|
|
|
272
265
|
exit 2
|
|
273
266
|
fi
|
|
274
267
|
|
|
275
|
-
MODE="${LOOM_DECK_SPAWN_MODE:-inline}"
|
|
276
|
-
PROFILE_UUID="${LOOM_DECK_PROFILE_UUID:-5a36ae48df1c4d4882f43060e3e59656}"
|
|
277
268
|
WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
|
|
278
269
|
|
|
279
270
|
# ── Titolo tab = label standard loom (matchabile da compass) ──────────────────
|
|
@@ -471,40 +462,34 @@ case "${MODEL:-}" in
|
|
|
471
462
|
esac
|
|
472
463
|
MODEL_FLAG="--model ${MODEL} "
|
|
473
464
|
|
|
474
|
-
# ──
|
|
465
|
+
# ── Chiave di stato per compass ──────────────────────────────────────────────
|
|
475
466
|
# Lo stato di una sessione (running/ask/done) viaggia verso compass via D-Bus
|
|
476
467
|
# keyed su $PTYXIS_PROFILE (hook Claude → `compass <stato>`), e compass lo mappa
|
|
477
|
-
# a un progetto SOLO se
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
# progetto dichiara → stato scartato,
|
|
481
|
-
# della FINESTRA non se ne accorge (passa dal titolo, non
|
|
482
|
-
# progetto risulta presente ma senza stato — un guasto che
|
|
483
|
-
# errore, solo come
|
|
484
|
-
# Rimedio: forzare in-tab
|
|
485
|
-
#
|
|
486
|
-
#
|
|
487
|
-
# use-custom-command=true, che fa scartare il comando inline — vedi il ramo
|
|
488
|
-
# `profile` sotto, che per questo deve riscrivere dconf.
|
|
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.
|
|
489
478
|
#
|
|
490
|
-
# `${VAR+set}`, non `${VAR:-}`: env settata anche a VUOTO = "
|
|
491
|
-
# (deterministico per i test
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
# (stato orfano) invece di rompere lo spawn.
|
|
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.
|
|
495
483
|
if [[ -n "${LOOM_DECK_STATE_PROFILE+set}" ]]; then
|
|
496
484
|
STATE_PROFILE="${LOOM_DECK_STATE_PROFILE}"
|
|
497
485
|
else
|
|
498
486
|
STATE_PROFILE=""
|
|
499
|
-
if [[ -n "$_cfg" ]]
|
|
500
|
-
|
|
501
|
-
if [[ -n "$_pid" ]]; then
|
|
502
|
-
STATE_PROFILE="$(dconf read "/org/lamemind/loom/projects/${_pid}/bindings/claude/profile" 2>/dev/null | tr -d "'" || true)"
|
|
503
|
-
fi
|
|
487
|
+
if [[ -n "$_cfg" ]]; then
|
|
488
|
+
STATE_PROFILE="$(jq -r '.id // empty' "$_cfg" 2>/dev/null || true)"
|
|
504
489
|
fi
|
|
505
490
|
fi
|
|
506
|
-
# Whitelist come per la nota del titolo: il valore entra in `bash -lc "…"`, e
|
|
507
|
-
#
|
|
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`:
|
|
508
493
|
# accetto l'alfabeto id-safe e scarto il resto invece di quotare.
|
|
509
494
|
if [[ -n "$STATE_PROFILE" && ! "$STATE_PROFILE" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
|
510
495
|
echo "profilo di stato ignorato (caratteri non ammessi): '${STATE_PROFILE}'" >&2
|
|
@@ -688,33 +673,10 @@ IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
|
688
673
|
# vedono comunque quello che vedevano prima.
|
|
689
674
|
printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
|
|
690
675
|
|
|
691
|
-
# Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova
|
|
692
|
-
#
|
|
693
|
-
# è l'unica differenza fra i due casi.
|
|
676
|
+
# Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova —
|
|
677
|
+
# `--new-window` è l'unica differenza fra i due casi.
|
|
694
678
|
if [[ $NEW_WINDOW -eq 1 ]]; then WIN_FLAG="--new-window"; else WIN_FLAG="--tab"; fi
|
|
695
679
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
# LOOM_TASK viaggia dentro il comando → nessuna scrittura dconf.
|
|
700
|
-
exec ptyxis "$WIN_FLAG" -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
|
|
701
|
-
;;
|
|
702
|
-
profile)
|
|
703
|
-
# --tab-with-profile: riusa il profilo (custom-command FISSO) → per iniettare
|
|
704
|
-
# LOOM_TASK+task va riscritto il custom-command al volo via dconf (stato condiviso).
|
|
705
|
-
# Con --new-window i due flag convivono: la finestra nasce e il profilo
|
|
706
|
-
# decide cosa gira nella sua prima tab.
|
|
707
|
-
dpath="/org/gnome/Ptyxis/Profiles/${PROFILE_UUID}/"
|
|
708
|
-
dconf write "${dpath}use-custom-command" true
|
|
709
|
-
dconf write "${dpath}custom-command" "'bash -lc \"${IN_TAB_CMD//\"/\\\"}\"'"
|
|
710
|
-
if [[ $NEW_WINDOW -eq 1 ]]; then
|
|
711
|
-
exec ptyxis --new-window --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
|
|
712
|
-
else
|
|
713
|
-
exec ptyxis --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
|
|
714
|
-
fi
|
|
715
|
-
;;
|
|
716
|
-
*)
|
|
717
|
-
echo "LOOM_DECK_SPAWN_MODE ignoto: '$MODE' (usa 'inline' o 'profile')" >&2
|
|
718
|
-
exit 2
|
|
719
|
-
;;
|
|
720
|
-
esac
|
|
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"
|