@mmmbuto/nexuscrew 0.8.32 → 0.8.34

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.
@@ -112,8 +112,22 @@ function parseFlags(argv, valueFlags) {
112
112
 
113
113
  function serve(opts = {}) {
114
114
  const serverStart = opts.serverStart || require('../server.js').start;
115
+ const platform = opts.platform || detectPlatform();
116
+ // A legacy Termux:Boot script can start the new package from a replaceable
117
+ // npm directory. Correct the live process before it can create the shared
118
+ // tmux server, then atomically refresh both boot definitions for next boot.
119
+ if (platform === 'termux') {
120
+ stabilizeTermuxWorkingDirectory({ ...opts, platform });
121
+ try {
122
+ repairTermuxBootDefinitions({ ...opts, platform, clearMarker: true });
123
+ } catch (error) {
124
+ const warn = opts.lifecycleWarn || ((message) => process.stderr.write(`${message}\n`));
125
+ warn(`nexuscrew: Termux boot auto-repair pending — ${String(error && error.message || error)}`);
126
+ }
127
+ }
115
128
  // Service manager e Termux:Boot entrano direttamente da `serve`, senza
116
- // passare per smartUp. Ripara solo fleet.json MANCANTE; un file invalido
129
+ // passare per smartUp. Su Termux ripara anche gli script di boot legacy.
130
+ // Per Fleet, ripara solo fleet.json MANCANTE; un file invalido
117
131
  // resta intatto e il provider continua a fallire chiuso.
118
132
  (opts.ensureFleetDefaultsImpl || ensureFleetDefaults)(opts);
119
133
  if (opts.pidfile) {
@@ -279,6 +293,37 @@ function restart(opts = {}) {
279
293
  const execImpl = opts.execImpl || execFileSync;
280
294
  const log = opts.log || console.log;
281
295
  if (!['linux', 'mac', 'termux'].includes(platform)) throw new Error(`restart: platform ${platform} non supportata`);
296
+ let termuxRepair = null;
297
+ if (platform === 'termux') {
298
+ try {
299
+ stabilizeTermuxWorkingDirectory({ ...opts, platform });
300
+ termuxRepair = repairTermuxBootDefinitions({
301
+ ...opts, platform, clearMarker: false,
302
+ });
303
+ } catch (error) {
304
+ return {
305
+ platform,
306
+ runtimeOwner: 'portable',
307
+ restarted: false,
308
+ reason: `Termux boot auto-repair failed: ${String(error && error.message || error)}`,
309
+ };
310
+ }
311
+ }
312
+ const complete = (result) => {
313
+ if (platform !== 'termux' || !result || result.restarted !== true || !termuxRepair?.markerPending) {
314
+ return result;
315
+ }
316
+ try {
317
+ clearServiceMigrationMarker(opts.home || require('node:os').homedir(), opts.serviceMigrationMarkerPath);
318
+ return result;
319
+ } catch (error) {
320
+ return {
321
+ ...result,
322
+ serviceRepairPending: true,
323
+ warning: `Termux boot repair marker cleanup pending: ${String(error && error.message || error)}`,
324
+ };
325
+ }
326
+ };
282
327
  const before = resolveRuntimeOwner({ ...opts, platform, execImpl });
283
328
 
284
329
  // A managed owner can use the service manager's atomic restart. In a
@@ -307,7 +352,7 @@ function restart(opts = {}) {
307
352
  execImpl('launchctl', ['kickstart', '-k', label], { stdio: 'ignore' });
308
353
  log(`restart: launchctl kickstart -k ${label}`);
309
354
  }
310
- return { platform, owner: before.owner, runtimeOwner: 'managed', restarted: true };
355
+ return complete({ platform, owner: before.owner, runtimeOwner: 'managed', restarted: true });
311
356
  } catch (error) {
312
357
  return { platform, owner: before.owner, runtimeOwner: 'managed', restarted: false, reason: String(error.message || error) };
313
358
  }
@@ -319,7 +364,7 @@ function restart(opts = {}) {
319
364
  const started = (opts.startPortableImpl || startPortable)({ ...opts, platform, spawnImpl: opts.spawnImpl });
320
365
  const ok = !!(started && started.started !== false);
321
366
  if (ok) log('restart: portable runtime started');
322
- return { platform, owner: before.owner, runtimeOwner: 'portable', restarted: ok, reason: ok ? undefined : started && started.reason };
367
+ return complete({ platform, owner: before.owner, runtimeOwner: 'portable', restarted: ok, reason: ok ? undefined : started && started.reason });
323
368
  }
324
369
 
325
370
  // `restart` on a stopped runtime follows the explicit boot owner; with boot
@@ -330,7 +375,7 @@ function restart(opts = {}) {
330
375
  : (opts.startPortableImpl || startPortable)({ ...opts, platform, spawnImpl: opts.spawnImpl });
331
376
  const ok = !!(started && started.started !== false);
332
377
  if (ok) log(`restart: ${useManaged ? 'managed' : 'portable'} runtime started`);
333
- return { platform, owner: before.owner, runtimeOwner: useManaged ? 'managed' : 'portable', restarted: ok, reason: ok ? undefined : started && started.reason };
378
+ return complete({ platform, owner: before.owner, runtimeOwner: useManaged ? 'managed' : 'portable', restarted: ok, reason: ok ? undefined : started && started.reason });
334
379
  }
335
380
 
336
381
  function wizardComplete(configPath) {
@@ -406,6 +451,63 @@ function clearServiceMigrationMarker(home, override) {
406
451
  }
407
452
  }
408
453
 
454
+ // Termux app updates replace the global npm package while existing boot
455
+ // scripts remain on disk. Always enter the stable HOME before serving or
456
+ // booting Fleet so a legacy script cannot make tmux inherit the package cwd.
457
+ function stabilizeTermuxWorkingDirectory(opts = {}) {
458
+ const platform = opts.platform || detectPlatform();
459
+ if (platform !== 'termux') return { platform, changed: false };
460
+ const home = opts.home || require('node:os').homedir();
461
+ (opts.chdirImpl || process.chdir)(home);
462
+ return { platform, changed: true, cwd: home };
463
+ }
464
+
465
+ // Refresh only an already-enabled Termux:Boot installation. Boot remains
466
+ // opt-in: missing scripts are never created by this repair path. A durable
467
+ // marker makes an interrupted refresh visible and retryable.
468
+ function repairTermuxBootDefinitions(opts = {}) {
469
+ const platform = opts.platform || detectPlatform();
470
+ const home = opts.home || require('node:os').homedir();
471
+ if (platform !== 'termux') return { platform, persistent: false, refreshed: false, markerPending: false };
472
+ const persistent = bootState({ ...opts, platform }).enabled;
473
+ if (!persistent) return { platform, persistent: false, refreshed: false, markerPending: false };
474
+
475
+ const markerPending = serviceMigrationPending(home, opts.serviceMigrationMarkerPath);
476
+ const refreshNeeded = serviceDefinitionNeedsRefresh(
477
+ platform, home, opts.installPath, opts.fleetInstallPath,
478
+ );
479
+ if (!markerPending && !refreshNeeded) {
480
+ return { platform, persistent: true, refreshed: false, markerPending: false };
481
+ }
482
+
483
+ if (refreshNeeded) {
484
+ ensureServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
485
+ const result = (opts.runInitImpl || runInit)({
486
+ ...opts,
487
+ home,
488
+ platform,
489
+ installBoot: true,
490
+ printUrl: false,
491
+ log: opts.lifecycleLog || (() => {}),
492
+ });
493
+ if (serviceDefinitionNeedsRefresh(platform, home, opts.installPath, opts.fleetInstallPath)) {
494
+ throw new Error('generated Termux boot definitions failed stable-HOME verification');
495
+ }
496
+ const failures = Array.isArray(result?.installFailures) ? result.installFailures : [];
497
+ if (failures.length) {
498
+ const components = [...new Set(failures.map((failure) => failure?.component)
499
+ .filter((component) => component === 'service' || component === 'fleet-companion'))];
500
+ throw new Error(`Termux boot definition install failed${components.length ? ` (${components.join(', ')})` : ''}`);
501
+ }
502
+ }
503
+
504
+ const pending = markerPending || refreshNeeded;
505
+ if (pending && opts.clearMarker !== false) {
506
+ clearServiceMigrationMarker(home, opts.serviceMigrationMarkerPath);
507
+ }
508
+ return { platform, persistent: true, refreshed: refreshNeeded, markerPending: pending };
509
+ }
510
+
409
511
  function portAvailable(port, host = '127.0.0.1') {
410
512
  return new Promise((resolve) => {
411
513
  const s = net.createServer();
@@ -742,6 +844,7 @@ function update(opts = {}) {
742
844
  // / cfg / exit (per test, no process.exit che uccide il runner).
743
845
  async function runFleetBoot(opts = {}) {
744
846
  const log = opts.log || console.log;
847
+ stabilizeTermuxWorkingDirectory(opts);
745
848
  const loadConfig = opts.loadConfig || require('../config.js').loadConfig;
746
849
  const selectProvider = opts.selectProvider || require('../fleet/provider.js').selectProvider;
747
850
  const { bootCells } = require('../fleet/boot.js');
@@ -1065,6 +1168,8 @@ module.exports = {
1065
1168
  serviceDefinitionNeedsRefresh,
1066
1169
  fleetServiceDefinitionNeedsRefresh,
1067
1170
  serviceMigrationPending,
1171
+ stabilizeTermuxWorkingDirectory,
1172
+ repairTermuxBootDefinitions,
1068
1173
  portAvailable, findAvailablePort, probeNexusCrew, waitForNexusCrew, openPwa, startPortable, wizardComplete,
1069
1174
  servicePinsLegacyPort,
1070
1175
  isServiceRunning, readRoles,
@@ -30,9 +30,24 @@ function removePidfile(p) {
30
30
  try { fs.unlinkSync(p); } catch (_) {}
31
31
  }
32
32
 
33
- function pidExists(pid) {
34
- try { process.kill(pid, 0); return true; }
35
- catch (e) { return e.code === 'EPERM'; } // EPERM = esiste ma non nostro
33
+ // A PID can exist without belonging to this UID. Android commonly reuses PIDs
34
+ // across app sandboxes; kill(pid, 0) then returns EPERM and /proc is hidden.
35
+ // Keep generic existence separate from NexusCrew ownership so a foreign PID
36
+ // can never keep one of our pidfiles "alive" forever.
37
+ function pidOwnership(pid, killImpl = process.kill) {
38
+ try {
39
+ killImpl(pid, 0);
40
+ return 'owned';
41
+ } catch (e) {
42
+ if (e && e.code === 'EPERM') return 'foreign';
43
+ if (e && e.code === 'ESRCH') return 'missing';
44
+ return 'unknown';
45
+ }
46
+ }
47
+
48
+ function pidExists(pid, killImpl = process.kill) {
49
+ const ownership = pidOwnership(pid, killImpl);
50
+ return ownership === 'owned' || ownership === 'foreign';
36
51
  }
37
52
 
38
53
  function readCmdline(pid) {
@@ -50,36 +65,46 @@ function cmdMatches(savedCmd, liveCmd) {
50
65
  return liveCmd.includes(savedCmd) || savedCmd.includes(liveCmd);
51
66
  }
52
67
 
53
- // true se il pid esiste E il cmd matcha (o non verificabile).
54
- function isAlive(meta) {
68
+ // true se il pid appartiene a questo UID E il cmd matcha (o non verificabile).
69
+ // EPERM is deliberately false: NexusCrew must neither adopt nor signal a
70
+ // process owned by another Android/Linux user.
71
+ function isAlive(meta, impl = {}) {
55
72
  if (!meta || !Number.isFinite(meta.pid)) return false;
56
- if (!pidExists(meta.pid)) return false;
73
+ if (pidOwnership(meta.pid, impl.killImpl || process.kill) !== 'owned') return false;
57
74
  if (meta.cmd) {
58
- const live = readCmdline(meta.pid);
75
+ const live = (impl.readCmdlineImpl || readCmdline)(meta.pid);
59
76
  if (live) return cmdMatches(meta.cmd, live);
60
77
  }
61
78
  return true;
62
79
  }
63
80
 
64
81
  // Rimuove pidfile stale (pid morto o non verificabile). Ritorna true se rimosso.
65
- function cleanStale(p) {
82
+ function cleanStale(p, impl = {}) {
66
83
  const meta = readPidfile(p);
67
84
  if (!meta) return false;
68
- if (!isAlive(meta)) { removePidfile(p); return true; }
85
+ if (!isAlive(meta, impl)) { removePidfile(p); return true; }
69
86
  return false;
70
87
  }
71
88
 
72
89
  // Kill verificato: legge pidfile, verifica pid+cmd, signal. MAI broad match by name.
73
90
  // Ritorna { killed, pid?, reason? }.
74
- function killPidfile(p, signal = 'SIGTERM') {
91
+ function killPidfile(p, signal = 'SIGTERM', impl = {}) {
75
92
  const meta = readPidfile(p);
76
93
  if (!meta) return { killed: false, reason: 'no pidfile' };
77
- if (!pidExists(meta.pid)) {
94
+ const killImpl = impl.killImpl || process.kill;
95
+ const ownership = pidOwnership(meta.pid, killImpl);
96
+ if (ownership === 'missing' || ownership === 'unknown') {
78
97
  removePidfile(p);
79
98
  return { killed: false, reason: 'stale (pid dead)' };
80
99
  }
100
+ if (ownership === 'foreign') {
101
+ // Never send a real signal after an EPERM ownership probe. The pidfile is
102
+ // ours; the process is not.
103
+ removePidfile(p);
104
+ return { killed: false, reason: 'stale (pid not owned)' };
105
+ }
81
106
  if (meta.cmd) {
82
- const live = readCmdline(meta.pid);
107
+ const live = (impl.readCmdlineImpl || readCmdline)(meta.pid);
83
108
  if (live && !cmdMatches(meta.cmd, live)) {
84
109
  // PID reuse: processo diverso. Non killare. Rimuovi pidfile stale.
85
110
  removePidfile(p);
@@ -87,7 +112,7 @@ function killPidfile(p, signal = 'SIGTERM') {
87
112
  }
88
113
  }
89
114
  try {
90
- process.kill(meta.pid, signal);
115
+ killImpl(meta.pid, signal);
91
116
  removePidfile(p);
92
117
  return { killed: true, pid: meta.pid };
93
118
  } catch (e) {
@@ -97,5 +122,5 @@ function killPidfile(p, signal = 'SIGTERM') {
97
122
 
98
123
  module.exports = {
99
124
  defaultPidfilePath, readPidfile, writePidfile, removePidfile,
100
- pidExists, readCmdline, isAlive, cleanStale, killPidfile,
125
+ pidOwnership, pidExists, readCmdline, isAlive, cleanStale, killPidfile,
101
126
  };
@@ -36,6 +36,20 @@ function minimalEnv() {
36
36
  return minimalRuntimeEnv(process.env, { home: os.homedir() });
37
37
  }
38
38
 
39
+ // tmux reports client-side connection failures through localized stderr. The
40
+ // migration classifier must see the stable POSIX wording on every platform,
41
+ // otherwise a missing first-boot socket could disable Fleet under a non-English
42
+ // locale. Restrict only the message locale: LC_ALL=C would also force ASCII
43
+ // character handling and tmux would sanitize the tab in our format string.
44
+ // Panes, the shared server and the inventory protocol keep a UTF-8 LC_CTYPE.
45
+ function tmuxInventoryEnv() {
46
+ const env = minimalEnv();
47
+ delete env.LC_ALL; // LC_ALL would override LC_MESSAGES and LC_CTYPE.
48
+ env.LANGUAGE = 'C';
49
+ env.LC_MESSAGES = 'C';
50
+ return env;
51
+ }
52
+
39
53
  // httpError(status, msg, data?, cause?) — structured HTTP error. `data` carries
40
54
  // arbitrary API detail for the response body; `cause` (T4) is the OPTIONAL
41
55
  // bounded failure triple {phase, code} of the up() boundary that failed. The
@@ -170,10 +184,10 @@ async function migrateLegacyTmuxSessions(tmuxBin, defs, { readonly = false } = {
170
184
  };
171
185
  }
172
186
  const listing = await tmuxExec(tmuxBin,
173
- ['list-sessions', '-F', '#{session_id}\t#{session_name}'], { env: minimalEnv() });
187
+ ['list-sessions', '-F', '#{session_id}\t#{session_name}'], { env: tmuxInventoryEnv() });
174
188
  if (listing.err) {
175
189
  const detail = boundedTmuxFailure(listing);
176
- if (/no server running|failed to connect|connection refused|no such file.*tmux/i.test(detail)) {
190
+ if (/no server running|failed to connect|connection refused|no such file.*tmux|error connecting to .*\(no such file or directory\)/i.test(detail)) {
177
191
  return { migrated: [], reason: 'no-tmux-server', needsPersistence: legacyMap.size > 0 };
178
192
  }
179
193
  throw tmuxMigrationError('TMUX_MIGRATION_LIST_FAILED',
package/lib/mcp/server.js CHANGED
@@ -25,6 +25,7 @@ const { loadConfig } = require('../config.js');
25
25
  const { readTokenSafe } = require('../auth/token.js');
26
26
  const { isValidSession } = require('../files/store.js');
27
27
  const VERSION = require('../../package.json').version;
28
+ const MCP_COMPANIONS = require('../../mcp-companions.json');
28
29
  const { TOOLS, IDENTITY_CODE, IDENTITY_REMEDIATION } = require('./tools.js');
29
30
  const cells = require('./cells.js');
30
31
 
@@ -38,6 +39,17 @@ const INVALID_REQUEST = -32600;
38
39
  const METHOD_NOT_FOUND = -32601;
39
40
  const INVALID_PARAMS = -32602;
40
41
 
42
+ function companionInstructions() {
43
+ const catalog = MCP_COMPANIONS.companions
44
+ .map((item) => `${item.id}: ${item.name} (${item.repository})`)
45
+ .join('; ');
46
+ return 'Discover the current client tools before recommending another MCP server. '
47
+ + 'If a requested capability is missing, these optional NexusCrew companions may cover it: '
48
+ + `${catalog}. Recommend only the capability actually needed and ask before installing `
49
+ + 'software, changing MCP configuration, starting services or requesting credentials. '
50
+ + 'NexusCrew does not install or configure companions automatically.';
51
+ }
52
+
41
53
  // --- identita' cella mittente ------------------------------------------------
42
54
  // Ordine (design §1, INVARIATO): $TMUX presente -> `tmux display-message -p '#S'`
43
55
  // (nome sessione reale); se fallisce/invalida -> fallback env NEXUSCREW_MCP_SESSION;
@@ -246,6 +258,7 @@ function createMcpServer(opts = {}) {
246
258
  protocolVersion: pv,
247
259
  capabilities: { tools: {} },
248
260
  serverInfo: { name: 'nexuscrew', version: VERSION },
261
+ instructions: companionInstructions(),
249
262
  });
250
263
  }
251
264
  if (method === 'ping') return reply(id, {});
@@ -130,7 +130,12 @@ function reconcileTunnelSupervisors({ home = os.homedir(), configuredNames = [],
130
130
  .filter((name) => store.NODE_NAME_RE.test(String(name || ''))));
131
131
  const stopOne = typeof stopImpl === 'function' ? stopImpl : stopTunnel;
132
132
  const result = { kept: [], stopped: [], cleaned: [], failed: [] };
133
- const safeAbsent = new Set(['no pidfile', 'stale (pid dead)', 'pid reuse (cmd mismatch)']);
133
+ const safeAbsent = new Set([
134
+ 'no pidfile',
135
+ 'stale (pid dead)',
136
+ 'stale (pid not owned)',
137
+ 'pid reuse (cmd mismatch)',
138
+ ]);
134
139
  for (const name of tunnelPidNames(home)) {
135
140
  if (keep.has(name)) { result.kept.push(name); continue; }
136
141
  try {
@@ -0,0 +1,103 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "kind": "nexuscrew-mcp-companions",
4
+ "policy": {
5
+ "optional": true,
6
+ "discoverToolsFirst": true,
7
+ "recommendOnlyForRequestedCapability": true,
8
+ "automaticInstall": false,
9
+ "automaticConfiguration": false
10
+ },
11
+ "companions": [
12
+ {
13
+ "id": "memory",
14
+ "name": "mcp-memory-rs",
15
+ "repository": "https://github.com/DioNanos/mcp-memory-rs",
16
+ "installation": "https://github.com/DioNanos/mcp-memory-rs#install",
17
+ "license": "Apache-2.0",
18
+ "bundledSkill": "skills/memory/SKILL.md",
19
+ "capabilities": [
20
+ "durable structured agent state",
21
+ "versioned categories",
22
+ "bounded append-only journals",
23
+ "full-text memory search"
24
+ ],
25
+ "primaryTools": [
26
+ "memory_read",
27
+ "memory_write",
28
+ "memory_append",
29
+ "memory_search",
30
+ "memory_status"
31
+ ]
32
+ },
33
+ {
34
+ "id": "msa",
35
+ "name": "mcp-vl-msa-rs",
36
+ "repository": "https://github.com/DioNanos/mcp-vl-msa-rs",
37
+ "installation": "https://github.com/DioNanos/mcp-vl-msa-rs#install",
38
+ "license": "Apache-2.0",
39
+ "bundledSkill": "skills/vl-msa/SKILL.md",
40
+ "capabilities": [
41
+ "persistent document collections",
42
+ "BM25 and optional hybrid retrieval",
43
+ "full-source grounding",
44
+ "bounded multi-hop retrieval"
45
+ ],
46
+ "primaryTools": [
47
+ "msa_index",
48
+ "msa_index_batch",
49
+ "msa_search",
50
+ "msa_fetch_doc",
51
+ "msa_remember",
52
+ "msa_forget",
53
+ "msa_interleave_round"
54
+ ]
55
+ },
56
+ {
57
+ "id": "crew",
58
+ "name": "mcp-crewd-rs",
59
+ "repository": "https://github.com/DioNanos/mcp-crewd-rs",
60
+ "installation": "https://github.com/DioNanos/mcp-crewd-rs#install",
61
+ "license": "Apache-2.0",
62
+ "bundledSkill": "skills/crew/SKILL.md",
63
+ "capabilities": [
64
+ "bounded AI worker spawning",
65
+ "task status and structured results",
66
+ "controlled inter-cell messaging"
67
+ ],
68
+ "primaryTools": [
69
+ "cell_spawn",
70
+ "cell_send_task",
71
+ "cell_status",
72
+ "cell_result",
73
+ "cell_list",
74
+ "cell_send",
75
+ "cell_ask"
76
+ ]
77
+ },
78
+ {
79
+ "id": "mail",
80
+ "name": "mcp-email-rs",
81
+ "repository": "https://github.com/DioNanos/mcp-email-rs",
82
+ "installation": "https://github.com/DioNanos/mcp-email-rs#install",
83
+ "license": "Apache-2.0",
84
+ "bundledSkill": "skills/mail-assistant/SKILL.md",
85
+ "capabilities": [
86
+ "IMAP mailbox discovery",
87
+ "mail search and reading",
88
+ "attachments and folders",
89
+ "drafts and optional SMTP sending"
90
+ ],
91
+ "primaryTools": [
92
+ "list_folders",
93
+ "list_emails",
94
+ "search_emails",
95
+ "get_email",
96
+ "download_attachment",
97
+ "create_draft",
98
+ "send_email",
99
+ "email_doctor"
100
+ ]
101
+ }
102
+ ]
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.32",
3
+ "version": "0.8.34",
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": {
@@ -13,6 +13,8 @@
13
13
  "frontend/dist/",
14
14
  "frontend/index.html",
15
15
  "CHANGELOG.md",
16
+ "MCP_COMPANIONS.md",
17
+ "mcp-companions.json",
16
18
  "LICENSE",
17
19
  "README.md",
18
20
  "NOTICE"
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: crew
3
+ description: Use when spawning or coordinating bounded AI worker cells through a Crew MCP fabric, including cell discovery, idempotent spawn, status monitoring, structured results, follow-up tasks, cancellation and the inter-cell message bus. Covers safe worktree isolation, durable audit reports and user-language selection.
4
+ ---
5
+
6
+ # Crew
7
+
8
+ Use Crew for bounded delegated work. A coordinator submits tasks to worker
9
+ cells through an MCP server; spawning is asynchronous and returns a thread ID.
10
+
11
+ ## Select response language
12
+
13
+ Choose the language for user-facing explanations and summaries in this order:
14
+
15
+ 1. the user's explicit language preference;
16
+ 2. the language of the current request;
17
+ 3. a reliable client or system locale;
18
+ 4. English.
19
+
20
+ Keep cell names, engine/profile IDs, thread IDs, tool names, paths and quoted
21
+ worker output unchanged unless the user explicitly asks to translate them.
22
+ Tell a worker which output language is required when its deliverable is
23
+ user-facing.
24
+
25
+ ## Discover before spawning
26
+
27
+ 1. Use `cell_list` to inspect the cells and engines visible to the caller.
28
+ 2. Select an engine/profile appropriate to the bounded task.
29
+ 3. Verify that the worker's `cwd` contains the instructions and project context
30
+ it needs.
31
+ 4. Do not assume an embedded worker inherits the coordinator's MCP servers.
32
+ Required servers must be configured explicitly in the Crew daemon and
33
+ verified inside a fresh worker.
34
+
35
+ If no Crew tools are exposed and this skill is packaged with NexusCrew, the
36
+ optional companion is documented in `../../MCP_COMPANIONS.md`. Explain the
37
+ missing capability and ask before installing, configuring or starting it.
38
+
39
+ ## Spawn, monitor and collect
40
+
41
+ 1. Call `cell_spawn` with a bounded task, explicit `cwd`, background mode and a
42
+ caller-stable `idempotency_key`.
43
+ 2. Keep the returned `crewd_thread_id`; a replayed idempotency key must resolve
44
+ to the existing thread rather than duplicate work.
45
+ 3. Monitor with `cell_status`. Use the host's loop or scheduler for waits rather
46
+ than a tight manual poll.
47
+ 4. Treat `idle` as normal turn completion. Also handle `timeout`, `failed`,
48
+ `interrupted` and `failed_unknown`; do not wait for a fictional `finished`
49
+ state.
50
+ 5. Read `cell_result` and inspect `exit_status`, `final_answer` and the bounded
51
+ event tail.
52
+ 6. Verify the worker's files and tests yourself before accepting code changes.
53
+
54
+ A timeout may leave partial edits. Never report success from a thread state
55
+ alone.
56
+
57
+ ## Isolate code work
58
+
59
+ - Do not let two mutating workers edit the same worktree concurrently.
60
+ - Give parallel code tasks separate git worktrees.
61
+ - Preserve unrelated user changes and review every worker diff.
62
+ - Use follow-up tasks only when the original thread and scope remain valid.
63
+ - Cancel only the exact thread the user authorized.
64
+
65
+ ## Require durable audit evidence
66
+
67
+ For audits, reviews and release gates:
68
+
69
+ 1. Choose an authorized absolute report path before spawning, preferably
70
+ outside the repository being audited.
71
+ 2. Require scope, evidence, findings by severity, residual risks and a terminal
72
+ verdict in the report.
73
+ 3. Require the worker callback to identify the report path and verdict.
74
+ 4. Verify that the report is a non-symlink regular file, read it completely and
75
+ confirm its verdict matches the callback.
76
+ 5. Recalculate important hashes and gates independently.
77
+
78
+ Chat output or `cell_result.final_answer` is not a substitute for the verified
79
+ report.
80
+
81
+ ## Use the message bus deliberately
82
+
83
+ - `cell_send` is fire-and-forget.
84
+ - `cell_ask` plus `cell_await` is appropriate when a reply is required.
85
+ - `cell_send_task` starts a follow-up turn on an existing thread.
86
+ - `cell_inbox` reads queued bus messages.
87
+
88
+ Transport receipts do not prove task acceptance or completion. Report only
89
+ results that have been verified.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Crew"
3
+ short_description: "Coordinate bounded AI worker cells"
4
+ default_prompt: "Use crew to delegate this bounded task and collect a verified result."