c8ctl-plugin-nano 1.38.0 → 1.39.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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +70 -48
  2. package/package.json +9 -9
package/c8ctl-plugin.js CHANGED
@@ -2483,54 +2483,43 @@ function resolveAutoRestConfig(camunda, env = process.env) {
2483
2483
  // exist" is answerable from that engine alone.
2484
2484
  //
2485
2485
  // `@nanobpm/agentic/demand` already reads deployed `taskDefinition` leaves over
2486
- // C8 REST (`process-definitions/search` → `/{key}/xml`), but its scanner reads
2487
- // type/element/process only. Not every service task is an agent task — plain
2488
- // connectors and record-keepers (e.g. `pr.record-plan`) are ordinary workers.
2489
- // The demand scanner is therefore extended HERE to read `zeebe:taskHeaders` and
2490
- // keep only leaves whose service task carries an `io.nanobpm.agentTask.` header
2491
- // (e.g. `senior:plan` carries `io.nanobpm.agentTask.task.prompt`; a record-keeper
2492
- // does not). Advertise the raw job-type string the engine matches (`senior:plan`)
2493
- // verbatim colon-named types are NOT forced through the agentic dot-grammar.
2486
+ // C8 REST (`process-definitions/search` → `/{key}/xml`). As of
2487
+ // `@nanobpm/agentic@0.4.0` its `scanTaskDefinitions(xml)` tags every leaf with a
2488
+ // canonical `agentic: boolean` true iff the service task declares a
2489
+ // `<zeebe:linkedResource linkName="prompt">` base-prompt side-car (its internal
2490
+ // `hasPromptLink`). That flag is the SINGLE SOURCE OF TRUTH for agentic-ness (see
2491
+ // the package's `demand/taskdef.d.ts` and nano-workforce SPEC "Agent job
2492
+ // contract"): every external agent task delivers its base prompt through a
2493
+ // `linkName="prompt"` linked resource, and no in-process worker task does. Not
2494
+ // every service task is an agent task — plain connectors and record-keepers
2495
+ // (e.g. `pr.record-plan`) are ordinary workers, and they carry no prompt link.
2496
+ //
2497
+ // Per AGENTS.md "Derivation Over Duplication: No Drift Surfaces", this plugin
2498
+ // CONSUMES that flag rather than re-implementing the scan, so the detector can
2499
+ // never drift out of lock-step with the package again (as it did in #95, when a
2500
+ // local copy keyed on the legacy `io.nanobpm.agentTask` header missed the current
2501
+ // linked-prompt marker). Advertise the raw job-type string the engine matches
2502
+ // (`senior:plan`) verbatim — colon-named types are NOT forced through the agentic
2503
+ // dot-grammar.
2494
2504
  // ---------------------------------------------------------------------------
2495
2505
 
2496
- // True iff a serviceTask body carries a `zeebe:taskHeader` under the agent-task
2497
- // namespace the marker that distinguishes an agent task from a plain
2498
- // connector / record-keeper. Matches the exact `io.nanobpm.agentTask` key and
2499
- // any flattened `io.nanobpm.agentTask.*` dotpath key (element templates emit the
2500
- // latter, e.g. `io.nanobpm.agentTask.task.prompt`).
2501
- function serviceTaskHasAgentHeader(body) {
2502
- const headerRe = /<zeebe:header\b[^>]*\bkey\s*=\s*"([^"]*)"/g;
2503
- let m;
2504
- while ((m = headerRe.exec(body)) !== null) {
2505
- const key = m[1];
2506
- if (key === AGENT_TASK_NS || key.startsWith(`${AGENT_TASK_NS}.`)) return true;
2507
- }
2508
- return false;
2509
- }
2510
-
2511
- // Scan one deployed BPMN document for its *agent* task-definition leaves: every
2512
- // `<bpmn:serviceTask>` carrying BOTH a non-empty `<zeebe:taskDefinition type>`
2513
- // AND an `io.nanobpm.agentTask.` task header. Returns `{ taskType, process }`
2514
- // leaves in first-occurrence order. This is the header-aware extension of the
2515
- // demand package's `scanTaskDefinitions` (which reads type/element/process only).
2516
- function scanAgentTaskLeaves(xml) {
2517
- const source = String(xml || '');
2518
- const procMatch = source.match(/<bpmn:process\b[^>]*\bid\s*=\s*"([^"]*)"/);
2519
- const proc = procMatch ? procMatch[1] : '';
2520
- const out = [];
2521
- const blockRe = /<bpmn:serviceTask\b[^>]*>([\s\S]*?)<\/bpmn:serviceTask>/g;
2522
- let block;
2523
- while ((block = blockRe.exec(source)) !== null) {
2524
- const body = block[1];
2525
- const tdMatch = body.match(/<zeebe:taskDefinition\b[^>]*>/);
2526
- if (!tdMatch) continue;
2527
- const typeMatch = tdMatch[0].match(/\btype\s*=\s*"([^"]*)"/);
2528
- const taskType = typeMatch ? typeMatch[1] : '';
2529
- if (!taskType) continue;
2530
- if (!serviceTaskHasAgentHeader(body)) continue;
2531
- out.push({ taskType, process: proc });
2506
+ // Scan one deployed BPMN document for its *agent* task-definition leaves: the
2507
+ // subset of `@nanobpm/agentic` `demand.scanTaskDefinitions(xml)` leaves whose
2508
+ // canonical `agentic` flag is set (i.e. the service task declares a
2509
+ // `linkName="prompt"` linked resource). Returns `{ taskType, process }` leaves in
2510
+ // first-occurrence order. The published `scanTaskDefinitions` is INJECTED so this
2511
+ // stays a pure, synchronous function; `readDeployedAgentJobTypes` supplies the
2512
+ // real one from the lazily-imported demand surface (`agentic.mjs`).
2513
+ function scanAgentTaskLeaves(xml, scanTaskDefinitions) {
2514
+ if (typeof scanTaskDefinitions !== 'function') {
2515
+ throw new TypeError(
2516
+ 'scanAgentTaskLeaves: `scanTaskDefinitions` must be an injected function ' +
2517
+ `(got ${typeof scanTaskDefinitions}); pass demand.scanTaskDefinitions from ./agentic.mjs`
2518
+ );
2532
2519
  }
2533
- return out;
2520
+ return scanTaskDefinitions(String(xml || ''))
2521
+ .filter((leaf) => leaf.agentic)
2522
+ .map((leaf) => ({ taskType: leaf.taskType, process: leaf.process }));
2534
2523
  }
