c8ctl-plugin-nano 1.18.1 → 1.20.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 +62 -1
- package/c8ctl-plugin.js +371 -37
- 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
|
|
@@ -189,6 +207,29 @@ c8ctl nano work reviewer # poll for work until Ctrl-C
|
|
|
189
207
|
c8ctl nano work reviewer --max-parallel 2 --job-timeout 600000
|
|
190
208
|
```
|
|
191
209
|
|
|
210
|
+
### Live profile reload (no restart on `assign`)
|
|
211
|
+
|
|
212
|
+
A running `c8ctl nano work <name>` **watches** the profile it is servicing. When
|
|
213
|
+
you extend or reduce that profile's capabilities in another terminal —
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
c8ctl nano assign reviewer fix-ci # add a capability to the live profile
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
— the supervisor reconciles its pollers in place: it **starts** pollers for the
|
|
220
|
+
newly added rank×capability job types and **gracefully drains** the pollers for
|
|
221
|
+
removed types — best-effort: each draining poller is given a bounded grace
|
|
222
|
+
window (`STOP_GRACE_MS`) for its in-flight jobs to finish before
|
|
223
|
+
it is stopped, so long-running work exceeding that window may still be
|
|
224
|
+
interrupted. Unchanged job types keep running undisturbed, so there is no need
|
|
225
|
+
to stop and restart the worker.
|
|
226
|
+
|
|
227
|
+
Only **job types** (rank + capabilities, plus any `--job-type` extras) reconcile
|
|
228
|
+
live. Changes to the profile's `command`, `model`, `sandbox`/`image`, or `env`
|
|
229
|
+
still require a restart to take effect. If the profile is deleted or the config
|
|
230
|
+
file is mid-write when the reload fires, the running workers are **kept** (never
|
|
231
|
+
torn down) and a warning is logged.
|
|
232
|
+
|
|
192
233
|
Each activated job runs the profile's command **once** (one-shot): the job is
|
|
193
234
|
serialized to JSON and piped to the CLI's **stdin** —
|
|
194
235
|
|
|
@@ -217,6 +258,16 @@ config`).
|
|
|
217
258
|
> Raising `--job-timeout` alone does **not** fix this — it moves both coupled
|
|
218
259
|
> deadlines together; widen `--lock-grace` (or keep the default) instead.
|
|
219
260
|
|
|
261
|
+
> **Long-poll window.** `--poll-timeout` (default `30000`ms) is how long the
|
|
262
|
+
> broker holds each `activateJobs` request open waiting for work before returning
|
|
263
|
+
> empty. A longer window keeps an idle worker on **one** connection for that whole
|
|
264
|
+
> window instead of reconnecting every few seconds — cutting the number of
|
|
265
|
+
> connection establishments, and thus the chances of hitting a transient connect
|
|
266
|
+
> error (`ECONNREFUSED` / connect-timeout) on a flaky link. It maps straight to
|
|
267
|
+
> the SDK's `pollTimeoutMs` → the broker's `requestTimeout`: `0` selects the
|
|
268
|
+
> broker's own default (~5s) and a negative value returns immediately when no job
|
|
269
|
+
> is available.
|
|
270
|
+
|
|
220
271
|
> **Trust boundary.** The profile `command` is run through a shell so you can
|
|
221
272
|
> write a full invocation (args, pipes, multi-word commands). It is
|
|
222
273
|
> **operator-authored** — only what you put in your own `config.json` is
|
|
@@ -246,6 +297,16 @@ Element templates emit flat dotpath header keys (strings); the plugin expands
|
|
|
246
297
|
them into a nested object and coerces `"true"/"false"` → bool and numeric
|
|
247
298
|
strings → int. The normalized shape is
|
|
248
299
|
`{ schemaVersion, repository{provider,url,ref,depth,submodules,authRef}, branch{base,create,push}, setup{commands,env,secretRefs}, task{prompt,promptFile,maxIterations,timeoutMs,allowPr,prBase} }`.
|
|
300
|
+
|
|
301
|
+
**Prompt = base + optional verbatim append.** The agent's prompt resolves to
|
|
302
|
+
`task.prompt` (typically a model header filled at deploy time), falling back to a
|
|
303
|
+
plain `prompt`/`task` variable. Because a header-delivered base prompt can't be
|
|
304
|
+
composed in FEEL, a task may supply per-instance context via **`task.appendPrompt`**
|
|
305
|
+
(reserved) or a plain **`appendPrompt`** variable — it is concatenated onto the base
|
|
306
|
+
**verbatim, with no injected separator** (the model's ioMapping owns any leading
|
|
307
|
+
separator/preamble), so a null/empty append leaves the base untouched. This lets the
|
|
308
|
+
static prompt live in a model header/side-car while the dynamic tail (e.g. plan-revision
|
|
309
|
+
feedback, a per-task brief) is built per instance.
|
|
249
310
|
On completion the plugin writes an **output envelope** back under
|
|
250
311
|
`io.nanobpm.agentResult` (`{schemaVersion, status, sandbox, image, output, truncated, stderrTruncated, exitCode, signal, error}`). When a repository was
|
|
251
312
|
provisioned (below) it also carries `{repository, branch, baseSha, headSha, commits[], pushed, pushError?, gitError?, pr?}`.
|
package/c8ctl-plugin.js
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
statfsSync,
|
|
44
44
|
lstatSync,
|
|
45
45
|
mkdtempSync,
|
|
46
|
+
watchFile,
|
|
47
|
+
unwatchFile,
|
|
46
48
|
} from 'node:fs';
|
|
47
49
|
import { randomUUID } from 'node:crypto';
|
|
48
50
|
import { homedir, platform as osPlatform, devNull } from 'node:os';
|
|
@@ -200,20 +202,39 @@ function getConfigFile() {
|
|
|
200
202
|
return join(getStateHome(), CONFIG_FILE);
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
function
|
|
205
|
+
function readConfigStrict() {
|
|
204
206
|
const file = getConfigFile();
|
|
205
207
|
if (!existsSync(file)) return {};
|
|
208
|
+
const cfg = JSON.parse(readFileSync(file, 'utf-8'));
|
|
209
|
+
return cfg && typeof cfg === 'object' ? cfg : {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readConfig() {
|
|
206
213
|
try {
|
|
207
|
-
|
|
208
|
-
return cfg && typeof cfg === 'object' ? cfg : {};
|
|
214
|
+
return readConfigStrict();
|
|
209
215
|
} catch {
|
|
216
|
+
// A malformed/torn config.json is swallowed here so ordinary callers get an
|
|
217
|
+
// empty map; callers that must tell "absent" from "unreadable" apart use
|
|
218
|
+
// readConfigStrict() directly and handle the throw.
|
|
210
219
|
return {};
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
function writeConfig(cfg) {
|
|
215
224
|
mkdirSync(getStateHome(), { recursive: true });
|
|
216
|
-
|
|
225
|
+
// Atomic write: serialize to a temp file in the same dir, then rename over the
|
|
226
|
+
// target. A rename is atomic on a POSIX filesystem, so a concurrent reader
|
|
227
|
+
// (e.g. `work`'s profile watcher, or another `assign`) never observes a
|
|
228
|
+
// half-written config.json and JSON.parse never sees a torn file.
|
|
229
|
+
const target = getConfigFile();
|
|
230
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
231
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
232
|
+
try {
|
|
233
|
+
renameSync(tmp, target);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
217
238
|
}
|
|
218
239
|
|
|
219
240
|
/**
|
|
@@ -368,7 +389,7 @@ function launcherEnvMarkers(resolved) {
|
|
|
368
389
|
// Argument parsing
|
|
369
390
|
// ---------------------------------------------------------------------------
|
|
370
391
|
|
|
371
|
-
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update', 'hire', 'work'];
|
|
392
|
+
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update', 'hire', 'assign', 'work'];
|
|
372
393
|
|
|
373
394
|
/**
|
|
374
395
|
* Parse positional args + flags into a normalized request.
|
|
@@ -1518,7 +1539,32 @@ function deriveJobLockMs(jobTimeoutMs, lockGraceMs) {
|
|
|
1518
1539
|
return { killMs, lockMs: killMs + grace };
|
|
1519
1540
|
}
|
|
1520
1541
|
|
|
1521
|
-
/**
|
|
1542
|
+
/**
|
|
1543
|
+
* Resolve the broker long-poll window (ms) each `activateJobs` request is held
|
|
1544
|
+
* open before returning empty. A longer window keeps an idle worker on ONE open
|
|
1545
|
+
* connection for that whole window instead of reconnecting every few seconds,
|
|
1546
|
+
* cutting the number of connection establishments — and thus the number of
|
|
1547
|
+
* chances to hit a transient connect failure (ECONNREFUSED / connect-timeout)
|
|
1548
|
+
* on a flaky link.
|
|
1549
|
+
*
|
|
1550
|
+
* The value is passed straight through to the SDK as `pollTimeoutMs` → the
|
|
1551
|
+
* broker's `requestTimeout`, so the documented broker semantics apply: `0` =
|
|
1552
|
+
* broker default (~5s), a negative value = return immediately when no job is
|
|
1553
|
+
* available. Parsing is `parseInt`-style: only a flag with no leading integer
|
|
1554
|
+
* (absent, blank, or non-numeric such as `"abc"`) falls back to the default,
|
|
1555
|
+
* while a leading integer with trailing junk (e.g. `"30000ms"`) is honoured as
|
|
1556
|
+
* that integer. `0` and negatives are honoured too (which is why this cannot
|
|
1557
|
+
* reuse `intFlag`, whose "> 0" guard would floor them to the default).
|
|
1558
|
+
*
|
|
1559
|
+
* @returns {number}
|
|
1560
|
+
*/
|
|
1561
|
+
function derivePollTimeoutMs(flagValue, dflt = 30_000) {
|
|
1562
|
+
if (flagValue === undefined || flagValue === null || String(flagValue).trim() === '') {
|
|
1563
|
+
return dflt;
|
|
1564
|
+
}
|
|
1565
|
+
const n = Number.parseInt(String(flagValue), 10);
|
|
1566
|
+
return Number.isFinite(n) ? n : dflt;
|
|
1567
|
+
}
|
|
1522
1568
|
function isValidProfileName(name) {
|
|
1523
1569
|
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
|
|
1524
1570
|
}
|
|
@@ -1540,6 +1586,21 @@ function jobTypeMatrix(rank, capabilities) {
|
|
|
1540
1586
|
return [...new Set(tokens)];
|
|
1541
1587
|
}
|
|
1542
1588
|
|
|
1589
|
+
/**
|
|
1590
|
+
* Diff a running set of job-type pollers against a desired set. Pure so the
|
|
1591
|
+
* profile-watch reconcile in `work` (which starts pollers for `added` types and
|
|
1592
|
+
* gracefully drains pollers for `removed` types) is unit-testable. Order in the
|
|
1593
|
+
* returned arrays is stable (desired order for `added`, current order for
|
|
1594
|
+
* `removed`) for deterministic logging.
|
|
1595
|
+
*/
|
|
1596
|
+
function diffJobTypes(current, desired) {
|
|
1597
|
+
const cur = new Set(current);
|
|
1598
|
+
const want = new Set(desired);
|
|
1599
|
+
const added = [...want].filter((t) => !cur.has(t));
|
|
1600
|
+
const removed = [...cur].filter((t) => !want.has(t));
|
|
1601
|
+
return { added, removed };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1543
1604
|
/** All persisted hire profiles, keyed by name. */
|
|
1544
1605
|
function readHires() {
|
|
1545
1606
|
const cfg = readConfig();
|
|
@@ -1548,6 +1609,16 @@ function readHires() {
|
|
|
1548
1609
|
return cfg.hires && typeof cfg.hires === 'object' && !Array.isArray(cfg.hires) ? cfg.hires : {};
|
|
1549
1610
|
}
|
|
1550
1611
|
|
|
1612
|
+
/**
|
|
1613
|
+
* Like readHires(), but propagates a malformed-config parse error instead of
|
|
1614
|
+
* swallowing it. Lets a caller distinguish "profile genuinely removed" from
|
|
1615
|
+
* "config temporarily unreadable/torn" so it can report an accurate reason.
|
|
1616
|
+
*/
|
|
1617
|
+
function readHiresStrict() {
|
|
1618
|
+
const cfg = readConfigStrict();
|
|
1619
|
+
return cfg.hires && typeof cfg.hires === 'object' && !Array.isArray(cfg.hires) ? cfg.hires : {};
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1551
1622
|
/** Persist a single hire profile into config.json under `hires`. */
|
|
1552
1623
|
function writeHire(profile) {
|
|
1553
1624
|
const cfg = readConfig();
|
|
@@ -1597,7 +1668,99 @@ function normalizeStoredProfile(name, profile) {
|
|
|
1597
1668
|
}
|
|
1598
1669
|
|
|
1599
1670
|
/**
|
|
1600
|
-
*
|
|
1671
|
+
* Merge additional capabilities into an already-normalized profile, returning a
|
|
1672
|
+
* new profile object with the union of capabilities (canonical order) and a
|
|
1673
|
+
* refreshed `updatedAt`. Pure (no config I/O), so it is unit-testable. Existing
|
|
1674
|
+
* fields — including `createdAt` — are preserved. `incoming` may be a
|
|
1675
|
+
* comma-string or an array. Returns `{ profile, added }`, where `added` lists
|
|
1676
|
+
* the newly gained capabilities (empty when the assign is a no-op).
|
|
1677
|
+
*/
|
|
1678
|
+
function applyAssign(existing, incoming, now = new Date().toISOString()) {
|
|
1679
|
+
const before = new Set(normalizeCapabilities(existing && existing.capabilities));
|
|
1680
|
+
const union = normalizeCapabilities([...before, ...normalizeCapabilities(incoming)]);
|
|
1681
|
+
const added = union.filter((c) => !before.has(c));
|
|
1682
|
+
return {
|
|
1683
|
+
profile: { ...existing, capabilities: union, updatedAt: now },
|
|
1684
|
+
added,
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Resolve the profile name and the raw comma-joined capability string for an
|
|
1690
|
+
* `assign` invocation from parsed positionals + flags. Pure (no I/O) so the
|
|
1691
|
+
* positional-slicing rules are unit-testable.
|
|
1692
|
+
*
|
|
1693
|
+
* When `--name` is supplied the name does NOT consume a positional, so every
|
|
1694
|
+
* positional is a capability. Otherwise the first positional is the name and
|
|
1695
|
+
* the rest are capabilities. `--capabilities a,b` is always appended.
|
|
1696
|
+
*/
|
|
1697
|
+
function resolveAssignInputs(req, flags) {
|
|
1698
|
+
const positional = Array.isArray(req?.positional) ? req.positional : [];
|
|
1699
|
+
const name = flags?.name ? String(flags.name).trim() : positional[0];
|
|
1700
|
+
const positionalCaps = flags?.name ? positional : positional.slice(1);
|
|
1701
|
+
const flagCaps = flags?.capabilities !== undefined ? String(flags.capabilities) : '';
|
|
1702
|
+
const incomingRaw = [...positionalCaps, flagCaps].filter(Boolean).join(',');
|
|
1703
|
+
return { name, incomingRaw };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* assign — grant new capabilities (roles) to an existing hire without
|
|
1708
|
+
* re-running `hire`. The profile name is positional[0] (or `--name`);
|
|
1709
|
+
* capabilities are the remaining positionals and/or `--capabilities a,b`.
|
|
1710
|
+
* Capabilities are unioned with the profile's existing set (additive; assign
|
|
1711
|
+
* never removes a role) and the updated rank×capability job-type matrix is
|
|
1712
|
+
* printed. Re-run `work` to pick up the new job types.
|
|
1713
|
+
*/
|
|
1714
|
+
async function assignCapabilities(req, flags) {
|
|
1715
|
+
const logger = getLogger();
|
|
1716
|
+
const { name, incomingRaw } = resolveAssignInputs(req, flags);
|
|
1717
|
+
if (!name) {
|
|
1718
|
+
logger.error('Usage: c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
|
|
1719
|
+
logger.info('Grant new capabilities to an existing hire. List profiles with: c8ctl nano hire --list');
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
if (!isValidProfileName(name)) {
|
|
1723
|
+
logger.error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore.`);
|
|
1724
|
+
process.exit(1);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
if (normalizeCapabilities(incomingRaw).length === 0) {
|
|
1728
|
+
logger.error('Provide at least one capability to assign.');
|
|
1729
|
+
logger.info(`Example: c8ctl nano assign ${name} code-review testing`);
|
|
1730
|
+
process.exit(1);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const raw = readHires()[name];
|
|
1734
|
+
if (!raw) {
|
|
1735
|
+
logger.error(`No hire named "${name}". List profiles with: c8ctl nano hire --list`);
|
|
1736
|
+
process.exit(1);
|
|
1737
|
+
}
|
|
1738
|
+
const normalized = normalizeStoredProfile(name, raw);
|
|
1739
|
+
if (normalized.error) {
|
|
1740
|
+
logger.error(`Cannot assign to "${name}": ${normalized.error}. Re-create it with: c8ctl nano hire`);
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Preserve createdAt (normalizeStoredProfile drops it) on the canonical form.
|
|
1745
|
+
const base = {
|
|
1746
|
+
...normalized.profile,
|
|
1747
|
+
createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : new Date().toISOString(),
|
|
1748
|
+
};
|
|
1749
|
+
const { profile, added } = applyAssign(base, incomingRaw);
|
|
1750
|
+
if (added.length === 0) {
|
|
1751
|
+
logger.info(`"${name}" already has: ${profile.capabilities.join(', ') || '(none)'} — no change.`);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
writeHire(profile);
|
|
1755
|
+
|
|
1756
|
+
const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
|
|
1757
|
+
logger.info(`Assigned to "${name}" [${profile.rank}]: +${added.join(', ')}`);
|
|
1758
|
+
logger.info(` capabilities: ${profile.capabilities.join(', ')}`);
|
|
1759
|
+
logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
|
|
1760
|
+
logger.info(`Restart its workers to pick up the new roles: c8ctl nano work ${name}`);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1601
1764
|
* field can also be supplied via a flag (--name/--rank/--command/--model/
|
|
1602
1765
|
* --capabilities) for scripting. Prompts only for the fields still missing.
|
|
1603
1766
|
* `--list` prints existing profiles instead.
|
|
@@ -2006,8 +2169,21 @@ function normalizeTaskEnvelope(customHeaders, variables) {
|
|
|
2006
2169
|
};
|
|
2007
2170
|
|
|
2008
2171
|
const task = isPlainObject(raw.task) ? raw.task : {};
|
|
2172
|
+
// Base prompt: the reserved `task.prompt` (typically a model header filled at deploy time),
|
|
2173
|
+
// else a plain `prompt`/`task` job variable (the pre-header delivery path).
|
|
2174
|
+
const basePrompt = str(task.prompt) ?? str(variables?.prompt) ?? str(variables?.task);
|
|
2175
|
+
// Verbatim dynamic append: a header-delivered base prompt can't be composed in FEEL, so a task
|
|
2176
|
+
// may supply per-instance context (e.g. plan-revision feedback, a per-task brief) via the
|
|
2177
|
+
// reserved `task.appendPrompt`, or a plain `appendPrompt` variable. It is concatenated onto the
|
|
2178
|
+
// base with NO injected separator — the caller (the model's ioMapping) owns any leading
|
|
2179
|
+
// separator/preamble — so a null/empty append leaves the base prompt untouched.
|
|
2180
|
+
const appendPrompt = str(task.appendPrompt) ?? str(variables?.appendPrompt);
|
|
2181
|
+
const prompt =
|
|
2182
|
+
appendPrompt != null && appendPrompt !== ''
|
|
2183
|
+
? `${basePrompt ?? ''}${appendPrompt}`
|
|
2184
|
+
: basePrompt;
|
|
2009
2185
|
env.task = {
|
|
2010
|
-
prompt
|
|
2186
|
+
prompt,
|
|
2011
2187
|
promptFile: str(task.promptFile),
|
|
2012
2188
|
maxIterations: coerceInt(task.maxIterations, undefined),
|
|
2013
2189
|
timeoutMs: coerceInt(task.timeoutMs, undefined),
|
|
@@ -2730,6 +2906,11 @@ async function workAgent(req, flags) {
|
|
|
2730
2906
|
// MUST enforce the clamped `jobKillMs` (not the raw --job-timeout), or a very
|
|
2731
2907
|
// large --job-timeout would outlive the broker lock and re-break the invariant.
|
|
2732
2908
|
const { killMs: jobKillMs, lockMs: jobLockMs } = deriveJobLockMs(jobTimeoutMs, lockGraceMs);
|
|
2909
|
+
// Broker long-poll window: how long each activateJobs request is held open
|
|
2910
|
+
// waiting for work. 30s default so idle workers hold one connection open ~30s
|
|
2911
|
+
// rather than reconnecting every few seconds — fewer reconnects, fewer chances
|
|
2912
|
+
// to hit a transient connect error on a flaky link. Passed to the SDK verbatim.
|
|
2913
|
+
const pollTimeoutMs = derivePollTimeoutMs(flags?.['poll-timeout']);
|
|
2733
2914
|
|
|
2734
2915
|
// Sandbox: flag overrides the stored profile default. `none` runs on the host
|
|
2735
2916
|
// (legacy); `docker`/`podman` run each job in a throwaway labelled container.
|
|
@@ -2837,15 +3018,19 @@ async function workAgent(req, flags) {
|
|
|
2837
3018
|
if (profileEnvKeys.length > 0) logger.info(` harness env: ${profileEnvKeys.join(', ')}`);
|
|
2838
3019
|
const extraNote = extraJobTypes.length > 0 ? ` (${extraJobTypes.length} via --job-type)` : '';
|
|
2839
3020
|
logger.info(` listening on ${jobTypes.length} job type(s)${extraNote}: ${jobTypes.join(' ')}`);
|
|
2840
|
-
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms`);
|
|
3021
|
+
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms; poll timeout: ${pollTimeoutMs}ms`);
|
|
2841
3022
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
2842
3023
|
|
|
2843
|
-
|
|
3024
|
+
// A per-job-type worker factory. Captures all the CLI-local + profile context
|
|
3025
|
+
// in closure scope so the profile watcher below can (re)spawn a poller for any
|
|
3026
|
+
// job type on demand without re-reading the flags.
|
|
3027
|
+
const makeWorker = (jobType) =>
|
|
2844
3028
|
camunda.createJobWorker({
|
|
2845
3029
|
jobType,
|
|
2846
3030
|
workerName: `${name}:${jobType}`,
|
|
2847
3031
|
maxParallelJobs,
|
|
2848
3032
|
jobTimeoutMs: jobLockMs,
|
|
3033
|
+
pollTimeoutMs,
|
|
2849
3034
|
jobHandler: async (job) => {
|
|
2850
3035
|
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
2851
3036
|
|
|
@@ -3020,35 +3205,172 @@ async function workAgent(req, flags) {
|
|
|
3020
3205
|
variables: { [AGENT_RESULT_KEY]: resultEnvelope },
|
|
3021
3206
|
});
|
|
3022
3207
|
},
|
|
3023
|
-
})
|
|
3024
|
-
|
|
3208
|
+
});
|
|
3209
|
+
|
|
3210
|
+
// Live worker registry keyed by job type, so the profile watcher can add or
|
|
3211
|
+
// drain individual pollers without disturbing the others. `draining` is the
|
|
3212
|
+
// shutdown latch (shared with the watcher so a reconcile can't race a stop).
|
|
3213
|
+
const workers = new Map();
|
|
3214
|
+
let draining = false;
|
|
3215
|
+
|
|
3216
|
+
const drainWorker = async (w) => {
|
|
3217
|
+
try {
|
|
3218
|
+
if (typeof w.stopGracefully === 'function') {
|
|
3219
|
+
await w.stopGracefully({ waitUpToMs: STOP_GRACE_MS });
|
|
3220
|
+
} else if (typeof w.stop === 'function') {
|
|
3221
|
+
await w.stop();
|
|
3222
|
+
}
|
|
3223
|
+
return true;
|
|
3224
|
+
} catch {
|
|
3225
|
+
return false; // best-effort: never let one worker's stop failure hang us
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
const spawnJobType = (jobType) => {
|
|
3230
|
+
if (workers.has(jobType)) return false;
|
|
3231
|
+
workers.set(jobType, makeWorker(jobType));
|
|
3232
|
+
return true;
|
|
3233
|
+
};
|
|
3234
|
+
|
|
3235
|
+
for (const jobType of jobTypes) spawnJobType(jobType);
|
|
3236
|
+
|
|
3237
|
+
// ---- Live profile watch: reconcile the poller set when the watched profile's
|
|
3238
|
+
// job types change (e.g. `c8ctl nano assign <name> …`) — start pollers for
|
|
3239
|
+
// added types, gracefully drain pollers for removed types — without a restart
|
|
3240
|
+
// and without disturbing unchanged types' in-flight work. ----
|
|
3241
|
+
const configFile = getConfigFile();
|
|
3242
|
+
const WATCH_INTERVAL_MS = 1500;
|
|
3243
|
+
let reconciling = false;
|
|
3244
|
+
// Set when a profile change arrives while a reconcile is already in flight, so
|
|
3245
|
+
// we run one more pass after the current drain completes instead of dropping
|
|
3246
|
+
// the update until the next change fires.
|
|
3247
|
+
let reconcileRequested = false;
|
|
3248
|
+
// Handle to the in-flight reconcile so shutdown can wait for it to finish
|
|
3249
|
+
// before snapshotting `workers` (avoids double-stops / missed drains).
|
|
3250
|
+
let inFlightReconcile = null;
|
|
3251
|
+
|
|
3252
|
+
// Desired job types from the CURRENT on-disk profile (matrix ∪ --job-type
|
|
3253
|
+
// extras). Returns { skip } for a transient/torn read, a vanished profile, or
|
|
3254
|
+
// an invalid edit — callers must then KEEP the running set, never tear down.
|
|
3255
|
+
const desiredJobTypes = () => {
|
|
3256
|
+
let stored;
|
|
3257
|
+
try {
|
|
3258
|
+
stored = readHiresStrict()[name];
|
|
3259
|
+
} catch {
|
|
3260
|
+
// config.json exists but doesn't parse (e.g. a torn write): the profile is
|
|
3261
|
+
// NOT necessarily gone, so don't claim it was deleted — skip this pass.
|
|
3262
|
+
return { skip: 'config unreadable' };
|
|
3263
|
+
}
|
|
3264
|
+
if (!stored) return { skip: 'deleted' };
|
|
3265
|
+
const norm = normalizeStoredProfile(name, stored);
|
|
3266
|
+
if (norm.error) return { skip: norm.error };
|
|
3267
|
+
const m = jobTypeMatrix(norm.profile.rank, norm.profile.capabilities);
|
|
3268
|
+
return { jobTypes: [...new Set([...m, ...extraJobTypes])] };
|
|
3269
|
+
};
|
|
3270
|
+
|
|
3271
|
+
const reconcile = () => {
|
|
3272
|
+
if (draining) return inFlightReconcile || Promise.resolve();
|
|
3273
|
+
if (reconciling) {
|
|
3274
|
+
// A change landed mid-reconcile — remember it so the current pass loops
|
|
3275
|
+
// once more rather than leaving the worker set stale until the next edit.
|
|
3276
|
+
// Return the ACTUAL in-flight promise (not a fresh short-lived one) so a
|
|
3277
|
+
// caller — including shutdown — waits for the real reconcile to finish.
|
|
3278
|
+
reconcileRequested = true;
|
|
3279
|
+
return inFlightReconcile || Promise.resolve();
|
|
3280
|
+
}
|
|
3281
|
+
reconciling = true;
|
|
3282
|
+
reconcileRequested = false;
|
|
3283
|
+
inFlightReconcile = (async () => {
|
|
3284
|
+
try {
|
|
3285
|
+
do {
|
|
3286
|
+
reconcileRequested = false;
|
|
3287
|
+
await runReconcilePass();
|
|
3288
|
+
} while (reconcileRequested && !draining);
|
|
3289
|
+
} finally {
|
|
3290
|
+
reconciling = false;
|
|
3291
|
+
inFlightReconcile = null;
|
|
3292
|
+
}
|
|
3293
|
+
})();
|
|
3294
|
+
return inFlightReconcile;
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
const runReconcilePass = async () => {
|
|
3298
|
+
const desired = desiredJobTypes();
|
|
3299
|
+
if (desired.skip) {
|
|
3300
|
+
if (desired.skip === 'deleted') {
|
|
3301
|
+
logger.warn(`Profile "${name}" is gone from config — keeping the current ${workers.size} worker(s) running.`);
|
|
3302
|
+
} else {
|
|
3303
|
+
logger.warn(`Profile "${name}" reload skipped — ${desired.skip}; keeping current workers.`);
|
|
3304
|
+
}
|
|
3305
|
+
return;
|
|
3306
|
+
}
|
|
3307
|
+
const { added, removed } = diffJobTypes([...workers.keys()], desired.jobTypes);
|
|
3308
|
+
if (added.length === 0 && removed.length === 0) return;
|
|
3309
|
+
logger.info(`Profile "${name}" changed — reconciling job types (+${added.length} / -${removed.length}).`);
|
|
3310
|
+
for (const jt of added) {
|
|
3311
|
+
spawnJobType(jt);
|
|
3312
|
+
logger.info(` + now listening on ${jt}`);
|
|
3313
|
+
}
|
|
3314
|
+
await Promise.all(
|
|
3315
|
+
removed.map(async (jt) => {
|
|
3316
|
+
const w = workers.get(jt);
|
|
3317
|
+
logger.info(` - draining ${jt} …`);
|
|
3318
|
+
const ok = await drainWorker(w);
|
|
3319
|
+
if (ok) {
|
|
3320
|
+
// Only drop it from the registry once it has actually stopped, so a
|
|
3321
|
+
// failed drain stays tracked and gets retried on the next reconcile
|
|
3322
|
+
// pass (or on shutdown) instead of leaking an untracked poller.
|
|
3323
|
+
workers.delete(jt);
|
|
3324
|
+
logger.info(` - stopped ${jt}`);
|
|
3325
|
+
} else {
|
|
3326
|
+
logger.warn(` - ${jt} did not stop cleanly; keeping it tracked so it is retried on the next reconcile or shutdown.`);
|
|
3327
|
+
}
|
|
3328
|
+
}),
|
|
3329
|
+
);
|
|
3330
|
+
logger.info(` now listening on ${workers.size} job type(s): ${[...workers.keys()].join(' ')}`);
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
// `watchFile` (polling stat) is deliberate over `fs.watch`: it survives the
|
|
3334
|
+
// atomic temp+rename that `writeConfig` does (fs.watch would rebind to the old
|
|
3335
|
+
// inode and go silent), and it's uniform across platforms. Profile edits are
|
|
3336
|
+
// rare + manual, so a ~1.5s poll latency is fine.
|
|
3337
|
+
watchFile(configFile, { interval: WATCH_INTERVAL_MS }, (curr, prev) => {
|
|
3338
|
+
// Fires each interval; act only on real changes. Compare mtime, ctime and
|
|
3339
|
+
// size, not mtime alone: on filesystems with coarse mtime resolution (or two
|
|
3340
|
+
// edits within one mtime tick) mtimeMs can be unchanged while size/ctimeMs
|
|
3341
|
+
// differ, and an mtime-only guard would skip a genuine profile update.
|
|
3342
|
+
if (
|
|
3343
|
+
curr.mtimeMs === prev.mtimeMs &&
|
|
3344
|
+
curr.ctimeMs === prev.ctimeMs &&
|
|
3345
|
+
curr.size === prev.size
|
|
3346
|
+
) return;
|
|
3347
|
+
// `reconcile()` owns the `inFlightReconcile` handle: a change arriving while
|
|
3348
|
+
// a reconcile is already running coalesces into the current pass and returns
|
|
3349
|
+
// that same in-flight promise, so shutdown always waits for the real one.
|
|
3350
|
+
reconcile().catch((err) => logger.warn(`profile reload failed: ${err?.message || err}`));
|
|
3351
|
+
});
|
|
3025
3352
|
|
|
3026
3353
|
// Keep the process alive until a stop signal, then drain gracefully.
|
|
3027
3354
|
await new Promise((resolve) => {
|
|
3028
|
-
let stopping = false;
|
|
3029
3355
|
const stop = async (signal) => {
|
|
3030
|
-
if (
|
|
3031
|
-
|
|
3032
|
-
|
|
3356
|
+
if (draining) return;
|
|
3357
|
+
draining = true;
|
|
3358
|
+
// Stop watching first so no new reconcile can be triggered, then wait for
|
|
3359
|
+
// any in-flight reconcile to finish before snapshotting `workers` — this
|
|
3360
|
+
// prevents double-stops, missed drains, or a wrong worker count on exit.
|
|
3361
|
+
unwatchFile(configFile);
|
|
3362
|
+
if (inFlightReconcile) {
|
|
3363
|
+
logger.info('Waiting for in-flight profile reconcile to finish before shutdown…');
|
|
3364
|
+
await inFlightReconcile;
|
|
3365
|
+
}
|
|
3366
|
+
const list = [...workers.values()];
|
|
3367
|
+
logger.info(`Received ${signal} — stopping ${list.length} worker(s)...`);
|
|
3033
3368
|
if (reaperTimer) clearInterval(reaperTimer);
|
|
3034
3369
|
if (runDirTimer) clearInterval(runDirTimer);
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
workers.map(async (w) => {
|
|
3038
|
-
try {
|
|
3039
|
-
if (typeof w.stopGracefully === 'function') {
|
|
3040
|
-
await w.stopGracefully({ waitUpToMs: STOP_GRACE_MS });
|
|
3041
|
-
} else if (typeof w.stop === 'function') {
|
|
3042
|
-
await w.stop();
|
|
3043
|
-
}
|
|
3044
|
-
} catch {
|
|
3045
|
-
// best-effort: never let one worker's stop failure hang shutdown
|
|
3046
|
-
stopFailures += 1;
|
|
3047
|
-
}
|
|
3048
|
-
}),
|
|
3049
|
-
);
|
|
3370
|
+
const results = await Promise.all(list.map(drainWorker));
|
|
3371
|
+
const stopFailures = results.filter((ok) => !ok).length;
|
|
3050
3372
|
if (stopFailures > 0) {
|
|
3051
|
-
logger.warn(`${stopFailures} of ${
|
|
3373
|
+
logger.warn(`${stopFailures} of ${list.length} worker(s) did not stop cleanly; some connections may still be open.`);
|
|
3052
3374
|
} else {
|
|
3053
3375
|
logger.info('All workers stopped.');
|
|
3054
3376
|
}
|
|
@@ -4373,6 +4695,7 @@ export {
|
|
|
4373
4695
|
webConsoleUrl,
|
|
4374
4696
|
consoleLinkLabel,
|
|
4375
4697
|
hireWorker,
|
|
4698
|
+
assignCapabilities,
|
|
4376
4699
|
};
|
|
4377
4700
|
export {
|
|
4378
4701
|
normalizeTaskEnvelope,
|
|
@@ -4407,9 +4730,13 @@ export {
|
|
|
4407
4730
|
agentRunsRoot,
|
|
4408
4731
|
ProvisionError,
|
|
4409
4732
|
normalizeStoredProfile,
|
|
4733
|
+
applyAssign,
|
|
4734
|
+
resolveAssignInputs,
|
|
4410
4735
|
jobTypeMatrix,
|
|
4736
|
+
diffJobTypes,
|
|
4411
4737
|
parseJobTypeFlags,
|
|
4412
4738
|
deriveJobLockMs,
|
|
4739
|
+
derivePollTimeoutMs,
|
|
4413
4740
|
AGENT_TASK_NS,
|
|
4414
4741
|
AGENT_RESULT_KEY,
|
|
4415
4742
|
RESULT_SENTINEL,
|
|
@@ -4495,12 +4822,12 @@ export const commands = {
|
|
|
4495
4822
|
workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
|
|
4496
4823
|
check: { type: 'boolean', description: 'update: only report whether a new release is available; do not install' },
|
|
4497
4824
|
binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
|
|
4498
|
-
name: { type: 'string', description: 'hire/work: agent profile name (alt to positional arg)' },
|
|
4825
|
+
name: { type: 'string', description: 'hire/work/assign: agent profile name (alt to positional arg)' },
|
|
4499
4826
|
rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
|
|
4500
4827
|
command: { type: 'string', description: 'hire: CLI command that runs the agent harness (e.g. copilot, claude, pi)' },
|
|
4501
4828
|
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.' },
|
|
4502
4829
|
model: { type: 'string', description: 'hire: model name passed to the harness (AGENT_MODEL)' },
|
|
4503
|
-
capabilities: { type: 'string', description: 'hire: comma-separated capability list' },
|
|
4830
|
+
capabilities: { type: 'string', description: 'hire/assign: comma-separated capability list' },
|
|
4504
4831
|
sandbox: { type: 'string', description: 'hire/work: execution sandbox none|docker|podman (default none). Containers isolate each job.' },
|
|
4505
4832
|
image: { type: 'string', description: 'hire/work: container image the agent runs in (required for --sandbox docker|podman)' },
|
|
4506
4833
|
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.' },
|
|
@@ -4515,6 +4842,7 @@ export const commands = {
|
|
|
4515
4842
|
'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
|
|
4516
4843
|
'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
|
|
4517
4844
|
'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)' },
|
|
4845
|
+
'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' },
|
|
4518
4846
|
'job-type': { type: 'string', multiple: true, description: 'work: extra job type to service alongside the rank×capability matrix (repeatable)' },
|
|
4519
4847
|
},
|
|
4520
4848
|
handler: async (args, flags) => {
|
|
@@ -4567,6 +4895,9 @@ export const commands = {
|
|
|
4567
4895
|
case 'hire':
|
|
4568
4896
|
await hireWorker(req, flags);
|
|
4569
4897
|
break;
|
|
4898
|
+
case 'assign':
|
|
4899
|
+
await assignCapabilities(req, flags);
|
|
4900
|
+
break;
|
|
4570
4901
|
case 'work':
|
|
4571
4902
|
await workAgent(req, flags);
|
|
4572
4903
|
break;
|
|
@@ -4659,7 +4990,8 @@ function printUsage() {
|
|
|
4659
4990
|
console.log(' c8ctl nano config');
|
|
4660
4991
|
console.log(' c8ctl nano update [--check]');
|
|
4661
4992
|
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]');
|
|
4662
|
-
console.log(' c8ctl nano
|
|
4993
|
+
console.log(' c8ctl nano assign <profileName> [<capability> ...] [--name <n>] [--capabilities <a,b>]');
|
|
4994
|
+
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]');
|
|
4663
4995
|
console.log('');
|
|
4664
4996
|
console.log('Subcommands:');
|
|
4665
4997
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -4674,6 +5006,7 @@ function printUsage() {
|
|
|
4674
5006
|
console.log(' config Show current configuration and on-disk locations');
|
|
4675
5007
|
console.log(' update Pull the latest published nano release (--check to only report)');
|
|
4676
5008
|
console.log(' hire Create a CLI agent worker profile (rank + capabilities → job-type matrix)');
|
|
5009
|
+
console.log(' assign Grant new capabilities (roles) to an existing hire (additive)');
|
|
4677
5010
|
console.log(' work Run a hired profile as Nano job workers, polling for work until Ctrl-C');
|
|
4678
5011
|
console.log('');
|
|
4679
5012
|
console.log('Options:');
|
|
@@ -4690,11 +5023,11 @@ function printUsage() {
|
|
|
4690
5023
|
console.log(' --purge stop: also delete per-node engine data');
|
|
4691
5024
|
console.log(' --force start: stop any existing cluster first');
|
|
4692
5025
|
console.log(' --workspace clean: also delete the workspace (models + workers)');
|
|
4693
|
-
console.log(' --name <n> hire/work: agent profile name (alt to positional arg)');
|
|
5026
|
+
console.log(' --name <n> hire/work/assign: agent profile name (alt to positional arg)');
|
|
4694
5027
|
console.log(' --rank <r> hire: agent rank (principal|senior|junior|decider)');
|
|
4695
5028
|
console.log(' --command <c> hire: CLI command that runs the agent harness');
|
|
4696
5029
|
console.log(' --model <m> hire: model name passed to the harness (AGENT_MODEL)');
|
|
4697
|
-
console.log(' --capabilities <a,b> hire: comma-separated capability list');
|
|
5030
|
+
console.log(' --capabilities <a,b> hire/assign: comma-separated capability list');
|
|
4698
5031
|
console.log(' --sandbox <s> hire/work: execution sandbox none|docker|podman (default none)');
|
|
4699
5032
|
console.log(' --image <ref> hire/work: container image the agent runs in (required for docker|podman)');
|
|
4700
5033
|
console.log(' --env NAME=VALUE hire/work: static env var for the harness (repeatable); persisted on hire, work extends/overrides');
|
|
@@ -4703,6 +5036,7 @@ function printUsage() {
|
|
|
4703
5036
|
console.log(' --job-type <token> work: extra job type to service alongside the rank×capability matrix (repeatable)');
|
|
4704
5037
|
console.log(' --job-timeout <ms> work: max harness runtime per job in ms (default 300000)');
|
|
4705
5038
|
console.log(' --lock-grace <ms> work: extra ms over --job-timeout for the broker activation lock (default 120000)');
|
|
5039
|
+
console.log(' --poll-timeout <ms> work: broker long-poll window per activateJobs request (default 30000; 0 = broker default, negative = immediate)');
|
|
4706
5040
|
console.log(' --secret-resolver <r> work: secret resolver for task secretRefs (host; default host)');
|
|
4707
5041
|
console.log(' --reap-age <ms> work: age before a finished agent container/workspace is reaped (default 3600000)');
|
|
4708
5042
|
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.20.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.20.0",
|
|
51
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.20.0",
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.20.0",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.20.0",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.20.0",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.20.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.20.0"
|
|
59
57
|
}
|
|
60
58
|
}
|