c8ctl-plugin-nano 1.14.1 → 1.16.0

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
@@ -169,6 +169,18 @@ capabilities `code-review, testing` the matrix is:
169
169
  so a BPMN service task can target a worker at any granularity by setting its job
170
170
  type to the matching token.
171
171
 
172
+ To also service a job type the matrix can't express — for example a code-first
173
+ [`@nanobpm/workflow`](https://www.npmjs.com/package/@nanobpm/workflow) flow whose
174
+ external task type is `<flowId>:<taskName>`, or any bespoke token — add one or
175
+ more `--job-type <token>` flags. They are serviced **in addition to** the
176
+ rank×capability matrix, so a single hired reviewer can drive both a model-first
177
+ `senior:pr-review` task and a code-first flow's task without re-hiring:
178
+
179
+ ```bash
180
+ c8ctl nano work reviewer --job-type convergence-loop:review-round
181
+ c8ctl nano work reviewer --job-type senior:pr-review --job-type senior:triage
182
+ ```
183
+
172
184
  ```bash
173
185
  c8ctl nano work reviewer # poll for work until Ctrl-C
174
186
  c8ctl nano work reviewer --max-parallel 2 --job-timeout 600000
package/c8ctl-plugin.js CHANGED
@@ -120,15 +120,6 @@ const PROCESSOS_STATE_FILE = 'processos.json';
120
120
  const PROCESSOS_DEFAULT_PORT = 8090;
121
121
  const DEFAULT_NANO_URL = 'http://localhost:8080';
122
122
 
123
- // Guided-journey deep links (ADR 0049 §2). The console renders a first-run tour
124
- // chosen by `…/console?tour=<journeyId>`, and the *command the user ran* already
125
- // encodes the persona — so the CLI bakes the journey into the console URL it
126
- // prints rather than making the console guess. These ids are a contract with the
127
- // console (`console/src/lib/tour`): an unknown id is silently ignored, so they
128
- // are asserted verbatim in the tests.
129
- const JOURNEY_LOCALDEV = 'localdev';
130
- const JOURNEY_AGENTIC_AUTHOR = 'agentic-author';
131
-
132
123
  // Passive update notifier (npm-style): refresh the latest published version
133
124
  // from the registry in a detached background process at most once per day, and
134
125
  // surface a one-line "update available" notice at most once per day. Never
@@ -459,27 +450,15 @@ function liveNodeCount(state) {
459
450
  }
460
451
 
461
452
  /**
462
- * Compose a web-console URL for a node's base URL, optionally carrying a guided
463
- * journey (`?tour=<id>`, ADR 0049 §2). Never hardcodes a port: `baseUrl` is the
464
- * real address the node came up on.
465
- */
466
- function webConsoleUrl(baseUrl, journey) {
467
- const base = `${baseUrl}/console`;
468
- return journey ? `${base}?tour=${encodeURIComponent(journey)}` : base;
469
- }
470
-
471
- /**
472
- * Base URL of the running cluster the user would actually open: the first
473
- * still-alive node, falling back to the first recorded node, then to the default
474
- * gateway URL when no cluster has been started. Used by hire/work, which surface
475
- * a console link for the agentic-SDLC journey.
453
+ * Compose a web-console URL for a node's base URL. Never hardcodes a port:
454
+ * `baseUrl` is the real address the node came up on.
455
+ *
456
+ * No longer carries a `?tour=<id>` deep link: onboarding is chosen in the
457
+ * console's own startup persona panel (nano-bpm #464), not sprayed across every
458
+ * command's output.
476
459
  */
477
- function runningConsoleBaseUrl(state = readState()) {
478
- if (state && Array.isArray(state.nodes) && state.nodes.length > 0) {
479
- const alive = state.nodes.find((n) => isPidAlive(n.pid));
480
- return (alive || state.nodes[0]).url;
481
- }
482
- return DEFAULT_NANO_URL;
460
+ function webConsoleUrl(baseUrl) {
461
+ return `${baseUrl}/console`;
483
462
  }
484
463
 
485
464
  /** Probe a node's always-on GET /v2/topology endpoint for reachability. */
@@ -861,7 +840,7 @@ async function printSummary(state) {
861
840
  console.log(` REST API ${entry.url}/v2`);
862
841
  console.log(` Topology ${entry.url}/v2/topology`);
863
842
  if (hasConsole) {
864
- console.log(` Web console ${webConsoleUrl(entry.url, JOURNEY_LOCALDEV)}`);
843
+ console.log(` Web console ${webConsoleUrl(entry.url)}`);
865
844
  console.log(` User guide ${entry.url}/docs`);
866
845
  }
867
846
  if (state.workspaceDir) {
@@ -1401,6 +1380,35 @@ function parseEnvPairs(input) {
1401
1380
  return { env, errors };
1402
1381
  }
1403
1382
 
1383
+ // A worker job-type token: rank/capability tokens use `:` (rank↔cap) and `+`
1384
+ // (combined caps) as delimiters, and code-first `@nanobpm/workflow` job types
1385
+ // are `<flowId>:<taskName>` or an explicit override. The first character must be
1386
+ // a letter, digit, or `_`; the remainder may also include `. : + -`. Mirrors the
1387
+ // SDK's assertJobType so a token authored on one side is accepted on the other.
1388
+ const JOB_TYPE_TOKEN_RE = /^[A-Za-z0-9_][A-Za-z0-9_.:+-]*$/;
1389
+
1390
+ // Parse repeatable `--job-type <token>` CLI input (string | string[]) into a
1391
+ // deduped, validated list of explicit job types a worker should also service,
1392
+ // in addition to its rank×capability matrix. Returns { jobTypes, errors }.
1393
+ function parseJobTypeFlags(input) {
1394
+ const list = input == null ? [] : (Array.isArray(input) ? input : [input]);
1395
+ const seen = new Set();
1396
+ const jobTypes = [];
1397
+ const errors = [];
1398
+ for (const item of list) {
1399
+ const token = String(item).trim();
1400
+ if (!token) { errors.push('--job-type must be a non-empty token'); continue; }
1401
+ if (!JOB_TYPE_TOKEN_RE.test(token)) {
1402
+ errors.push(`--job-type "${token}" is invalid (must match ${JOB_TYPE_TOKEN_RE.source})`);
1403
+ continue;
1404
+ }
1405
+ if (seen.has(token)) continue;
1406
+ seen.add(token);
1407
+ jobTypes.push(token);
1408
+ }
1409
+ return { jobTypes, errors };
1410
+ }
1411
+
1404
1412
  /** A profile name must be a safe, filesystem/token-friendly slug. */
1405
1413
  function isValidProfileName(name) {
1406
1414
  return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
@@ -1633,7 +1641,6 @@ async function hireWorker(req, flags) {
1633
1641
  if (envKeys.length > 0) logger.info(` env: ${envKeys.join(', ')}`);
1634
1642
  logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
1635
1643
  logger.info(`Put it to work with: c8ctl nano work ${name}`);
1636
- logger.info(`Open the console: ${webConsoleUrl(runningConsoleBaseUrl(), JOURNEY_AGENTIC_AUTHOR)}`);
1637
1644
  }
1638
1645
 
1639
1646
  /**
@@ -2658,6 +2665,16 @@ async function workAgent(req, flags) {
2658
2665
  }
2659
2666
 
2660
2667
  const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
2668
+ // Optional explicit job types (repeatable `--job-type`), serviced in addition
2669
+ // to the rank×capability matrix. This lets a hired profile also drive a pool
2670
+ // keyed on a token the matrix can't express — e.g. a code-first
2671
+ // `@nanobpm/workflow` flow whose external task type isn't a `rank:cap` token.
2672
+ const { jobTypes: extraJobTypes, errors: jobTypeErrors } = parseJobTypeFlags(flags?.['job-type']);
2673
+ if (jobTypeErrors.length > 0) {
2674
+ logger.error(jobTypeErrors.join('; '));
2675
+ process.exit(1);
2676
+ }
2677
+ const jobTypes = [...new Set([...matrix, ...extraJobTypes])];
2661
2678
  const camunda = globalThis.c8ctl.createClient();
2662
2679
 
2663
2680
  logger.info(`Putting "${name}" [${profile.rank}] to work → ${profile.command}`);
@@ -2665,12 +2682,12 @@ async function workAgent(req, flags) {
2665
2682
  logger.info(` sandbox: ${sandbox}${isContainer ? ` (image ${image})` : ''}`);
2666
2683
  const profileEnvKeys = Object.keys(profileEnv);
2667
2684
  if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
2668
- logger.info(` listening on ${matrix.length} job type(s): ${matrix.join(' ')}`);
2685
+ const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
2686
+ logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
2669
2687
  logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobTimeoutMs}ms`);
