@mmmbuto/nexuscrew 0.9.28 → 0.9.30

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/LIVE_PROMPT.md +1 -1
  3. package/docs/NOTIFICATIONS.md +37 -0
  4. package/docs/OPERATIONS.md +20 -0
  5. package/frontend/dist/assets/index-BbcjoZS0.js +167 -0
  6. package/frontend/dist/assets/index-DqCfZFGi.css +32 -0
  7. package/frontend/dist/index.html +2 -2
  8. package/frontend/dist/sw.js +91 -10
  9. package/frontend/dist/version.json +1 -1
  10. package/lib/audio/groups.js +1 -1
  11. package/lib/auth/token.js +4 -4
  12. package/lib/cli/commands.js +70 -15
  13. package/lib/cli/doctor.js +5 -5
  14. package/lib/cli/fleet-service.js +7 -7
  15. package/lib/cli/init.js +55 -31
  16. package/lib/cli/pidfile.js +1 -1
  17. package/lib/cli/service.js +124 -8
  18. package/lib/cli/stable-alias.js +4 -4
  19. package/lib/cli/url.js +1 -1
  20. package/lib/config.js +36 -6
  21. package/lib/files/routes.js +1 -1
  22. package/lib/fleet/boot.js +2 -2
  23. package/lib/fleet/builtin.js +15 -15
  24. package/lib/fleet/causes.js +1 -1
  25. package/lib/fleet/cell-exec.js +23 -23
  26. package/lib/fleet/cell-hold.js +6 -6
  27. package/lib/fleet/cell-lease-server.js +106 -106
  28. package/lib/fleet/cell-lease.js +21 -21
  29. package/lib/fleet/definitions.js +10 -10
  30. package/lib/fleet/identity-authority.js +1 -1
  31. package/lib/fleet/launch-broker.js +13 -13
  32. package/lib/fleet/launch.js +15 -15
  33. package/lib/fleet/lease-client.js +23 -23
  34. package/lib/fleet/lease-routes.js +3 -3
  35. package/lib/fleet/lease-verifier.js +22 -22
  36. package/lib/fleet/managed.js +35 -35
  37. package/lib/fleet/prompt-delivery.js +13 -13
  38. package/lib/fleet/routes.js +15 -10
  39. package/lib/fleet/runtime.js +24 -24
  40. package/lib/live-host/bridge.js +53 -43
  41. package/lib/live-host/federation.js +97 -0
  42. package/lib/live-host/routes.js +166 -8
  43. package/lib/live-host/store.js +44 -6
  44. package/lib/mcp/server.js +14 -14
  45. package/lib/mcp/tools.js +5 -5
  46. package/lib/nodes/access-presets.js +258 -0
  47. package/lib/nodes/commands.js +198 -4
  48. package/lib/nodes/store.js +152 -9
  49. package/lib/nodes/tunnel.js +7 -7
  50. package/lib/notify/ask-answer-service.js +184 -0
  51. package/lib/notify/ask-receipts.js +197 -0
  52. package/lib/notify/ask-relay.js +152 -0
  53. package/lib/notify/asks.js +28 -3
  54. package/lib/notify/event-feed-acl.js +95 -0
  55. package/lib/notify/event-feed-asks-routes.js +144 -0
  56. package/lib/notify/event-feed-client.js +434 -0
  57. package/lib/notify/event-feed-history.js +115 -0
  58. package/lib/notify/event-feed-producers.js +175 -0
  59. package/lib/notify/event-feed-routes.js +254 -0
  60. package/lib/notify/event-feed.js +328 -0
  61. package/lib/notify/events.js +1 -1
  62. package/lib/notify/notifier.js +3 -3
  63. package/lib/notify/push-relay.js +240 -0
  64. package/lib/notify/push.js +8 -8
  65. package/lib/notify/routes.js +62 -23
  66. package/lib/proxy/federation.js +132 -12
  67. package/lib/proxy/node-proxy.js +13 -13
  68. package/lib/proxy/panel-auth.js +4 -4
  69. package/lib/proxy/panel-proxy.js +2 -2
  70. package/lib/proxy/resource-acl.js +145 -0
  71. package/lib/server.js +396 -40
  72. package/lib/settings/pairing-coordinator.js +1 -1
  73. package/lib/settings/routes.js +45 -32
  74. package/lib/update/manager.js +1 -1
  75. package/lib/update/runner.js +1 -1
  76. package/lib/voice/transcribe.js +1 -1
  77. package/lib/ws/bridge.js +3 -3
  78. package/package.json +1 -1
  79. package/skills/live/SKILL.md +12 -0
  80. package/frontend/dist/assets/index-CWra6B79.js +0 -167
  81. package/frontend/dist/assets/index-D6CIfCjv.css +0 -32
