c8ctl-plugin-nano 1.17.0 → 1.18.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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/c8ctl-plugin.js +139 -13
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -150,6 +150,9 @@ c8ctl nano hire
150
150
  c8ctl nano hire --name reviewer --rank senior --command copilot \
151
151
  --model gpt-5 --capabilities code-review,testing
152
152
 
153
+ # Give the harness command-line switches (e.g. run copilot with --allow-all)
154
+ c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all
155
+
153
156
  # List profiles
154
157
  c8ctl nano hire --list
155
158
  ```
@@ -204,6 +207,16 @@ the job with a decremented retry count, and a job that outlives `--job-timeout`
204
207
  is killed. Profiles are stored in the plugin's `config.json` (see `c8ctl nano
205
208
  config`).
206
209
 
210
+ > **Activation lock vs. kill deadline.** `--job-timeout` is the harness *kill*
211
+ > deadline. The broker's job-activation lock is derived as `--job-timeout +
212
+ > --lock-grace` (grace defaults to `120000`ms) so it strictly outlasts the kill:
213
+ > the worker always reports the outcome (complete/fail) before the lock lapses.
214
+ > Without that gap a lock expiring exactly as the harness dies lets the broker
215
+ > re-activate the still-retryable job (a second agent starts) and the stale
216
+ > `fail` is rejected with a 409 "job cannot be failed in the current state".
217
+ > Raising `--job-timeout` alone does **not** fix this — it moves both coupled
218
+ > deadlines together; widen `--lock-grace` (or keep the default) instead.
219
+
207
220
  > **Trust boundary.** The profile `command` is run through a shell so you can
208
221
  > write a full invocation (args, pipes, multi-word commands). It is
209
222
  > **operator-authored** — only what you put in your own `config.json` is
@@ -327,6 +340,24 @@ envelope layers on top (job-specific tuning wins), and the reserved `AGENT_*`
327
340
  variables and resolved secrets always win over user-supplied env so they can't be
328
341
  shadowed. For **secret** values use `secretRefs`, not `--env`.
329
342
 
343
+ **Command-line switches.** Some harnesses take switches rather than env vars —
344
+ e.g. `copilot --allow-all`. Append them to the harness command with a repeatable
345
+ `--arg` (each `--arg` is one argv token). They are persisted on the profile at
346
+ hire time and can be extended at work time:
347
+
348
+ ```bash
349
+ c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all
350
+ c8ctl nano work coder --arg --verbose # appends to the profile args
351
+ ```
352
+
353
+ The command is spawned through a shell (so `command` still resolves on `PATH`),
354
+ but each `--arg` is shell-quoted as a single literal token, so a value with
355
+ spaces or shell metacharacters can't break out or inject. They apply on the
356
+ container path on any OS, and on the host path on POSIX systems. **On a Windows
357
+ host (`sandbox=none`), `--arg` is rejected** with a clear error — the POSIX
358
+ quoting isn't honoured by `cmd.exe` — so use a container sandbox
359
+ (`--sandbox docker|podman`) or bake the switches into `--command` there.
360
+
330
361
  **Disk hygiene.** Host job **workspaces** and container sandboxes both get
331
362
  automatic cleanup so leaked artifacts can't fill the disk. Workspaces under
332
363
  `<state>/agent-runs` are removed after each job and swept at startup + on
package/c8ctl-plugin.js CHANGED
@@ -1416,6 +1416,39 @@ function parseEnvPairs(input) {
1416
1416
  return { env, errors };
1417
1417
  }
1418
1418
 
1419
+ // Normalize a stored/CLI argument list (string | string[]) into a clean string[]:
1420
+ // each entry is one whole argv token (e.g. "--allow-all"), coerced to a string,
1421
+ // with null/undefined and empty tokens dropped. Interior whitespace is preserved
1422
+ // so a single arg may carry a value like "--foo=a b" intact.
1423
+ function normalizeArgList(input) {
1424
+ const list = input == null ? [] : (Array.isArray(input) ? input : [input]);
1425
+ const out = [];
1426
+ for (const item of list) {
1427
+ if (item == null) continue;
1428
+ const s = String(item);
1429
+ if (s.length === 0) continue;
1430
+ out.push(s);
1431
+ }
1432
+ return out;
1433
+ }
1434
+
1435
+ // POSIX single-quote a string so it survives `sh -c`/shell:true as one literal
1436
+ // argv token, no matter what it contains (spaces, $, quotes, globs). Empty
1437
+ // string → ''. This is what keeps structured `--arg` values injection-safe even
1438
+ // though the harness is spawned through a shell (for PATH resolution).
1439
+ function shQuote(s) {
1440
+ return `'${String(s).replace(/'/g, `'\\''`)}'`;
1441
+ }
1442
+
1443
+ // Build the shell command line for the agent harness: the base command followed
1444
+ // by each structured argument, shell-quoted. With no args the command is used
1445
+ // verbatim (preserving pre-existing hires that baked switches into the command).
1446
+ function buildAgentCommandLine(command, args) {
1447
+ const list = normalizeArgList(args);
1448
+ if (list.length === 0) return command;
1449
+ return `${command} ${list.map(shQuote).join(' ')}`;
1450
+ }
1451
+
1419
1452
  // A worker job-type token: rank/capability tokens use `:` (rank↔cap) and `+`
