c8ctl-plugin-nano 1.24.2 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -479,6 +479,7 @@ c8ctl nano supervisor
479
479
  c8ctl nano supervisor status # id, state, pid, restarts, uptime
480
480
  c8ctl nano supervisor add reviewer --max-parallel 2 # add + spawn a worker (forwards work flags)
481
481
  c8ctl nano supervisor add reviewer --name reviewer-2 # a SECOND reviewer, named so it stays distinct
482
+ c8ctl nano supervisor add reviewer --instances 3 # add 3 distinct auto-named reviewers in one call
482
483
  c8ctl nano supervisor restart reviewer # by worker id or profile name
483
484
  c8ctl nano supervisor remove coder # stop + drop a worker (also: `all`)
484
485
  c8ctl nano supervisor logs reviewer --follow # tail a worker's log (or the daemon's)
@@ -489,9 +490,13 @@ Each worker has a **name** — its supervisor id and the broker `workerName` it
489
490
  registers under. Pass `--name` on `supervisor add` (or `work`) to set it;
490
491
  omit it and one is auto-generated as `‹host›-‹profile›-‹random›`, so you can
491
492
  run **several instances of the same profile** and they stay distinct
492
- end-to-end (status, logs, and at the broker). `restart`/`remove` accept either
493
- a worker id **or** a profile name targeting a profile affects *every*
494
- instance of it.
493
+ end-to-end (status, logs, and at the broker). To scale a hire to several
494
+ instances in one call, pass `--instances N` on `supervisor add` it spawns N
495
+ distinct auto-named workers of the profile at once (default 1, capped per call).
496
+ Because each instance needs its own distinct name, `--instances N` (for N > 1)
497
+ cannot be combined with `--name`; omit `--name` to let them auto-name.
498
+ `restart`/`remove` accept either a worker id **or** a profile name — targeting a
499
+ profile affects *every* instance of it.
495
500
 
