c8ctl-plugin-nano 1.30.0 → 1.32.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 +95 -2
- package/c8ctl-plugin.js +403 -35
- package/package.json +8 -8
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
|
|
@@ -612,7 +696,7 @@ release onto a machine that already has nano installed:
|
|
|
612
696
|
|
|
613
697
|
```bash
|
|
614
698
|
c8ctl nano update # check npm for a newer release and install it
|
|
615
|
-
c8ctl nano update --check #
|
|
699
|
+
c8ctl nano update --check # report whether an update is available (no install)
|
|
616
700
|
```
|
|
617
701
|
|
|
618
702
|
`update` compares the installed plugin version against the latest published on
|
|
@@ -622,6 +706,15 @@ with it. It only ever drives npm — it never touches the private upstream sourc
|
|
|
622
706
|
so it works for any npm-installed user. After updating, restart any running
|
|
623
707
|
cluster (`c8ctl nano restart`) so it picks up the new binary.
|
|
624
708
|
|
|
709
|
+
Whenever an update is available, `update` (and `update --check`) also prints a
|
|
710
|
+
**changelog of what changed since the installed release** — the per-version
|
|
711
|
+
"Features" / "Bug Fixes" notes pulled from the plugin's public
|
|
712
|
+
[GitHub Releases](https://github.com/jwulf/c8ctl-plugin-nano/releases) (where
|
|
713
|
+
semantic-release records them). This lookup is best-effort and non-blocking: if
|
|
714
|
+
GitHub is unreachable or rate-limited it degrades to a link to the releases page
|
|
715
|
+
and the update proceeds normally. Set `GH_TOKEN` (or `GITHUB_TOKEN`) to raise the
|
|
716
|
+
unauthenticated API rate limit.
|
|
717
|
+
|
|
625
718
|
If the plugin is running from a local checkout rather than a global npm install,
|
|
626
719
|
`update` prints the manual command instead of reinstalling in place.
|
|
627
720
|
|
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
|
-
|
|
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:
|
|
2276
|
-
// else
|
|
2277
|
-
|
|
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
|
-
// ----
|
|
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}` : ''}]`
|
|
@@ -5613,6 +5789,183 @@ function compareSemver(a, b) {
|
|
|
5613
5789
|
return 0;
|
|
5614
5790
|
}
|
|
5615
5791
|
|
|
5792
|
+
// ---------------------------------------------------------------------------
|
|
5793
|
+
// Update changelog. `update --check` (and the pre-pull path of a real update)
|
|
5794
|
+
// shows what changed between the installed release and latest. The authoritative
|
|
5795
|
+
// source is this plugin's PUBLIC GitHub Releases — semantic-release records the
|
|
5796
|
+
// generated notes there (@semantic-release/github). The committed CHANGELOG.md
|
|
5797
|
+
// is deliberately NOT maintained (the release config dropped the changelog/git
|
|
5798
|
+
// plugins so it never pushes to the protected `main`) and isn't even in the npm
|
|
5799
|
+
// `files`, so it can't be the source. Every lookup here is best-effort and
|
|
5800
|
+
// non-blocking: any failure (offline, rate-limited, private) degrades to a link,
|
|
5801
|
+
// never to a failed `update`.
|
|
5802
|
+
// ---------------------------------------------------------------------------
|
|
5803
|
+
|
|
5804
|
+
/** `owner/repo` parsed from the plugin package's `repository` field (null if absent). */
|
|
5805
|
+
function githubRepoSlug() {
|
|
5806
|
+
try {
|
|
5807
|
+
const pkg = JSON.parse(readFileSync(join(pluginDir, 'package.json'), 'utf8'));
|
|
5808
|
+
const raw = pkg?.repository?.url ?? (typeof pkg?.repository === 'string' ? pkg.repository : '');
|
|
5809
|
+
const m = String(raw).match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?(?:[/#?].*)?$/i);
|
|
5810
|
+
return m ? m[1] : null;
|
|
5811
|
+
} catch {
|
|
5812
|
+
return null;
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
|
|
5816
|
+
/**
|
|
5817
|
+
* Keep only the releases strictly newer than `currentVersion` and no newer than
|
|
5818
|
+
* `latestVersion` (when known), newest-first. Pure over its `releases` input (an
|
|
5819
|
+
* array of GitHub release objects) so it is unit-testable without a network call.
|
|
5820
|
+
*/
|
|
5821
|
+
function filterReleasesSince(releases, currentVersion, latestVersion) {
|
|
5822
|
+
if (!Array.isArray(releases)) return [];
|
|
5823
|
+
const items = [];
|
|
5824
|
+
for (const r of releases) {
|
|
5825
|
+
if (!r || r.draft || r.prerelease) continue;
|
|
5826
|
+
const tag = r.tag_name || r.name || '';
|
|
5827
|
+
const norm = String(tag).replace(/^v/, '');
|
|
5828
|
+
// Require a plain vX.Y.Z tag: a prerelease/build suffix (e.g. -rc.1, +meta)
|
|
5829
|
+
// must exclude the release rather than be normalised away into the window.
|
|
5830
|
+
if (!/^\d+\.\d+\.\d+$/.test(norm)) continue;
|
|
5831
|
+
const ver = norm;
|
|
5832
|
+
if (!currentVersion || compareSemver(ver, currentVersion) <= 0) continue;
|
|
5833
|
+
if (latestVersion && compareSemver(ver, latestVersion) > 0) continue;
|
|
5834
|
+
items.push({ version: ver, tag, body: r.body || '', url: r.html_url || '' });
|
|
5835
|
+
}
|
|
5836
|
+
items.sort((a, b) => compareSemver(b.version, a.version));
|
|
5837
|
+
return items;
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
/**
|
|
5841
|
+
* Render one semantic-release release body to tight terminal lines: drop the
|
|
5842
|
+
* redundant `# [x.y.z](…)` header, turn `### Features` into a `Features:` label,
|
|
5843
|
+
* flatten `* **scope:** subject ([abc](url))` bullets to `• scope: subject`
|
|
5844
|
+
* (stripping any `([label](url))` commit/PR link groups and inlining any
|
|
5845
|
+
* remaining `[text](url)` as its text). Returns an array of already-indented lines.
|
|
5846
|
+
*/
|
|
5847
|
+
function renderReleaseBody(body) {
|
|
5848
|
+
const out = [];
|
|
5849
|
+
for (const line of String(body).split(/\r?\n/)) {
|
|
5850
|
+
if (/^#{1,2}\s+\[?\d+\.\d+\.\d+/.test(line)) continue; // redundant version header
|
|
5851
|
+
const heading = line.match(/^#{2,3}\s+(.*\S)\s*$/);
|
|
5852
|
+
if (heading) {
|
|
5853
|
+
out.push(` ${heading[1]}:`);
|
|
5854
|
+
continue;
|
|
5855
|
+
}
|
|
5856
|
+
const bullet = line.match(/^\s*[*-]\s+(.*)$/);
|
|
5857
|
+
if (bullet) {
|
|
5858
|
+
let text = bullet[1]
|
|
5859
|
+
.replace(/\s*\(\[[^\]]*\]\([^)]*\)\)/g, '') // ([label](url)) commit/PR link groups
|
|
5860
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // inline [text](url) -> text
|
|
5861
|
+
.replace(/\*\*(.*?)\*\*/g, '$1') // **scope** -> scope
|
|
5862
|
+
.replace(/\s+/g, ' ')
|
|
5863
|
+
.trim();
|
|
5864
|
+
if (text) out.push(` \u2022 ${text}`);
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
return out;
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5870
|
+
/** Cap on release pages walked, so a repo with a huge history can never hang the walk. */
|
|
5871
|
+
const RELEASE_PAGE_LIMIT = 20;
|
|
5872
|
+
|
|
5873
|
+
/**
|
|
5874
|
+
* True once a page contains a published (non-draft/non-prerelease) vX.Y.Z release
|
|
5875
|
+
* at or below `current`. Because the releases API returns newest-first, everything
|
|
5876
|
+
* after that point is older than the installed version, so the walk can stop.
|
|
5877
|
+
*/
|
|
5878
|
+
function reachedInstalledRelease(page, current) {
|
|
5879
|
+
if (!current || !Array.isArray(page)) return false;
|
|
5880
|
+
for (const r of page) {
|
|
5881
|
+
if (!r || r.draft || r.prerelease) continue;
|
|
5882
|
+
const norm = String(r.tag_name || r.name || '').replace(/^v/, '');
|
|
5883
|
+
if (!/^\d+\.\d+\.\d+$/.test(norm)) continue;
|
|
5884
|
+
if (compareSemver(norm, current) <= 0) return true;
|
|
5885
|
+
}
|
|
5886
|
+
return false;
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
/**
|
|
5890
|
+
* Fetch this plugin's GitHub releases newer than `current` (best-effort; null on
|
|
5891
|
+
* any failure). Paginates newest-first, stopping as soon as it reaches the
|
|
5892
|
+
* installed release (or a bounded page cap), so the window stays accurate even
|
|
5893
|
+
* when the installed version is far behind and there are >100 releases since.
|
|
5894
|
+
*/
|
|
5895
|
+
async function fetchReleaseNotesSince(slug, current, latest, timeoutMs = 5000) {
|
|
5896
|
+
if (!slug) return null;
|
|
5897
|
+
try {
|
|
5898
|
+
const ctrl = new AbortController();
|
|
5899
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
5900
|
+
const headers = {
|
|
5901
|
+
accept: 'application/vnd.github+json',
|
|
5902
|
+
'user-agent': 'c8ctl-plugin-nano',
|
|
5903
|
+
'x-github-api-version': '2022-11-28',
|
|
5904
|
+
};
|
|
5905
|
+
const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
5906
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
5907
|
+
try {
|
|
5908
|
+
const all = [];
|
|
5909
|
+
for (let page = 1; page <= RELEASE_PAGE_LIMIT; page++) {
|
|
5910
|
+
const res = await fetch(
|
|
5911
|
+
`https://api.github.com/repos/${slug}/releases?per_page=100&page=${page}`,
|
|
5912
|
+
{ headers, redirect: 'follow', signal: ctrl.signal },
|
|
5913
|
+
);
|
|
5914
|
+
if (!res.ok) return null;
|
|
5915
|
+
const arr = await res.json();
|
|
5916
|
+
if (!Array.isArray(arr) || arr.length === 0) break;
|
|
5917
|
+
all.push(...arr);
|
|
5918
|
+
// Newest-first: once we hit the installed release (or a short final
|
|
5919
|
+
// page), everything remaining is older than `current` — stop early.
|
|
5920
|
+
if (arr.length < 100 || reachedInstalledRelease(arr, current)) break;
|
|
5921
|
+
}
|
|
5922
|
+
return filterReleasesSince(all, current, latest);
|
|
5923
|
+
} finally {
|
|
5924
|
+
clearTimeout(timer);
|
|
5925
|
+
}
|
|
5926
|
+
} catch {
|
|
5927
|
+
return null;
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
/**
|
|
5932
|
+
* Print the changelog between the installed release and `latest`. Best-effort:
|
|
5933
|
+
* on any fetch failure it prints a single line pointing at the releases page and
|
|
5934
|
+
* returns, so it can never block or fail an `update`.
|
|
5935
|
+
*/
|
|
5936
|
+
async function printChangelogSince(_name, current, latest) {
|
|
5937
|
+
const logger = getLogger();
|
|
5938
|
+
const slug = githubRepoSlug();
|
|
5939
|
+
const releasesUrl = slug ? `https://github.com/${slug}/releases` : null;
|
|
5940
|
+
const releases = await fetchReleaseNotesSince(slug, current, latest);
|
|
5941
|
+
|
|
5942
|
+
if (releases === null) {
|
|
5943
|
+
if (releasesUrl) logger.info(`See what changed: ${releasesUrl}`);
|
|
5944
|
+
logger.info('');
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
if (releases.length === 0) {
|
|
5948
|
+
// Nothing resolved between the two (only a build-metadata bump, or a
|
|
5949
|
+
// degraded resolution: current is null / tags don't match vX.Y.Z). Point
|
|
5950
|
+
// at the releases page so the best-effort feature still leaves a trail.
|
|
5951
|
+
if (releasesUrl) logger.info(`See what changed: ${releasesUrl}`);
|
|
5952
|
+
logger.info('');
|
|
5953
|
+
return;
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
logger.info(`What's changed since v${current ?? '?'}:`);
|
|
5957
|
+
logger.info('');
|
|
5958
|
+
for (const rel of releases) {
|
|
5959
|
+
logger.info(` v${rel.version}`);
|
|
5960
|
+
const lines = renderReleaseBody(rel.body);
|
|
5961
|
+
if (lines.length === 0) logger.info(' (no notes)');
|
|
5962
|
+
else for (const l of lines) logger.info(l);
|
|
5963
|
+
logger.info('');
|
|
5964
|
+
}
|
|
5965
|
+
if (releasesUrl) logger.info(`Full release notes: ${releasesUrl}`);
|
|
5966
|
+
logger.info('');
|
|
5967
|
+
}
|
|
5968
|
+
|
|
5616
5969
|
/**
|
|
5617
5970
|
* Resolve how npm must be spawned on the given platform. Spawning `npm`
|
|
5618
5971
|
* directly is not portable: on Windows npm is a `npm.cmd` shim, so bare
|
|
@@ -5745,7 +6098,8 @@ function manualUpdateCommand(name, info) {
|
|
|
5745
6098
|
return ` npm install -g ${name}@latest`;
|
|
5746
6099
|
}
|
|
5747
6100
|
|
|
5748
|
-
function updatePlugin(req) {
|
|
6101
|
+
async function updatePlugin(req) {
|
|
6102
|
+
const logger = getLogger();
|
|
5749
6103
|
const { name, version: current } = pluginPackage();
|
|
5750
6104
|
|
|
5751
6105
|
// The nano server binary ships with the plugin as its platform package
|
|
@@ -5766,44 +6120,47 @@ function updatePlugin(req) {
|
|
|
5766
6120
|
const info = pluginInstallInfo();
|
|
5767
6121
|
const manual = manualUpdateCommand(name, info);
|
|
5768
6122
|
|
|
5769
|
-
|
|
6123
|
+
logger.info(`Installed: ${name} v${current ?? '?'}${nanoNote}`);
|
|
5770
6124
|
|
|
5771
6125
|
let latest;
|
|
5772
6126
|
try {
|
|
5773
6127
|
latest = npmLatestVersion(name);
|
|
5774
6128
|
} catch (err) {
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
6129
|
+
logger.info(`Could not check npm for updates: ${err.message}`);
|
|
6130
|
+
logger.info('Pull the latest release manually with:');
|
|
6131
|
+
logger.info(manual);
|
|
5778
6132
|
return;
|
|
5779
6133
|
}
|
|
5780
|
-
|
|
5781
|
-
|
|
6134
|
+
logger.info(`Latest: ${name} v${latest} (npm)`);
|
|
6135
|
+
logger.info('');
|
|
5782
6136
|
|
|
5783
6137
|
if (current && compareSemver(current, latest) >= 0) {
|
|
5784
6138
|
if (!nanoBin) {
|
|
5785
6139
|
// Plugin is current but npm never fetched the matching server binary.
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
6140
|
+
logger.info('Plugin is current, but the nano server binary is not installed for this platform.');
|
|
6141
|
+
logger.info('Provision it by reinstalling the plugin so npm fetches the platform package:');
|
|
6142
|
+
logger.info(' c8ctl sync plugin');
|
|
5789
6143
|
return;
|
|
5790
6144
|
}
|
|
5791
|
-
|
|
6145
|
+
logger.info('Already on the latest release — nothing to do.');
|
|
5792
6146
|
return;
|
|
5793
6147
|
}
|
|
5794
6148
|
|
|
5795
|
-
|
|
6149
|
+
logger.info(`Update available: v${current ?? '?'} -> v${latest}`);
|
|
6150
|
+
logger.info('');
|
|
6151
|
+
|
|
6152
|
+
await printChangelogSince(name, current, latest);
|
|
5796
6153
|
|
|
5797
6154
|
if (req.check) {
|
|
5798
|
-
|
|
5799
|
-
|
|
6155
|
+
logger.info('Run `c8ctl nano update` to pull it (or manually):');
|
|
6156
|
+
logger.info(manual);
|
|
5800
6157
|
return;
|
|
5801
6158
|
}
|
|
5802
6159
|
|
|
5803
6160
|
if (info.mode === 'local') {
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
6161
|
+
logger.info('This plugin runs from a local checkout, so it cannot self-update in place.');
|
|
6162
|
+
logger.info('Update it with:');
|
|
6163
|
+
logger.info(manual);
|
|
5807
6164
|
return;
|
|
5808
6165
|
}
|
|
5809
6166
|
|
|
@@ -5812,8 +6169,8 @@ function updatePlugin(req) {
|
|
|
5812
6169
|
? ['install', `${name}@${latest}`, '--prefix', info.prefix]
|
|
5813
6170
|
: ['install', '-g', `${name}@${latest}`];
|
|
5814
6171
|
const where = info.mode === 'managed' ? 'the c8ctl plugin store' : "npm's global prefix";
|
|
5815
|
-
|
|
5816
|
-
|
|
6172
|
+
logger.info(`Pulling ${name}@${latest} into ${where}...`);
|
|
6173
|
+
logger.info('');
|
|
5817
6174
|
try {
|
|
5818
6175
|
runNpm(installArgs, { stdio: 'inherit' });
|
|
5819
6176
|
} catch (err) {
|
|
@@ -5830,14 +6187,14 @@ function updatePlugin(req) {
|
|
|
5830
6187
|
`npm ${installArgs.join(' ')} failed${code}. ${hint}`,
|
|
5831
6188
|
);
|
|
5832
6189
|
}
|
|
5833
|
-
|
|
6190
|
+
logger.info('');
|
|
5834
6191
|
if (info.mode === 'managed') {
|
|
5835
|
-
|
|
6192
|
+
logger.info(`Updated to v${latest}. The new plugin and bundled nano server load on your next c8ctl command.`);
|
|
5836
6193
|
} else {
|
|
5837
|
-
|
|
6194
|
+
logger.info(`Updated to v${latest}.`);
|
|
5838
6195
|
}
|
|
5839
|
-
|
|
5840
|
-
|
|
6196
|
+
logger.info('Restart any running cluster to use the new server binary:');
|
|
6197
|
+
logger.info(' c8ctl nano restart');
|
|
5841
6198
|
}
|
|
5842
6199
|
|
|
5843
6200
|
// ---------------------------------------------------------------------------
|
|
@@ -6885,6 +7242,7 @@ function parseProcessosRequest(args, flags) {
|
|
|
6885
7242
|
export { resolveBinary, findBinary, launcherEnvMarkers };
|
|
6886
7243
|
export { setConfig, unsetConfig, readConfig, writeConfig, getConfigFile, SETTING_ALIASES };
|
|
6887
7244
|
export { buildNpmInvocation };
|
|
7245
|
+
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
|
6888
7246
|
export {
|
|
6889
7247
|
webConsoleUrl,
|
|
6890
7248
|
consoleLinkLabel,
|
|
@@ -6894,6 +7252,12 @@ export {
|
|
|
6894
7252
|
export {
|
|
6895
7253
|
normalizeTaskEnvelope,
|
|
6896
7254
|
collectEnvelopeFrom,
|
|
7255
|
+
parseLinkedResources,
|
|
7256
|
+
pickLinkedResource,
|
|
7257
|
+
resolveBrokerRestConfig,
|
|
7258
|
+
resourceContentUrl,
|
|
7259
|
+
fetchLinkedResourceContent,
|
|
7260
|
+
resolveLinkedPrompt,
|
|
6897
7261
|
coerceBool,
|
|
6898
7262
|
coerceInt,
|
|
6899
7263
|
deepMerge,
|
|
@@ -6940,6 +7304,8 @@ export {
|
|
|
6940
7304
|
derivePollTimeoutMs,
|
|
6941
7305
|
AGENT_TASK_NS,
|
|
6942
7306
|
AGENT_RESULT_KEY,
|
|
7307
|
+
LINKED_RESOURCES_HEADER,
|
|
7308
|
+
DEFAULT_PROMPT_LINK_NAME,
|
|
6943
7309
|
RESULT_SENTINEL,
|
|
6944
7310
|
RESERVED_RESULT_KEYS,
|
|
6945
7311
|
SANDBOXES,
|
|
@@ -7006,16 +7372,18 @@ export const metadata = {
|
|
|
7006
7372
|
{ command: 'c8ctl nano set model-dir <path>', description: 'Set the workspace dir (models + workers)' },
|
|
7007
7373
|
{ command: 'c8ctl nano config', description: 'Show current plugin configuration and paths' },
|
|
7008
7374
|
{ command: 'c8ctl nano update', description: 'Pull the latest published nano release (re-installs via npm)' },
|
|
7009
|
-
{ command: 'c8ctl nano update --check', description: 'Check
|
|
7375
|
+
{ command: 'c8ctl nano update --check', description: 'Check for a newer nano release and show the changelog since the installed version (no install)' },
|
|
7010
7376
|
{ command: 'c8ctl nano hire', description: 'Interactively create a CLI agent worker profile (name, rank, command, model, capabilities)' },
|
|
7011
7377
|
{ command: 'c8ctl nano hire --name reviewer --rank senior --command copilot --model gpt-5 --capabilities code-review,testing', description: 'Create a profile non-interactively' },
|
|
7012
7378
|
{ command: 'c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all', description: 'Hire copilot with a command-line switch (copilot --allow-all)' },
|
|
7013
7379
|
{ 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
7380
|
{ command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
|
|
7015
7381
|
{ 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' },
|
|
7382
|
+
{ 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
7383
|
{ 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
7384
|
{ command: 'c8ctl nano work reviewer', description: 'Spawn Nano job workers for the "reviewer" profile and poll for work' },
|
|
7018
7385
|
{ command: 'c8ctl nano work coder --sandbox docker --image ghcr.io/acme/agent:1', description: 'Run jobs in isolated containers with disk-hygiene reaping' },
|
|
7386
|
+
{ 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
7387
|
{ command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
|
|
7020
7388
|
{ command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
|
|
7021
7389
|
{ command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, serviced job / idle, restarts, uptime) without the console' },
|
|
@@ -7061,7 +7429,7 @@ export const commands = {
|
|
|
7061
7429
|
purge: { type: 'boolean', description: 'stop/restart: also delete per-node engine data' },
|
|
7062
7430
|
force: { type: 'boolean', description: 'start: stop any existing cluster first' },
|
|
7063
7431
|
workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
|
|
7064
|
-
check: { type: 'boolean', description: 'update:
|
|
7432
|
+
check: { type: 'boolean', description: 'update: report whether a new release is available (with the changelog since the installed version); do not install' },
|
|
7065
7433
|
binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
|
|
7066
7434
|
name: { type: 'string', description: 'work/supervisor add: worker name (auto ‹host›-‹profile›-‹random› if omitted); hire/assign: agent profile name' },
|
|
7067
7435
|
rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
|
|
@@ -7140,7 +7508,7 @@ export const commands = {
|
|
|
7140
7508
|
showConfig();
|
|
7141
7509
|
break;
|
|
7142
7510
|
case 'update':
|
|
7143
|
-
updatePlugin(req);
|
|
7511
|
+
await updatePlugin(req);
|
|
7144
7512
|
break;
|
|
7145
7513
|
case 'hire':
|
|
7146
7514
|
await hireWorker(req, flags);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.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.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.32.0",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.32.0",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.32.0",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.32.0",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.32.0",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.32.0",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.32.0"
|
|
67
67
|
}
|
|
68
68
|
}
|