1420
1453
  // (combined caps) as delimiters, and code-first `@nanobpm/workflow` job types
1421
1454
  // are `<flowId>:<taskName>` or an explicit override. The first character must be
@@ -1445,6 +1478,46 @@ function parseJobTypeFlags(input) {
1445
1478
  return { jobTypes, errors };
1446
1479
  }
1447
1480
 
1481
+ /**
1482
+ * The broker job-activation lock must strictly outlast the harness kill
1483
+ * deadline: the worker has to report the outcome (complete/fail) before the lock
1484
+ * lapses, or the broker re-activates the still-retryable job (a second agent
1485
+ * starts) and the stale `fail` is rejected 409 "job cannot be failed in the
1486
+ * current state". So lock = kill + grace. Non-finite or non-positive inputs
1487
+ * fall back to fixed defaults (5m kill / 2m grace). All inputs are coerced to
1488
+ * safe positive integers (positive fractional values floor to at least 1) and
1489
+ * grace is capped so a positive kill always fits and `kill + grace` stays within
1490
+ * the safe-integer range, so the invariant lock > kill holds strictly for every
1491
+ * accepted input — including values at or beyond 2^53 where float addition
1492
+ * would otherwise round `kill + grace` back down to `kill`.
1493
+ *
1494
+ * Returns BOTH derived deadlines from this single computation so the caller
1495
+ * never re-derives (and drifts): `killMs` is the *clamped* harness kill deadline
1496
+ * the caller must actually enforce, and `lockMs` is the broker activation lock.
1497
+ * The caller must use `killMs` — not the raw input — for the harness timeout, or
1498
+ * the lock > kill invariant breaks for large inputs (the raw input can exceed
1499
+ * the clamped `killMs`, and thus reach or exceed `lockMs`).
1500
+ *
1501
+ * @returns {{ killMs: number, lockMs: number }}
1502
+ */
1503
+ function deriveJobLockMs(jobTimeoutMs, lockGraceMs) {
1504
+ const MAX = Number.MAX_SAFE_INTEGER;
1505
+ const toSafeMs = (value, fallback) => {
1506
+ // Floor positive fractional values to at least 1 so a sub-millisecond input
1507
+ // (e.g. 0.5) never collapses to a non-positive value.
1508
+ const n = Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
1509
+ return Math.min(n, MAX);
1510
+ };
1511
+ // Cap grace to MAX - 1 so there is always room for a positive kill while
1512
+ // keeping kill + grace within the safe-integer range.
1513
+ const grace = Math.min(toSafeMs(lockGraceMs, 2 * 60_000), MAX - 1);
1514
+ // Cap kill so kill + grace stays a safe integer and floor it at 1 so the
1515
+ // internal kill is always positive; the sum is then exact and strictly
1516
+ // greater than kill (never equal to it via float rounding).
1517
+ const killMs = Math.max(1, Math.min(toSafeMs(jobTimeoutMs, 5 * 60_000), MAX - grace));
1518
+ return { killMs, lockMs: killMs + grace };
1519
+ }
1520
+
1448
1521
  /** A profile name must be a safe, filesystem/token-friendly slug. */
