c8ctl-plugin-nano 1.18.0 → 1.19.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 +49 -1
- package/c8ctl-plugin.js +213 -12
- package/package.json +8 -10
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ It adds a single `nano` command:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
c8ctl nano start|status|stop|restart|logs|pause|resume|clean|set|config|update
|
|
27
|
-
c8ctl nano hire|work #
|
|
27
|
+
c8ctl nano hire|assign|work # hire/assign manage agent profiles; work runs one as a Nano job worker
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`nano start N` spawns **N** nanobpmn node processes wired to talk to each other
|
|
@@ -157,6 +157,24 @@ c8ctl nano hire --name coder --rank senior --command copilot --arg --allow-all
|
|
|
157
157
|
c8ctl nano hire --list
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
**`assign <name> [capabilities...]`** grants new capabilities (roles) to an
|
|
161
|
+
existing hire without re-running `hire`. Capabilities are **added** to (unioned
|
|
162
|
+
with) the profile's current set — `assign` never removes a role — and the
|
|
163
|
+
updated job-type matrix is printed. Restart the profile's workers so they pick
|
|
164
|
+
up the new job types:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Give an existing reviewer two more capabilities
|
|
168
|
+
c8ctl nano assign reviewer triage refactoring
|
|
169
|
+
|
|
170
|
+
# --capabilities works too (comma-separated), equivalent to the positionals above
|
|
171
|
+
c8ctl nano assign reviewer --capabilities triage,refactoring
|
|
172
|
+
|
|
173
|
+
# then restart its workers to service the new job types
|
|
174
|
+
c8ctl nano work reviewer
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
|
|
160
178
|
**`work <name>`** loads the profile, connects with the c8ctl SDK client, and
|
|
161
179
|
registers one job worker per token in the **rank × capability matrix**, then
|
|
162
180
|
polls for work in the foreground until Ctrl-C. For rank `senior` and
|
|
@@ -207,6 +225,26 @@ the job with a decremented retry count, and a job that outlives `--job-timeout`
|
|
|
207
225
|
is killed. Profiles are stored in the plugin's `config.json` (see `c8ctl nano
|
|
208
226
|
config`).
|
|
209
227
|
|
|
228
|
+
> **Activation lock vs. kill deadline.** `--job-timeout` is the harness *kill*
|
|
229
|
+
> deadline. The broker's job-activation lock is derived as `--job-timeout +
|
|
230
|
+
> --lock-grace` (grace defaults to `120000`ms) so it strictly outlasts the kill:
|
|
231
|
+
> the worker always reports the outcome (complete/fail) before the lock lapses.
|
|
232
|
+
> Without that gap a lock expiring exactly as the harness dies lets the broker
|
|
233
|
+
> re-activate the still-retryable job (a second agent starts) and the stale
|
|
234
|
+
> `fail` is rejected with a 409 "job cannot be failed in the current state".
|
|
235
|
+
> Raising `--job-timeout` alone does **not** fix this — it moves both coupled
|
|
236
|
+
> deadlines together; widen `--lock-grace` (or keep the default) instead.
|
|
237
|
+
|
|
238
|
+
> **Long-poll window.** `--poll-timeout` (default `30000`ms) is how long the
|
|
239
|
+
> broker holds each `activateJobs` request open waiting for work before returning
|
|
240
|
+
> empty. A longer window keeps an idle worker on **one** connection for that whole
|
|
241
|
+
> window instead of reconnecting every few seconds — cutting the number of
|
|
242
|
+
> connection establishments, and thus the chances of hitting a transient connect
|
|
243
|
+
> error (`ECONNREFUSED` / connect-timeout) on a flaky link. It maps straight to
|
|
244
|
+
> the SDK's `pollTimeoutMs` → the broker's `requestTimeout`: `0` selects the
|
|
245
|
+
> broker's own default (~5s) and a negative value returns immediately when no job
|
|
246
|
+
> is available.
|
|
247
|
+
|
|
210
248
|
> **Trust boundary.** The profile `command` is run through a shell so you can
|
|
211
249
|
> write a full invocation (args, pipes, multi-word commands). It is
|
|
212
250
|
> **operator-authored** — only what you put in your own `config.json` is
|
|
@@ -236,6 +274,16 @@ Element templates emit flat dotpath header keys (strings); the plugin expands
|
|
|
236
274
|
them into a nested object and coerces `"true"/"false"` → bool and numeric
|
|
237
275
|
strings → int. The normalized shape is
|
|
238
276
|
`{ schemaVersion, repository{provider,url,ref,depth,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
|
|
277
|
+
|
|
278
|
+
**Prompt = base + optional verbatim append.** The agent's prompt resolves to
|
|
279
|
+
`task.prompt` (typically a model header filled at deploy time), falling back to a
|
|
280
|
+
plain `prompt`/`task` variable. Because a header-delivered base prompt can't be
|
|
281
|
+
composed in FEEL, a task may supply per-instance context via **`task.appendPrompt`**
|
|
282
|
+
(reserved) or a plain **`appendPrompt`** variable — it is concatenated onto the base
|
|
283
|
+
**verbatim, with no injected separator** (the model's ioMapping owns any leading
|
|
284
|
+
separator/preamble), so a null/empty append leaves the base untouched. This lets the
|
|
285
|
+
static prompt live in a model header/side-car while the dynamic tail (e.g. plan-revision
|
|
286
|
+
feedback, a per-task brief) is built per instance.
|
|
239
287
|
On completion the plugin writes an **output envelope** back under
|
|
240
288
|
`io.nanobpm.agentResult` (`{schemaVersion, status, sandbox, image, output, truncated, stderrTruncated, exitCode, signal, error}`). When a repository was
|
|
241
289
|
provisioned (below) it also carries `{repository, branch, baseSha, headSha, commits[], pushed, pushError?, gitError?, pr?}`.
|
package/c8ctl-plugin.js
CHANGED
|
@@ -368,7 +368,7 @@ function launcherEnvMarkers(resolved) {
|
|
|
368
368
|
// Argument parsing
|
|
369
369
|
// ---------------------------------------------------------------------------
|
|
370
370
|
|
|
371
|
-
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update', 'hire', 'work'];
|
|
371
|
+
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update', 'hire', 'assign', 'work'];
|
|
372
372
|
|
|
373
373
|
/**
|
|
374
374
|
* Parse positional args + flags into a normalized request.
|
|
@@ -1478,7 +1478,72 @@ function parseJobTypeFlags(input) {
|
|
|
1478
1478
|
return { jobTypes, errors };
|
|
1479
1479
|
}
|
|
1480
1480
|
|
|
1481
|
-
/**
|
|
1481
|
+
/**
|
|
1482
|
+
* The broker job-activation lock must strictly outlast the harness kill
|
|
1483
|
+
* deadline: the worker has to report the outcome (complete/fail) before the lock
|
|
1484
|
+
* lapses, or the broker re-activates the still-retryable job (a second agent
|
|
1485
|
+
* starts) and the stale `fail` is rejected 409 "job cannot be failed in the
|
|
1486
|
+
* current state". So lock = kill + grace. Non-finite or non-positive inputs
|
|
1487
|
+
* fall back to fixed defaults (5m kill / 2m grace). All inputs are coerced to
|
|
1488
|
+
* safe positive integers (positive fractional values floor to at least 1) and
|
|
1489
|
+
* grace is capped so a positive kill always fits and `kill + grace` stays within
|
|
1490
|
+
* the safe-integer range, so the invariant lock > kill holds strictly for every
|
|
1491
|
+
* accepted input — including values at or beyond 2^53 where float addition
|
|
1492
|
+
* would otherwise round `kill + grace` back down to `kill`.
|
|
1493
|
+
*
|
|
1494
|
+
* Returns BOTH derived deadlines from this single computation so the caller
|
|
1495
|
+
* never re-derives (and drifts): `killMs` is the *clamped* harness kill deadline
|
|
1496
|
+
* the caller must actually enforce, and `lockMs` is the broker activation lock.
|
|
1497
|
+
* The caller must use `killMs` — not the raw input — for the harness timeout, or
|
|
1498
|
+
* the lock > kill invariant breaks for large inputs (the raw input can exceed
|
|
1499
|
+
* the clamped `killMs`, and thus reach or exceed `lockMs`).
|
|
1500
|
+
*
|
|
1501
|
+
* @returns {{ killMs: number, lockMs: number }}
|
|
1502
|
+
*/
|
|
1503
|
+
function deriveJobLockMs(jobTimeoutMs, lockGraceMs) {
|
|
1504
|
+
const MAX = Number.MAX_SAFE_INTEGER;
|
|
1505
|
+
const toSafeMs = (value, fallback) => {
|
|
1506
|
+
// Floor positive fractional values to at least 1 so a sub-millisecond input
|
|
1507
|
+
// (e.g. 0.5) never collapses to a non-positive value.
|
|
1508
|
+
const n = Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
|
|
1509
|
+
return Math.min(n, MAX);
|
|
1510
|
+
};
|
|
1511
|
+
// Cap grace to MAX - 1 so there is always room for a positive kill while
|
|
1512
|
+
// keeping kill + grace within the safe-integer range.
|
|
1513
|
+
const grace = Math.min(toSafeMs(lockGraceMs, 2 * 60_000), MAX - 1);
|
|
1514
|
+
// Cap kill so kill + grace stays a safe integer and floor it at 1 so the
|
|
1515
|
+
// internal kill is always positive; the sum is then exact and strictly
|
|
1516
|
+
// greater than kill (never equal to it via float rounding).
|
|
1517
|
+
const killMs = Math.max(1, Math.min(toSafeMs(jobTimeoutMs, 5 * 60_000), MAX - grace));
|
|
1518
|
+
return { killMs, lockMs: killMs + grace };
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Resolve the broker long-poll window (ms) each `activateJobs` request is held
|
|
1523
|
+
* open before returning empty. A longer window keeps an idle worker on ONE open
|
|
1524
|
+
* connection for that whole window instead of reconnecting every few seconds,
|
|
1525
|
+
* cutting the number of connection establishments — and thus the number of
|
|
1526
|
+
* chances to hit a transient connect failure (ECONNREFUSED / connect-timeout)
|
|
1527
|
+
* on a flaky link.
|
|
1528
|
+
*
|
|
1529
|
+
* The value is passed straight through to the SDK as `pollTimeoutMs` → the
|
|
1530
|
+
* broker's `requestTimeout`, so the documented broker semantics apply: `0` =
|
|
1531
|
+
* broker default (~5s), a negative value = return immediately when no job is
|
|
1532
|
+
* available. Parsing is `parseInt`-style: only a flag with no leading integer
|
|
1533
|
+
* (absent, blank, or non-numeric such as `"abc"`) falls back to the default,
|
|
1534
|
+
* while a leading integer with trailing junk (e.g. `"30000ms"`) is honoured as
|
|
1535
|
+
* that integer. `0` and negatives are honoured too (which is why this cannot
|
|
1536
|
+
* reuse `intFlag`, whose "> 0" guard would floor them to the default).
|
|
1537
|
+
*
|
|
1538
|
+
* @returns {number}
|
|
1539
|
+
*/
|
|
1540
|
+
function derivePollTimeoutMs(flagValue, dflt = 30_000) {
|
|
1541
|
+
if (flagValue === undefined || flagValue === null || String(flagValue).trim() === '') {
|
|
1542
|
+
return dflt;
|
|
1543
|
+
}
|
|
1544
|
+
const n = Number.parseInt(String(flagValue), 10);
|
|
1545
|
+
return Number.isFinite(n) ? n : dflt;
|
|
1546
|
+
}
|
|
1482
1547
|
function isValidProfileName(name) {
|
|
1483
1548
|
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
|
|
1484
1549
|
}
|
|
@@ -1557,7 +1622,99 @@ function normalizeStoredProfile(name, profile) {
|
|
|
1557
1622
|
}
|
|
1558
1623
|
|
|
1559
1624
|
/**
|
|
1560
|
-
*
|
|
1625
|
+
* Merge additional capabilities into an already-normalized profile, returning a
|
|
1626
|
+
* new profile object with the union of capabilities (canonical order) and a
|
|
1627
|
+
* refreshed `updatedAt`. Pure (no config I/O), so it is unit-testable. Existing
|
|
1628
|
+
* fields — including `createdAt` — are preserved. `incoming` may be a
|
|
1629
|
+
* comma-string or an array. Returns `{ profile, added }`, where `added` lists
|
|
1630
|
+
* the newly gained capabilities (empty when the assign is a no-op).
|
|
1631
|
+
*/
|
|
1632
|
+
function applyAssign(existing, incoming, now = new Date().toISOString()) {
|
|
1633
|
+
const before = new Set(normalizeCapabilities(existing && existing.capabilities));
|
|
1634
|
+
const union = normalizeCapabilities([...before, ...normalizeCapabilities(incoming)]);
|
|
1635
|
+
const added = union.filter((c) => !before.has(c));
|
|
1636
|
+
return {
|
|
1637
|
+
profile: { ...existing, capabilities: union, updatedAt: now },
|
|
1638
|
+
added,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/**
|
|
1643
|
+
* Resolve the profile name and the raw comma-joined capability string for an
|
|
1644
|
+
* `assign` invocation from parsed positionals + flags. Pure (no I/O) so the
|
|
1645
|
+
* positional-slicing rules are unit-testable.
|
|
1646
|
+
*
|
|
1647
|
+
* When `--name` is supplied the name does NOT consume a positional, so every
|
|
1648
|
+
* positional is a capability. Otherwise the first positional is the name and
|
|
1649
|
+
* the rest are capabilities. `--capabilities a,b` is always appended.
|
|
1650
|
+
*/
|
|
1651
|
+
function resolveAssignInputs(req, flags) {
|
|
1652
|
+
const positional = Array.isArray(req?.positional) ? req.positional : [];
|
|
1653
|
+
const name = flags?.name ? String(flags.name).trim() : positional[0];
|
|
1654
|
+
const positionalCaps = flags?.name ? positional : positional.slice(1);
|
|
1655
|
+
const flagCaps = flags?.capabilities !== undefined ? String(flags.capabilities) : '';
|
|
1656
|
+
const incomingRaw = [...positionalCaps, flagCaps].filter(Boolean).join(',');
|
|
1657
|
+
return { name, incomingRaw };
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* assign — grant new capabilities (roles) to an existing hire without
|
|
1662
|
+
* re-running `hire`. The profile name is positional[0] (or `--name`);
|
|
1663
|
+
* capabilities are the remaining positionals and/or `--capabilities a,b`.
|
|
1664
|
+
* Capabilities are unioned with the profile's existing set (additive; assign
|
|
1665
|
+
* never removes a role) and the updated rank×capability job-type matrix is
|
|
1666
|
+
* printed. Re-run `work` to pick up the new job types.
|
|
1667
|
+
*/
|
|
1668
|
+
async function assignCapabilities(req, flags) {
|
|
1669
|
+
const logger = getLogger();
|
|
1670
|
+
const { name, incomingRaw } = resolveAssignInputs(req, flags);
|
|
1671
|
+
if (!name) {
|
|
1672
|
+
logger.error('Usage: c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
|
|
1673
|
+
logger.info('Grant new capabilities to an existing hire. List profiles with: c8ctl nano hire --list');
|
|
1674
|
+
process.exit(1);
|
|
1675
|
+
}
|
|
1676
|
+
if (!isValidProfileName(name)) {
|
|
1677
|
+
logger.error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore.`);
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (normalizeCapabilities(incomingRaw).length === 0) {
|
|
1682
|
+
logger.error('Provide at least one capability to assign.');
|
|
1683
|
+
logger.info(`Example: c8ctl nano assign ${name} code-review testing`);
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const raw = readHires()[name];
|
|
1688
|
+
if (!raw) {
|
|
1689
|
+
logger.error(`No hire named "${name}". List profiles with: c8ctl nano hire --list`);
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
const normalized = normalizeStoredProfile(name, raw);
|
|
1693
|
+
if (normalized.error) {
|
|
1694
|
+
logger.error(`Cannot assign to "${name}": ${normalized.error}. Re-create it with: c8ctl nano hire`);
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Preserve createdAt (normalizeStoredProfile drops it) on the canonical form.
|
|
1699
|
+
const base = {
|
|
1700
|
+
...normalized.profile,
|
|
1701
|
+
createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : new Date().toISOString(),
|
|
1702
|
+
};
|
|
1703
|
+
const { profile, added } = applyAssign(base, incomingRaw);
|
|
1704
|
+
if (added.length === 0) {
|
|
1705
|
+
logger.info(`"${name}" already has: ${profile.capabilities.join(', ') || '(none)'} — no change.`);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
writeHire(profile);
|
|
1709
|
+
|
|
1710
|
+
const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
|
|
1711
|
+
logger.info(`Assigned to "${name}" [${profile.rank}]: +${added.join(', ')}`);
|
|
1712
|
+
logger.info(` capabilities: ${profile.capabilities.join(', ')}`);
|
|
1713
|
+
logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
|
|
1714
|
+
logger.info(`Restart its workers to pick up the new roles: c8ctl nano work ${name}`);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1561
1718
|
* field can also be supplied via a flag (--name/--rank/--command/--model/
|
|
1562
1719
|
* --capabilities) for scripting. Prompts only for the fields still missing.
|
|
1563
1720
|
* `--list` prints existing profiles instead.
|
|
@@ -1966,8 +2123,21 @@ function normalizeTaskEnvelope(customHeaders, variables) {
|
|
|
1966
2123
|
};
|
|
1967
2124
|
|
|
1968
2125
|
const task = isPlainObject(raw.task) ? raw.task : {};
|
|
2126
|
+
// Base prompt: the reserved `task.prompt` (typically a model header filled at deploy time),
|
|
2127
|
+
// else a plain `prompt`/`task` job variable (the pre-header delivery path).
|
|
2128
|
+
const basePrompt = str(task.prompt) ?? str(variables?.prompt) ?? str(variables?.task);
|
|
2129
|
+
// Verbatim dynamic append: a header-delivered base prompt can't be composed in FEEL, so a task
|
|
2130
|
+
// may supply per-instance context (e.g. plan-revision feedback, a per-task brief) via the
|
|
2131
|
+
// reserved `task.appendPrompt`, or a plain `appendPrompt` variable. It is concatenated onto the
|
|
2132
|
+
// base with NO injected separator — the caller (the model's ioMapping) owns any leading
|
|
2133
|
+
// separator/preamble — so a null/empty append leaves the base prompt untouched.
|
|
2134
|
+
const appendPrompt = str(task.appendPrompt) ?? str(variables?.appendPrompt);
|
|
2135
|
+
const prompt =
|
|
2136
|
+
appendPrompt != null && appendPrompt !== ''
|
|
2137
|
+
? `${basePrompt ?? ''}${appendPrompt}`
|
|
2138
|
+
: basePrompt;
|
|
1969
2139
|
env.task = {
|
|
1970
|
-
prompt
|
|
2140
|
+
prompt,
|
|
1971
2141
|
promptFile: str(task.promptFile),
|
|
1972
2142
|
maxIterations: coerceInt(task.maxIterations, undefined),
|
|
1973
2143
|
timeoutMs: coerceInt(task.timeoutMs, undefined),
|
|
@@ -2679,6 +2849,22 @@ async function workAgent(req, flags) {
|
|
|
2679
2849
|
};
|
|
2680
2850
|
const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
|
|
2681
2851
|
const jobTimeoutMs = intFlag(flags?.['job-timeout'], 5 * 60_000);
|
|
2852
|
+
// The broker's job-activation lock MUST outlast the harness kill deadline: the
|
|
2853
|
+
// worker has to report the outcome (complete/fail) before the lock lapses,
|
|
2854
|
+
// otherwise the broker re-activates the still-retryable job (a second agent
|
|
2855
|
+
// starts) and the stale `fail` is rejected with a 409 "job cannot be failed in
|
|
2856
|
+
// the current state". So lock = harness kill + grace. Raising --job-timeout
|
|
2857
|
+
// alone does not help — it moves both coupled deadlines together.
|
|
2858
|
+
const lockGraceMs = intFlag(flags?.['lock-grace'], 2 * 60_000);
|
|
2859
|
+
// deriveJobLockMs is the single source of truth for both deadlines: the harness
|
|
2860
|
+
// MUST enforce the clamped `jobKillMs` (not the raw --job-timeout), or a very
|
|
2861
|
+
// large --job-timeout would outlive the broker lock and re-break the invariant.
|
|
2862
|
+
const { killMs: jobKillMs, lockMs: jobLockMs } = deriveJobLockMs(jobTimeoutMs, lockGraceMs);
|
|
2863
|
+
// Broker long-poll window: how long each activateJobs request is held open
|
|
2864
|
+
// waiting for work. 30s default so idle workers hold one connection open ~30s
|
|
2865
|
+
// rather than reconnecting every few seconds — fewer reconnects, fewer chances
|
|
2866
|
+
// to hit a transient connect error on a flaky link. Passed to the SDK verbatim.
|
|
2867
|
+
const pollTimeoutMs = derivePollTimeoutMs(flags?.['poll-timeout']);
|
|
2682
2868
|
|
|
2683
2869
|
// Sandbox: flag overrides the stored profile default. `none` runs on the host
|
|
2684
2870
|
// (legacy); `docker`/`podman` run each job in a throwaway labelled container.
|
|
@@ -2786,7 +2972,7 @@ async function workAgent(req, flags) {
|
|
|
2786
2972
|
if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
|
|
2787
2973
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
2788
2974
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
2789
|
-
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${
|
|
2975
|
+
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms; poll timeout: ${pollTimeoutMs}ms`);
|
|
2790
2976
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
2791
2977
|
|
|
2792
2978
|
const workers = jobTypes.map((jobType) =>
|
|
@@ -2794,7 +2980,8 @@ async function workAgent(req, flags) {
|
|
|
2794
2980
|
jobType,
|
|
2795
2981
|
workerName: `${name}:${jobType}`,
|
|
2796
2982
|
maxParallelJobs,
|
|
2797
|
-
jobTimeoutMs,
|
|
2983
|
+
jobTimeoutMs: jobLockMs,
|
|
2984
|
+
pollTimeoutMs,
|
|
2798
2985
|
jobHandler: async (job) => {
|
|
2799
2986
|
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
2800
2987
|
|
|
@@ -2877,7 +3064,7 @@ async function workAgent(req, flags) {
|
|
|
2877
3064
|
} catch { resultDir = null; resultFile = null; }
|
|
2878
3065
|
|
|
2879
3066
|
result = await runAgentJob(profile, job, {
|
|
2880
|
-
timeoutMs:
|
|
3067
|
+
timeoutMs: jobKillMs,
|
|
2881
3068
|
envelope,
|
|
2882
3069
|
sandbox,
|
|
2883
3070
|
image,
|
|
@@ -4322,6 +4509,7 @@ export {
|
|
|
4322
4509
|
webConsoleUrl,
|
|
4323
4510
|
consoleLinkLabel,
|
|
4324
4511
|
hireWorker,
|
|
4512
|
+
assignCapabilities,
|
|
4325
4513
|
};
|
|
4326
4514
|
export {
|
|
4327
4515
|
normalizeTaskEnvelope,
|
|
@@ -4356,8 +4544,12 @@ export {
|
|
|
4356
4544
|
agentRunsRoot,
|
|
4357
4545
|
ProvisionError,
|
|
4358
4546
|
normalizeStoredProfile,
|
|
4547
|
+
applyAssign,
|
|
4548
|
+
resolveAssignInputs,
|
|
4359
4549
|
jobTypeMatrix,
|
|
4360
4550
|
parseJobTypeFlags,
|
|
4551
|
+
deriveJobLockMs,
|
|
4552
|
+
derivePollTimeoutMs,
|
|
4361
4553
|
AGENT_TASK_NS,
|
|
4362
4554
|
AGENT_RESULT_KEY,
|
|
4363
4555
|
RESULT_SENTINEL,
|
|
@@ -4443,12 +4635,12 @@ export const commands = {
|
|
|
4443
4635
|
workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
|
|
4444
4636
|
check: { type: 'boolean', description: 'update: only report whether a new release is available; do not install' },
|
|
4445
4637
|
binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
|
|
4446
|
-
name: { type: 'string', description: 'hire/work: agent profile name (alt to positional arg)' },
|
|
4638
|
+
name: { type: 'string', description: 'hire/work/assign: agent profile name (alt to positional arg)' },
|
|
4447
4639
|
rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
|
|
4448
4640
|
command: { type: 'string', description: 'hire: CLI command that runs the agent harness (e.g. copilot, claude, pi)' },
|
|
4449
4641
|
arg: { type: 'string', multiple: true, description: 'hire/work: command-line switch/arg appended to the harness command (repeatable), e.g. --arg --allow-all. Persisted on hire; work appends more.' },
|
|
4450
4642
|
model: { type: 'string', description: 'hire: model name passed to the harness (AGENT_MODEL)' },
|
|
4451
|
-
capabilities: { type: 'string', description: 'hire: comma-separated capability list' },
|
|
4643
|
+
capabilities: { type: 'string', description: 'hire/assign: comma-separated capability list' },
|
|
4452
4644
|
sandbox: { type: 'string', description: 'hire/work: execution sandbox none|docker|podman (default none). Containers isolate each job.' },
|
|
4453
4645
|
image: { type: 'string', description: 'hire/work: container image the agent runs in (required for --sandbox docker|podman)' },
|
|
4454
4646
|
env: { type: 'string', multiple: true, description: 'hire/work: static env var for the harness as NAME=VALUE (repeatable); persisted on hire, work extends/overrides. E.g. permission toggles.' },
|
|
@@ -4462,6 +4654,8 @@ export const commands = {
|
|
|
4462
4654
|
list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
|
|
4463
4655
|
'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
|
|
4464
4656
|
'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
|
|
4657
|
+
'lock-grace': { type: 'string', description: 'work: extra ms added to --job-timeout to derive the broker activation lock, so the worker reports before the lock lapses (default 120000)' },
|
|
4658
|
+
'poll-timeout': { type: 'string', description: 'work: broker long-poll window in ms each activateJobs request is held open (fewer reconnects → fewer transient connect errors); default 30000, 0 = broker default, negative = return immediately' },
|
|
4465
4659
|
'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
|
|
4466
4660
|
},
|
|
4467
4661
|
handler: async (args, flags) => {
|
|
@@ -4514,6 +4708,9 @@ export const commands = {
|
|
|
4514
4708
|
case 'hire':
|
|
4515
4709
|
await hireWorker(req, flags);
|
|
4516
4710
|
break;
|
|
4711
|
+
case 'assign':
|
|
4712
|
+
await assignCapabilities(req, flags);
|
|
4713
|
+
break;
|
|
4517
4714
|
case 'work':
|
|
4518
4715
|
await workAgent(req, flags);
|
|
4519
4716
|
break;
|
|
@@ -4606,7 +4803,8 @@ function printUsage() {
|
|
|
4606
4803
|
console.log(' c8ctl nano config');
|
|
4607
4804
|
console.log(' c8ctl nano update [--check]');
|
|
4608
4805
|
console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--arg <switch> ...] [--model <m>] [--capabilities <a,b>] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--list]');
|
|
4609
|
-
console.log(' c8ctl nano
|
|
4806
|
+
console.log(' c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
|
|
4807
|
+
console.log(' c8ctl nano work <profileName> [--arg <switch> ...] [--max-parallel <n>] [--job-timeout <ms>] [--lock-grace <ms>] [--poll-timeout <ms>] [--job-type <token> ...] [--sandbox none|docker|podman] [--image <ref>] [--env NAME=VALUE ...] [--secret-resolver host] [--min-free-mb <n>] [--clone-timeout <ms>] [--keep-runs] [--stream]');
|
|
4610
4808
|
console.log('');
|
|
4611
4809
|
console.log('Subcommands:');
|
|
4612
4810
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -4621,6 +4819,7 @@ function printUsage() {
|
|
|
4621
4819
|
console.log(' config Show current configuration and on-disk locations');
|
|
4622
4820
|
console.log(' update Pull the latest published nano release (--check to only report)');
|
|
4623
4821
|
console.log(' hire Create a CLI agent worker profile (rank + capabilities → job-type matrix)');
|
|
4822
|
+
console.log(' assign Grant new capabilities (roles) to an existing hire (additive)');
|
|
4624
4823
|
console.log(' work Run a hired profile as Nano job workers, polling for work until Ctrl-C');
|
|
4625
4824
|
console.log('');
|
|
4626
4825
|
console.log('Options:');
|
|
@@ -4637,11 +4836,11 @@ function printUsage() {
|
|
|
4637
4836
|
console.log(' --purge stop: also delete per-node engine data');
|
|
4638
4837
|
console.log(' --force start: stop any existing cluster first');
|
|
4639
4838
|
console.log(' --workspace clean: also delete the workspace (models + workers)');
|
|
4640
|
-
console.log(' --name <n> hire/work: agent profile name (alt to positional arg)');
|
|
4839
|
+
console.log(' --name <n> hire/work/assign: agent profile name (alt to positional arg)');
|
|
4641
4840
|
console.log(' --rank <r> hire: agent rank (principal|senior|junior|decider)');
|
|
4642
4841
|
console.log(' --command <c> hire: CLI command that runs the agent harness');
|
|
4643
4842
|
console.log(' --model <m> hire: model name passed to the harness (AGENT_MODEL)');
|
|
4644
|
-
console.log(' --capabilities <a,b> hire: comma-separated capability list');
|
|
4843
|
+
console.log(' --capabilities <a,b> hire/assign: comma-separated capability list');
|
|
4645
4844
|
console.log(' --sandbox <s> hire/work: execution sandbox none|docker|podman (default none)');
|
|
4646
4845
|
console.log(' --image <ref> hire/work: container image the agent runs in (required for docker|podman)');
|
|
4647
4846
|
console.log(' --env NAME=VALUE hire/work: static env var for the harness (repeatable); persisted on hire, work extends/overrides');
|
|
@@ -4649,6 +4848,8 @@ function printUsage() {
|
|
|
4649
4848
|
console.log(' --max-parallel <n> work: max concurrent jobs per worker (default 1)');
|
|
4650
4849
|
console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
|
|
4651
4850
|
console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
|
|
4851
|
+
console.log(' --lock-grace <ms> work: extra ms over --job-timeout for the broker activation lock (default 120000)');
|
|
4852
|
+
console.log(' --poll-timeout <ms> work: broker long-poll window per activateJobs request (default 30000; 0 = broker default, negative = immediate)');
|
|
4652
4853
|
console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
|
|
4653
4854
|
console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
|
|
4654
4855
|
console.log(' --reap-interval <ms> work: how often to sweep finished agent containers/workspaces (default 300000)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.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",
|
|
@@ -42,19 +42,17 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@commitlint/cli": "^20.4.1",
|
|
44
44
|
"@commitlint/config-conventional": "^20.4.1",
|
|
45
|
-
"@semantic-release/changelog": "^6.0.3",
|
|
46
45
|
"@semantic-release/exec": "^7.1.0",
|
|
47
|
-
"@semantic-release/git": "^10.0.1",
|
|
48
46
|
"@semantic-release/github": "^12.0.6",
|
|
49
47
|
"semantic-release": "^25.0.3"
|
|
50
48
|
},
|
|
51
49
|
"optionalDependencies": {
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
57
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
58
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
50
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.19.0",
|
|
51
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.19.0",
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.19.0",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.19.0",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.19.0",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.19.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.19.0"
|
|
59
57
|
}
|
|
60
58
|
}
|