496
501
  Each worker takes the **same flags as `nano work`** (`--max-parallel`,
497
502
  `--recovery-window`, `--idle-timeout`, `--job-timeout`, `--poll-timeout`,
package/c8ctl-plugin.js CHANGED
@@ -1730,13 +1730,14 @@ function resolveAssignInputs(req, flags) {
1730
1730
  * capabilities are the remaining positionals and/or `--capabilities a,b`.
1731
1731
  * Capabilities are unioned with the profile's existing set (additive; assign
1732
1732
  * never removes a role) and the updated rank×capability job-type matrix is
1733
- * printed. Re-run `work` to pick up the new job types.
1733
+ * printed. Running workers hot-reload the new job types within ~1.5s — no
1734
+ * restart needed.
1734
1735
  */
1735
1736
  async function assignCapabilities(req, flags) {
1736
1737
  const logger = getLogger();
1737
1738
  const { name, incomingRaw } = resolveAssignInputs(req, flags);
1738
1739
  if (!name) {
1739
- logger.error('Usage: c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
1740
+ logger.error('Usage: c8ctl nano assign <profileName> <cap[,cap...]> [--name <n>] [--capabilities <a,b>]');
1740
1741
  logger.info('Grant new capabilities to an existing hire. List profiles with: c8ctl nano hire --list');
1741
1742
  process.exit(1);
1742
1743
  }
@@ -1747,7 +1748,7 @@ async function assignCapabilities(req, flags) {
1747
1748
 
1748
1749
  if (normalizeCapabilities(incomingRaw).length === 0) {
1749
1750
  logger.error('Provide at least one capability to assign.');
1750
- logger.info(`Example: c8ctl nano assign ${name} code-review testing`);
1751
+ logger.info(`Example: c8ctl nano assign ${name} code-review,testing`);
1751
1752
  process.exit(1);
1752
1753
  }
1753
1754
 
@@ -1778,7 +1779,7 @@ async function assignCapabilities(req, flags) {
1778
1779
  logger.info(`Assigned to "${name}" [${profile.rank}]: +${added.join(', ')}`);
1779
1780
  logger.info(` capabilities: ${profile.capabilities.join(', ')}`);
1780
1781
  logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
1781
- logger.info(`Restart its workers to pick up the new roles: c8ctl nano work ${name}`);
1782
+ logger.info(`Running workers pick this up automatically within ~1.5s no restart needed.`);
1782
1783
  }
1783
1784
 
1784
1785
  /**
@@ -3701,6 +3702,58 @@ function extractNameFlag(parts) {
3701
3702
  return { name: name != null && name.trim() !== '' ? name.trim() : undefined, rest };
3702
3703
  }
3703
3704
 
3705
+ /**
3706
+ * Upper bound on how many workers a single `supervisor add --instances N` may
3707
+ * spawn. Not a fleet cap (add again to grow further) — a typo guard, so a
3708
+ * fat-fingered `--instances 100000` can't fork-bomb the host in one keystroke.
3709
+ */
3710
+ const MAX_ADD_INSTANCES = 64;
3711
+
3712
+ /**
3713
+ * Parse the `--instances N` count for `supervisor add`. Accepts undefined/blank
3714
+ * (defaults to 1), and a whole number in `[1, MAX_ADD_INSTANCES]`. Rejects
3715
+ * non-integers, zero/negatives, and anything over the cap with a clear message.
3716
+ * When a flag is repeated the last occurrence wins (arrays are tolerated).
3717
+ * Returns `{ count }` on success or `{ error }` on rejection. Pure.
3718
+ */
3719
+ function parseInstancesCount(raw) {
3720
+ const v = Array.isArray(raw) ? raw[raw.length - 1] : raw;
3721
+ if (v === undefined || v === null || (typeof v === 'string' && v.trim() === '')) {
3722
+ return { count: 1 };
3723
+ }
3724
+ const s = String(v).trim();
3725
+ if (!/^\d+$/.test(s)) return { error: `Invalid --instances "${v}": use a whole number between 1 and ${MAX_ADD_INSTANCES}.` };
3726
+ const n = Number.parseInt(s, 10);
3727
+ if (n < 1) return { error: `Invalid --instances "${v}": use a whole number between 1 and ${MAX_ADD_INSTANCES}.` };
3728
+ if (n > MAX_ADD_INSTANCES) return { error: `--instances ${n} exceeds the ${MAX_ADD_INSTANCES}-per-command cap; run "supervisor add" again to add more.` };
3729
+ return { count: n };
3730
+ }
3731
+
3732
+ /**
3733
+ * Split `--instances N` / `--instances=N` out of a raw token list, returning
3734
+ * `{ count, rest, error }` where `rest` is the remaining work flags (with the
3735
+ * flag removed so it is never forwarded to `nano work`). Used by the interactive
3736
+ * console's `add`, whose tokens aren't parsed by the CLI flag layer. Last
3737
+ * occurrence wins; a trailing `--instances` with no value is treated as absent
3738
+ * (count 1), mirroring `extractNameFlag`. Pure.
3739
+ */
3740
+ function extractInstancesFlag(parts) {
3741
+ const rest = [];
3742
+ let raw;
3743
+ const list = Array.isArray(parts) ? parts : [];
3744
+ for (let i = 0; i < list.length; i++) {
3745
+ const tok = String(list[i]);
3746
+ const eq = /^--instances=(.*)$/.exec(tok);
3747
+ if (eq) { raw = eq[1]; continue; }
3748
+ if (tok === '--instances') {
3749
+ if (i + 1 < list.length) { raw = String(list[i + 1]); i++; }
3750
+ continue;
3751
+ }
3752
+ rest.push(tok);
3753
+ }
3754
+ return { ...parseInstancesCount(raw), rest };
3755
+ }
3756
+
3704
3757
  /** Assign a unique, stable worker id from a profile name (pure). */
3705
3758
  function supervisorWorkerId(profile, taken) {
3706
3759
  const base = String(profile || '').trim() || 'worker';
@@ -4479,14 +4532,31 @@ async function supervisorAddCmd(req, flags) {
4479
4532
  const logger = getLogger();
4480
4533
  // The positional profile is what runs; `--name` names this worker instance
4481
4534
  // (forwarded to the child as `nano work … --name`, and used as its supervisor
4482
- // id). Omit `--name` to auto-generate ‹host›-‹profile›-‹random›.
4535
+ // id). Omit `--name` to auto-generate ‹host›-‹profile›-‹random›. `--instances N`
4536
+ // spawns N distinct auto-named workers of the profile in one call.
4483
4537
  const profile = req.positional[1];
4484
- if (!profile) { logger.error('Usage: c8ctl nano supervisor add <profile> [--name <worker>] [work flags]'); process.exit(1); }
4538
+ if (!profile) { logger.error('Usage: c8ctl nano supervisor add <profile> [--name <worker>] [--instances <n>] [work flags]'); process.exit(1); }
4539
+ const { count, error } = parseInstancesCount(flags?.instances);
4540
+ if (error) { logger.error(error); process.exit(1); }
4485
4541
  const name = flags?.name ? String(flags.name).trim() : undefined;
4542
+ // A single `--name` can't apply to several distinct workers (each needs its
4543
+ // own broker workerName / supervisor id), so reject the combination and steer
4544
+ // the operator to auto-naming.
4545
+ if (name && count > 1) {
4546
+ logger.error('--name cannot be combined with --instances > 1 (each instance needs a distinct name); omit --name to auto-name them.');
4547
+ process.exit(1);
4548
+ }
4486
4549
  await startSupervisorDaemon();
4487
- const res = await supervisorRequest({ op: 'add', profile, name, args: reconstructWorkArgs(flags) });
4488
- if (res.ok) logger.info(`Added worker "${res.worker.id}" (profile ${profile}); pid ${res.worker.pid ?? 'starting'}.`);
4489
- else { logger.error(`Could not add "${profile}": ${res.error}`); process.exit(1); }
4550
+ const workArgs = reconstructWorkArgs(flags);
4551
+ let added = 0;
4552
+ let failed = 0;
4553
+ for (let i = 0; i < count; i++) {
4554
+ const res = await supervisorRequest({ op: 'add', profile, name, args: workArgs });
4555
+ if (res.ok) { added++; logger.info(`Added worker "${res.worker.id}" (profile ${profile}); pid ${res.worker.pid ?? 'starting'}.`); }
4556
+ else { failed++; logger.error(`Could not add "${profile}": ${res.error}`); }
4557
+ }
4558
+ if (count > 1) logger.info(`Added ${added}/${count} instance(s) of "${profile}".`);
4559
+ if (failed > 0) process.exit(1);
4490
4560
  }
4491
4561
 
4492
4562
  async function supervisorRemoveCmd(req) {
@@ -4636,16 +4706,21 @@ async function attachSupervisorConsole(state) {
4636
4706
  switch (cmd) {
4637
4707
  case '': break;
4638
4708
  case 'help':
4639
- out('Commands: status | add <profile> [--name <worker>] [work flags] |');
4709
+ out('Commands: status | add <profile> [--name <worker>] [--instances <n>] [work flags] |');
4640
4710
  out(' remove <id|profile|all> | restart <id|profile|all> |');
4641
4711
  out(' logs [id] | detach | stop | help');
4642
4712
  break;
4643
4713
  case 'status': sock.write(encodeFrame({ op: 'status' })); break;
4644
4714
  case 'add': {
4645
4715
  const profile = parts.shift();
4646
- if (!profile) { out('usage: add <profile> [--name <worker>] [work flags]'); break; }
4716
+ if (!profile) { out('usage: add <profile> [--name <worker>] [--instances <n>] [work flags]'); break; }
4647
4717
  const { name, rest } = extractNameFlag(parts);
4648
- sock.write(encodeFrame({ op: 'add', profile, name, args: rest }));
4718
+ const { count, rest: workArgs, error } = extractInstancesFlag(rest);
4719
+ if (error) { out(error); break; }
4720
+ if (name && count > 1) { out('--name cannot be combined with --instances > 1; omit --name to auto-name them.'); break; }
4721
+ for (let i = 0; i < count; i++) {
4722
+ sock.write(encodeFrame({ op: 'add', profile, name, args: workArgs }));
4723
+ }
4649
4724
  break;
4650
4725
  }
4651
4726
  case 'remove': case 'rm': {
@@ -6107,6 +6182,8 @@ export {
6107
6182
  isValidWorkerName,
6108
6183
  randomNameSuffix,
6109
6184
  extractNameFlag,
6185
+ parseInstancesCount,
6186
+ extractInstancesFlag,
6110
6187
  redactWorkArgs,
6111
6188
  supervisorBackoffMs,
6112
6189
  encodeFrame,
@@ -6120,6 +6197,7 @@ export {
6120
6197
  runSupervisorDaemon,
6121
6198
  startSupervisorDaemon,
6122
6199
  supervisorRequest,
6200
+ supervisorAddCmd,
6123
6201
  runningSupervisor,
6124
6202
  readSupervisorState,
6125
6203
  clearSupervisorState,
@@ -6163,12 +6241,14 @@ export const metadata = {
6163
6241
  { command: 'c8ctl nano hire --name coder --rank senior --command copilot --env COPILOT_ENABLE_ALL_TOOLS=1', description: 'Persist a harness startup env var (e.g. permissions) on the profile' },
6164
6242
  { command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
6165
6243
  { command: 'c8ctl nano hire --name coder --rank senior --command "agent-harness" --sandbox docker --image ghcr.io/acme/agent:1', description: 'Create a profile that runs each job in a throwaway Docker container' },
6244
+ { command: 'c8ctl nano assign reviewer code-review,testing', description: 'Grant more capabilities (comma-separated, like hire) to an existing hire — additive; running workers hot-reload it' },
6166
6245
  { command: 'c8ctl nano work reviewer', description: 'Spawn Nano job workers for the "reviewer" profile and poll for work' },
6167
6246
  { command: 'c8ctl nano work coder --sandbox docker --image ghcr.io/acme/agent:1', description: 'Run jobs in isolated containers with disk-hygiene reaping' },
6168
6247
  { command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
6169
6248
  { command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
6170
6249
  { command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, serviced job / idle, restarts, uptime) without the console' },
6171
6250
  { command: 'c8ctl nano supervisor add decider --max-parallel 2', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
6251
+ { command: 'c8ctl nano supervisor add reviewer --instances 3', description: 'Add 3 distinct auto-named instances of a profile in one call' },
6172
6252
  { command: 'c8ctl nano supervisor restart reviewer', description: 'Restart a supervised worker by id or profile' },
6173
6253
  { command: 'c8ctl nano supervisor stop', description: 'Stop the supervisor daemon and all its workers' },
6174
6254
  ],
@@ -6236,6 +6316,7 @@ export const commands = {
6236
6316
  'poll-timeout': { type: 'string', description: 'work: broker long-poll window in ms each activateJobs request is held open (fewer reconnects → fewer transient connect errors); default 30000, 0 = broker default, negative = return immediately' },
6237
6317
  'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
6238
6318
  worker: { type: 'string', multiple: true, description: 'supervisor start: profile to launch as a supervised worker (repeatable)' },
6319
+ instances: { type: 'string', description: `supervisor add: spawn N distinct auto-named instances of the profile in one call (default 1, max ${MAX_ADD_INSTANCES}; cannot combine with --name)` },
6239
6320
  attach: { type: 'boolean', description: 'supervisor start: attach the interactive console after starting the daemon' },
6240
6321
  },
6241
6322
  handler: async (args, flags) => {
@@ -6386,7 +6467,7 @@ function printUsage() {
6386
6467
  console.log(' c8ctl nano config');
6387
6468
  console.log(' c8ctl nano update [--check]');
6388
6469
  console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--arg <switch> ...] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
6389
- console.log(' c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
6470
+ console.log(' c8ctl nano assign <profileName> <cap[,cap...]> [--name <n>] [--capabilities <a,b>]');
6390
6471
  console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--recovery-window <ms>] [--idle-timeout <ms>] [--job-timeout <ms>] [--poll-timeout <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
6391
6472
  console.log(' c8ctl nano supervisor [start|status|add|remove|restart|stop|logs|attach] ... (manage many workers from one terminal)');
6392
6473
  console.log('');
@@ -6403,7 +6484,7 @@ function printUsage() {
6403
6484
  console.log(' config Show current configuration and on-disk locations');
6404
6485
  console.log(' update Pull the latest published nano release (--check to only report)');
6405
6486
  console.log(' hire Create a CLI agent worker profile (rank + capabilities → job-type matrix)');
6406
- console.log(' assign Grant new capabilities (roles) to an existing hire (additive)');
6487
+ console.log(' assign Grant new capabilities (roles) to an existing hire (additive; comma-separated; workers hot-reload)');
6407
6488
  console.log(' work Run a hired profile as Nano job workers, polling for work until Ctrl-C');
6408
6489
  console.log(' supervisor Run/manage a fleet of workers from one terminal (detachable console + non-interactive control)');
6409
6490
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.24.2",
3
+ "version": "1.25.1",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -47,12 +47,12 @@
47
47
  "semantic-release": "^25.0.3"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.24.2",
51
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.24.2",
52
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.24.2",
53
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.24.2",
54
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.24.2",
55
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.24.2",
56
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.24.2"
50
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.25.1",
51
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.25.1",
52
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.25.1",
53
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.25.1",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.25.1",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.25.1",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.25.1"
57
57
  }
58
58
  }