c8ctl-plugin-nano 1.44.1 → 1.44.3

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/agentic.mjs CHANGED
@@ -74,6 +74,15 @@ export * as presence from '@nanobpm/agentic/presence';
74
74
  export * as relay from '@nanobpm/agentic/relay';
75
75
  export * as transcript from '@nanobpm/agentic/transcript';
76
76
 
77
+ // ACP → transcript bridge (nanobpm/nano-ide#534). The canonical, pure wire seam
78
+ // that turns one raw ACP `session/update` into the exact `{ nwfTranscriptEvent:
79
+ // 1, kind, … }` transcript-chunk bytes the cockpit decodes — `classifyUpdate`
80
+ // composed with `encodeTranscriptEvent` behind the single `acpUpdateToTranscriptChunk`
81
+ // helper. The ACP executor (`spawnCaptureAcp`) consumes it through THIS surface
82
+ // instead of hand-rolling an envelope grammar, so there is one producer of the
83
+ // wire and the shared conformance corpus pins it to the hub.
84
+ export * as sessionAcp from '@nanobpm/agentic/session/acp';
85
+
77
86
  // ---------------------------------------------------------------------------
78
87
  // Demand read — @nanobpm/agentic/demand.
79
88
  //
package/c8ctl-plugin.js CHANGED
@@ -63,6 +63,11 @@ import { platformForHost } from './platforms.mjs';
63
63
  import { createWorkChannel, redactAgenticUrl, buildAgenticUrl } from './work-channel.mjs';
64
64
  import { createRelaySession, roleTerminalMode } from './work-relay.mjs';
65
65
  import { createBufferMonitor, resolveBufferCapacity } from './work-buffer.mjs';
66
+ // Canonical ACP → transcript wire bridge (nanobpm/nano-ide#534), consumed through
67
+ // the single agentic import surface. `acpUpdateToTranscriptChunk(update)` maps one
68
+ // raw ACP `session/update` to the exact transcript-chunk bytes the cockpit decodes,
69
+ // replacing the plugin's former hand-rolled `nwfTranscriptEvent` envelope grammar.
70
+ import { sessionAcp as agenticSessionAcp } from './agentic.mjs';
66
71
 
67
72
  const requireFromHere = createRequire(import.meta.url);
68
73
  const pluginDir = dirname(fileURLToPath(import.meta.url));
@@ -188,6 +193,28 @@ function getLogger() {
188
193
  };
189
194
  }
190
195
 
