@mmmbuto/nexuscrew 0.9.12 → 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.
- package/frontend/dist/assets/{index-CBoobbt_.js → index-BBCrZoxR.js} +35 -35
- package/frontend/dist/assets/index-keXh4CAm.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/lib/fleet/builtin.js +7 -4
- package/lib/fleet/catalogs/opencode-go.json +48 -1
- package/lib/fleet/cell-lease-server.js +95 -33
- package/lib/fleet/managed.js +121 -40
- package/lib/mcp/server.js +60 -16
- package/lib/mcp/tools.js +6 -0
- package/package.json +1 -1
- package/skills/cellforge/SKILL.md +169 -0
- package/skills/cellforge/assets/checkpoint.template.md +79 -0
- package/skills/cellforge/assets/internal-prompt.template.md +62 -0
- package/skills/cellforge/references/anatomy.md +73 -0
- package/skills/cellforge/references/audit.md +94 -0
- package/skills/cellforge/references/choosing.md +75 -0
- package/skills/cellforge/references/definition.md +107 -0
- package/skills/cellforge/references/lifecycle.md +100 -0
- package/skills/cellforge/references/operations.md +167 -0
- package/frontend/dist/assets/index-CWMKoCx-.css +0 -32
package/lib/fleet/managed.js
CHANGED
|
@@ -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
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
// V-69
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
|
|
1760
|
-
//
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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 ->
|
|
71
|
-
// (nome sessione reale); se fallisce/invalida -> fallback env
|
|
72
|
-
// altrimenti null. I tool che RICHIEDONO la sessione
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
130
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|