@mmmbuto/nexuscrew 0.9.11 → 0.9.13

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.
@@ -152,16 +152,26 @@ const ALIBABA_PI_MODELS = Object.freeze([
152
152
  // Il catalogo live li pubblicizza comunque; qui non entrano.
153
153
  // - grok-4.5 solo su Responses: su Chat risponde 503 e Messages lo rifiuta
154
154
  // esplicitamente ("not supported for format anthropic").
155
+ // - deepseek-v4-flash-vision-exp: MISURATO 2026-08-24 sul gateway
156
+ // opencode.ai/zen/go -> 200 su tutti e tre i wire (Responses
157
+ // status=completed, Messages stop_reason=end_turn, Chat finish_reason=stop).
158
+ // E' l'unico id di questo provider che accetta IMMAGINI, quindi la misura
159
+ // che conta non e' il 200: e' stata fatta mandando un PNG e chiedendo il
160
+ // colore, due volte con due colori diversi (rosso -> "Rosso", verde ->
161
+ // "verde") e su tutte e tre le wire. Un solo colore non avrebbe distinto
162
+ // "ha visto l'immagine" da "ha indovinato". Formati immagine per wire:
163
+ // Messages `source.base64`, Chat `image_url` data URI, Responses
164
+ // `input_image`.
155
165
  const OPENCODE_GO_MESSAGES_MODELS = Object.freeze([
156
- 'deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
166
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
157
167
  'minimax-m3', 'minimax-m2.7', 'minimax-m2.5',
158
168
  'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
159
169
  ]);
160
170
  const OPENCODE_GO_RESPONSES_MODELS = Object.freeze([
161
- 'deepseek-v4-flash', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.5', 'glm-5.2', 'glm-5.1', 'glm-5',
171
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'gpt-5.6-luna', 'grok-4.5', 'glm-5.2', 'glm-5.1', 'glm-5',
162
172
  ]);
163
173
  const OPENCODE_GO_CHAT_MODELS = Object.freeze([
164
- 'deepseek-v4-flash', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
174
+ 'deepseek-v4-flash', 'deepseek-v4-flash-vision-exp', 'deepseek-v4-pro', 'glm-5.2', 'glm-5.1', 'glm-5',
165
175
  'kimi-k3', 'kimi-k2.7-code', 'kimi-k2.6', 'kimi-k2.5',
166
176
  'minimax-m3', 'minimax-m2.7', 'minimax-m2.5',
167
177
  'qwen3.8-max', 'qwen3.7-max', 'qwen3.7-plus', 'qwen3.6-plus', 'qwen3.5-plus',
@@ -181,6 +191,7 @@ const OPENCODE_GO_API_BASE = 'https://opencode.ai/zen/go/v1';
181
191
  // come se ne avesse meno, il sospetto va qui prima che sul client.
182
192
  const OPENCODE_GO_LIMITS = Object.freeze({
183
193
  'deepseek-v4-flash': { context: 1000000, output: 384000 },
194
+ 'deepseek-v4-flash-vision-exp': { context: 1000000, output: 384000 },
184
195
  'deepseek-v4-pro': { context: 1000000, output: 384000 },
185
196
  'glm-5.2': { context: 1000000, output: 131072 },
186
197
  'glm-5.1': { context: 202752, output: 32768 },
@@ -214,12 +225,17 @@ function opencodeGoContextFor(model) {
214
225
  // quindi e' riuso di una dichiarazione esistente. Sugli altri non c'e'
215
226
  // precedente e non si estrapola.
216
227
  const OPENCODE_GO_PI_COMPAT = Object.freeze(['glm-5.2', 'deepseek-v4-pro']);
228
+ // Gli id di questo provider che accettano immagini. Vive qui e non dentro il
229
+ // map perche' la stessa domanda la fa anche il catalogo JSON
230
+ // (`input_modalities`): due copie che divergono in silenzio sarebbero una cella
231
+ // che dichiara di vedere e un client che non gli manda mai un'immagine.
232
+ const OPENCODE_GO_VISION = Object.freeze(new Set(['deepseek-v4-flash-vision-exp']));
217
233
  const OPENCODE_GO_PI_MODELS = Object.freeze(OPENCODE_GO_CHAT_MODELS.map((id) => Object.freeze({
218
234
  id,
219
235
  name: id,
220
236
  api: 'openai-completions',
221
237
  reasoning: false,
222
- input: ['text'],
238
+ input: OPENCODE_GO_VISION.has(id) ? ['text', 'image'] : ['text'],
223
239
  contextWindow: OPENCODE_GO_LIMITS[id].context,
224
240
  maxTokens: OPENCODE_GO_LIMITS[id].output,
225
241
  cost: ZERO_COST,
@@ -325,14 +341,32 @@ const CATALOG = Object.freeze([
325
341
  // default 'standard'; unsafe -> --always-approve (flag reale di `grok`).
326
342
  { id: 'grok.native', client: 'grok', provider: 'native', label: 'Grok account (CLI login)', auth: 'login', protocol: 'grok_native', core: true },
327
343
 
328
- // VL/Vivling (repository `vl`): runtime TUI locale. Auth propria del
329
- // runtime (default local Ollama, o VL_API_KEY/--profile in config.toml):
330
- // NexusCrew non legge ne' copia credenziali (auth 'none'). Il CLI non ha flag
331
- // prompt, --model ne' di approvazione (`vl [OPTIONS]`): lancia la TUI senza
332
- // argomenti, standard-only. Backfill idempotente pattern Kimi (NESSUN platform
333
- // gate: musl-friendly ovunque); binario assente -> describeManaged non
334
- // configurato. Non e' un default seed; nessun remote-control (rc assente).
344
+ // VL/Vivling (repository `vl`): runtime TUI locale. Auth propria del runtime
345
+ // in OGNI variante (config.toml / VL_API_KEY): NexusCrew non legge ne' copia
346
+ // credenziali (auth 'none') la chiave, dove serve, sale per NOME via
347
+ // envPassthrough (D3), mai come valore, mai su argv. vl.native e' «usa la tua
348
+ // configurazione»: NESSUNA env provider/base_url, perche' i default interni del
349
+ // runtime sono gia' openai-compat + localhost:11434 e le VL_* ambientali
350
+ // sovrascrivono il config.toml (comporle qui cancellerebbe default_profile in
351
+ // silenzio). Le varianti remote compongono SEMPRE la coppia: la' la variante
352
+ // e' la scelta. Il modello scelto nella UI viaggia via VL_MODEL, il prompt di
353
+ // cella via VL_SYSTEM_APPEND_FILE (gate 0.3.1); `vl --profile` esiste ma resta
354
+ // dell'operatore. Backfill idempotente pattern Kimi (NESSUN platform gate);
355
+ // standard-only in ogni variante. Non e' un default seed; nessun remote-control.
335
356
  { id: 'vl.native', client: 'vl', provider: 'native', label: 'VL', auth: 'none', protocol: 'vl_native', core: true },
357
+ // Destinazione unica e sensata: l'API Anthropic vera. La chiave sale per nome:
358
+ // chi non ha VL_API_KEY in nessuna credential source vede la cella rifiutarsi
359
+ // col motivo che la nomina, non partire muta. (Il runtime rifiuta da solo anche
360
+ // i subscription token, fail-closed sul backend nativo.)
361
+ { id: 'vl.anthropic', client: 'vl', provider: 'anthropic', label: 'Anthropic', auth: 'none', endpoint: 'https://api.anthropic.com', protocol: 'vl_native', vlProvider: 'anthropic', envPassthrough: ['VL_API_KEY'], core: true },
362
+ // Endpoint dichiarato dall'operatore. I protocolli sono ESATTAMENTE i tre
363
+ // provider che il runtime parla (anthropic | anthropic-bearer | openai-compat,
364
+ // vivling config/mod.rs): una voce che porti a un dialetto inesistente e'
365
+ // peggio che non averla. Per anthropic-bearer l'endpoint e' obbligatorio per
366
+ // costruzione (baseUrl del custom): un default spedirebbe un bearer token
367
+ // all'API vera di Anthropic. La chiave resta opt-in per nome (envPassthrough):
368
+ // un custom verso Ollama remoto senza auth non dichiara nulla.
369
+ { id: 'vl.custom', client: 'vl', provider: 'custom', label: 'Custom endpoint', auth: 'dynamic', protocol: 'openai-compat', protocols: ['anthropic', 'anthropic-bearer', 'openai-compat'], custom: true, core: true },
336
370
 
337
371
  // Pi usa i suoi provider ID reali direttamente. I provider OAuth non
338
372
  // richiedono env key.
@@ -408,6 +442,23 @@ function profileFor(client, provider, credentialProfile) {
408
442
  && (p.credentialProfile || '') === (credentialProfile || '')) || null;
409
443
  }
410
444
 
445
+ // D3: validazione unica dell'allowlist envPassthrough (nomi, mai valori): la
446
+ // usano sia la dichiarazione dell'operatore sia il default portato dal profilo
447
+ // del catalogo, cosi' i due percorsi non possono divergere sui vincoli.
448
+ function sanitizeEnvPassthrough(list) {
449
+ if (!Array.isArray(list) || !list.length || list.length > MAX_ENV_PASSTHROUGH) return null;
450
+ const seen = new Set();
451
+ const names = [];
452
+ for (const raw of list) {
453
+ if (typeof raw !== 'string') return null;
454
+ const name = raw.trim();
455
+ if (!ENV_KEY_RE.test(name) || seen.has(name)) return null;
456
+ seen.add(name);
457
+ names.push(name);
458
+ }
459
+ return names;
460
+ }
461
+
411
462
  function normalizeManagedSpec(value, { extraModels = null } = {}) {
412
463
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
413
464
  if (Object.keys(value).some((k) => !MANAGED_KEYS.has(k))) return null;
@@ -467,9 +518,14 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
467
518
  const providerId = typeof value.providerId === 'string' && value.providerId ? value.providerId : 'nexuscrew-custom';
468
519
  if (!displayName || displayName.length > 64 || /[\x00-\x1f\x7f]/.test(displayName)) return null;
469
520
  if (!validBaseUrl(baseUrl)) return null;
470
- if (!ENV_KEY_RE.test(envKey) || !PROVIDER_ID_RE.test(providerId)) return null;
521
+ // vl: la chiave NON viaggia come envKey copiata dallo store (pattern degli
522
+ // altri client) ma per NOME via envPassthrough (D3) — un custom vl verso un
523
+ // endpoint senza auth (Ollama remoto) non dichiara nessuna chiave, quindi
524
+ // envKey vuota e' legittima solo per vl. Se dichiarata, resta un nome valido.
525
+ if (profile.client === 'vl' ? (envKey !== '' && !ENV_KEY_RE.test(envKey)) : !ENV_KEY_RE.test(envKey)) return null;
526
+ if (!PROVIDER_ID_RE.test(providerId)) return null;
471
527
  if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return null;
472
- Object.assign(out, { displayName, baseUrl, envKey, protocol, providerId });
528
+ Object.assign(out, { displayName, baseUrl, ...(envKey ? { envKey } : {}), protocol, providerId });
473
529
  }
474
530
  // D3: envPassthrough e' un'allowlist di NOMI di variabili d'ambiente che il
475
531
  // child deve ricevere, risolti a runtime dalle credentialSources (dopo i rami
@@ -479,17 +535,16 @@ function normalizeManagedSpec(value, { extraModels = null } = {}) {
479
535
  // le variabili che il suo runtime legge: il nome non e' fisso nel codice vl
480
536
  // (vivling/src/main.rs), quindi lo dichiara l'operatore che conosce la sua config.
481
537
  if (value.envPassthrough !== undefined) {
482
- if (!Array.isArray(value.envPassthrough) || !value.envPassthrough.length
483
- || value.envPassthrough.length > MAX_ENV_PASSTHROUGH) return null;
484
- const seen = new Set();
485
- const names = [];
486
- for (const raw of value.envPassthrough) {
487
- if (typeof raw !== 'string') return null;
488
- const name = raw.trim();
489
- if (!ENV_KEY_RE.test(name) || seen.has(name)) return null;
490
- seen.add(name);
491
- names.push(name);
492
- }
538
+ const names = sanitizeEnvPassthrough(value.envPassthrough);
539
+ if (!names) return null;
540
+ out.envPassthrough = names;
541
+ } else if (Array.isArray(profile.envPassthrough) && profile.envPassthrough.length) {
542
+ // Il profilo puo' portare NOMI di default (es. vl.anthropic dichiara
543
+ // VL_API_KEY): la dichiarazione dell'operatore vince, e il default passa
544
+ // per la stessa validazione — una voce di catalogo malformata si rifiuta
545
+ // da sola invece di partire con nomi spurii.
546
+ const names = sanitizeEnvPassthrough(profile.envPassthrough);
547
+ if (!names) return null;
493
548
  out.envPassthrough = names;
494
549
  }
495
550
  return out;
@@ -990,7 +1045,12 @@ function describeManaged(spec, cfg = {}) {
990
1045
  // not inspect or copy that store; delegate native-provider auth to Pi.
991
1046
  const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom'
992
1047
  && profile.delegatePiAuth !== false;
993
- const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none' || !!cred.value;
1048
+ // vl: l'autenticazione e' del runtime in OGNI variante (auth 'none', o chiave
1049
+ // che sale per NOME via envPassthrough/D3 quando il profilo la dichiara). Il
1050
+ // verdetto configured non puo' dipendere da una credenziale che NexusCrew non
1051
+ // possiede: il fail-closed giusto e' quello del D3, che NOMINA il nome mancante.
1052
+ const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none'
1053
+ || profile.client === 'vl' || !!cred.value;
994
1054
  let configured = !!binary && authConfigured;
995
1055
  let reason;
996
1056
  if (!binary) {
@@ -1750,21 +1810,42 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1750
1810
  // TUI interattivo nella cwd della cella.
1751
1811
  if (model) args.push('--model', model);
1752
1812
  } else if (spec.client === 'vl') {
1753
- // VL/Vivling runtime TUI: auth e modello gestiti dal runtime (config.toml /
1754
- // env VL_*), niente credenziali su argv. `vl [OPTIONS]` non accetta ne'
1755
- // --model ne' un prompt: lancia la TUI senza argomenti nella cwd della cella.
1756
- // V-69 il modello scelto nella UI smette di essere inerte: la tripletta
1757
- // VL_MODEL/VL_PROVIDER/VL_BASE_URL esiste nel runtime da sempre, quindi si
1758
- // compone senza gate appena un modello e' scelto. vl.native e' il profilo
1759
- // Ollama locale del catalogo: la coppia provider/base_url esplicita segue
1760
- // quella scelta (127.0.0.1 come gli altri profili ollama), cosi' il modello
1761
- // scelto finisce dove vive e non dove punta un eventuale profilo del
1762
- // config.toml dell'operatore. Senza modello NESSUNA env modello: chi usa il
1763
- // proprio config.toml continua a comandarlo lui, come oggi.
1764
- if (model) {
1765
- env.VL_MODEL = model;
1766
- env.VL_PROVIDER = 'openai-compat';
1767
- env.VL_BASE_URL = 'http://127.0.0.1:11434';
1813
+ // VL/Vivling runtime TUI: auth gestita dal runtime in ogni variante
1814
+ // (config.toml / VL_API_KEY per nome via envPassthrough, D3), mai
1815
+ // credenziali su argv. Il modello scelto nella UI viaggia via VL_MODEL
1816
+ // (V-69), il prompt di cella via file sul system del runtime
1817
+ // (VL_SYSTEM_APPEND_FILE, gate qui sotto); `vl --profile` esiste ma resta
1818
+ // dell'operatore. Nessun --model su argv: la TUI parte senza argomenti.
1819
+ if (model) env.VL_MODEL = model;
1820
+ // vl.native e' «usa la tua configurazione»: NESSUNA env provider/base_url.
1821
+ // I default interni del runtime sono gia' openai-compat + localhost:11434,
1822
+ // e le VL_* ambientali battono il config.toml comporle qui non aggiunge
1823
+ // nulla e cancella in silenzio default_profile. Le varianti remote invece
1824
+ // compongono SEMPRE la coppia, anche senza modello: la' la variante e' la
1825
+ // scelta, e un fallback silenzioso manderebbe la cella altrove mentre la
1826
+ // UI dice il contrario.
1827
+ if (spec.provider === 'custom') {
1828
+ // il protocollo scelto nel custom E' il provider wire: il ramo custom di
1829
+ // normalizeManagedSpec ha gia' garantito che sia uno dei tre che il
1830
+ // runtime parla e che baseUrl esista (endpoint obbligatorio: un default
1831
+ // su anthropic-bearer spedirebbe un bearer all'API vera di Anthropic).
1832
+ env.VL_PROVIDER = spec.protocol;
1833
+ env.VL_BASE_URL = spec.baseUrl;
1834
+ } else if (spec.provider !== 'native') {
1835
+ // Variante remota: la coppia e' obbligatoria, non condizionale. Un profilo
1836
+ // senza vlProvider o endpoint non e' «un caso in cui non componiamo
1837
+ // nulla»: e' uno stato impossibile del catalogo che, degradando, manderebbe
1838
+ // la cella sui default interni (Ollama locale) mentre la UI dice il
1839
+ // contrario. Si rifiuta NOMINANDO il campo che manca — stessa forma del
1840
+ // fail-closed delle chiavi, che dice quale variabile manca, non «errore».
1841
+ if (!profile.vlProvider) {
1842
+ return { ok: false, info, reason: `vl profile ${profile.id}: campo vlProvider mancante — variante remota rifiutata invece di ricadere sui default locali in silenzio` };
1843
+ }
1844
+ if (!profile.endpoint) {
1845
+ return { ok: false, info, reason: `vl profile ${profile.id}: campo endpoint mancante — variante remota rifiutata invece di ricadere sui default locali in silenzio` };
1846
+ }
1847
+ env.VL_PROVIDER = profile.vlProvider;
1848
+ env.VL_BASE_URL = profile.endpoint;
1768
1849
  }
1769
1850
  // V-69 — prompt per-cella: file composto sul system del runtime
1770
1851
  // (VL_SYSTEM_APPEND_FILE). Il gate di versione sta nel codice, non nella
package/lib/mcp/server.js CHANGED
@@ -67,10 +67,19 @@ function companionInstructions() {
67
67
  }
68
68
 
69
69
  // --- identita' cella mittente ------------------------------------------------
70
- // Ordine (design §1, INVARIATO): $TMUX presente -> `tmux display-message -p '#S'`
71
- // (nome sessione reale); se fallisce/invalida -> fallback env NEXUSCREW_MCP_SESSION;
72
- // altrimenti null. I tool che RICHIEDONO la sessione restano fail-closed.
73
- // execFile argv diretto: mai shell.
70
+ // Ordine delle sorgenti (design §1, INVARIATO): $TMUX presente -> tmux
71
+ // display-message (nome sessione reale); se fallisce/invalida -> fallback env
72
+ // NEXUSCREW_MCP_SESSION; altrimenti null. I tool che RICHIEDONO la sessione
73
+ // restano fail-closed. execFile argv diretto: mai shell.
74
+ //
75
+ // P0: il nome da tmux si chiede con `-t $TMUX_PANE` — target esplicito al
76
+ // PANE del chiamante, deterministico e indipendente dall'environ ereditato.
77
+ // Senza `-t` il CLI tmux risolve il pane dall'ENVIRON DEL PROCESSO FIGLIO:
78
+ // se quel pane è vivo risponde correttamente, ma se è morto (environ stale,
79
+ // l'incidente di partenza) ricade sul CLIENT ATTACHED attivo e risponde rc=0
80
+ // col nome di quel client — attribuzione errata con sembianze di successo.
81
+ // Comportamento misurato su tmux 3.4 con `-t`: pane morto -> rc=0 e stdout
82
+ // VUOTO (non un errore): il vuoto è il segnale dello stantio.
74
83
  //
75
84
  // `resolveIdentity` rende OSSERVABILE la sorgente della risoluzione (P0):
76
85
  // ritorna { session, source, code, envPresence, requiredEnvVars, remediation }
@@ -114,25 +123,60 @@ function resolveIdentity({ env, tmuxBin, execFileImpl }) {
114
123
  session: null, source: 'missing', code: codeWhenMissing(),
115
124
  envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
116
125
  });
126
+ // P0: pane stantio o non verificabile -> NON attribuire. source 'stale-pane'
127
+ // nomina il problema; code STALE_PANE (tools.js).
128
+ const stalePane = () => ({
129
+ session: null, source: 'stale-pane', code: IDENTITY_CODE.STALE_PANE,
130
+ envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
131
+ });
132
+ // P0/R2: tmux e fallback env dicono sessioni DIVERSE entrambe valide:
133
+ // identità ambigua -> NON attribuire (nemmeno il fallback: è parte del
134
+ // conflitto). Il code nomina il mismatch, che INVALID non direbbe.
135
+ const sessionMismatch = () => ({
136
+ session: null, source: 'session-mismatch', code: IDENTITY_CODE.SESSION_MISMATCH,
137
+ envPresence, requiredEnvVars: IDENTITY_REQUIRED_ENV_VARS, remediation: IDENTITY_REMEDIATION,
138
+ });
117
139
 
118
140
  return new Promise((resolve) => {
119
- if (!tmuxPresent) {
141
+ // Precedenza preservata: prima il fallback env valido, poi l'esito negativo
142
+ // dato (`missing` storico o `stalePane` P0).
143
+ const settle = (otherwise) => {
120
144
  const fb = tryFallback();
121
- return resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
122
- }
145
+ resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : otherwise());
146
+ };
147
+
148
+ if (!tmuxPresent) return settle(missing);
149
+
150
+ // Formato del pane id tmux: `%` + cifre. Un TMUX_PANE malformato non viene
151
+ // MAI spedito a tmux (argv diretto, ma niente pattern inattesi) e il pane
152
+ // resta non verificabile -> fail-closed.
153
+ const rawPane = typeof e.TMUX_PANE === 'string' ? e.TMUX_PANE.trim() : '';
154
+ const paneId = /^%\d+$/.test(rawPane) ? rawPane : null;
155
+ if (!paneId) return settle(stalePane);
156
+
123
157
  try {
124
- execFileImpl(tmuxBin, ['display-message', '-p', '#S'], { timeout: 3000 }, (err, stdout) => {
125
- if (!err) {
126
- const name = String(stdout || '').trim();
127
- if (isValidSession(name)) return resolve(ok(name, 'tmux'));
158
+ execFileImpl(tmuxBin, ['display-message', '-t', paneId, '-p', '#S'], { timeout: 3000 }, (err, stdout) => {
159
+ if (err) {
160
+ // tmux irraggiungibile/rotto (rc!=0): NON è il percorso dello stantio
161
+ // (un pane morto risponde rc=0, vedi header). Comportamento storico.
162
+ return settle(missing);
163
+ }
164
+ const name = String(stdout || '').trim();
165
+ // tmux 3.4, misura dell'audit (probe A1/A2): pane morto con -t ->
166
+ // rc=0 e stdout VUOTO. Il vuoto è il segnale dello stantio.
167
+ if (!name) return settle(stalePane);
168
+ if (isValidSession(name)) {
169
+ // R2: se il fallback env è valido ma dice un'altra sessione, le due
170
+ // fonti si contraddicono -> ambiguo, non si attribuisce.
171
+ const fb = tryFallback();
172
+ if (typeof fb === 'string' && fb !== name) return resolve(sessionMismatch());
173
+ return resolve(ok(name, 'tmux'));
128
174
  }
129
- // tmux fallito/invalido -> precedenza preservata: fallback env.
130
- const fb = tryFallback();
131
- resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
175
+ // nome non vuoto ma invalido: precedenza preservata, come da design §1.
176
+ settle(missing);
132
177
  });
133
178
  } catch (_) {
134
- const fb = tryFallback();
135
- resolve(typeof fb === 'string' ? ok(fb, 'NEXUSCREW_MCP_SESSION') : missing());
179
+ settle(missing);
136
180
  }
137
181
  });
138
182
  }
package/lib/mcp/tools.js CHANGED
@@ -38,6 +38,12 @@ const IDENTITY_CODE = Object.freeze({
38
38
  OK: 'OK',
39
39
  MISSING: 'NEXUSCREW_MCP_IDENTITY_MISSING',
40
40
  INVALID: 'NEXUSCREW_MCP_IDENTITY_INVALID',
41
+ // P0: il pane del chiamante non esiste più (o TMUX_PANE è assente/malformato):
42
+ // l'identità non viene attribuita via tmux.
43
+ STALE_PANE: 'NEXUSCREW_MCP_IDENTITY_STALE_PANE',
44
+ // P0/R2: tmux e NEXUSCREW_MCP_SESSION sono entrambi validi ma dicono sessioni
45
+ // diverse: identità ambigua, non si attribuisce nessuna delle due.
46
+ SESSION_MISMATCH: 'NEXUSCREW_MCP_IDENTITY_SESSION_MISMATCH',
41
47
  });
42
48
 
43
49
  // Remediation senza segreti/valori: nomi soltanto, compatibile con
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: cellforge
3
+ description: Use when creating, changing, auditing or retiring a NexusCrew cell — an AI working identity with its own engine, prompt, workspace, memory and lifecycle documents. Covers first-run setup on a fresh install ("set up a cell for marketing", "add a research assistant", "I need a cell that does X"), changing an existing cell (engine, model, internal prompt, permissions, working directory), and auditing cells against the standard. Use it even when the request sounds like a small edit — a cell lives in three places at once, and changing one of them alone is the most common way to end up with a cell that half-exists.
4
+ ---
5
+
6
+ # CellForge — build a cell that actually exists
7
+
8
+ A **cell** is a stable working identity: a name, an engine, a working directory,
9
+ an internal prompt, a memory namespace and a set of documents that survive
10
+ restarts. It is not a process and not a chat window.
11
+
12
+ The single most useful thing to know before touching anything:
13
+
14
+ > **A cell lives in three places at once, and a cell that exists in only two of
15
+ > them is broken in a way nothing reports.**
16
+
17
+ | where | what lives there | who writes it |
18
+ |---|---|---|
19
+ | **Definition** | the runtime record: id, engine, model, working directory, internal prompt, permissions | the NexusCrew API — never the file by hand |
20
+ | **Runtime workspace** | `~/NexusFiles/<session>/` — inbox, outbox, links to the cell's documents | the service, plus the cell itself |
21
+ | **Canonical documents** | the cell's prompt, checkpoint and history | the cell, following its own protocol |
22
+
23
+ Where the canonical documents live is an **install convention, not a product
24
+ rule** — ask, do not assume a repository exists.
25
+
26
+ Create only the definition and the cell boots into an empty identity with no
27
+ memory of what it is for. Create only the documents and nothing runs. Both
28
+ failures look like "it sort of works" for a while.
29
+
30
+ ## Before you build: ask, don't assume
31
+
32
+ A cell is cheap to create and expensive to live with, because a vague one
33
+ produces vague work forever. Four questions decide everything else, and you
34
+ should ask them in the user's own terms rather than presenting a form:
35
+
36
+ 1. **What is this cell for?** One sentence a stranger could act on. "Marketing"
37
+ is a department, not a purpose; "drafts and reviews campaign copy, owns the
38
+ content calendar" is a purpose.
39
+ 2. **What does it own, and what is off-limits?** Ownership is what makes a cell
40
+ different from a chat: a directory, a repository, a domain of documents. The
41
+ off-limits half matters as much — it is what keeps two cells from fighting.
42
+ 3. **What must it never do without asking?** Publishing, sending, deleting,
43
+ deploying, spending. Write these down now; they become the part of the prompt
44
+ that protects the user later.
45
+ 4. **Does it work alone, or with a reviewer?** A cell that implements and audits
46
+ its own work will approve it. If the work needs a verdict, plan a second cell
47
+ whose independence is structural, not a promise.
48
+
49
+ If the user cannot answer (1) crisply, that is the finding — help them sharpen it
50
+ before creating anything. A cell created around a fuzzy purpose is the most
51
+ expensive thing in this skill.
52
+
53
+ Then propose the engine, the tools and the MCP servers **as a recommendation
54
+ with reasons**, and let the user correct you. See `references/choosing.md` for
55
+ how to match a purpose to an engine and a toolset without over-fitting to the
56
+ roles that already exist on the install.
57
+
58
+ ## Building it
59
+
60
+ Work in this order. Each step is verifiable, and the order exists so that a
61
+ failure leaves a cell that plainly does not exist, rather than one that
62
+ half-does.
63
+
64
+ **0. Look before you build.** One call to the fleet status gives you the
65
+ existing cells (so you do not collide), the engine ids you need to fill the
66
+ definition, and the capabilities you actually have. `references/operations.md`
67
+ has this and every other "how do I observe it" answer — read it first if you
68
+ are about to touch a real install.
69
+
70
+ 1. **Write the documents first.** Ask where they should live if the install has
71
+ no convention yet, and prefer somewhere version-controlled: the checkpoint's
72
+ value comes from its history. The prompt and the checkpoint are the cell's
73
+ identity; the definition just launches it. Templates in `assets/`. Adapt
74
+ them — a template pasted unchanged is how you get twelve cells that all
75
+ describe themselves the same way.
76
+ 2. **Choose the id carefully — it is immutable**, and it will appear in the
77
+ workspace path, in the memory namespaces and in every reference from another
78
+ cell. Name the function, not the moment: `Marketing` outlives
79
+ `Marketing-Q4-launch`.
80
+ 3. **Create the definition through the API**, never by editing the definitions
81
+ file. The API validates, enforces caps, and refuses states the file format
82
+ would happily hold. `references/definition.md` has the schema, the fields
83
+ that are immutable after creation, and the exact endpoints.
84
+ 4. **Sort out its memory** — state and journal are two different mechanisms,
85
+ not two files (`references/lifecycle.md`). How a namespace comes into being
86
+ depends on the memory server, so confirm the cell can write and read back
87
+ rather than assuming a creation step exists.
88
+ 5. **Boot it, then verify it is the cell you meant**: right engine, right
89
+ working directory, **prompt actually delivered**. That last one cannot be
90
+ read off the definition — the field is present either way — so check it the
91
+ way `operations.md` describes for the engine's delivery mode.
92
+
93
+ ## Changing a cell that already exists
94
+
95
+ Two fields are **immutable after creation**: the cell's `id` and its tmux
96
+ session. Everything else is patchable, but the interesting failures are not
97
+ about what is allowed — they are about what changes underneath:
98
+
99
+ - **Changing the engine can silently change the prompt delivery.** Engines
100
+ differ in how the internal prompt reaches the cell (command-line flag versus
101
+ typed into the session). A prompt that worked may simply stop arriving.
102
+ Verify delivery after an engine change; do not assume it carried over.
103
+ - **Changing the working directory changes which instructions the cell loads**,
104
+ because per-directory instruction files are picked up by location. A cell can
105
+ keep its name and quietly become a different worker.
106
+ - **Editing the internal prompt does not touch the documents**, and editing the
107
+ documents does not touch the internal prompt. They are separate stores. When
108
+ a cell's purpose changes, both change — otherwise the cell is told one thing
109
+ at boot and another by its own canon.
110
+
111
+ Apply changes one at a time and confirm each. A batch patch that fails partway
112
+ leaves you guessing which half landed.
113
+
114
+ ## Auditing
115
+
116
+ Audit answers one question: **does this cell exist completely, and does it match
117
+ the standard?** Run through `references/audit.md`, which is written as checks
118
+ with an expected observation rather than a list of virtues.
119
+
120
+ Two rules make an audit worth running:
121
+
122
+ - **Report before you repair.** Show the findings and the proposed fixes, and
123
+ apply them only when the user says so. An audit that silently fixes things
124
+ teaches the user that the audit is the thing that changes their system, and
125
+ they stop running it.
126
+ - **A check you have never seen fail proves nothing.** If a check passes on
127
+ every cell you point it at, break one deliberately — on a throwaway cell —
128
+ and confirm the check goes red. Checks that cannot fail are decoration, and
129
+ they are worse than no check because they are believed.
130
+
131
+ ## Rules that protect the user
132
+
133
+ - **Never put a secret in the internal prompt.** With flag-style delivery the
134
+ prompt becomes a command-line argument, and command lines are readable by
135
+ other processes belonging to the same user. Reference credentials by the name
136
+ of the mechanism that holds them; never by value.
137
+ - **Confirm before anything outward-facing or hard to undo** — removing a cell,
138
+ stopping one that is mid-task, overwriting a checkpoint. Creating is cheap;
139
+ removing throws away the identity and the history attached to it.
140
+ - **A cell's checkpoint belongs to that cell.** Do not write another cell's
141
+ checkpoint even to be helpful: the owner will overwrite it, and both of you
142
+ will believe a state that no longer holds.
143
+ - **Prefer portable forms.** Where a definition can express a path relative to
144
+ the user's home directory, use it: absolute paths bind the cell to one machine
145
+ and quietly break when the definition is restored on another.
146
+ - **The running system wins over any document, including this one.** Check the
147
+ live listing and the tool surface actually exposed before acting on what you
148
+ remember.
149
+
150
+ ## Reference material
151
+
152
+ Read the file that matches what you are doing; they are written to be read one
153
+ at a time.
154
+
155
+ - `references/operations.md` — **how to look and how to act**: the read calls,
156
+ what capabilities mean and why they are discovered rather than granted, how to
157
+ verify prompt delivery, and which parts depend on the install rather than the
158
+ product. Read this one before touching a real system.
159
+ - `references/anatomy.md` — the three places, what is a real file and what is a
160
+ link, and how to tell a complete cell from a half one.
161
+ - `references/definition.md` — the definition schema, validation limits,
162
+ immutable fields, and the endpoints that write it.
163
+ - `references/lifecycle.md` — prompt, checkpoint, history and memory: who
164
+ writes what, when, and the difference between durable state and a bounded
165
+ journal.
166
+ - `references/choosing.md` — matching purpose to engine, tools and MCP servers,
167
+ including how to handle a role the install has never seen before.
168
+ - `references/audit.md` — the checks, each with what you should observe.
169
+ - `assets/` — starting templates for the internal prompt and the checkpoint.
@@ -0,0 +1,79 @@
1
+ # Checkpoint — starting template
2
+
3
+ The checkpoint answers one question: **if this session ended right now, what
4
+ would the next one need in order to continue?**
5
+
6
+ That question decides what belongs here better than any rule. It is a resume
7
+ point, not a diary — finished work, transcripts and long output belong in the
8
+ archived history, which the tooling creates for you every time you replace this
9
+ file.
10
+
11
+ Keep the section order: a reader must find the current task, the evidence, the
12
+ blockers and the next step in the same place every time.
13
+
14
+ ---
15
+
16
+ ```markdown
17
+ # {{CELL_ID}} — ACTIVE WORK
18
+
19
+ **Status:** {{OPEN | CLOSED}} — {{date, time}}. {{One line: what is true right now.}}
20
+ **Updated:** {{date, time, timezone}}
21
+
22
+ **Execution node:** {{host}} — {{device identity}}
23
+ **Sole owner:** cell {{CELL_ID}}
24
+
25
+ ## History
26
+
27
+ {{Generated by the tooling — links to archived snapshots. Do not hand-edit.}}
28
+
29
+ ## Current task
30
+
31
+ {{The task still authorised and unfinished. "None" is a valid and useful
32
+ answer — write it rather than leaving the section stale.}}
33
+
34
+ ## Current state
35
+
36
+ {{The reality needed to resume: what was done, what was decided, what changed
37
+ underneath. Keep only what is still true — this is the section that rots.}}
38
+
39
+ ## Current evidence
40
+
41
+ {{Measurements, versions, commit ids, command output that a resuming session
42
+ would otherwise have to re-derive. Prefer a value you produced over one you
43
+ remember.}}
44
+
45
+ ## Blockers
46
+
47
+ | | what | who decides |
48
+ |---|---|---|
49
+ | ⏸ | {{what is blocked, and precisely what unblocks it}} | {{owner of the decision}} |
50
+
51
+ ## Next step
52
+
53
+ {{The single next action. If it is "wait", say what for and how you will know
54
+ it arrived.}}
55
+ ```
56
+
57
+ ---
58
+
59
+ ## The three ways checkpoints go wrong
60
+
61
+ **It becomes a diary.** Every session appends, nothing is ever removed, and
62
+ eventually it is too long to read at startup — so nobody reads it, and the
63
+ checkpoint stops working while still being dutifully updated. Cure: when you
64
+ update, delete what is no longer true. The history keeps it.
65
+
66
+ **It is closed when it should be open.** A crash, a restart, a device change or
67
+ running out of context are interruptions, not completions. Closing after one of
68
+ those tells the next session there is nothing to resume, and the work is lost
69
+ quietly. Only verified completion closes a task.
70
+
71
+ **It records the plan instead of the result.** "Applied the fix" and "the fix is
72
+ verified" are different states, and only one of them is safe to resume from.
73
+ Where they differ, write which one you are in.
74
+
75
+ ## Write it even when nothing happened
76
+
77
+ A session that starts, checks, finds nothing to do and updates the timestamp has
78
+ produced real information: the state was verified at a known moment. That is not
79
+ the same as no one having looked.