@@ -415,14 +415,14 @@ const OPENCODE_GO_PI_MODELS = Object.freeze(OPENCODE_GO_CHAT_MODELS.map((id) =>
415
415
 
416
416
  const CUSTOM_KEYS = ['displayName', 'protocol', 'baseUrl', 'envKey', 'providerId'];
417
417
  const MANAGED_KEYS = new Set(['client', 'provider', 'credentialProfile', 'model', 'permissionPolicy', 'credentialSourcePolicy', 'envPassthrough', ...CUSTOM_KEYS]);
418
- // D3: massimo numero di nomi in `envPassthrough`. L'allowlist e' opt-in e per
418
+ // Massimo numero di nomi in `envPassthrough`. L'allowlist e' opt-in e per
419
419
  // nome, mai un passthrough in blocco: un tetto basso ferma una lista incontrollata.
420
420
  const MAX_ENV_PASSTHROUGH = 32;
421
421
  const CODEX_APP_SERVER_IDENTITY_REQUIRED = 'CODEX_APP_SERVER_IDENTITY_REQUIRED';
422
422
  const CODEX_APP_SERVER_IDENTITY_REQUIRED_DEFAULT = '1';
423
423
  // Explicit credential source policy. Default 'auto' preserves the legacy
424
424
  // resolution order (runtime -> store -> shell -> key files -> legacy) so a
425
- // pre-WP1 fleet.json migrates no-op: no existing cell changes resolution.
425
+ // Pre- fleet.json migrates no-op: no existing cell changes resolution.
426
426
  const CREDENTIAL_SOURCES = Object.freeze(['environment', 'nexuscrew-store', 'auto']);
427
427
  // I valori che `credential().source` puo' assumere, e che escono verso la UI in
428
428
  // `describeManaged().credentialSource` / `describeCatalogCredential()`. La UI li
@@ -517,7 +517,7 @@ const CATALOG = Object.freeze([
517
517
  // VL/Vivling (repository `vl`): runtime TUI locale. Auth propria del runtime
518
518
  // in OGNI variante (config.toml / VL_API_KEY): NexusCrew non legge ne' copia
519
519
  // credenziali (auth 'none') — la chiave, dove serve, sale per NOME via
520
- // envPassthrough (D3), mai come valore, mai su argv. vl.native e' «usa la tua
520
+ // EnvPassthrough, mai come valore, mai su argv. vl.native e' «usa la tua
521
521
  // configurazione»: NESSUNA env provider/base_url, perche' i default interni del
522
522
  // runtime sono gia' openai-compat + localhost:11434 e le VL_* ambientali
523
523
  // sovrascrivono il config.toml (comporle qui cancellerebbe default_profile in
@@ -615,7 +615,7 @@ function profileFor(client, provider, credentialProfile) {
615
615
  && (p.credentialProfile || '') === (credentialProfile || '')) || null;
616
616
  }
617
617
 
618
- // D3: validazione unica dell'allowlist envPassthrough (nomi, mai valori): la
618
+ // Validazione unica dell'allowlist envPassthrough (nomi, mai valori): la
619
619
  // usano sia la dichiarazione dell'operatore sia il default portato dal profilo
620
620
  // del catalogo, cosi' i due percorsi non possono divergere sui vincoli.
621
621
  function sanitizeEnvPassthrough(list) {
@@ -700,7 +700,7 @@ function normalizeManagedSpec(value, { extraModels = null, engineId = null, onIs
700
700
  if (!displayName || displayName.length > 64 || /[\x00-\x1f\x7f]/.test(displayName)) return null;
701
701
  if (!validBaseUrl(baseUrl)) return null;
702
702
  // vl: la chiave NON viaggia come envKey copiata dallo store (pattern degli
703
- // altri client) ma per NOME via envPassthrough (D3) — un custom vl verso un
703
+ // Altri client) ma per NOME via envPassthrough — un custom vl verso un
704
704
  // endpoint senza auth (Ollama remoto) non dichiara nessuna chiave, quindi
705
705
  // envKey vuota e' legittima solo per vl. Se dichiarata, resta un nome valido.
706
706
  if (profile.client === 'vl' ? (envKey !== '' && !ENV_KEY_RE.test(envKey)) : !ENV_KEY_RE.test(envKey)) return issue('envKey', envKey, `consentito ${ENV_KEY_RE} (obbligatorio; vuoto ammesso solo per il client vl)`);
@@ -708,7 +708,7 @@ function normalizeManagedSpec(value, { extraModels = null, engineId = null, onIs
708
708
  if (!model || !(profile.protocols || [profile.protocol]).includes(protocol)) return issue('protocol', protocol, `protocollo non supportato dal profilo (ammessi: ${(profile.protocols || [profile.protocol]).join(', ')})`);
709
709
  Object.assign(out, { displayName, baseUrl, ...(envKey ? { envKey } : {}), protocol, providerId });
710
710
  }
711
- // D3: envPassthrough e' un'allowlist di NOMI di variabili d'ambiente che il
711
+ // EnvPassthrough e' un'allowlist di NOMI di variabili d'ambiente che il
712
712
  // child deve ricevere, risolti a runtime dalle credentialSources (dopo i rami
713
713
  // provider in resolveManagedEngine). E' opt-in e per nome: MAI un passthrough
714
714
  // in blocco dell'ambiente, solo i nomi elencati — ciascuno un nome env valido.
@@ -1227,9 +1227,9 @@ function describeManaged(spec, cfg = {}) {
1227
1227
  const delegatedPiAuth = profile.client === 'pi' && profile.provider !== 'custom'
1228
1228
  && profile.delegatePiAuth !== false;
1229
1229
  // vl: l'autenticazione e' del runtime in OGNI variante (auth 'none', o chiave
1230
- // che sale per NOME via envPassthrough/D3 quando il profilo la dichiara). Il
1230
+ // Che sale per NOME via envPassthrough/ quando il profilo la dichiara). Il
1231
1231
  // verdetto configured non puo' dipendere da una credenziale che NexusCrew non
1232
- // possiede: il fail-closed giusto e' quello del D3, che NOMINA il nome mancante.
1232
+ // Possiede: il fail-closed giusto e' quello del, che NOMINA il nome mancante.
1233
1233
  const authConfigured = delegatedPiAuth || profile.auth === 'login' || profile.auth === 'none'
1234
1234
  || profile.client === 'vl' || !!cred.value;
1235
1235
  let configured = !!binary && authConfigured;
@@ -1564,11 +1564,11 @@ function writePiProviderExtension(spec, home, declaredModels) {
1564
1564
  return target;
1565
1565
  }
1566
1566
 
1567
- // V-69 — prompt per-cella per il runtime vl. Dalla 0.3.1 vl legge
1568
- // VL_SYSTEM_APPEND_FILE e COMPONE il file sul proprio system (VL_SYSTEM resta
1569
- // «sostituisci» e non si tocca: un wrapper vivo dipende da quel significato).
1570
- // Il file porta il prompt della cella e le istruzioni companion MCP: vl non ha
1571
- // client MCP, quello e' l'unico punto in cui il testo lo raggiunge. Stessa
1567
+ // Per-cell prompt for the vl runtime: since 0.3.1 vl reads
1568
+ // VL_SYSTEM_APPEND_FILE and composes the file on its own system (VL_SYSTEM
1569
+ // keeps its "replace" meaning and is untouched: a live wrapper depends on it).
1570
+ // The file carries the cell prompt and the MCP companion instructions: vl has
1571
+ // no MCP client, that is the only point where the text reaches it.
1572
1572
  // forma e stesso posto di writePiProviderExtension: generato sotto
1573
1573
  // ~/.nexuscrew, atomico (tmp+rename), mai credenziali — il contenuto e'
1574
1574
  // cell.prompt piu' testo statico, nessun valore letto dalle sorgenti
@@ -1775,9 +1775,9 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
1775
1775
  // divergere. Divergono, e una cella standalone muore sull'handshake.
1776
1776
  let identityChannel = null;
1777
1777
  let identityAuthorityUnavailable = '';
1778
- // V-69: motivo per cui il prompt per-cella di vl NON e' stato composto
1779
- // (versione del runtime che non regge, o file non scrivibile in sicurezza).
1780
- // Stringa vuota = consegnato. Sale con l'engine risolto come vlPromptDegraded.
1778
+ // Why the vl per-cell prompt was NOT composed (runtime version that cannot
1779
+ // hold it, or a file that cannot be written safely).
1780
+ // Empty string = delivered. Surfaces with the engine resolved as vlPromptDegraded.
1781
1781
  let vlPromptDegraded = '';
1782
1782
  // I due profili Claude con configurazione privata: perdono gli MCP del file
1783
1783
  // principale e vanno ricollegati (vedi `sharedMcpArgs`).
@@ -2042,12 +2042,12 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2042
2042
  // TUI interattivo nella cwd della cella.
2043
2043
  if (model) args.push('--model', model);
2044
2044
  } else if (spec.client === 'vl') {
2045
- // VL/Vivling runtime TUI: auth gestita dal runtime in ogni variante
2046
- // (config.toml / VL_API_KEY per nome via envPassthrough, D3), mai
2047
- // credenziali su argv. Il modello scelto nella UI viaggia via VL_MODEL
2048
- // (V-69), il prompt di cella via file sul system del runtime
2049
- // (VL_SYSTEM_APPEND_FILE, gate qui sotto); `vl --profile` esiste ma resta
2050
- // dell'operatore. Nessun --model su argv: la TUI parte senza argomenti.
2045
+ // VL/Vivling runtime TUI: auth is handled by the runtime in every variant
2046
+ // (config.toml / VL_API_KEY by name via envPassthrough), never
2047
+ // credentials on argv. The model chosen in the UI travels via VL_MODEL
2048
+ // and the per-cell prompt via a file on the runtime system
2049
+ // (VL_SYSTEM_APPEND_FILE, gate below); `vl --profile` remains
2050
+ // the operator's choice. No --model on argv: the TUI starts without arguments.
2051
2051
  if (model) env.VL_MODEL = model;
2052
2052
  // vl.native e' «usa la tua configurazione»: NESSUNA env provider/base_url.
2053
2053
  // I default interni del runtime sono gia' openai-compat + localhost:11434,
@@ -2079,12 +2079,12 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2079
2079
  env.VL_PROVIDER = profile.vlProvider;
2080
2080
  env.VL_BASE_URL = profile.endpoint;
2081
2081
  }
2082
- // V-69 — prompt per-cella: file composto sul system del runtime
2083
- // (VL_SYSTEM_APPEND_FILE). Il gate di versione sta nel codice, non nella
2084
- // speranza: sotto la 0.3.1 la variabile e' ignorata in silenzio, e una
2085
- // cella che parte senza il proprio prompt senza dirlo e' il difetto che
2086
- // questo ramo chiude, non il rimedio. Degradare = NON comporre la env
2087
- // (mai una promessa che il runtime scarta) + dichiarare il motivo.
2082
+ // Per-cell prompt: file composed on the runtime system
2083
+ // (VL_SYSTEM_APPEND_FILE). The version gate lives in code, not in hope:
2084
+ // below 0.3.1 the variable is silently ignored, and a cell starting without
2085
+ // its own prompt without saying so is the defect this branch closes,
2086
+ // not the remedy. Degrading = NOT composing the env (never a promise the
2087
+ // runtime discards) + declaring the reason.
2088
2088
  const gate = vlSystemAppendGate(info.binary, cfg);
2089
2089
  if (gate.ok) {
2090
2090
  try {
@@ -2190,17 +2190,17 @@ function resolveManagedEngine(engine, cell, cfg = {}) {
2190
2190
  // (finding separato, fuori da questa patch).
2191
2191
  const promptViaDelivery = spec.client === 'kimi'
2192
2192
  || (spec.client === 'claude' && spec.provider === 'kimi-code');
2193
- // vl e' una TUI senza superficie prompt (`vl [OPTIONS]`, nessun flag prompt):
2194
- // non riceve MAI il prompt di cella su argv — dalla V-69 viaggia nel file
2195
- // per-cella composto dal ramo vl (VL_SYSTEM_APPEND_FILE). send-keys non si
2196
- // applica: il classifier di deliverBootstrapPrompt copre solo kimi/claude, e
2197
- // vl parte subito senza attese.
2193
+ // vl is a TUI without a prompt surface (`vl [OPTIONS]`, no prompt flag):
2194
+ // it NEVER receives the per-cell prompt on argv — it travels in the
2195
+ // per-cell file composed by the vl branch (VL_SYSTEM_APPEND_FILE).
2196
+ // send-keys does not apply: the deliverBootstrapPrompt classifier covers
2197
+ // only kimi/claude, and vl starts immediately without waits.
2198
2198
  if (spec.client !== 'shell' && spec.client !== 'vl' && !promptViaDelivery && cell?.prompt) args.push(cell.prompt);
2199
2199
  // nexuscrew-store source: neutralize the profile's env set in the composed
2200
2200
  // child env (unset, never empty), so the runtime cannot leak credentials that
2201
2201
  // the local store is meant to own.
2202
2202
  applyStoreNeutralization(env, spec, profile);
2203
- // D3: resolve envPassthrough AFTER the provider branches. Each declared name is
2203
+ // Resolve envPassthrough AFTER the provider branches. Each declared name is
2204
2204
  // read from the credentialSources (same order as credential() 'auto': runtime
2205
2205
  // -> store -> shell -> key files -> legacy) and injected into the child env.
2206
2206
  // Per nome, mai in blocco. A name that is declared but absent from every source
@@ -2244,7 +2244,7 @@ function publicCatalog() {
2244
2244
  auth: p.auth, endpoint: p.endpoint || '', model: p.model || '', models: [...(p.models || [])],
2245
2245
  protocols: [...(p.protocols || [p.protocol])], supportsUnsafe: !['pi', 'shell', 'vl'].includes(p.client), requiresModel: !!p.requiresModel || !!p.custom,
2246
2246
  permissionPolicyDefault: p.client === 'claude' ? 'unsafe' : 'standard',
2247
- // DEC2: solo il client claude riceve MCP gestito da NexusCrew (cellMcpArgs/
2247
+ // Solo il client claude riceve MCP gestito da NexusCrew (cellMcpArgs
2248
2248
  // sharedMcpArgs nel ramo claude di resolveManagedEngine). Per ogni altro
2249
2249
  // client (codex, vl, kimi, pi, agy, grok) `cell.mcp` e' INERTE: la cella lo
2250
2250
  // accetta ma non ha effetto, perche' i server MCP li registra il client nel
@@ -28,7 +28,7 @@ const PANE_STATES = Object.freeze([
28
28
  'ready', 'busy', 'not-ready-auth', 'not-ready-consent', 'not-ready-onboarding', 'unknown',
29
29
  ]);
30
30
 
31
- // Esiti bounded della consegna (prompt.reason in API: closed enum, G5).
31
+ // Esiti bounded della consegna (prompt.reason in API: closed enum).
32
32
  const DELIVERY_STATES = Object.freeze([
33
33
  'submitted', // paste ok + pane riverificato + Enter ok
34
34
  'staged-not-submitted', // paste ok, Enter fallito: testo forse nel composer
@@ -36,16 +36,16 @@ const DELIVERY_STATES = Object.freeze([
36
36
  'failed-pre-paste', // resolve/load certamente falliti (anche dopo 1 retry)
37
37
  'skipped-not-ready', // classifier not-ready a fine attesa bounded (kind in notReady)
38
38
  'skipped-unknown', // classifier mai ready entro il timeout
39
- 'prompt-rejected', // byte di controllo nel prompt (policy §9e)
40
- 'cancelled', // generazione terminata durante l'attesa/consegna (R3)
39
+ 'prompt-rejected', // control bytes found in the prompt (policy check)
40
+ 'cancelled', // Generazione terminata durante l'attesa/consegna
41
41
  'report-timeout', // up() non ha ricevuto l'esito del launcher entro il bound
42
42
  ]);
43
43
 
44
- // Codici bounded actionRequired (G5). Nessuna inferenza "rifiutato": il marker
44
+ // Codici bounded actionRequired. Nessuna inferenza "rifiutato": il marker
45
45
  // TUI prova solo che serve un'azione nel terminale della cella.
46
46
  const ACTION_CODES = Object.freeze(['KIMI_AUTH_ACTION_REQUIRED', 'CLIENT_INTERACTION_REQUIRED']);
47
47
 
48
- // Slug recovery bounded (R10): l'API trasporta SOLO {code, recovery}; il testo
48
+ // Slug recovery bounded: l'API trasporta SOLO {code, recovery}; il testo
49
49
  // e' mappato localmente dalla PWA via i18n (fleet-recovery-<slug>), mai inviato
50
50
  // dal server (un nodo remoto federato non deve poter iniettare testo libero).
51
51
  const RECOVERY_SLUGS = Object.freeze([
@@ -65,7 +65,7 @@ const MODAL_NOT_READY = Object.freeze([
65
65
 
66
66
  // Marker ancorati alla CODA visibile (status bar / righe finali): lo stesso
67
67
  // testo in scrollback, nel corpo conversazione o in forma diversa non deve
68
- // classificare (R7: il marker auth e' la stringa ESATTA di status bar, non un
68
+ // Classificare (il marker auth e' la stringa ESATTA di status bar, non un
69
69
  // "Not logged in" qualunque). Live-verificati: claude 2.1.220 (status bar
70
70
  // "Not logged in · Run /login"), kimi 0.31.1 (welcome "Run /login or
71
71
  // /provider", "Model: not set").
@@ -99,7 +99,7 @@ function classifyPane(captured, client) {
99
99
  for (const [state, re] of TAIL_NOT_READY) if (re.test(tail)) return state;
100
100
  if (BUSY_TAIL.test(tail)) return 'busy';
101
101
  if (client === 'kimi') {
102
- // READY positivo Kimi: box di input corrente E modello configurato (G2).
102
+ // READY positivo Kimi: box di input corrente E modello configurato.
103
103
  // Il solo "Model:" non basta; la sola box non basta (presente anche da
104
104
  // logged-out). Il welcome logged-out e' gia' intercettato dai marker
105
105
  // not-ready qui sopra; se scrolla via senza login resta 'unknown' (safe).
@@ -132,7 +132,7 @@ function classifyPane(captured, client) {
132
132
  }
133
133
 
134
134
  // vlPaneReadiness(tmuxBin, target, opts) -> { ready, degraded }
135
- // DEC1: content-readiness di una cella vl via pane — il marcatore [o] + coda
135
+ // Content-readiness di una cella vl via pane — il marcatore [o] + coda
136
136
  // `esc quit · ^y yield` riconosciuto da classifyPane(text,'vl'). E' il PUNTO DI
137
137
  // INNESTO della readiness vl: quando arrivera' l'adapter di runtime, la
138
138
  // readiness si misurera' sul socket di controllo (vl-core/src/transport.rs) e
@@ -174,7 +174,7 @@ function clampInt(value, dflt, min, max) {
174
174
  // paneTarget (%N opzionale), readyWaitMs, pollMs, settleMs, tmpdir,
175
175
  // tmuxExecImpl, captureImpl, sleepImpl, nowImpl, fsImpl, isCancelled }
176
176
  // reason e' SEMPRE lo state (closed enum); notReady e' il kind bounded o ''.
177
- // isCancelled() (R3): valutato ad OGNI poll, prima del paste e prima dell'
177
+ // IsCancelled: valutato ad OGNI poll, prima del paste e prima dell'
178
178
  // Enter; se true la consegna si ferma subito ('cancelled') senza paste/Enter —
179
179
  // cell-exec cancella la delivery quando la generazione termina, cosi' un
180
180
  // polling in volo non puo' mai iniettare nella generazione successiva.
@@ -219,7 +219,7 @@ async function deliverBootstrapPrompt(opts = {}) {
219
219
 
220
220
  // (2) Consegna AT-MOST-ONCE: un solo paste per generazione. Il retry copre
221
221
  // SOLO fallimenti certi pre-paste (resolve/load); dopo qualsiasi tentativo
222
- // di paste-buffer non esiste retry automatico (G1).
222
+ // Di paste-buffer non esiste retry automatico.
223
223
  let attempts = 0;
224
224
  for (;;) {
225
225
  if (cancelled()) return done(false, 'cancelled', '', attempts);
@@ -240,7 +240,7 @@ async function deliverBootstrapPrompt(opts = {}) {
240
240
  if (cancelled()) return done(false, 'delivery-unknown', '', attempts);
241
241
  const enter = await exec(tmuxBin, ['send-keys', '-t', stage.paneId, 'Enter'], { env });
242
242
  if (enter.err) return done(false, 'staged-not-submitted', '', attempts);
243
- // R9: cancel con Enter appena partito -> la generazione e' morta con un
243
+ // Cancel con Enter appena partito -> la generazione e' morta con un
244
244
  // submit in volo: esito incerto (residuo PTY possibile), mai 'submitted'.
245
245
  if (cancelled()) return done(false, 'delivery-unknown', '', attempts);
246
246
  return done(true, 'submitted', '', attempts);
@@ -259,7 +259,7 @@ async function deliverBootstrapPrompt(opts = {}) {
259
259
  }
260
260
  }
261
261
 
262
- // waitDeliveryReport(tmuxBin, target, opts) -> delivery-like | null (R2).
262
+ // WaitDeliveryReport(tmuxBin, target, opts) -> delivery-like null.
263
263
  // Il launcher supervisionato (cell-exec) e' l'UNICO owner della consegna per
264
264
  // TUTTE le generazioni degli engine Kimi e pubblica l'esito bounded sull'
265
265
  // opzione tmux di pane @nc_delivery ('<state>' o '<state>:<notReady>', solo
@@ -294,7 +294,7 @@ async function waitDeliveryReport(tmuxBin, target, { env, exec, sleepImpl, nowIm
294
294
  // actionRequiredFor(client, provider, delivery) -> null | { code, recovery }
295
295
  // Mappa bounded delivery -> azione operatore. Solo skip per not-ready/unknown
296
296
  // producono actionRequired; i fallimenti di trasporto restano nel prompt.state.
297
- // R10: SOLO {code, recovery} closed enum/slug — il testo e' i18n locale PWA.
297
+ // SOLO {code, recovery} closed enum/slug — il testo e' i18n locale PWA.
298
298
  function actionRequiredFor(client, provider, delivery) {
299
299
  if (!delivery || delivery.delivered) return null;
300
300
  if (delivery.state !== 'skipped-not-ready' && delivery.state !== 'skipped-unknown') return null;
@@ -10,9 +10,9 @@ function capList(fleet) {
10
10
  return typeof fleet.capabilities === 'function' ? fleet.capabilities() : DEFAULT_CAPS;
11
11
  }
12
12
 
13
- // Design §9c: una route per un metodo non supportato dal provider ritorna 501
14
- // (mai 404/502 ambigui). Gli errori nativi del builtin (400/403/409) passano
15
- // invece nel guard con il loro status.
13
+ // A route for a method unsupported by the provider returns 501
14
+ // (never ambiguous 404/502). The builtin native errors (400/403/409) pass
15
+ // through the guard with their own status.
16
16
  function requireCap(fleet, cap) {
17
17
  if (!capList(fleet).includes(cap)) {
18
18
  const e = new Error('not supported by this fleet provider');
@@ -53,6 +53,11 @@ function fleetRoutes(fleetP, cfg = {}) {
53
53
  if (opts.action) emit('info', 'FLEET_ACTION_COMPLETED', 'Fleet action completed', {
54
54
  action: opts.action, cell: safeCell(body), state: opts.action === 'down' ? 'stopped' : 'ready',
55
55
  });
56
+ if (opts.action && typeof cfg.onFleetStateChange === 'function') {
57
+ try {
58
+ cfg.onFleetStateChange({ cell: safeCell(body), active: opts.action !== 'down', action: opts.action });
59
+ } catch (_) { /* observability never breaks the action */ }
60
+ }
56
61
  res.json(result);
57
62
  } catch (e) {
58
63
  if (opts.action) {
@@ -63,7 +68,7 @@ function fleetRoutes(fleetP, cfg = {}) {
63
68
  else emit('warn', 'FLEET_ACTION_FAILED', 'Fleet action failed', {
64
69
  action: opts.action, cell: safeCell(req.body), state: 'failed',
65
70
  status: e.status || 500,
66
- // Cause-preserving (T4): bounded enum only (lib/fleet/causes.js). The
71
+ // Cause-preserving: bounded enum only (lib/fleet/causes.js). The
67
72
  // cause triple is {status, code, phase}; no free message, cwd/path,
68
73
  // argv, env, prompt, token or credential ever enters here. Untagged /
69
74
  // legacy errors degrade to the bounded UNKNOWN fallback.
@@ -81,8 +86,8 @@ function fleetRoutes(fleetP, cfg = {}) {
81
86
  }
82
87
  };
83
88
 
84
- // /status espone anche `provider` e `capabilities` (design §9b/§9c), oltre ai
85
- // campi storici (available/cells/engines).
89
+ // /status also exposes `provider` and `capabilities`, besides the
90
+ // historical fields (available/cells/engines).
86
91
  r.get('/status', async (_req, res) => {
87
92
  try {
88
93
  const fleet = await fleetP;
@@ -90,10 +95,10 @@ function fleetRoutes(fleetP, cfg = {}) {
90
95
  return res.json({
91
96
  available: false,
92
97
  provider: fleet.provider || 'disabled',
93
- bootOwner: 'none', // §9b: provider non disponibile -> nessun boot owner
98
+ bootOwner: 'none', // provider unavailable -> no boot owner
94
99
  capabilities: capList(fleet),
95
100
  ...(fleet.reason ? { reason: fleet.reason } : {}),
96
- // R33: il codice macchina del boot bloccato viaggia fino al client.
101
+ // Il codice macchina del boot bloccato viaggia fino al client.
97
102
  // La policy di lettura decide su QUESTO, non sulla prosa del reason
98
103
  // (la prosa resta ripiego per i reason che un codice non ce l'hanno).
99
104
  // Senza campo dal provider, la risposta semplicemente non lo ha.
@@ -143,8 +148,8 @@ function fleetRoutes(fleetP, cfg = {}) {
143
148
  return f.boot(String(b.cell || ''), b.enabled === true);
144
149
  }, { mutate: true }));
145
150
 
146
- // --- Estensione B4.2: schema + define/edit/remove (engine e cell) ---
147
- // Ogni route negozia la capability del provider: mancante → 501 (§9c).
151
+ // --- Extension: schema + define/edit/remove (engine and cell) ---
152
+ // Every route negotiates the provider capability: missing -> 501.
148
153
  r.get('/schema', guard((f) => { requireCap(f, 'schema'); return f.schema(); }));
149
154
  r.get('/definitions', guard((f) => { requireCap(f, 'definitions'); return f.definitions(); }));
150
155
  r.get('/credentials/status', guard((f) => {
@@ -200,7 +200,7 @@ function createBuiltinRuntime(ctx) {
200
200
  return cache.defs.cells.some((c) => c.tmuxSession === String(name));
201
201
  }
202
202
 
203
- // up — ordine obbligatorio (design §9a, task B4.2).
203
+ // up — mandatory order.
204
204
  // Le override {engine,boot} del contratto route sono ignorate: il builtin e'
205
205
  // definitions-driven (l'engine della cella e' quello dichiarato; boot e' uno
206
206
  // stato persistente gestito da boot()). Lancia SENZA shell.
@@ -220,12 +220,12 @@ function createBuiltinRuntime(ctx) {
220
220
  throw httpError(400, `engine managed non configurato (${engine.id}): ${resolved.reason}`, null, { phase: 'preflight', code });
221
221
  }
222
222
  launchEngine = resolved.engine;
223
- // V-69: una cella vl il cui runtime non regge il prompt per-cella
224
- // (VL_SYSTEM_APPEND_FILE, 0.3.1+) DEGRADA dichiarando — stessa forma di
225
- // readinessDegraded (DEC1): la cella parte senza la propria identita' e
226
- // l'operatore lo vede (log qui + flag nel risultato, consumato dalla UI).
227
- // Mai in silenzio: la partenza silenziosa senza prompt e' il difetto che
228
- // V-69 chiude.
223
+ // A vl cell whose runtime cannot hold the per-cell prompt
224
+ // (VL_SYSTEM_APPEND_FILE, 0.3.1+) DEGRADES by declaring it — same
225
+ // shape as readinessDegraded: the cell starts without its own identity
226
+ // and the operator sees it (log here + flag in the result, consumed by the UI).
227
+ // Never in silence: a silent start without the prompt is the defect
228
+ // this closes.
229
229
  if (launchEngine.vlPromptDegraded) {
230
230
  const log = typeof cfg.log === 'function' ? cfg.log : console.warn;
231
231
  log(`fleet vl ${cell.id}: ${launchEngine.vlPromptDegraded}`);
@@ -311,7 +311,7 @@ function createBuiltinRuntime(ctx) {
311
311
  readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
312
312
  // Solo gli engine managed Kimi (kimi.native / claude.kimi-code) hanno
313
313
  // la delivery classificata at-most-once ai restart; gli engine custom
314
- // send-keys conservano il reinject legacy senza Enter (0.8.47 G4).
314
+ // Send-keys conservano il reinject legacy senza Enter (0.8.47).
315
315
  client: engine.managed && engine.managed.client === 'kimi' ? 'kimi'
316
316
  : (engine.managed && engine.managed.provider === 'kimi-code' ? 'claude' : ''),
317
317
  readyWaitMs: Math.max(0, Math.min(120000, Number(cfg.bootstrapReadyWaitMs) || 15000)),
@@ -337,11 +337,11 @@ function createBuiltinRuntime(ctx) {
337
337
  };
338
338
  const tmuxChild = composeClientInvocation(tmuxLaunchEngine, cell);
339
339
 
340
- // Avvio staged (design §3.3): crea il pane con un placeholder inerte trusted
341
- // (cell-hold.js), arma remain-on-exit window-local sul @N, poi respawn-pane -k
342
- // verso cell-exec sul %N esatto. Cosi' remain-on-exit e' gia' ON quando il vero
343
- // child puo' terminare: nessuna finestra scomparsa, nessun NEW_SESSION_FAILED
344
- // che maschera l'exit reale. Il nome sessione e' gia' validato tmux-safe (v2
340
+ // Staged start: creates the pane with a trusted inert placeholder
341
+ // (cell-hold.js), arms window-local remain-on-exit on the @N, then
342
+ // respawn-pane -k to cell-exec on the exact %N. So remain-on-exit is already
343
+ // ON when the real child can terminate: no vanished window, no
344
+ // NEW_SESSION_FAILED masking the real exit.
345
345
  // per id puntati, definitions.js); gli step critici usano gli ID restituiti
346
346
  // da tmux ($N/@N/%N). respawn-pane -k preserva il pane ID: il %N catturato
347
347
  // qui resta valido per readiness e prompt. Nessuna shell string, nessun
@@ -357,7 +357,7 @@ function createBuiltinRuntime(ctx) {
357
357
  // new-session fallita: nessuna sessione creata. Revoca il ticket (il child
358
358
  // non partira' mai) prima di propagare l'errore.
359
359
  try { await launchBroker.revoke?.(ticket.nonce); } catch (_) { /* best-effort */ }
360
- // Redazione (§9h): lo stderr di tmux puo' ecoare argv/env del comando lanciato.
360
+ // Redaction: the tmux stderr can echo argv/env of the launched command.
361
361
  const dup = /duplicate session/i.test(create.stderr);
362
362
  const why = dup
363
363
  ? 'sessione già in esecuzione'
@@ -434,8 +434,8 @@ function createBuiltinRuntime(ctx) {
434
434
  ['respawn-pane', '-k', '-c', realCwd, '-t', paneId, tmuxChild.command, ...tmuxChild.args],
435
435
  { env: minimalEnv() });
436
436
  if (respawn.err) {
437
- // respawn fallito dopo issue(): il nonce va revocato/consumato prima
438
- // dell'errore (design §3.3), poi cleanup della sessione-pannello.
437
+ // respawn failed after issue(): the nonce must be revoked/consumed first
438
+ // before surfacing the error, then cleanup of the panel session.
439
439
  await cleanupLaunch();
440
440
  throw httpError(500,
441
441
  `tmux respawn-pane failed: ${redactSecrets(respawn.stderr.trim() || respawn.err.message, launchEngine, cell)}`,
@@ -478,7 +478,7 @@ function createBuiltinRuntime(ctx) {
478
478
  `comando Shell terminato subito${status}: ${diagnostic || 'verifica command, PATH e configurazione della shell'}`,
479
479
  null, { phase: 'readiness', code: 'SHELL_COMMAND_FAILED' });
480
480
  }
481
- // Cause-preserving (T4): distinguish a cell-client spawn failure (the
481
+ // Cause-preserving: distinguish a cell-client spawn failure (the
482
482
  // captured pane carries the stable 'cell spawn failed:' marker produced by
483
483
  // cell-exec.js) from a generic early exit. Both stay on the readiness
484
484
  // surface; the spawn branch keeps CELL_SPAWN_FAILED sanitized downstream.
@@ -491,7 +491,7 @@ function createBuiltinRuntime(ctx) {
491
491
  ['set-option', '-w', '-t', readiness.target, 'remain-on-exit', 'off'], { env: minimalEnv(), timeoutMs: 2000 });
492
492
  }
493
493
 
494
- // DEC1: content-readiness vl (marcatore [o] nel pane). MAI fail-closed: se
494
+ // Content-readiness vl (marcatore [o] nel pane). MAI fail-closed: se
495
495
  // entro il timeout il marcatore non compare DEGRADA e procede come se pronta
496
496
  // (una cella che parte oggi deve partire anche domani), lasciando traccia
497
497
  // nel risultato. vlPaneReadiness e' il punto di innesto sostituibile quando
@@ -514,14 +514,14 @@ function createBuiltinRuntime(ctx) {
514
514
  };
515
515
  }
516
516
 
517
- // (6) prompt: due percorsi distinti (0.8.47, R2 single-owner).
517
+ // (6) prompt: two distinct paths (0.8.47, single-owner).
518
518
  // - managed kimi.native / claude.kimi-code: la consegna e' posseduta SOLO
519
519
  // dal supervisore (cell-exec) per TUTTE le generazioni; qui si legge
520
520
  // l'esito bounded (@nc_delivery sul pane) con attesa bounded. Mai paste/
521
521
  // Enter dal runtime: niente doppia delivery se gen0 muore durante
522
522
  // l'attesa e gen1 parte sotto il supervisore.
523
523
  // - engine custom promptMode 'send-keys': injectPrompt legacy (bracketed
524
- // paste SENZA Enter, contratto invariato — G4), target %N esatto.
524
+ // Paste SENZA Enter, contratto invariato —), target %N esatto.
525
525
  let prompt = null;
526
526
  let actionRequired = null;
527
527
  if (launchEngine.promptMode === 'send-keys' && cell.prompt) {
@@ -551,7 +551,7 @@ function createBuiltinRuntime(ctx) {
551
551
  env: minimalEnv(),
552
552
  readyMs: cfg.sendKeysReadyMs != null ? cfg.sendKeysReadyMs : readyMs,
553
553
  target,
554
- engine: launchEngine, cell, // per la redazione del reason se paste-buffer fallisce (§9h)
554
+ engine: launchEngine, cell, // to redact the reason if the paste-buffer fails
555
555
  });
556
556
  }
557
557
  }
@@ -560,9 +560,9 @@ function createBuiltinRuntime(ctx) {
560
560
  ok: true, cell: cellId, session: cell.tmuxSession, prompt,
561
561
  ...(actionRequired ? { actionRequired } : {}),
562
562
  ...(readinessDegraded ? { readinessDegraded: true } : {}),
563
- // V-69: prompt di cella non consegnato a una cella vl (runtime vecchio o
564
- // file non scrivibile). Booleano strict come readinessDegraded: il
565
- // payload puo' arrivare da un nodo federato e il testo e' i18n locale.
563
+ // Per-cell prompt not delivered to a vl cell (old runtime or
564
+ // unwritable file). Strict boolean like readinessDegraded: the
565
+ // payload may come from a federated node and the text is local i18n.
566
566
  ...(launchEngine.vlPromptDegraded ? { vlPromptDegraded: true } : {}),
567
567
  };
568
568
  }