2670
- logger.info(` console: ${webConsoleUrl(runningConsoleBaseUrl(), JOURNEY_AGENTIC_AUTHOR)}`);
2671
2688
  logger.info('Polling for work — press Ctrl-C to stop.');
2672
2689
 
2673
- const workers = matrix.map((jobType) =>
2690
+ const workers = jobTypes.map((jobType) =>
2674
2691
  camunda.createJobWorker({
2675
2692
  jobType,
2676
2693
  workerName: `${name}:${jobType}`,
@@ -4200,10 +4217,7 @@ export { resolveBinary, findBinary, launcherEnvMarkers };
4200
4217
  export { buildNpmInvocation };
4201
4218
  export {
4202
4219
  webConsoleUrl,
4203
- runningConsoleBaseUrl,
4204
4220
  hireWorker,
4205
- JOURNEY_LOCALDEV,
4206
- JOURNEY_AGENTIC_AUTHOR,
4207
4221
  };
4208
4222
  export {
4209
4223
  normalizeTaskEnvelope,
@@ -4236,6 +4250,7 @@ export {
4236
4250
  ProvisionError,
4237
4251
  normalizeStoredProfile,
4238
4252
  jobTypeMatrix,
4253
+ parseJobTypeFlags,
4239
4254
  AGENT_TASK_NS,
4240
4255
  AGENT_RESULT_KEY,
4241
4256
  RESULT_SENTINEL,
@@ -4338,6 +4353,7 @@ export const commands = {
4338
4353
  list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
4339
4354
  'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
4340
4355
  'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
4356
+ 'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
4341
4357
  },
4342
4358
  handler: async (args, flags) => {
4343
4359
  const logger = getLogger();
@@ -4481,7 +4497,7 @@ function printUsage() {
4481
4497
  console.log(' c8ctl nano config');
4482
4498
  console.log(' c8ctl nano update [--check]');
4483
4499
  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]');
4484
- console.log(' c8ctl nano work <profileName> [--max-parallel <n>] [--job-timeout <ms>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
4500
+ 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]');
4485
4501
  console.log('');
4486
4502
  console.log('Subcommands:');
4487
4503
  console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
@@ -4522,6 +4538,7 @@ function printUsage() {
4522
4538
  console.log(' --env NAME=VALUE hire/work: static env var for the harness (repeatable); persisted on hire, work extends/overrides');
4523
4539
  console.log(' --list hire: list existing agent profiles instead of creating one');
4524
4540
  console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
4541
+ console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
4525
4542
  console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
4526
4543
  console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
4527
4544
  console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.14.1",
3
+ "version": "1.16.0",
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.14.1",
53
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.14.1",
54
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.14.1",
55
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.14.1",
56
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.14.1",
57
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.14.1",
58
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.14.1"
52
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.16.0",
53
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.16.0",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.16.0",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.16.0",
56
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.16.0",
57
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.16.0",
58
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.16.0"
59
59
  }
60
60
  }