1449
1522
  function isValidProfileName(name) {
1450
1523
  return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
@@ -1513,6 +1586,7 @@ function normalizeStoredProfile(name, profile) {
1513
1586
  name,
1514
1587
  rank,
1515
1588
  command,
1589
+ args: normalizeArgList(profile.args),
1516
1590
  model: typeof profile.model === 'string' ? profile.model.trim() : '',
1517
1591
  capabilities: normalizeCapabilities(profile.capabilities),
1518
1592
  sandbox,
@@ -1541,7 +1615,7 @@ async function hireWorker(req, flags) {
1541
1615
  logger.info('Hired agent profiles:');
1542
1616
  for (const name of names.sort()) {
1543
1617
  const p = hires[name];
1544
- logger.info(` ${name} [${p.rank}] ${p.command} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'})`);
1618
+ logger.info(` ${name} [${p.rank}] ${buildAgentCommandLine(p.command, p.args)} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'})`);
1545
1619
  }
1546
1620
  logger.info('');
1547
1621
  logger.info('Put one to work with: c8ctl nano work <name>');
@@ -1557,11 +1631,14 @@ async function hireWorker(req, flags) {
1557
1631
  let capabilities = flags?.capabilities !== undefined ? flags.capabilities : undefined;
1558
1632
  let sandbox = flags?.sandbox !== undefined ? String(flags.sandbox).trim().toLowerCase() : undefined;
1559
1633
  let image = flags?.image !== undefined ? String(flags.image).trim() : undefined;
1634
+ // Structured command-line switches appended to the command when spawned, e.g.
1635
+ // `--arg --allow-all` for `copilot`. Repeatable; each --arg is one argv token.
1636
+ const commandArgs = normalizeArgList(flags?.arg);
1560
1637
  const envFromFlags = flags?.env !== undefined;
1561
1638
  const { env: profileEnv, errors: envErrors } = parseEnvPairs(flags?.env);
1562
1639
  if (envErrors.length > 0) {
1563
1640
  logger.error(envErrors.join('; '));
1564
- logger.info('Example: c8ctl nano hire --name coder --rank senior --command copilot --env COPILOT_ENABLE_ALL_TOOLS=1 --env FOO=bar');
1641
+ logger.info('Example: c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all --env COPILOT_ENABLE_ALL_TOOLS=1');
1565
1642
  process.exit(1);
1566
1643
  }
1567
1644
 
@@ -1659,6 +1736,7 @@ async function hireWorker(req, flags) {
1659
1736
  name,
1660
1737
  rank,
1661
1738
  command,
1739
+ args: commandArgs,
1662
1740
  model: model || '',
1663
1741
  capabilities: normalizeCapabilities(capabilities),
1664
1742
  sandbox,
@@ -1669,9 +1747,10 @@ async function hireWorker(req, flags) {
1669
1747
  writeHire(profile);
1670
1748
 
1671
1749
  const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
1672
- logger.info(`${existed ? 'Updated' : 'Hired'} "${name}" [${profile.rank}] → ${profile.command}`);
1750
+ logger.info(`${existed ? 'Updated' : 'Hired'} "${name}" [${profile.rank}] → ${buildAgentCommandLine(profile.command, profile.args)}`);
1673
1751
  logger.info(` model: ${profile.model || '(none)'}`);
1674
1752
  logger.info(` capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
1753
+ if (profile.args.length > 0) logger.info(` args: ${profile.args.map(shQuote).join(' ')}`);
1675
1754
  logger.info(` sandbox: ${profile.sandbox}${CONTAINER_SANDBOXES.has(profile.sandbox) ? ` (image ${profile.image})` : ''}`);
1676
1755
  const envKeys = Object.keys(profile.env);
1677
1756
  if (envKeys.length > 0) logger.info(` env: ${envKeys.join(', ')}`);
@@ -2450,9 +2529,13 @@ function baseAgentEnv(profile, job) {
2450
2529
  * Both paths resolve to the same result contract.
2451
2530
  */
2452
2531
  function runAgentJob(profile, job, opts = {}) {
2453
- const { timeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr } = opts;
2532
+ const { timeoutMs, envelope, sandbox = 'none', image, runId, secretEnv = {}, passThroughSecretNames = [], cwd, extraEnv = {}, profileEnv = {}, resultFile, stream = false, streamPrefix = '', onStreamOut, onStreamErr, args: commandArgs } = opts;
2454
2533
  const payload = JSON.stringify(buildAgentPayload(profile, job, envelope));
2455
2534
  const agentEnv = baseAgentEnv(profile, job);
2535
+ // The harness command line: the profile command plus its structured switches
2536
+ // (persisted `--arg`s, possibly extended at work time via opts.args), each
2537
+ // shell-quoted. Spawned through a shell so `command` still resolves on PATH.
2538
+ const commandLine = buildAgentCommandLine(profile.command, commandArgs ?? profile.args);
2456
2539
  // Static, non-secret env for the harness: the worker/profile's env (e.g. a
2457
2540
  // harness's permission toggles) plus the per-job envelope's setup.env
2458
2541
  // (job-specific tuning wins over the profile default). Reserved AGENT_* and
@@ -2461,9 +2544,16 @@ function runAgentJob(profile, job, opts = {}) {
2461
2544
 
2462
2545
  if (!CONTAINER_SANDBOXES.has(sandbox)) {
2463
2546
  // Host: hand the agent the result file by its real path.
2547
+ // Defense in depth: --arg tokens are POSIX single-quoted, which cmd.exe on
2548
+ // a Windows host does not honour, so args would be mis-parsed under the
2549
+ // shell:true spawn. workAgent already rejects this at startup, but guard the
2550
+ // spawn site too so the invariant holds for any direct caller of runAgentJob.
2551
+ if (commandLine !== profile.command && process.platform === 'win32') {
2552
+ return Promise.resolve({ ok: false, exitCode: null, stdout: '', stderr: '', error: 'command-line args (--arg) are not supported for host execution on Windows; use a container sandbox or bake switches into the command', truncated: false, stderrTruncated: false });
2553
+ }
2464
2554
  const resultEnv = resultFile ? { [AGENT_RESULT_FILE_ENV]: resultFile } : {};
2465
2555
  return spawnCaptureOneShot({
2466
- command: profile.command,
2556
+ command: commandLine,
2467
2557
  shell: true,
2468
2558
  // Own process group so the timeout handler can kill the whole tree.
2469
2559
  detached: process.platform !== 'win32',
@@ -2516,7 +2606,7 @@ function runAgentJob(profile, job, opts = {}) {
2516
2606
  ...mountArgs,
2517
2607
  ...envArgs,
2518
2608
  image,
2519
- 'sh', '-c', profile.command,
2609
+ 'sh', '-c', commandLine,
2520
2610
  ];
2521
2611
 
2522
2612
  return spawnCaptureOneShot({
@@ -2618,12 +2708,28 @@ async function workAgent(req, flags) {
2618
2708
  }
2619
2709
  const profileEnv = { ...profile.env, ...workEnv };
2620
2710
 
2711
+ // Structured command-line switches: the profile's persisted `--arg`s, extended
2712
+ // by any work-time `--arg` (appended). Lets an operator add switches (e.g.
2713
+ // `--allow-all`) at dispatch time without re-hiring.
2714
+ const effectiveArgs = [...profile.args, ...normalizeArgList(flags?.arg)];
2715
+
2621
2716
  const intFlag = (v, dflt) => {
2622
2717
  const n = Number.parseInt(String(v ?? ''), 10);
2623
2718
  return Number.isFinite(n) && n > 0 ? n : dflt;
2624
2719
  };
2625
2720
  const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
2626
2721
  const jobTimeoutMs = intFlag(flags?.['job-timeout'], 5 * 60_000);
2722
+ // The broker's job-activation lock MUST outlast the harness kill deadline: the
2723
+ // worker has to report the outcome (complete/fail) before the lock lapses,
2724
+ // otherwise the broker re-activates the still-retryable job (a second agent
2725
+ // starts) and the stale `fail` is rejected with a 409 "job cannot be failed in
2726
+ // the current state". So lock = harness kill + grace. Raising --job-timeout
2727
+ // alone does not help — it moves both coupled deadlines together.
2728
+ const lockGraceMs = intFlag(flags?.['lock-grace'], 2 * 60_000);
2729
+ // deriveJobLockMs is the single source of truth for both deadlines: the harness
2730
+ // MUST enforce the clamped `jobKillMs` (not the raw --job-timeout), or a very
2731
+ // large --job-timeout would outlive the broker lock and re-break the invariant.
2732
+ const { killMs: jobKillMs, lockMs: jobLockMs } = deriveJobLockMs(jobTimeoutMs, lockGraceMs);
2627
2733
 
2628
2734
  // Sandbox: flag overrides the stored profile default. `none` runs on the host
2629
2735
  // (legacy); `docker`/`podman` run each job in a throwaway labelled container.
@@ -2638,6 +2744,17 @@ async function workAgent(req, flags) {
2638
2744
  logger.error(`--sandbox ${sandbox} requires an --image (or hire the profile with --image).`);
2639
2745
  process.exit(1);
2640
2746
  }
2747
+ // Structured --arg tokens are POSIX single-quoted (shQuote) for the harness
2748
+ // shell. On the host path that shell is the platform default — cmd.exe on
2749
+ // Windows, which does not honour single quotes — so the quoting would leak
2750
+ // literal quote characters and mis-parse the switches. The container path
2751
+ // always targets the image's `sh`, so it stays correct regardless of host OS.
2752
+ // Fail fast with actionable guidance rather than silently corrupting argv.
2753
+ if (!isContainer && effectiveArgs.length > 0 && process.platform === 'win32') {
2754
+ logger.error('--arg is not supported for host execution on Windows (cmd.exe does not honour POSIX quoting).');
2755
+ logger.error('Use a container sandbox (--sandbox docker|podman --image <ref>) or bake the switches into --command.');
2756
+ process.exit(1);
2757
+ }
2641
2758
 
2642
2759
  const secretResolver = makeSecretResolver(flags?.['secret-resolver']);
2643
2760
  if (!secretResolver) {
@@ -2713,14 +2830,14 @@ async function workAgent(req, flags) {
2713
2830
  const jobTypes = [...new Set([...matrix, ...extraJobTypes])];
2714
2831
  const camunda = globalThis.c8ctl.createClient();
2715
2832
 
2716
- logger.info(`Putting "${name}" [${profile.rank}] to work → ${profile.command}`);
2833
+ logger.info(`Putting "${name}" [${profile.rank}] to work → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
2717
2834
  logger.info(` model: ${profile.model || '(none)'}; capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
2718
2835
  logger.info(` sandbox: ${sandbox}${isContainer ? ` (image ${image})` : ''}`);
2719
2836
  const profileEnvKeys = Object.keys(profileEnv);
2720
2837
  if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
2721
2838
  const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
2722
2839
  logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
2723
- logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobTimeoutMs}ms`);
2840
+ logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms`);
2724
2841
  logger.info('Polling for work — press Ctrl-C to stop.');
2725
2842
 
2726
2843
  const workers = jobTypes.map((jobType) =>
@@ -2728,9 +2845,9 @@ async function workAgent(req, flags) {
2728
2845
  jobType,
2729
2846
  workerName: `${name}:${jobType}`,
2730
2847
  maxParallelJobs,
2731
- jobTimeoutMs,
2848
+ jobTimeoutMs: jobLockMs,
2732
2849
  jobHandler: async (job) => {
2733
- logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${profile.command}`);
2850
+ logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
2734
2851
 
2735
2852
  // Disk-budget admission shed: if the engine data root is below the free
2736
2853
  // floor, don't start a container — fail (retryable) so work sheds until
@@ -2811,7 +2928,7 @@ async function workAgent(req, flags) {
2811
2928
  } catch { resultDir = null; resultFile = null; }
2812
2929
 
2813
2930
  result = await runAgentJob(profile, job, {
2814
- timeoutMs: jobTimeoutMs,
2931
+ timeoutMs: jobKillMs,
2815
2932
  envelope,
2816
2933
  sandbox,
2817
2934
  image,
@@ -2824,6 +2941,7 @@ async function workAgent(req, flags) {
2824
2941
  resultFile,
2825
2942
  stream,
2826
2943
  streamPrefix: `[${jobType} ${job.jobKey}] `,
2944
+ args: effectiveArgs,
2827
2945
  // Route the --stream tee through c8ctl's output-mode-aware logger so
2828
2946
  // spying never corrupts a structured/JSON output mode.
2829
2947
  onStreamOut: stream ? (line) => logger.info(line) : undefined,
@@ -4273,6 +4391,9 @@ export {
4273
4391
  sanitizeResultVars,
4274
4392
  parseEnvPairs,
4275
4393
  normalizeEnvMap,
4394
+ normalizeArgList,
4395
+ shQuote,
4396
+ buildAgentCommandLine,
4276
4397
  reapAgentContainers,
4277
4398
  diskBudgetOk,
4278
4399
  containerEngineAvailable,
@@ -4288,6 +4409,7 @@ export {
4288
4409
  normalizeStoredProfile,
4289
4410
  jobTypeMatrix,
4290
4411
  parseJobTypeFlags,
4412
+ deriveJobLockMs,
4291
4413
  AGENT_TASK_NS,
4292
4414
  AGENT_RESULT_KEY,
4293
4415
  RESULT_SENTINEL,
@@ -4327,6 +4449,7 @@ export const metadata = {
4327
4449
  { command: 'c8ctl nano update --check', description: 'Check whether a newer nano release is available' },
4328
4450
  { command: 'c8ctl nano hire', description: 'Interactively create a CLI agent worker profile (name, rank, command, model, capabilities)' },
4329
4451
  { command: 'c8ctl nano hire --name reviewer --rank senior --command copilot --model gpt-5 --capabilities code-review,testing', description: 'Create a profile non-interactively' },
4452
+ { command: 'c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all', description: 'Hire copilot with a command-line switch (copilot --allow-all)' },
4330
4453
  { 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' },
4331
4454
  { command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
4332
4455
  { 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' },
@@ -4375,6 +4498,7 @@ export const commands = {
4375
4498
  name: { type: 'string', description: 'hire/work: agent profile name (alt to positional arg)' },
4376
4499
  rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
4377
4500
  command: { type: 'string', description: 'hire: CLI command that runs the agent harness (e.g. copilot, claude, pi)' },
4501
+ arg: { type: 'string', multiple: true, description: 'hire/work: command-line switch/arg appended to the harness command (repeatable), e.g. --arg --allow-all. Persisted on hire; work appends more.' },
4378
4502
  model: { type: 'string', description: 'hire: model name passed to the harness (AGENT_MODEL)' },
4379
4503
  capabilities: { type: 'string', description: 'hire: comma-separated capability list' },
4380
4504
  sandbox: { type: 'string', description: 'hire/work: execution sandbox none|docker|podman (default none). Containers isolate each job.' },
@@ -4390,6 +4514,7 @@ export const commands = {
4390
4514
  list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
4391
4515
  'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
4392
4516
  'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
4517
+ 'lock-grace': { type: 'string', description: 'work: extra ms added to --job-timeout to derive the broker activation lock, so the worker reports before the lock lapses (default 120000)' },
4393
4518
  'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
4394
4519
  },
4395
4520
  handler: async (args, flags) => {
@@ -4533,8 +4658,8 @@ function printUsage() {
4533
4658
  console.log(' c8ctl nano set <bin|model-dir> <path>');
4534
4659
  console.log(' c8ctl nano config');
4535
4660
  console.log(' c8ctl nano update [--check]');
4536
- console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
4537
- console.log(' c8ctl nano work <profileName> [--max-parallel <n>] [--job-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]');
4661
+ 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]');
4662
+ console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--job-timeout <ms>] [--lock-grace <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]');
4538
4663
  console.log('');
4539
4664
  console.log('Subcommands:');
4540
4665
  console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
@@ -4577,6 +4702,7 @@ function printUsage() {
4577
4702
  console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
4578
4703
  console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
4579
4704
  console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
4705
+ console.log(' --lock-grace <ms> work: extra ms over --job-timeout for the broker activation lock (default 120000)');
4580
4706
  console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
4581
4707
  console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
4582
4708
  console.log(' --reap-interval <ms> work: how often to sweep finished agent containers/workspaces (default 300000)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.17.0",
3
+ "version": "1.18.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",
@@ -49,12 +49,12 @@
49
49
  "semantic-release": "^25.0.3"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.17.0",
53
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.17.0",
54
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.17.0",
55
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.17.0",
56
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.17.0",
57
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.17.0",
58
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.17.0"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.18.1",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.18.1",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.18.1",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.18.1",
56
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.18.1",
57
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.18.1",
58
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.18.1"
59
59
  }
60
60
  }