2535
2524
 
2536
2525
  // Read the distinct deployed *agent* job types through a demand C8RestReader
@@ -2539,12 +2528,13 @@ function scanAgentTaskLeaves(xml) {
2539
2528
  // optional `scope` narrows to one app/network — kept only when the leaf's
2540
2529
  // `bpmn:process` id equals or is prefixed by the scope string.
2541
2530
  async function readDeployedAgentJobTypes(reader, { scope = '' } = {}) {
2531
+ const { demand } = await import('./agentic.mjs');
2542
2532
  const keys = await reader.searchProcessDefinitionKeys();
2543
2533
  const seen = new Set();
2544
2534
  const out = [];
2545
2535
  for (const key of keys) {
2546
2536
  const xml = await reader.getProcessDefinitionXml(key);
2547
- for (const leaf of scanAgentTaskLeaves(xml)) {
2537
+ for (const leaf of scanAgentTaskLeaves(xml, demand.scanTaskDefinitions)) {
2548
2538
  if (scope && !(leaf.process === scope || leaf.process.startsWith(scope))) continue;
2549
2539
  if (seen.has(leaf.taskType)) continue;
2550
2540
  seen.add(leaf.taskType);
@@ -4702,7 +4692,9 @@ async function workAgent(req, flags) {
4702
4692
  // no capability, no app enrol endpoint, no channel connection. It is the
4703
4693
  // mutually-exclusive counterpart to capability-resolved SERVE: in `--auto`
4704
4694
  // the rank×capability matrix is bypassed entirely (any deployed agent job is
4705
- // served, gated only by the `io.nanobpm.agentTask.` task header), and the
4695
+ // served, gated only by the leaf's canonical `agentic` flag the
4696
+ // `linkName="prompt"` linked-resource marker read by
4697
+ // `@nanobpm/agentic`'s demand scanner), and the
4706
4698
  // desired set is reconciled by polling the engine rather than watching the
4707
4699
  // profile. `--auto-scope <process-id|prefix>` narrows the blast radius to one
4708
4700
  // app/network; without it, every agent job type on the engine is served.
@@ -5836,6 +5828,10 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
5836
5828
  startedAtMs,
5837
5829
  lastExit: w.lastExit ?? null,
5838
5830
  args: Array.isArray(w.args) ? w.args : [],
5831
+ // The per-worker log path (logs/supervisor/worker-<id>.log). Carried through
5832
+ // so `supervisor status` can surface it (see formatSupervisorLogsLines);
5833
+ // null when the source record predates it (e.g. an older persisted state).
5834
+ logFile: w.logFile ?? null,
5839
5835
  activity,
5840
5836
  engine,
5841
5837
  agentic,
@@ -6042,6 +6038,30 @@ function createSupervisorLiveView({
6042
6038
  };
6043
6039
  }
6044
6040
 
6041
+ /**
6042
+ * The `Logs:` block for `supervisor status`, derived purely from the status
6043
+ * payload so it renders identically for a live daemon status and a synthesized
6044
+ * one. Lists the daemon log and each worker's log file, plus a hint at the
6045
+ * existing tailer. Returns an empty array (no section) when no log path is
6046
+ * known — an older persisted state, or a dead daemon whose frame carried none.
6047
+ */
6048
+ function formatSupervisorLogsLines(status) {
6049
+ const daemonLog = status?.daemon?.logFile || null;
6050
+ const workers = Array.isArray(status?.workers) ? status.workers : [];
6051
+ const workerLogs = workers
6052
+ .filter((w) => w && w.logFile)
6053
+ .map((w) => ({ label: String(w.id), path: String(w.logFile) }));
6054
+ const entries = [];
6055
+ if (daemonLog) entries.push({ label: 'daemon', path: String(daemonLog) });
6056
+ for (const w of workerLogs) entries.push(w);
6057
+ if (entries.length === 0) return [];
6058
+ const labelWidth = Math.max(...entries.map((e) => e.label.length));
6059
+ const out = ['', 'Logs:'];
6060
+ for (const e of entries) out.push(` ${e.label.padEnd(labelWidth)} ${e.path}`);
6061
+ out.push(' View: c8ctl nano supervisor logs [<id>] [--follow]');
6062
+ return out;
6063
+ }
6064
+
6045
6065
  /** Render a supervisor status object as an aligned text table. */
6046
6066
  function formatSupervisorStatus(status) {
6047
6067
  const lines = [];
@@ -6055,6 +6075,7 @@ function formatSupervisorStatus(status) {
6055
6075
  lines.push('');
6056
6076
  if (workers.length === 0) {
6057
6077
  lines.push(' No workers. Add one with: c8ctl nano supervisor add <profile>');
6078
+ lines.push(...formatSupervisorLogsLines(status));
6058
6079
  return lines.join('\n');
6059
6080
  }
6060
6081
  const rows = workers.map((w) => ({
@@ -6079,6 +6100,7 @@ function formatSupervisorStatus(status) {
6079
6100
  const fmt = (r) => ' ' + cols.map((c) => r[c].padEnd(width[c])).join(' ');
6080
6101
  lines.push(fmt(head));
6081
6102
  for (const r of rows) lines.push(fmt(r));
6103
+ lines.push(...formatSupervisorLogsLines(status));
6082
6104
  return lines.join('\n');
6083
6105
  }
6084
6106
 
@@ -8881,7 +8903,6 @@ export {
8881
8903
  jobTypeMatrix,
8882
8904
  diffJobTypes,
8883
8905
  parseJobTypeFlags,
8884
- serviceTaskHasAgentHeader,
8885
8906
  scanAgentTaskLeaves,
8886
8907
  readDeployedAgentJobTypes,
8887
8908
  resolveAutoJobTypes,
@@ -8912,6 +8933,7 @@ export {
8912
8933
  formatDuration,
8913
8934
  summarizeSupervisorWorker,
8914
8935
  formatSupervisorStatus,
8936
+ formatSupervisorLogsLines,
8915
8937
  reageSupervisorStatus,
8916
8938
  clampToWidth,
8917
8939
  createSupervisorLiveView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.38.0",
3
+ "version": "1.39.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",
@@ -52,17 +52,17 @@
52
52
  "semantic-release": "^25.0.3"
53
53
  },
54
54
  "dependencies": {
55
- "@nanobpm/agentic": "^0.1.0",
55
+ "@nanobpm/agentic": "^0.4.0",
56
56
  "@nanobpm/urban-agent-client": "^0.1.4"
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.38.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.38.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.38.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.38.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.38.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.38.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.38.0"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.39.0",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.39.0",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.39.0",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.39.0",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.39.0",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.39.0",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.39.0"
67
67
  }
68
68
  }