@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 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 al profilo bindato al progetto nel registry (`bindings/claude`), letto da dconf: è la chiave con cui loom-compass associa a un progetto lo stato annunciato dagli hook (running/ask/done). Una tab `ptyxis --tab` nuda erediterebbe il profilo di default, e l'annuncio finirebbe keyed su un UUID che nessun progetto dichiara — pallino fermo su idle, senza alcun errore visibile. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
409
- (settata a vuoto → nessun annuncio); progetto non registrato → nessun prefisso.
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, headerColumns } from './tasks.js';
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 posizioni vengono dall'HEADER (`headerColumns`, stessa fonte di
87
- * `parseTasks`), mai da un indice cablato: qui si SCRIVE su disco, quindi una
88
- * colonna in più o in meno non produrrebbe una lettura sbagliata ma un titolo
89
- * sovrascritto col glifo. Header assente → `ok:false`, non si tira a indovinare.
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 cols = lines.reduce((found, l) => found ?? headerColumns(l), null);
108
- if (!cols)
109
- return { content, ok: false };
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
- return line; // riga malformata → non toccarla
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
- return cells.join('|');
132
- });
133
- return { content: ok ? out.join('\n') : content, ok };
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
- // Estrae le righe `| Tnn | Pri | Prog | Task |` della Tasks Overview. Righe
63
- // header/separatore non matchano `^T\d+$` → scartate.
64
- //
65
- // Le righe si parsano solo DOPO aver incontrato l'header: senza indici derivati
66
- // non si tira a indovinare, la lista resta vuota. Un fallimento visibile invece
67
- // di una cella letta al posto sbagliato.
68
- export function parseTasks(content) {
69
- const tasks = [];
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
- for (const line of content.split('\n')) {
84
+ lines.forEach((line, index) => {
72
85
  const header = headerColumns(line);
73
86
  if (header) {
74
87
  cols = header;
75
- continue;
88
+ return;
76
89
  }
77
90
  if (!cols)
78
- continue;
91
+ return;
79
92
  const t = line.trim();
80
- if (!t.startsWith('|'))
81
- continue;
82
- const cells = t.split('|').map((c) => c.trim());
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 id = cells[1];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.72.2",
3
+ "version": "0.73.1",
4
4
  "description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/deck-run CHANGED
@@ -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
- # Modalità spawn (LOOM_DECK_SPAWN_MODE):
49
- # inline → ptyxis --tab -- <cmd> (default) comando inline, LOOM_TASK diretta, zero dconf
50
- # profile → ptyxis --tab-with-profile=<UUID> riusa il profilo, riscrive il custom-command via dconf
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: bindings/claude
66
- # del progetto, letto da dconf; settata a vuoto = nessun annuncio)
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
- # ── Profilo di stato per compass ─────────────────────────────────────────────
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 quell'UUID compare in `bindings/<surface>/profile` del
478
- # registry. Ma nel modo `inline` la tab nasce da `ptyxis --tab` nudo, che eredita
479
- # il profilo DEFAULT di Ptyxis: l'annuncio arriva keyed su un UUID che nessun
480
- # progetto dichiara → stato scartato, pallino fermo su idle per sempre. Il match
481
- # della FINESTRA non se ne accorge (passa dal titolo, non dal profilo), quindi il
482
- # progetto risulta presente ma senza stato — un guasto che non si manifesta come
483
- # errore, solo come pallino che non cambia mai.
484
- # Rimedio: forzare in-tab la PTYXIS_PROFILE del binding claude del progetto, così
485
- # l'annuncio è keyed sull'UUID che compass cerca davvero. Il profilo bindato NON
486
- # è usabile come profilo vero della tab (`--tab-with-profile`): ha
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 = "salta il lookup"
491
- # (deterministico per i test, che altrimenti leggerebbero il dconf della macchina
492
- # su cui gira la suite). Non settata = risolvi dal registry.
493
- # Assente/non risolvibile → nessun prefisso: si degrada al comportamento di prima
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" ]] && command -v dconf >/dev/null 2>&1; then
500
- _pid="$(jq -r '.id // empty' "$_cfg" 2>/dev/null || true)"
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 un
507
- # registry è pur sempre un file editabile a mano. Un UUID Ptyxis è esadecimale;
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. Una
692
- # variabile e non due rami duplicati per ogni modalità di spawn — `--new-window`
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
- case "$MODE" in
697
- inline)
698
- # --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.
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"