c8ctl-plugin-nano 1.38.1 → 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 +69 -77
  2. package/package.json +9 -9
package/c8ctl-plugin.js CHANGED
@@ -2483,82 +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:header` (inside its
2497
- // `zeebe:taskHeaders`) under the agent-task
2498
- // namespace the LEGACY marker that distinguishes an agent task from a plain
2499
- // connector / record-keeper. Matches the exact `io.nanobpm.agentTask` key and
2500
- // any flattened `io.nanobpm.agentTask.*` dotpath key (element templates emit the
2501
- // latter, e.g. `io.nanobpm.agentTask.task.prompt`). Older deployments carry this;
2502
- // the current `@nanobpm/workflow` toolchain emits the linked-prompt marker below
2503
- // instead, so BOTH must be recognised (jwulf/c8ctl-plugin-nano#95).
2504
- function serviceTaskHasAgentHeader(body) {
2505
- const headerRe = /<zeebe:header\b[^>]*\bkey\s*=\s*"([^"]*)"/g;
2506
- let m;
2507
- while ((m = headerRe.exec(body)) !== null) {
2508
- const key = m[1];
2509
- if (key === AGENT_TASK_NS || key.startsWith(`${AGENT_TASK_NS}.`)) return true;
2510
- }
2511
- return false;
2512
- }
2513
-
2514
- // True iff a serviceTask body links a prompt resource — the CURRENT canonical
2515
- // agent-task marker emitted by `@nanobpm/workflow` / the Urban toolchain:
2516
- // `<zeebe:linkedResource … resourceType="GenericScript" linkName="prompt" />`.
2517
- // An agent task links the model/prompt the harness runs; a plain connector /
2518
- // record-keeper does not. Matched by the `linkName="prompt"` binding (attribute
2519
- // order-independent) so a compiled model with no `io.nanobpm.agentTask` header
2520
- // is still discovered (jwulf/c8ctl-plugin-nano#95). Kept in lock-step with the
2521
- // authored nano-workforce models (resources/processes/*.bpmn), where every
2522
- // `senior:*` service task carries this binding and none carry the legacy header.
2523
- // NOTE: this mirrors `@nanobpm/agentic@0.4.0`'s released `scanTaskDefinitions`
2524
- // `agentic` flag (its internal `hasPromptLink`); #102 tracks replacing this local
2525
- // copy by consuming that detector once the `^0.1.0 → ^0.4.0` bump is vetted.
2526
- function serviceTaskHasLinkedPrompt(body) {
2527
- const re = new RegExp(`<zeebe:linkedResource\\b[^>]*\\blinkName\\s*=\\s*"${DEFAULT_PROMPT_LINK_NAME}"`);
2528
- return re.test(String(body || ''));
2529
- }
2530
-
2531
- // True iff a serviceTask is an *agent* task — by EITHER the legacy
2532
- // `io.nanobpm.agentTask.*` header OR the current linked-prompt marker. Either
2533
- // alone is sufficient; deployments in the wild carry one or the other.
2534
- function serviceTaskIsAgentTask(body) {
2535
- return serviceTaskHasAgentHeader(body) || serviceTaskHasLinkedPrompt(body);
2536
- }
2537
-
2538
- // Scan one deployed BPMN document for its *agent* task-definition leaves: every
2539
- // `<bpmn:serviceTask>` carrying BOTH a non-empty `<zeebe:taskDefinition type>`
2540
- // AND an agent-task marker (legacy `io.nanobpm.agentTask.` header OR a
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
2541
2509
  // `linkName="prompt"` linked resource). Returns `{ taskType, process }` leaves in
2542
- // first-occurrence order. This is the agent-aware extension of the demand
2543
- // package's `scanTaskDefinitions` (which reads type/element/process only).
2544
- function scanAgentTaskLeaves(xml) {
2545
- const source = String(xml || '');
2546
- const procMatch = source.match(/<bpmn:process\b[^>]*\bid\s*=\s*"([^"]*)"/);
2547
- const proc = procMatch ? procMatch[1] : '';
2548
- const out = [];
2549
- const blockRe = /<bpmn:serviceTask\b[^>]*>([\s\S]*?)<\/bpmn:serviceTask>/g;
2550
- let block;
2551
- while ((block = blockRe.exec(source)) !== null) {
2552
- const body = block[1];
2553
- const tdMatch = body.match(/<zeebe:taskDefinition\b[^>]*>/);
2554
- if (!tdMatch) continue;
2555
- const typeMatch = tdMatch[0].match(/\btype\s*=\s*"([^"]*)"/);
2556
- const taskType = typeMatch ? typeMatch[1] : '';
2557
- if (!taskType) continue;
2558
- if (!serviceTaskIsAgentTask(body)) continue;
2559
- out.push({ taskType, process: proc });
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
+ );
2560
2519
  }
2561
- return out;
2520
+ return scanTaskDefinitions(String(xml || ''))
2521
+ .filter((leaf) => leaf.agentic)
2522
+ .map((leaf) => ({ taskType: leaf.taskType, process: leaf.process }));
2562
2523
  }
2563
2524
 
2564
2525
  // Read the distinct deployed *agent* job types through a demand C8RestReader
@@ -2567,12 +2528,13 @@ function scanAgentTaskLeaves(xml) {
2567
2528
  // optional `scope` narrows to one app/network — kept only when the leaf's
2568
2529
  // `bpmn:process` id equals or is prefixed by the scope string.
2569
2530
  async function readDeployedAgentJobTypes(reader, { scope = '' } = {}) {
2531
+ const { demand } = await import('./agentic.mjs');
2570
2532
  const keys = await reader.searchProcessDefinitionKeys();
2571
2533
  const seen = new Set();
2572
2534
  const out = [];
2573
2535
  for (const key of keys) {
2574
2536
  const xml = await reader.getProcessDefinitionXml(key);
2575
- for (const leaf of scanAgentTaskLeaves(xml)) {
2537
+ for (const leaf of scanAgentTaskLeaves(xml, demand.scanTaskDefinitions)) {
2576
2538
  if (scope && !(leaf.process === scope || leaf.process.startsWith(scope))) continue;
2577
2539
  if (seen.has(leaf.taskType)) continue;
2578
2540
  seen.add(leaf.taskType);
@@ -4730,7 +4692,9 @@ async function workAgent(req, flags) {
4730
4692
  // no capability, no app enrol endpoint, no channel connection. It is the
4731
4693
  // mutually-exclusive counterpart to capability-resolved SERVE: in `--auto`
4732
4694
  // the rank×capability matrix is bypassed entirely (any deployed agent job is
4733
- // 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
4734
4698
  // desired set is reconciled by polling the engine rather than watching the
4735
4699
  // profile. `--auto-scope <process-id|prefix>` narrows the blast radius to one
4736
4700
  // app/network; without it, every agent job type on the engine is served.
@@ -5864,6 +5828,10 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
5864
5828
  startedAtMs,
5865
5829
  lastExit: w.lastExit ?? null,
5866
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,
5867
5835
  activity,
5868
5836
  engine,
5869
5837
  agentic,
@@ -6070,6 +6038,30 @@ function createSupervisorLiveView({
6070
6038
  };
6071
6039
  }
6072
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
+
6073
6065
  /** Render a supervisor status object as an aligned text table. */
6074
6066
  function formatSupervisorStatus(status) {
6075
6067
  const lines = [];
@@ -6083,6 +6075,7 @@ function formatSupervisorStatus(status) {
6083
6075
  lines.push('');
6084
6076
  if (workers.length === 0) {
6085
6077
  lines.push(' No workers. Add one with: c8ctl nano supervisor add <profile>');
6078
+ lines.push(...formatSupervisorLogsLines(status));
6086
6079
  return lines.join('\n');
6087
6080
  }
6088
6081
  const rows = workers.map((w) => ({
@@ -6107,6 +6100,7 @@ function formatSupervisorStatus(status) {
6107
6100
  const fmt = (r) => ' ' + cols.map((c) => r[c].padEnd(width[c])).join(' ');
6108
6101
  lines.push(fmt(head));
6109
6102
  for (const r of rows) lines.push(fmt(r));
6103
+ lines.push(...formatSupervisorLogsLines(status));
6110
6104
  return lines.join('\n');
6111
6105
  }
6112
6106
 
@@ -8909,9 +8903,6 @@ export {
8909
8903
  jobTypeMatrix,
8910
8904
  diffJobTypes,
8911
8905
  parseJobTypeFlags,
8912
- serviceTaskHasAgentHeader,
8913
- serviceTaskHasLinkedPrompt,
8914
- serviceTaskIsAgentTask,
8915
8906
  scanAgentTaskLeaves,
8916
8907
  readDeployedAgentJobTypes,
8917
8908
  resolveAutoJobTypes,
@@ -8942,6 +8933,7 @@ export {
8942
8933
  formatDuration,
8943
8934
  summarizeSupervisorWorker,
8944
8935
  formatSupervisorStatus,
8936
+ formatSupervisorLogsLines,
8945
8937
  reageSupervisorStatus,
8946
8938
  clampToWidth,
8947
8939
  createSupervisorLiveView,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.38.1",
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.1",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.38.1",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.38.1",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.38.1",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.38.1",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.38.1",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.38.1"
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
  }