c8ctl-plugin-nano 1.30.0 → 1.31.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
@@ -219,6 +219,66 @@ name is generated, so two `work reviewer` processes never collide at the
219
219
  broker. (`--name` names the worker; the profile to run is always the
220
220
  positional argument.)
221
221
 
222
+ ### Live agent visibility: the `/agentic` channel + live terminals
223
+
224
+ A running worker can appear **live on the Workforce visibility page** and stream
225
+ its agents' terminals to an operator's cockpit. This rides the app's agentic
226
+ channel (ADR 0056), served **same-port** on the app's own HTTP base URL at path
227
+ **`/agentic`** — not a sidecar, so there's no extra port to open.
228
+
229
+ **Connecting.** Enrolment is opt-in: a worker only connects when it has both an
230
+ ADR 0028 **identity token** and a **capability credential** (the same `?token=…`
231
+ pattern the blackboard uses). Point it at the app and hand it the two secrets via
232
+ env (or persisted config); the channel URL defaults to the configured nano URL:
233
+
234
+ ```bash
235
+ # Enrol this worker on the app's same-port /agentic channel
236
+ export NANO_AGENTIC_URL=http://localhost:8080 # app base URL; channel is served at /agentic
237
+ export NANO_AGENTIC_TOKEN=<identity-token> # ADR 0028 identity
238
+ export NANO_AGENTIC_CREDENTIAL=<capability-cred> # capability credential
239
+ c8ctl nano work reviewer
240
+ # agentic channel: announcing presence as ‹worker› on ws://localhost:8080/agentic
241
+ ```
242
+
243
+ Without both secrets the worker runs **exactly as before, off the channel** — no
244
+ visibility, no relay, nothing else changes. A valid identity + capability
245
+ connects; an invalid identity is rejected (unauthorized) and a missing capability
246
+ is rejected (forbidden).
247
+
248
+ **How presence appears.** On connect the worker **announces** its identity, its
249
+ `host`, and the set of `jobKeys` it is currently running, then **heartbeats** to
250
+ stay live and **re-announces** after a reconnect so its row survives a hub
251
+ restart. When `work` stops it **deregisters cleanly**, so the worker disappears
252
+ from the visibility page on exit.
253
+
254
+ **Opting a role into a live terminal (PTY vs pipe).** Each role chooses whether
255
+ its agent harness runs on a full **PTY** (a real terminal — streamed on the relay
256
+ lane and **steerable**: an operator's keystrokes reach the running agent) or a
257
+ plain **pipe** (streamed, not interactive). It's a **per-role opt-in**, set on
258
+ the hire profile and defaulting to `pipe`:
259
+
260
+ ```bash
261
+ # Hire a role whose harness runs on a full, steerable PTY
262
+ c8ctl nano hire --name coder --rank senior --command copilot --terminal pty
263
+
264
+ # Override the mode for a one-off worker without re-hiring
265
+ NANO_AGENTIC_TERMINAL=pipe c8ctl nano work coder
266
+ ```
267
+
268
+ A PTY needs the optional native `node-pty` dependency; when it is
269
+ **unavailable** (not installed, or Windows) a `pty` role **gracefully falls
270
+ back to a pipe** that still relays. (If `node-pty` is present but a PTY can't be
271
+ spawned at runtime, that job fails rather than falling back.) Each job's terminal
272
+ streams on its own relay stream
273
+ named `job:‹jobKey›`, so output and steer-in are routed from the `jobKey` alone.
274
+
275
+ **Surviving a hub outage.** A worker that starts **before** the app, or survives
276
+ a **hub restart**, buffers its outbound frames in a **bounded** local ring and
277
+ **drains them in order** on reconnect (no loss or reorder within the bound). The
278
+ bound is operator-tunable for a long expected outage; raise it with
279
+ `NANO_AGENTIC_BUFFER_CAPACITY` (frames). When the bound is hit the worker warns
280
+ rather than silently shedding.
281
+
222
282
  ### Live profile reload (no restart on `assign`)
223
283
 
224
284
  A running `c8ctl nano work <name>` **watches** the profile it is servicing. When
@@ -332,8 +392,32 @@ composed in FEEL, a task may supply per-instance context via **`task.appendPromp
332
392
  separator/preamble), so a null/empty append leaves the base untouched. This lets the
333
393
  static prompt live in a model header/side-car while the dynamic tail (e.g. plan-revision
334
394
  feedback, a per-task brief) is built per instance.
395
+
396
+ **Live prompts via linked resources.** A service task can declare a Zeebe/Camunda-parity
397
+ **linked resource** for its prompt instead of baking it into a model header:
398
+
399
+ ```xml
400
+ <zeebe:linkedResources>
401
+ <zeebe:linkedResource resourceId="plan.md" bindingType="latest" linkName="prompt"/>
402
+ </zeebe:linkedResources>
403
+ ```
404
+
405
+ At job activation the engine resolves the `resourceId` to the **latest** deployed key and
406
+ delivers a `linkedResources` custom header (`[{resourceKey, resourceType, linkName}]`). The
407
+ header carries the **key, not the content** — the worker fetches the bytes over the broker
408
+ REST API (`GET /v2/resources/{resourceKey}/content/binary`, reusing the same nano endpoint
409
+ the worker already talks to; override with `NANO_REST_URL`/`NANO_REST_TOKEN`) and uses the
410
+ UTF-8 content as the **base prompt**. The entry whose `linkName` is `prompt` wins over the
411
+ header-baked `task.prompt` chain; `appendPrompt` still composes onto it. Redeploying just
412
+ the resource updates the prompt for the **next activation** — no process redeploy, no
413
+ worker restart. Jobs without `linkedResources` behave exactly as before (fallback chain).
414
+ A declared prompt resource that can't be fetched **fails the job** (retryable provisioning
415
+ error) rather than running an agent with an empty prompt, and the resolved `resourceKey` is
416
+ logged and echoed on the output envelope as `promptResourceKey` for audit (the engine keeps
417
+ only `latest`, so the key is the reproducibility handle).
418
+
335
419
  On completion the plugin writes an **output envelope** back under
336
- `io.nanobpm.agentResult` (`{schemaVersion, status, sandbox, image, output, truncated, stderrTruncated, exitCode, signal, error}`). When a repository was
420
+ `io.nanobpm.agentResult` (`{schemaVersion, status, sandbox, image, output, truncated, stderrTruncated, exitCode, signal, error, promptResourceKey?}`). When a repository was
337
421
  provisioned (below) it also carries `{repository, branch, baseSha, headSha, commits[], pushed, pushError?, gitError?, pr?}`.
338
422
 
339
423
  **Git provisioning (host).** When `--sandbox none` (the default) and the envelope
package/c8ctl-plugin.js CHANGED
@@ -2072,6 +2072,15 @@ function killTree(child) {
2072
2072
  // envelope is written back on the job's completion variables.
2073
2073
  const AGENT_TASK_NS = 'io.nanobpm.agentTask';
2074
2074
  const AGENT_RESULT_KEY = 'io.nanobpm.agentResult';
2075
+ // Zeebe/Camunda-parity linked resources (issue #63). At job activation the
2076
+ // engine resolves each declared `<zeebe:linkedResource resourceId … linkName>`
2077
+ // to the LATEST deployed key and delivers this custom header: a JSON array of
2078
+ // `{ resourceKey, resourceType, linkName }`. The header carries the key, not the
2079
+ // content — the worker fetches the bytes over the broker REST API. The entry
2080
+ // whose `linkName` matches DEFAULT_PROMPT_LINK_NAME supplies the agent's base
2081
+ // prompt (live-updatable by redeploying just the resource).
2082
+ const LINKED_RESOURCES_HEADER = 'linkedResources';
2083
+ const DEFAULT_PROMPT_LINK_NAME = 'prompt';
2075
2084
  const TASK_ENVELOPE_SCHEMA_VERSION = 1;
2076
2085
  // The result-envelope version is intentionally independent of the task-envelope
2077
2086
  // version so the two contracts can evolve separately without silently coupling.
@@ -2236,7 +2245,13 @@ function collectEnvelopeFrom(source) {
2236
2245
 
2237
2246
  // Normalize the assembled envelope to schema v1, coercing header string values
2238
2247
  // (element templates write everything as strings) into bool/int as needed.
2239
- function normalizeTaskEnvelope(customHeaders, variables) {
2248
+ //
2249
+ // `opts.basePromptOverride` (issue #63): when a `linkName: prompt` linked
2250
+ // resource resolves, its fetched content is passed here and WINS over the
2251
+ // header-baked `task.prompt` / `variables.prompt` / `variables.task` chain. The
2252
+ // FEEL-composed `appendPrompt` concatenation is unchanged — it still appends to
2253
+ // whatever base was resolved.
2254
+ function normalizeTaskEnvelope(customHeaders, variables, opts = {}) {
2240
2255
  const raw = deepMerge(collectEnvelopeFrom(customHeaders), collectEnvelopeFrom(variables));
2241
2256
  const str = (v) => (v == null ? undefined : String(v));
2242
2257
  const env = {
@@ -2272,9 +2287,12 @@ function normalizeTaskEnvelope(customHeaders, variables) {
2272
2287
  };
2273
2288
 
2274
2289
  const task = isPlainObject(raw.task) ? raw.task : {};
2275
- // Base prompt: the reserved `task.prompt` (typically a model header filled at deploy time),
2276
- // else a plain `prompt`/`task` job variable (the pre-header delivery path).
2277
- const basePrompt = str(task.prompt) ?? str(variables?.prompt) ?? str(variables?.task);
2290
+ // Base prompt: an override supplied by a resolved `linkName: prompt` linked
2291
+ // resource (issue #63) wins; else the reserved `task.prompt` (typically a model
2292
+ // header filled at deploy time), else a plain `prompt`/`task` job variable (the
2293
+ // pre-header delivery path).
2294
+ const overrideBase = opts && opts.basePromptOverride != null ? String(opts.basePromptOverride) : undefined;
2295
+ const basePrompt = overrideBase ?? str(task.prompt) ?? str(variables?.prompt) ?? str(variables?.task);
2278
2296
  // Verbatim dynamic append: a header-delivered base prompt can't be composed in FEEL, so a task
2279
2297
  // may supply per-instance context (e.g. plan-revision feedback, a per-task brief) via the
2280
2298
  // reserved `task.appendPrompt`, or a plain `appendPrompt` variable. It is concatenated onto the
@@ -2297,7 +2315,133 @@ function normalizeTaskEnvelope(customHeaders, variables) {
2297
2315
  return env;
2298
2316
  }
2299
2317
 
2300
- // ---- Secret resolution (pluggable; only host-env implemented for now) ------
2318
+ // ---- Linked resources live agent prompt (issue #63) ----------------------
2319
+ // A job's `linkedResources` activation header carries KEYS (not content); the
2320
+ // worker resolves each to the LATEST deployed bytes over the broker REST API, so
2321
+ // an agent prompt can be updated by redeploying one Markdown resource — no
2322
+ // process redeploy, no harness restart.
2323
+
2324
+ // Parse the `linkedResources` custom header off an activated job. The engine
2325
+ // delivers a JSON array; element-template/header transport stringifies it. Also
2326
+ // tolerate an already-parsed array. Anything malformed/absent → [] (fallback
2327
+ // path). Never throws.
2328
+ function parseLinkedResources(customHeaders) {
2329
+ if (!isPlainObject(customHeaders)) return [];
2330
+ const raw = customHeaders[LINKED_RESOURCES_HEADER];
2331
+ if (raw == null) return [];
2332
+ let val = raw;
2333
+ if (typeof raw === 'string') {
2334
+ if (!raw.trim()) return [];
2335
+ try { val = JSON.parse(raw); } catch { return []; }
2336
+ }
2337
+ if (!Array.isArray(val)) return [];
2338
+ return val.filter((e) => isPlainObject(e) && e.resourceKey != null);
2339
+ }
2340
+
2341
+ // Select the linked resource that supplies the base prompt: the first entry
2342
+ // whose `linkName` matches (default `prompt`). Returns the entry or null.
2343
+ function pickLinkedResource(linkedResources, linkName = DEFAULT_PROMPT_LINK_NAME) {
2344
+ if (!Array.isArray(linkedResources)) return null;
2345
+ return linkedResources.find((e) => isPlainObject(e) && String(e.linkName) === String(linkName)) || null;
2346
+ }
2347
+
2348
+ // The broker REST base URL + optional bearer the harness uses to fetch resource
2349
+ // content — the SAME nano endpoint the worker already talks to (env wins over
2350
+ // persisted config, falling back to the default localhost port). A local nano
2351
+ // cluster is unauthenticated, so the token is optional; when set (e.g. against a
2352
+ // secured gateway) it is sent as a Bearer credential.
2353
+ // Same-origin test for two URLs (protocol + host + port). Returns false if
2354
+ // either string is missing or unparseable, so a token is never forwarded to an
2355
+ // endpoint we can't positively confirm matches.
2356
+ function sameOrigin(a, b) {
2357
+ if (!a || !b) return false;
2358
+ try {
2359
+ return new URL(a).origin === new URL(b).origin;
2360
+ } catch {
2361
+ return false;
2362
+ }
2363
+ }
2364
+
2365
+ function resolveBrokerRestConfig(env = process.env) {
2366
+ // readConfig() swallows parse/IO errors and never throws (returns {}), so no
2367
+ // local try/catch is needed here.
2368
+ const cfg = readConfig() || {};
2369
+ const baseUrl =
2370
+ env.NANO_REST_URL ||
2371
+ env.NANO_BASE_URL ||
2372
+ cfg.nanoUrl ||
2373
+ DEFAULT_NANO_URL;
2374
+ // An explicit REST token always wins. The agentic identity token is only a
2375
+ // fallback for single-token deployments where the broker REST endpoint IS the
2376
+ // agentic endpoint — so only forward it when the REST base URL is same-origin
2377
+ // as the agentic URL. This prevents leaking the identity token to a different
2378
+ // NANO_REST_URL host when no REST token is set (see resolveAgenticConfig).
2379
+ let token = env.NANO_REST_TOKEN || '';
2380
+ if (!token) {
2381
+ const agenticToken = env.NANO_AGENTIC_TOKEN || cfg.agenticToken || '';
2382
+ const agenticUrl =
2383
+ env.NANO_AGENTIC_URL ||
2384
+ cfg.agenticUrl ||
2385
+ cfg.nanoUrl ||
2386
+ env.NANO_BASE_URL ||
2387
+ DEFAULT_NANO_URL;
2388
+ if (agenticToken && sameOrigin(baseUrl, agenticUrl)) token = agenticToken;
2389
+ }
2390
+ return { baseUrl, token };
2391
+ }
2392
+
2393
+ // Build the content endpoint. Per issue #63 / nano-bpm #759 the non-binary
2394
+ // `/content` variant is deprecated for non-RPA types (Markdown → 406), so the
2395
+ // worker always fetches `/content/binary`.
2396
+ function resourceContentUrl(baseUrl, resourceKey) {
2397
+ const base = String(baseUrl || '').replace(/\/+$/, '');
2398
+ return `${base}/v2/resources/${encodeURIComponent(String(resourceKey))}/content/binary`;
2399
+ }
2400
+
2401
+ // Fetch a linked resource's bytes and decode UTF-8. A non-2xx or network error
2402
+ // is surfaced as a ProvisionError so the caller fails the job with a clear
2403
+ // provisioning message (never runs an agent with a silently-empty prompt).
2404
+ async function fetchLinkedResourceContent(resourceKey, opts = {}) {
2405
+ const { baseUrl, token, fetchImpl = fetch, timeoutMs = 15_000 } = opts;
2406
+ const url = resourceContentUrl(baseUrl, resourceKey);
2407
+ const controller = new AbortController();
2408
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
2409
+ let res;
2410
+ try {
2411
+ const headers = { Accept: 'application/octet-stream' };
2412
+ if (token) headers.Authorization = `Bearer ${token}`;
2413
+ res = await fetchImpl(url, { method: 'GET', headers, signal: controller.signal });
2414
+ } catch (err) {
2415
+ throw new ProvisionError(`prompt resource ${resourceKey} fetch failed: ${err && err.message ? err.message : String(err)}`);
2416
+ } finally {
2417
+ if (timer) clearTimeout(timer);
2418
+ }
2419
+ if (!res || !res.ok) {
2420
+ const status = res ? res.status : '?';
2421
+ throw new ProvisionError(`prompt resource ${resourceKey} fetch failed: HTTP ${status} from ${url}`);
2422
+ }
2423
+ try {
2424
+ return await res.text();
2425
+ } catch (err) {
2426
+ throw new ProvisionError(`prompt resource ${resourceKey} decode failed: ${err && err.message ? err.message : String(err)}`);
2427
+ }
2428
+ }
2429
+
2430
+ // Resolve the base prompt from a `linkName: prompt` linked resource, if the job
2431
+ // declares one. Returns `{ basePrompt, resourceKey }` when a prompt resource is
2432
+ // present and fetched, or null when no such entry exists (→ fall back to the
2433
+ // header-baked task.prompt chain). Throws a ProvisionError when a declared
2434
+ // prompt resource cannot be fetched — a provisioning failure, not a silent
2435
+ // empty-prompt run.
2436
+ async function resolveLinkedPrompt(customHeaders, opts = {}) {
2437
+ const { linkName = DEFAULT_PROMPT_LINK_NAME, baseUrl, token, fetchImpl, timeoutMs } = opts;
2438
+ const entry = pickLinkedResource(parseLinkedResources(customHeaders), linkName);
2439
+ if (!entry) return null;
2440
+ const resourceKey = entry.resourceKey;
2441
+ const basePrompt = await fetchLinkedResourceContent(resourceKey, { baseUrl, token, fetchImpl, timeoutMs });
2442
+ return { basePrompt, resourceKey, resourceType: entry.resourceType ?? null, linkName: String(linkName) };
2443
+ }
2444
+
2301
2445
  // Secrets are referenced by NAME, never by value, in the model. A resolver maps
2302
2446
  // a name → value at run time. The wrapper injects them into the child ENV, so
2303
2447
  // values never appear in argv or `docker inspect`.
@@ -3416,7 +3560,7 @@ function runAgentJob(profile, job, opts = {}) {
3416
3560
 
3417
3561
  // Shape the io.nanobpm.agentResult output envelope. When a repository was
3418
3562
  // provisioned (increment 2a), the `git` block adds branch/commits/push/PR facts.
3419
- function buildResultEnvelope(result, { sandbox, image, git, result: agentResult } = {}) {
3563
+ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult, promptResourceKey } = {}) {
3420
3564
  const status = result.ok ? 'completed' : (result.timedOut ? 'timedOut' : 'failed');
3421
3565
  const env = {
3422
3566
  schemaVersion: RESULT_ENVELOPE_SCHEMA_VERSION,
@@ -3430,6 +3574,10 @@ function buildResultEnvelope(result, { sandbox, image, git, result: agentResult
3430
3574
  signal: result.signal ?? null,
3431
3575
  error: result.error ?? null,
3432
3576
  };
3577
+ // Audit (issue #63): record which linked-resource key supplied the base prompt.
3578
+ // The engine only keeps `latest` per resourceId (no pinning), so recording the
3579
+ // resolved key is the only reproducibility handle for which prompt version ran.
3580
+ if (promptResourceKey != null) env.promptResourceKey = String(promptResourceKey);
3433
3581
  // The agent's structured result (as returned via $AGENT_RESULT_FILE / sentinel),
3434
3582
  // preserved verbatim for auditability even when merged into the completion vars.
3435
3583
  if (isPlainObject(agentResult)) env.result = agentResult;
@@ -3685,6 +3833,10 @@ async function workAgent(req, flags) {
3685
3833
  const jobTypes = [...new Set([...matrix, ...extraJobTypes])];
3686
3834
  const camunda = globalThis.c8ctl.createClient();
3687
3835
 
3836
+ // Broker REST endpoint for live linked-resource prompts (issue #63) — the same
3837
+ // nano endpoint this worker already talks to. Resolved once at startup.
3838
+ const restConfig = resolveBrokerRestConfig();
3839
+
3688
3840
  logger.info(`Putting "${name}" [${profile.rank}] to work → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
3689
3841
  logger.info(` worker: ${workerName}`);
3690
3842
  logger.info(` model: ${profile.model || '(none)'}; capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
@@ -3853,9 +4005,33 @@ async function workAgent(req, flags) {
3853
4005
  }
3854
4006
  }
3855
4007
 
4008
+ // Live agent prompt (issue #63): if the job declares a `linkName: prompt`
4009
+ // linked resource, fetch its LATEST deployed content and use it as the
4010
+ // base prompt (it wins over the header-baked task.prompt). A declared
4011
+ // prompt resource that can't be fetched is a provisioning failure — fail
4012
+ // (retryable) rather than run an agent with an empty prompt.
4013
+ let promptResourceKey = null;
4014
+ let basePromptOverride;
4015
+ try {
4016
+ const linked = await resolveLinkedPrompt(job.customHeaders ?? {}, {
4017
+ baseUrl: restConfig.baseUrl,
4018
+ token: restConfig.token,
4019
+ });
4020
+ if (linked) {
4021
+ basePromptOverride = linked.basePrompt;
4022
+ promptResourceKey = linked.resourceKey;
4023
+ logger.info(`[${jobType}] job ${job.jobKey} base prompt from linked resource key ${promptResourceKey} (linkName=${linked.linkName}, ${Buffer.byteLength(String(basePromptOverride), 'utf8')} bytes)`);
4024
+ }
4025
+ } catch (err) {
4026
+ const retries = Math.max(0, (Number(job.retries) || 1) - 1);
4027
+ const msg = err instanceof ProvisionError ? err.message : `prompt resource fetch failed: ${err.message}`;
4028
+ logger.warn(`[${jobType}] job ${job.jobKey} not provisioned — ${msg}; retries left ${retries}`);
4029
+ return job.fail({ errorMessage: msg.slice(0, 2000), retries, retryBackOff: 15_000 });
4030
+ }
4031
+
3856
4032
  // Assemble + normalize the task envelope from headers (defaults) and
3857
4033
  // variables (overrides), then resolve any secrets it references.
3858
- const envelope = normalizeTaskEnvelope(job.customHeaders ?? {}, job.variables ?? {});
4034
+ const envelope = normalizeTaskEnvelope(job.customHeaders ?? {}, job.variables ?? {}, { basePromptOverride });
3859
4035
  const { resolved, missing, names } = resolveJobSecrets(secretResolver, envelope);
3860
4036
  if (missing.length > 0) {
3861
4037
  const retries = Math.max(0, (Number(job.retries) || 1) - 1);
@@ -4000,7 +4176,7 @@ async function workAgent(req, flags) {
4000
4176
  if (resultDir) { try { rmSync(resultDir, { recursive: true, force: true }); } catch { /* best effort */ } liveRunDirs.delete(resultDir); }
4001
4177
  const resultVars = sanitizeResultVars(rawResult);
4002
4178
 
4003
- const resultEnvelope = buildResultEnvelope(result, { sandbox, image, git: gitResult, result: rawResult });
4179
+ const resultEnvelope = buildResultEnvelope(result, { sandbox, image, git: gitResult, result: rawResult, promptResourceKey });
4004
4180
  if (result.ok) {
4005
4181
  const gitNote = gitResult
4006
4182
  ? ` [${gitResult.branch ? `branch ${gitResult.branch}` : 'detached HEAD'}: ${gitResult.commits.length} commit(s), ${gitResult.branch ? (gitResult.pushed ? 'pushed' : (gitResult.pushError ? 'push FAILED' : 'not pushed')) : 'no branch to push'}${gitResult.pr?.found ? `, PR #${gitResult.pr.number}` : ''}]`
@@ -6894,6 +7070,12 @@ export {
6894
7070
  export {
6895
7071
  normalizeTaskEnvelope,
6896
7072
  collectEnvelopeFrom,
7073
+ parseLinkedResources,
7074
+ pickLinkedResource,
7075
+ resolveBrokerRestConfig,
7076
+ resourceContentUrl,
7077
+ fetchLinkedResourceContent,
7078
+ resolveLinkedPrompt,
6897
7079
  coerceBool,
6898
7080
  coerceInt,
6899
7081
  deepMerge,
@@ -6940,6 +7122,8 @@ export {
6940
7122
  derivePollTimeoutMs,
6941
7123
  AGENT_TASK_NS,
6942
7124
  AGENT_RESULT_KEY,
7125
+ LINKED_RESOURCES_HEADER,
7126
+ DEFAULT_PROMPT_LINK_NAME,
6943
7127
  RESULT_SENTINEL,
6944
7128
  RESERVED_RESULT_KEYS,
6945
7129
  SANDBOXES,
@@ -7013,9 +7197,11 @@ export const metadata = {
7013
7197
  { 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' },
7014
7198
  { command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
7015
7199
  { 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' },
7200
+ { command: 'c8ctl nano hire --name coder --rank senior --command copilot --terminal pty', description: 'Opt this role into a full, steerable live terminal (PTY) streamed on the agentic relay lane (default: pipe)' },
7016
7201
  { command: 'c8ctl nano assign reviewer code-review,testing', description: 'Grant more capabilities (comma-separated, like hire) to an existing hire — additive; running workers hot-reload it' },
7017
7202
  { command: 'c8ctl nano work reviewer', description: 'Spawn Nano job workers for the "reviewer" profile and poll for work' },
7018
7203
  { command: 'c8ctl nano work coder --sandbox docker --image ghcr.io/acme/agent:1', description: 'Run jobs in isolated containers with disk-hygiene reaping' },
7204
+ { command: 'NANO_AGENTIC_URL=http://localhost:8080 NANO_AGENTIC_TOKEN=<identity-token> NANO_AGENTIC_CREDENTIAL=<capability-cred> c8ctl nano work reviewer', description: 'Enrol the worker on the app\'s same-port /agentic channel so it appears live (presence + relay terminals) on the Workforce visibility page' },
7019
7205
  { command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
7020
7206
  { command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
7021
7207
  { command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, serviced job / idle, restarts, uptime) without the console' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.30.0",
3
+ "version": "1.31.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",
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "optionalDependencies": {
59
59
  "node-pty": "^1.0.0",
60
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.30.0",
61
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.30.0",
62
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.30.0",
63
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.30.0",
64
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.30.0",
65
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.30.0",
66
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.30.0"
60
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.31.0",
61
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.31.0",
62
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.31.0",
63
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.31.0",
64
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.31.0",
65
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.31.0",
66
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.31.0"
67
67
  }
68
68
  }