196
+ /**
197
+ * True when c8ctl's effective output mode is JSON.
198
+ *
199
+ * The plugin used to declare its own `--json` flag, but it collided with
200
+ * c8ctl's new global `--json` and was dropped (see #122). As a result the
201
+ * global `--json` (and persisted `output json` / `C8CTL_OUTPUT_MODE`) is no
202
+ * longer delivered to the plugin as `flags.json`. Read the host's effective
203
+ * output mode directly — the same signal c8ctl's own bundled plugins use — so
204
+ * `workforce list/status` still emit machine-readable JSON on stdout.
205
+ */
206
+ function hostOutputIsJson() {
207
+ const c = globalThis.c8ctl;
208
+ if (!c) return false;
209
+ try {
210
+ if (c.outputMode === 'json') return true;
211
+ const l = typeof c.getLogger === 'function' ? c.getLogger() : null;
212
+ return !!l && l.mode === 'json';
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
191
218
  /** Expand a leading `~` to the user's home directory. */
192
219
  function expandHome(p) {
193
220
  if (!p) return p;
@@ -4143,15 +4170,17 @@ const ACP_MAX_LINE_BYTES = 8 * 1024 * 1024; // 8 MiB
4143
4170
 
4144
4171
  // Drive an ACP (Agent Client Protocol) agent over JSON-RPC 2.0 on stdio.
4145
4172
  //
4146
- // This executor drives ACP end-to-end. Each `session/update` is mapped to a
4147
- // typed `nwfTranscriptEvent` envelope (#110, step 2) and published on the relay
4148
- // session's typed publish seam (`relayTap.relayEnvelope`) — the rich cockpit
4149
- // format its derive+render consumes. The raw relay TRANSPORT is untouched (still
4150
- // `relaySession.relay(text)`); only the payload shape on the lane changes. When
4151
- // an update has no typed mapping, or the relay exposes no typed seam (minimal
4152
- // mode / a plain tap), it falls back to the step-1 human-TEXT chunk on the same
4153
- // lane (`relayTap.onData`) so nothing is dropped. The raw JSON-RPC is never
4154
- // tee'd either way.
4173
+ // This executor drives ACP end-to-end. Each `session/update` is mapped to the
4174
+ // CANONICAL transcript-chunk wire form via the shared `@nanobpm/agentic` bridge
4175
+ // (`acpUpdateToTranscriptChunk` — nanobpm/nano-ide#534) and published on the relay
4176
+ // session's transcript-chunk seam (`relayTap.relayTranscriptChunk`) the exact
4177
+ // `{ nwfTranscriptEvent: 1, kind, }` bytes the cockpit's `parseTranscriptEvent` +
4178
+ // `deriveView` decode into messages / tool cards. The raw relay TRANSPORT is
4179
+ // untouched (still `relaySession.relay(text)`); only the payload shape on the lane
4180
+ // changes. When an update has no canonical mapping (an `ignored` classification), or
4181
+ // the relay exposes no transcript seam (minimal mode / a plain tap), it falls back
4182
+ // to the human-TEXT chunk on the same lane (`relayTap.onData`) so nothing is
4183
+ // dropped. The raw JSON-RPC is never tee'd either way.
4155
4184
  //
4156
4185
  // Framing: ACP frames are newline-delimited JSON-RPC 2.0 messages on stdio (one
4157
4186
  // compact JSON object per line, `\n`-terminated). We implement a tiny inline
@@ -4234,9 +4263,9 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4234
4263
 
4235
4264
  // Local mirrors of a human text chunk: the --stream spy tee and the byte-
4236
4265
  // capped stdout capture (what the result envelope carries). Deliberately does
4237
- // NOT touch the relay lane, so a typed-transcript update can mirror its human
4238
- // text locally (for the result + spy) WITHOUT also re-emitting raw text onto
4239
- // the relay lane — which, in step 2, carries the typed envelope instead.
4266
+ // NOT touch the relay lane, so a canonical-transcript update can mirror its
4267
+ // human text locally (for the result + spy) WITHOUT also re-emitting raw text
4268
+ // onto the relay lane — which carries the canonical transcript chunk instead.
4240
4269
  const captureHuman = (text) => {
4241
4270
  if (!text) return;
4242
4271
  const buf = Buffer.from(text, 'utf8');
@@ -4251,8 +4280,8 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4251
4280
  // use: the relay tap (framed + jobKey-tagged by the caller) and the local
4252
4281
  // --stream spy tee. Byte-capped like the raw captures. This is the minimal-
4253
4282
  // mode text path — used for stderr, and as the fallback for any session/update
4254
- // that has no typed nwfTranscriptEvent mapping (or when the relay exposes no
4255
- // typed publish seam).
4283
+ // that has no canonical transcript mapping (or when the relay exposes no
4284
+ // transcript-chunk seam).
4256
4285
  const emitHuman = (text) => {
4257
4286
  if (!text) return;
4258
4287
  if (relayTap && typeof relayTap.onData === 'function') {
@@ -4373,75 +4402,45 @@ function spawnCaptureAcp({ command, args = [], cwd, env, stdinData, timeoutMs, i
4373
4402
  }
4374
4403
  };
4375
4404
 
4376
- // #110 step 2: map an ACP session/update to a typed `nwfTranscriptEvent`
4377
- // envelope the rich cockpit wire format (the existing downstream
4378
- // derive+render consumes it). Returns null for an update kind we don't model,
4379
- // so the caller falls back to the minimal human-text path (nothing dropped,
4380
- // no regression vs step 1). The `text` field carries the same plain text the
4381
- // fallback would relay, so a lightweight consumer can still render it.
4382
- const TRANSCRIPT_EVENT_TYPE = 'nwfTranscriptEvent';
4383
- const TRANSCRIPT_EVENT_VERSION = 1;
4384
- const mapTranscriptEnvelope = (update) => {
4385
- if (!update || typeof update !== 'object') return null;
4386
- const kind = update.sessionUpdate || update.type;
4387
- if (!kind) return null;
4388
- const base = { type: TRANSCRIPT_EVENT_TYPE, v: TRANSCRIPT_EVENT_VERSION, ts: Date.now() };
4389
- // Optional fields stay `undefined` (JSON encoding omits them) rather than
4390
- // becoming explicit `null`s, and `??` preserves empty strings — so
4391
- // consumers see omitted/optional strings, not coerced nulls. `status`
4392
- // falls through to the kind's default only when genuinely absent.
4393
- const toolOf = (u, defaultStatus) => ({
4394
- id: u.toolCallId ?? undefined,
4395
- title: u.title ?? undefined,
4396
- status: u.status ?? defaultStatus ?? undefined,
4397
- kind: u.kind ?? undefined,
4398
- });
4399
- switch (kind) {
4400
- case 'agent_message_chunk':
4401
- return { ...base, kind: 'message', role: 'agent', text: acpTextOf(update.content) };
4402
- case 'agent_thought_chunk':
4403
- return { ...base, kind: 'thought', role: 'agent', text: acpTextOf(update.content) };
4404
- case 'user_message_chunk':
4405
- return { ...base, kind: 'message', role: 'user', text: acpTextOf(update.content) };
4406
- case 'tool_call':
4407
- // `text` mirrors the fallback's plain text so the typed envelope stays
4408
- // self-contained for lightweight renderers (matches the stated contract).
4409
- return { ...base, kind: 'tool_call', text: describeUpdate(update), tool: toolOf(update, 'pending') };
4410
- case 'tool_call_update':
4411
- return { ...base, kind: 'tool_call_update', text: describeUpdate(update), tool: toolOf(update, undefined) };
4412
- case 'plan':
4413
- // Carry the actual plan entries (rich cockpit renders them), not a
4414
- // count — an absent/malformed payload stays `undefined` (omitted).
4415
- return { ...base, kind: 'plan', entries: Array.isArray(update.entries) ? update.entries : undefined };
4416
- default:
4417
- // Unmodelled kind → no typed envelope; caller uses the text fallback.
4418
- return null;
4419
- }
4405
+ // #110 / nanobpm/nano-ide#534: map an ACP session/update to the CANONICAL
4406
+ // transcript-chunk wire form via the shared `@nanobpm/agentic` bridge
4407
+ // `classifyUpdate` composed with `encodeTranscriptEvent` behind the single
4408
+ // `acpUpdateToTranscriptChunk` helper. It returns the exact
4409
+ // `{ nwfTranscriptEvent: 1, kind, }` bytes the cockpit's `parseTranscriptEvent`
4410
+ // decodes and `deriveView` folds into messages / tool cards, or `null` for an
4411
+ // update with no canonical meaning (an `ignored` classification: a plan, an
4412
+ // intermediate tool_call_update, a non-text chunk, or a malformed update). No
4413
+ // envelope grammar or vocab is hand-rolled here anymore — the marker, version,
4414
+ // kinds and fields all come from the package, so a producer and a consumer can
4415
+ // never diverge on the wire again. `null` (and any bridge throw) falls through
4416
+ // to the minimal human-text path below, so nothing is ever dropped.
4417
+ const encodeTranscriptChunk = (update) => {
4418
+ try { return agenticSessionAcp.acpUpdateToTranscriptChunk(update); }
4419
+ catch { return null; }
4420
4420
  };
4421
4421
 
4422
- // Publish a session/update: prefer the typed nwfTranscriptEvent envelope on
4423
- // the relay's typed publish seam; fall back to the minimal human-text path
4424
- // when the update has no typed mapping OR the relay exposes no typed seam, so
4425
- // nothing is ever dropped (no regression vs minimal mode).
4422
+ // Publish a session/update: prefer the canonical transcript chunk on the
4423
+ // relay's transcript-chunk seam; fall back to the minimal human-text path when
4424
+ // the update has no canonical mapping OR the relay exposes no transcript seam,
4425
+ // so nothing is ever dropped (no regression vs minimal mode).
4426
4426
  const emitTranscript = (update) => {
4427
- const env = mapTranscriptEnvelope(update);
4428
- if (env && relayTap && typeof relayTap.relayEnvelope === 'function') {
4429
- // Only skip the text fallback when the typed publish ACTUALLY succeeded.
4427
+ const chunk = encodeTranscriptChunk(update);
4428
+ if (chunk && relayTap && typeof relayTap.relayTranscriptChunk === 'function') {
4429
+ // Only skip the text fallback when the chunk publish ACTUALLY succeeded.
4430
4430
  // If the seam throws (a downstream tap implementation, not just the
4431
- // built-in best-effort stringify guard), the envelope never reached the
4432
- // relay lane — so we must fall through to the text path or that update
4433
- // would be silently dropped, breaking the "nothing is ever dropped"
4434
- // guarantee.
4431
+ // built-in best-effort guard), the chunk never reached the relay lane — so
4432
+ // we must fall through to the text path or that update would be silently
4433
+ // dropped, breaking the "nothing is ever dropped" guarantee.
4435
4434
  let published = false;
4436
- try { relayTap.relayEnvelope(env); published = true; } catch { /* relay best-effort */ }
4435
+ try { relayTap.relayTranscriptChunk(chunk); published = true; } catch { /* relay best-effort */ }
4437
4436
  if (published) {
4438
4437
  // Mirror the human text locally (spy tee + captured stdout) so the
4439
4438
  // result envelope and --stream spy are unchanged — without re-emitting
4440
- // raw text onto the relay lane, which now carries the typed envelope.
4439
+ // raw text onto the relay lane, which now carries the canonical chunk.
4441
4440
  captureHuman(describeUpdate(update));
4442
4441
  return;
4443
4442
  }
4444
- // Typed publish threw → fall through to the text lane below.
4443
+ // Chunk publish threw → fall through to the text lane below.
4445
4444
  }
4446
4445
  // Fallback: minimal text-chunk path (relay text + spy tee + capture).
4447
4446
  emitHuman(describeUpdate(update));
@@ -4803,17 +4802,18 @@ function runAgentJob(profile, job, opts = {}) {
4803
4802
  const relayTap = relaySession
4804
4803
  ? {
4805
4804
  onData: (buf) => relaySession.relay(buf),
4806
- // #110 step 2: typed transcript publish seam. The ACP producer maps each
4807
- // session/update to an `nwfTranscriptEvent` envelope and publishes it
4808
- // here; we JSON-encode it (newline-delimited) onto the SAME relay lane
4809
- // the raw relay TRANSPORT (ring/QoS/offsets/jobKey routing) is unchanged,
4810
- // still `relaySession.relay(text)`. Consumers (cockpit derive+render)
4811
- // parse the envelope; unmapped updates fall back to the `onData` text path
4812
- // so nothing is dropped (no regression vs the minimal-mode floor).
4813
- // Best-effort: a bad envelope (circular refs / BigInt making
4814
- // JSON.stringify throw) must never crash the worker, so swallow here.
4815
- relayEnvelope: (env) => {
4816
- try { relaySession.relay(`${JSON.stringify(env)}\n`); } catch { /* relay best-effort */ }
4805
+ // #110 / nanobpm/nano-ide#534: canonical transcript-chunk publish seam. The
4806
+ // ACP producer maps each session/update to the exact `{ nwfTranscriptEvent:
4807
+ // 1, kind, }` chunk bytes via `acpUpdateToTranscriptChunk` and hands them
4808
+ // here PRE-ENCODED; we relay them verbatim (newline-framed) onto the SAME
4809
+ // relay lane — the raw relay TRANSPORT (ring/QoS/offsets/jobKey routing) is
4810
+ // unchanged, still `relaySession.relay(text)`. Consumers (cockpit
4811
+ // parseTranscriptEvent + deriveView) decode the chunk; unmapped updates fall
4812
+ // back to the `onData` text path so nothing is dropped (no regression vs the
4813
+ // minimal-mode floor). Best-effort: a relay-transport failure must never
4814
+ // crash the worker, so swallow here.
4815
+ relayTranscriptChunk: (chunk) => {
4816
+ try { relaySession.relay(`${chunk}\n`); } catch { /* relay best-effort */ }
4817
4817
  },
4818
4818
  attachSteer: (write) => relaySession.attachSteer(write),
4819
4819
  }
@@ -8893,7 +8893,7 @@ async function workforceRemoveCmd(req, flags, manifestName) {
8893
8893
 
8894
8894
  async function workforceListCmd(req, flags, manifestName) {
8895
8895
  const logger = getLogger();
8896
- const json = coerceBool(flags?.json, false);
8896
+ const json = flags?.json === false ? false : (coerceBool(flags?.json, false) || hostOutputIsJson());
8897
8897
  const explicitManifest = lastManifestValue(flags) !== '';
8898
8898
  const manifest = readWorkforceManifestStrict(manifestName);
8899
8899
  const others = explicitManifest ? null : listWorkforceManifestNames();
@@ -9007,7 +9007,7 @@ async function workforceStartCmd(req, flags, manifestName) {
9007
9007
 
9008
9008
  async function workforceStatusCmd(req, flags, manifestName) {
9009
9009
  const logger = getLogger();
9010
- const json = coerceBool(flags?.json, false);
9010
+ const json = flags?.json === false ? false : (coerceBool(flags?.json, false) || hostOutputIsJson());
9011
9011
  const manifest = readWorkforceManifestStrict(manifestName);
9012
9012
  const { running, reachable, workers: live } = await fetchSupervisorWorkers();
9013
9013
  // When the daemon is up but its status socket can't be reached, `live` is
@@ -10958,6 +10958,7 @@ export {
10958
10958
  formatWorkforceStatus,
10959
10959
  workforceManifestName,
10960
10960
  lastManifestValue,
10961
+ hostOutputIsJson,
10961
10962
  };
10962
10963
 
10963
10964
  export const metadata = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.44.1",
3
+ "version": "1.44.3",
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.4.0",
56
- "@nanobpm/urban-agent-client": "^0.1.4"
55
+ "@nanobpm/agentic": "^0.10.0",
56
+ "@nanobpm/urban-agent-client": "^0.1.10"
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.1",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.1",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.1",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.1",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.1",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.1",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.1"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.3",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.3",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.3",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.3",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.3",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.3",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.3"
67
67
  }
68
68
  }