c8ctl-plugin-nano 1.7.2 → 1.9.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 +82 -0
- package/c8ctl-plugin.js +526 -4
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -130,6 +130,73 @@ c8ctl nano set model-dir ~/bpmn-workspace
|
|
|
130
130
|
This creates `~/bpmn-workspace/models/` and `~/bpmn-workspace/workers/`. Restart a
|
|
131
131
|
running cluster for a workspace change to take effect.
|
|
132
132
|
|
|
133
|
+
## CLI agent workers: `hire` / `work`
|
|
134
|
+
|
|
135
|
+
Beyond BPMN service-task workers (code in the workspace `workers/` dir), the
|
|
136
|
+
plugin can turn an interactive **CLI agent harness** (Copilot CLI, Claude CLI,
|
|
137
|
+
`pi`, "little coder", …) into a Nano job worker.
|
|
138
|
+
|
|
139
|
+
**`hire`** persists an agent *profile* — a name, a **rank**
|
|
140
|
+
(`principal|senior|junior|decider`), the **command** that starts the CLI, a
|
|
141
|
+
**model** name, and a list of **capabilities**:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Interactive
|
|
145
|
+
c8ctl nano hire
|
|
146
|
+
|
|
147
|
+
# Or non-interactively
|
|
148
|
+
c8ctl nano hire --name reviewer --rank senior --command copilot \
|
|
149
|
+
--model gpt-5 --capabilities code-review,testing
|
|
150
|
+
|
|
151
|
+
# List profiles
|
|
152
|
+
c8ctl nano hire --list
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**`work <name>`** loads the profile, connects with the c8ctl SDK client, and
|
|
156
|
+
registers one job worker per token in the **rank × capability matrix**, then
|
|
157
|
+
polls for work in the foreground until Ctrl-C. For rank `senior` and
|
|
158
|
+
capabilities `code-review, testing` the matrix is:
|
|
159
|
+
|
|
160
|
+
| Token | Meaning |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `senior` | rank alone |
|
|
163
|
+
| `senior:code-review` | rank + one capability (spread) |
|
|
164
|
+
| `senior:testing` | rank + one capability (spread) |
|
|
165
|
+
| `senior:code-review+testing` | rank + all capabilities, sorted (combined) |
|
|
166
|
+
|
|
167
|
+
so a BPMN service task can target a worker at any granularity by setting its job
|
|
168
|
+
type to the matching token.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
c8ctl nano work reviewer # poll for work until Ctrl-C
|
|
172
|
+
c8ctl nano work reviewer --max-parallel 2 --job-timeout 600000
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each activated job runs the profile's command **once** (one-shot): the job is
|
|
176
|
+
serialized to JSON and piped to the CLI's **stdin** —
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{ "jobKey": "...", "jobType": "senior:code-review", "processInstanceKey": "...",
|
|
180
|
+
"prompt": "<variables.prompt ?? variables.task>", "variables": {},
|
|
181
|
+
"profile": { "name": "reviewer", "rank": "senior", "model": "gpt-5",
|
|
182
|
+
"capabilities": ["code-review", "testing"] } }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
and the profile/model are also exported as `AGENT_PROFILE`, `AGENT_RANK`,
|
|
186
|
+
`AGENT_MODEL`, `AGENT_CAPABILITIES`, `AGENT_JOB_TYPE` env vars. On exit `0` the
|
|
187
|
+
job is **completed** with `{ output: <stdout>, exitCode: 0 }` (captured output is
|
|
188
|
+
capped at 1 MiB, with a `truncated` flag when exceeded); any other exit **fails**
|
|
189
|
+
the job with a decremented retry count, and a job that outlives `--job-timeout`
|
|
190
|
+
is killed. Profiles are stored in the plugin's `config.json` (see `c8ctl nano
|
|
191
|
+
config`).
|
|
192
|
+
|
|
193
|
+
> **Trust boundary.** The profile `command` is run through a shell so you can
|
|
194
|
+
> write a full invocation (args, pipes, multi-word commands). It is
|
|
195
|
+
> **operator-authored** — only what you put in your own `config.json` is
|
|
196
|
+
> shell-interpreted. Untrusted job data reaches the harness solely as stdin JSON
|
|
197
|
+
> and `AGENT_*` env vars, never interpolated into the command line, so process
|
|
198
|
+
> variables cannot inject shell commands.
|
|
199
|
+
|
|
133
200
|
## Cleaning up disk
|
|
134
201
|
|
|
135
202
|
```bash
|
|
@@ -169,6 +236,21 @@ and the history cap.
|
|
|
169
236
|
> ⚠️ With `--in-memory`, restart recovers nothing, and Raft/replicated logs are
|
|
170
237
|
> not persisted. Use it for stress/throughput testing, not durability testing.
|
|
171
238
|
|
|
239
|
+
## Console profile (`--console` / `--profile`)
|
|
240
|
+
|
|
241
|
+
The server ships a browser console. Pick how much of it is exposed at runtime:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
c8ctl nano start # studio (default): full IDE + authoring API
|
|
245
|
+
c8ctl nano start --console observe # observability views only; authoring refused (403)
|
|
246
|
+
c8ctl nano start --console off # headless: no console router at all
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- Values: `studio` (default), `observe`, `off`. `--profile` is an alias for
|
|
250
|
+
`--console`, and an inherited `NANOBPMN_CONSOLE` env var is honored when neither
|
|
251
|
+
flag is passed. The plugin passes the choice through as `NANOBPMN_CONSOLE` on
|
|
252
|
+
every node.
|
|
253
|
+
|
|
172
254
|
## Configuration (`set` / `config`)
|
|
173
255
|
|
|
174
256
|
Persistent settings are stored in `<state home>/config.json`:
|
package/c8ctl-plugin.js
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
* NANOBPMN_RF replication factor (1 = single-homed, no Raft)
|
|
14
14
|
* NANOBPMN_RAFT set when RF > 1 to enable per-partition Raft
|
|
15
15
|
* NANOBPMN_DATA_DIR this node's engine data directory
|
|
16
|
+
* NANOBPMN_CONSOLE runtime console profile (off | observe | studio)
|
|
17
|
+
* NANOBPMN_NODE_BIN Node path for the server's worker fallback runtime
|
|
16
18
|
*
|
|
17
19
|
* This plugin spawns N detached node processes wired to talk to each other on
|
|
18
20
|
* localhost, tracks them in a state file, and stops them on request.
|
|
19
21
|
*
|
|
20
22
|
* Usage:
|
|
21
23
|
* c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>]
|
|
22
|
-
* [--in-memory] [--history-max <n>]
|
|
24
|
+
* [--in-memory] [--history-max <n>] [--console <profile>]
|
|
23
25
|
* c8ctl nano status
|
|
24
26
|
* c8ctl nano stop [--purge]
|
|
25
27
|
* c8ctl nano logs [<nodeId>] [--follow]
|
|
@@ -43,6 +45,7 @@ import { homedir, platform as osPlatform } from 'node:os';
|
|
|
43
45
|
import { join, isAbsolute, resolve as resolvePath, dirname, sep } from 'node:path';
|
|
44
46
|
import { createRequire } from 'node:module';
|
|
45
47
|
import { fileURLToPath } from 'node:url';
|
|
48
|
+
import { createInterface } from 'node:readline/promises';
|
|
46
49
|
import { platformForHost } from './platforms.mjs';
|
|
47
50
|
|
|
48
51
|
const requireFromHere = createRequire(import.meta.url);
|
|
@@ -334,6 +337,18 @@ function findBinary(flags) {
|
|
|
334
337
|
*/
|
|
335
338
|
function launcherEnvMarkers(resolved) {
|
|
336
339
|
const markers = { NANOBPMN_LAUNCHER: 'c8ctl-plugin-nano' };
|
|
340
|
+
|
|
341
|
+
// This launcher IS a Node runtime, so hand the server a known-good Node path
|
|
342
|
+
// for its worker fallback (Deno-preferred, Node >= 22.6). Avoid pinning an
|
|
343
|
+
// older Node runtime (the plugin supports Node >=18) so the server can still
|
|
344
|
+
// fall back to a newer Node on PATH when available.
|
|
345
|
+
const [nodeMajor, nodeMinor, nodePatch] = process.versions.node
|
|
346
|
+
.split('.')
|
|
347
|
+
.map((n) => Number.parseInt(n, 10));
|
|
348
|
+
const nodeOk =
|
|
349
|
+
nodeMajor > 22 ||
|
|
350
|
+
(nodeMajor === 22 && (nodeMinor > 6 || (nodeMinor === 6 && nodePatch >= 0)));
|
|
351
|
+
if (nodeOk) markers.NANOBPMN_NODE_BIN = process.execPath;
|
|
337
352
|
const { version } = pluginPackage();
|
|
338
353
|
// The plugin version is the update unit's "current" in the npm channel's
|
|
339
354
|
// version space (same space as `npm view <plugin> version` -> latest), so the
|
|
@@ -349,7 +364,7 @@ function launcherEnvMarkers(resolved) {
|
|
|
349
364
|
// Argument parsing
|
|
350
365
|
// ---------------------------------------------------------------------------
|
|
351
366
|
|
|
352
|
-
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update'];
|
|
367
|
+
const VALID_SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'log', 'restart', 'pause', 'resume', 'clean', 'set', 'config', 'update', 'hire', 'work'];
|
|
353
368
|
|
|
354
369
|
/**
|
|
355
370
|
* Parse positional args + flags into a normalized request.
|
|
@@ -381,6 +396,7 @@ function parseRequest(args, flags) {
|
|
|
381
396
|
capture: Boolean(flags?.capture),
|
|
382
397
|
inMemory: Boolean(flags?.['in-memory'] || flags?.['no-journal']),
|
|
383
398
|
historyMax: intFlag('history-max'),
|
|
399
|
+
console: flags?.console ?? flags?.profile,
|
|
384
400
|
workspace: Boolean(flags?.workspace),
|
|
385
401
|
check: Boolean(flags?.check),
|
|
386
402
|
binary: flags?.binary,
|
|
@@ -529,6 +545,26 @@ async function waitForHealthy(url, timeoutMs = READINESS_TIMEOUT_MS) { const st
|
|
|
529
545
|
// start
|
|
530
546
|
// ---------------------------------------------------------------------------
|
|
531
547
|
|
|
548
|
+
/** Runtime console profiles the server understands (nano-bpm ADR 0035 §C). */
|
|
549
|
+
const CONSOLE_PROFILES = ['off', 'observe', 'studio'];
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Resolves the runtime console profile to pass through as NANOBPMN_CONSOLE.
|
|
553
|
+
* Precedence: --console/--profile flag > inherited NANOBPMN_CONSOLE env >
|
|
554
|
+
* 'studio' (the full IDE, our default). Unknown values are rejected so a typo
|
|
555
|
+
* fails fast here rather than silently degrading the console in the server.
|
|
556
|
+
*/
|
|
557
|
+
function resolveConsoleProfile(reqConsole) {
|
|
558
|
+
const raw = reqConsole ?? process.env.NANOBPMN_CONSOLE ?? 'studio';
|
|
559
|
+
const profile = String(raw).trim().toLowerCase();
|
|
560
|
+
if (!CONSOLE_PROFILES.includes(profile)) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`invalid console profile "${raw}" (use one of: ${CONSOLE_PROFILES.join(', ')})`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return profile;
|
|
566
|
+
}
|
|
567
|
+
|
|
532
568
|
async function startCluster(req) {
|
|
533
569
|
const logger = getLogger();
|
|
534
570
|
|
|
@@ -557,6 +593,7 @@ async function startCluster(req) {
|
|
|
557
593
|
const capture = Boolean(req.capture);
|
|
558
594
|
const inMemory = Boolean(req.inMemory);
|
|
559
595
|
const historyMax = req.historyMax;
|
|
596
|
+
const consoleProfile = resolveConsoleProfile(req.console);
|
|
560
597
|
|
|
561
598
|
if (partitions < nodeCount) {
|
|
562
599
|
logger.warn(
|
|
@@ -630,7 +667,8 @@ async function startCluster(req) {
|
|
|
630
667
|
`Starting Nano BPM cluster: ${nodeCount} node(s), ${partitions} partition(s), ` +
|
|
631
668
|
`RF=${rf}${raft ? ', Raft on' : ''}${capture ? ', trace capture on' : ''}` +
|
|
632
669
|
`${inMemory ? ', in-memory (no disk)' : ''}` +
|
|
633
|
-
`${historyMax !== undefined ? `, history-max=${historyMax}` : ''}
|
|
670
|
+
`${historyMax !== undefined ? `, history-max=${historyMax}` : ''}` +
|
|
671
|
+
`${consoleProfile !== 'studio' ? `, console=${consoleProfile}` : ''}`,
|
|
634
672
|
);
|
|
635
673
|
logger.info(`Binary: ${binary}`);
|
|
636
674
|
logger.info(`Workspace: ${workspaceDir} (models/, workers/)`);
|
|
@@ -667,6 +705,10 @@ async function startCluster(req) {
|
|
|
667
705
|
// Shared, persistent authoring workspace (models + workers). Lives
|
|
668
706
|
// outside the per-node data dir so "nano clean" never wipes it.
|
|
669
707
|
NANOBPMN_WORKSPACE_DIR: workspaceDir,
|
|
708
|
+
// Runtime console profile (off | observe | studio). Default studio (full
|
|
709
|
+
// IDE); pass-through so --console/--profile or an inherited NANOBPMN_CONSOLE
|
|
710
|
+
// picks the observability-only or headless surface. See nano-bpm ADR 0035 §C.
|
|
711
|
+
NANOBPMN_CONSOLE: consoleProfile,
|
|
670
712
|
};
|
|
671
713
|
// Storage axis: an on-disk journal + read-model under the per-node data dir
|
|
672
714
|
// (default), or a fully in-memory engine (in-memory journal + :memory: read
|
|
@@ -1252,6 +1294,461 @@ function showConfig() {
|
|
|
1252
1294
|
console.log(' Change with: c8ctl nano set bin <path> | c8ctl nano set model-dir <path>');
|
|
1253
1295
|
}
|
|
1254
1296
|
|
|
1297
|
+
// ---------------------------------------------------------------------------
|
|
1298
|
+
// hire / work — CLI agent harness workers.
|
|
1299
|
+
//
|
|
1300
|
+
// A "hire" is a persisted agent profile (name, rank, CLI command, model,
|
|
1301
|
+
// capabilities). "work <name>" turns that profile into a set of Nano job
|
|
1302
|
+
// workers: one per job-type in the rank×capability matrix. When a job is
|
|
1303
|
+
// activated, the profile's CLI command is spawned fresh (one-shot), fed the job
|
|
1304
|
+
// as JSON on stdin, and its stdout is returned as the job's `output` variable.
|
|
1305
|
+
// ---------------------------------------------------------------------------
|
|
1306
|
+
|
|
1307
|
+
const RANKS = ['principal', 'senior', 'junior', 'decider'];
|
|
1308
|
+
|
|
1309
|
+
/** Normalize a capability list: trim, drop empties, de-dupe, sort (canonical). */
|
|
1310
|
+
function normalizeCapabilities(input) {
|
|
1311
|
+
const raw = Array.isArray(input)
|
|
1312
|
+
? input
|
|
1313
|
+
: String(input || '').split(',');
|
|
1314
|
+
return [...new Set(raw.map((c) => String(c).trim().toLowerCase()).filter(Boolean))].sort();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** A profile name must be a safe, filesystem/token-friendly slug. */
|
|
1318
|
+
function isValidProfileName(name) {
|
|
1319
|
+
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]*$/i.test(name);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/**
|
|
1323
|
+
* The job-type matrix a worker subscribes to, from a profile's rank
|
|
1324
|
+
* and sorted capabilities [c1, c2, ...]:
|
|
1325
|
+
* - `rank` (rank alone)
|
|
1326
|
+
* - `rank:c1`, `rank:c2` (rank + a single capability, "spread")
|
|
1327
|
+
* - `rank:c1+c2+...` (rank + all capabilities combined; only when >1 cap)
|
|
1328
|
+
* Delimiters: `:` separates rank from capabilities, `+` joins combined caps.
|
|
1329
|
+
* Capabilities are sorted so the combined token is canonical/predictable.
|
|
1330
|
+
*/
|
|
1331
|
+
function jobTypeMatrix(rank, capabilities) {
|
|
1332
|
+
const caps = normalizeCapabilities(capabilities);
|
|
1333
|
+
const tokens = [rank];
|
|
1334
|
+
for (const c of caps) tokens.push(`${rank}:${c}`);
|
|
1335
|
+
if (caps.length > 1) tokens.push(`${rank}:${caps.join('+')}`);
|
|
1336
|
+
return [...new Set(tokens)];
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/** All persisted hire profiles, keyed by name. */
|
|
1340
|
+
function readHires() {
|
|
1341
|
+
const cfg = readConfig();
|
|
1342
|
+
// A JSON array is `typeof === 'object'` but drops string-keyed writes on
|
|
1343
|
+
// JSON.stringify, so treat only plain objects as a valid hires map.
|
|
1344
|
+
return cfg.hires && typeof cfg.hires === 'object' && !Array.isArray(cfg.hires) ? cfg.hires : {};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/** Persist a single hire profile into config.json under `hires`. */
|
|
1348
|
+
function writeHire(profile) {
|
|
1349
|
+
const cfg = readConfig();
|
|
1350
|
+
if (!cfg.hires || typeof cfg.hires !== 'object' || Array.isArray(cfg.hires)) cfg.hires = {};
|
|
1351
|
+
cfg.hires[profile.name] = profile;
|
|
1352
|
+
writeConfig(cfg);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Validate and normalize a stored profile before use so a hand-edited or
|
|
1357
|
+
* version-skewed config.json can't produce undefined job types or an invalid
|
|
1358
|
+
* spawn. Returns the normalized profile, or a { error } describing the problem.
|
|
1359
|
+
*/
|
|
1360
|
+
function normalizeStoredProfile(name, profile) {
|
|
1361
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
|
|
1362
|
+
return { error: `profile "${name}" is not an object` };
|
|
1363
|
+
}
|
|
1364
|
+
const rank = String(profile.rank || '').trim().toLowerCase();
|
|
1365
|
+
if (!RANKS.includes(rank)) {
|
|
1366
|
+
return { error: `profile "${name}" has an invalid rank "${profile.rank}" (expected one of: ${RANKS.join(', ')})` };
|
|
1367
|
+
}
|
|
1368
|
+
const command = String(profile.command || '').trim();
|
|
1369
|
+
if (!command) {
|
|
1370
|
+
return { error: `profile "${name}" has no command to run` };
|
|
1371
|
+
}
|
|
1372
|
+
return {
|
|
1373
|
+
profile: {
|
|
1374
|
+
name,
|
|
1375
|
+
rank,
|
|
1376
|
+
command,
|
|
1377
|
+
model: typeof profile.model === 'string' ? profile.model.trim() : '',
|
|
1378
|
+
capabilities: normalizeCapabilities(profile.capabilities),
|
|
1379
|
+
},
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* hire — create (or overwrite) an agent profile. Interactive by default; every
|
|
1385
|
+
* field can also be supplied via a flag (--name/--rank/--command/--model/
|
|
1386
|
+
* --capabilities) for scripting. Prompts only for the fields still missing.
|
|
1387
|
+
* `--list` prints existing profiles instead.
|
|
1388
|
+
*/
|
|
1389
|
+
async function hireWorker(req, flags) {
|
|
1390
|
+
const logger = getLogger();
|
|
1391
|
+
|
|
1392
|
+
if (flags?.list) {
|
|
1393
|
+
const hires = readHires();
|
|
1394
|
+
const names = Object.keys(hires);
|
|
1395
|
+
if (names.length === 0) {
|
|
1396
|
+
logger.info('No hires yet. Create one with: c8ctl nano hire');
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
logger.info('Hired agent profiles:');
|
|
1400
|
+
for (const name of names.sort()) {
|
|
1401
|
+
const p = hires[name];
|
|
1402
|
+
logger.info(` ${name} [${p.rank}] ${p.command} (model: ${p.model || '-'}; caps: ${normalizeCapabilities(p.capabilities).join(', ') || '-'})`);
|
|
1403
|
+
}
|
|
1404
|
+
logger.info('');
|
|
1405
|
+
logger.info('Put one to work with: c8ctl nano work <name>');
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// Seed from flags; prompt for anything still missing. Trim string flags so a
|
|
1410
|
+
// stray space can't be persisted into config.json or the spawned command.
|
|
1411
|
+
let name = flags?.name ? String(flags.name).trim() : req.positional[0];
|
|
1412
|
+
let rank = flags?.rank ? String(flags.rank).trim().toLowerCase() : undefined;
|
|
1413
|
+
let command = flags?.command !== undefined ? String(flags.command).trim() : undefined;
|
|
1414
|
+
let model = flags?.model !== undefined ? String(flags.model).trim() : undefined;
|
|
1415
|
+
let capabilities = flags?.capabilities !== undefined ? flags.capabilities : undefined;
|
|
1416
|
+
|
|
1417
|
+
const missingRequired = !name || !rank || !command;
|
|
1418
|
+
const missingOptional = model === undefined || capabilities === undefined;
|
|
1419
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
1420
|
+
|
|
1421
|
+
// Non-interactively only name/rank/command are required; model and
|
|
1422
|
+
// capabilities are optional (they default to empty), matching how the
|
|
1423
|
+
// interactive prompts label them.
|
|
1424
|
+
if (missingRequired && !interactive) {
|
|
1425
|
+
logger.error('Non-interactive: provide at least --name, --rank and --command.');
|
|
1426
|
+
logger.info('Example: c8ctl nano hire --name reviewer --rank senior --command copilot --model gpt-5 --capabilities code-review,testing');
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
if (interactive && (missingRequired || missingOptional)) {
|
|
1431
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1432
|
+
try {
|
|
1433
|
+
console.log('Hire a CLI agent worker. Press Ctrl-C to cancel.');
|
|
1434
|
+
console.log('');
|
|
1435
|
+
while (!name) {
|
|
1436
|
+
const ans = (await rl.question('Profile name: ')).trim();
|
|
1437
|
+
if (isValidProfileName(ans)) { name = ans; break; }
|
|
1438
|
+
console.log(' Please use letters, digits, dot, dash or underscore.');
|
|
1439
|
+
}
|
|
1440
|
+
while (!rank) {
|
|
1441
|
+
const ans = (await rl.question(`Rank (${RANKS.join('|')}): `)).trim().toLowerCase();
|
|
1442
|
+
if (RANKS.includes(ans)) { rank = ans; break; }
|
|
1443
|
+
console.log(` Rank must be one of: ${RANKS.join(', ')}`);
|
|
1444
|
+
}
|
|
1445
|
+
while (!command) {
|
|
1446
|
+
const ans = (await rl.question('CLI command (e.g. copilot, claude, pi): ')).trim();
|
|
1447
|
+
if (ans) { command = ans; break; }
|
|
1448
|
+
console.log(' A command is required.');
|
|
1449
|
+
}
|
|
1450
|
+
if (model === undefined) {
|
|
1451
|
+
model = (await rl.question('Model name (optional): ')).trim();
|
|
1452
|
+
}
|
|
1453
|
+
if (capabilities === undefined) {
|
|
1454
|
+
capabilities = (await rl.question('Capabilities (comma-separated, optional): ')).trim();
|
|
1455
|
+
}
|
|
1456
|
+
} finally {
|
|
1457
|
+
rl.close();
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Optional fields default to empty when omitted (e.g. scripted invocations).
|
|
1462
|
+
if (model === undefined) model = '';
|
|
1463
|
+
if (capabilities === undefined) capabilities = '';
|
|
1464
|
+
|
|
1465
|
+
if (!isValidProfileName(name)) {
|
|
1466
|
+
logger.error(`Invalid profile name "${name}". Use letters, digits, dot, dash or underscore.`);
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
if (!RANKS.includes(rank)) {
|
|
1470
|
+
logger.error(`Invalid rank "${rank}". Must be one of: ${RANKS.join(', ')}`);
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
if (!command) {
|
|
1474
|
+
logger.error('A CLI command is required.');
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const existed = Boolean(readHires()[name]);
|
|
1479
|
+
const profile = {
|
|
1480
|
+
name,
|
|
1481
|
+
rank,
|
|
1482
|
+
command,
|
|
1483
|
+
model: model || '',
|
|
1484
|
+
capabilities: normalizeCapabilities(capabilities),
|
|
1485
|
+
createdAt: new Date().toISOString(),
|
|
1486
|
+
};
|
|
1487
|
+
writeHire(profile);
|
|
1488
|
+
|
|
1489
|
+
const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
|
|
1490
|
+
logger.info(`${existed ? 'Updated' : 'Hired'} "${name}" [${profile.rank}] → ${profile.command}`);
|
|
1491
|
+
logger.info(` model: ${profile.model || '(none)'}`);
|
|
1492
|
+
logger.info(` capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
|
|
1493
|
+
logger.info(` job types (${matrix.length}): ${matrix.join(' ')}`);
|
|
1494
|
+
logger.info(`Put it to work with: c8ctl nano work ${name}`);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* Concatenate captured Buffer chunks into a UTF-8 string, dropping a trailing
|
|
1499
|
+
* incomplete multibyte sequence (which the byte cap may have split) so decoding
|
|
1500
|
+
* never emits a replacement char or pushes the string over the byte cap.
|
|
1501
|
+
*/
|
|
1502
|
+
function joinCapped(chunks) {
|
|
1503
|
+
if (!chunks.length) return '';
|
|
1504
|
+
let buf = Buffer.concat(chunks);
|
|
1505
|
+
let i = buf.length - 1;
|
|
1506
|
+
let cont = 0;
|
|
1507
|
+
while (i >= 0 && (buf[i] & 0xc0) === 0x80 && cont < 3) { i -= 1; cont += 1; }
|
|
1508
|
+
if (i >= 0) {
|
|
1509
|
+
const lead = buf[i];
|
|
1510
|
+
let needed;
|
|
1511
|
+
if ((lead & 0x80) === 0x00) needed = 0;
|
|
1512
|
+
else if ((lead & 0xe0) === 0xc0) needed = 1;
|
|
1513
|
+
else if ((lead & 0xf0) === 0xe0) needed = 2;
|
|
1514
|
+
else if ((lead & 0xf8) === 0xf0) needed = 3;
|
|
1515
|
+
else needed = -1;
|
|
1516
|
+
if (needed > 0 && cont < needed) buf = buf.subarray(0, i);
|
|
1517
|
+
}
|
|
1518
|
+
return buf.toString('utf8');
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Kill a spawned child and its whole process tree. With `detached: true` on
|
|
1523
|
+
* POSIX the child leads its own process group, so a negative PID signals every
|
|
1524
|
+
* process in that group (shell wrapper + the actual harness command). Falls
|
|
1525
|
+
* back to a plain child.kill() on Windows or if the group signal fails.
|
|
1526
|
+
*/
|
|
1527
|
+
function killTree(child) {
|
|
1528
|
+
const pid = child.pid;
|
|
1529
|
+
if (process.platform !== 'win32' && typeof pid === 'number') {
|
|
1530
|
+
try { process.kill(-pid, 'SIGKILL'); return; } catch { /* fall through */ }
|
|
1531
|
+
}
|
|
1532
|
+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* Run a single activated job through the profile's CLI command (one-shot):
|
|
1537
|
+
* spawn the command fresh, pipe the job as JSON on stdin, capture stdout, and
|
|
1538
|
+
* resolve to a job action. Exit 0 → complete with { output, exitCode }; any
|
|
1539
|
+
* other exit (or spawn failure) → fail with a decremented retry count.
|
|
1540
|
+
* A child that outlives `timeoutMs` is killed and reported as a failure so it
|
|
1541
|
+
* never leaks a worker slot.
|
|
1542
|
+
*/
|
|
1543
|
+
function runAgentJob(profile, job, timeoutMs) {
|
|
1544
|
+
return new Promise((resolve) => {
|
|
1545
|
+
const variables = job.variables && typeof job.variables === 'object' ? job.variables : {};
|
|
1546
|
+
const payload = {
|
|
1547
|
+
jobKey: job.jobKey,
|
|
1548
|
+
jobType: job.type,
|
|
1549
|
+
processInstanceKey: job.processInstanceKey ?? null,
|
|
1550
|
+
elementInstanceKey: job.elementInstanceKey ?? null,
|
|
1551
|
+
elementId: job.elementId ?? null,
|
|
1552
|
+
bpmnProcessId: job.bpmnProcessId ?? job.processDefinitionId ?? null,
|
|
1553
|
+
prompt: variables.prompt ?? variables.task ?? null,
|
|
1554
|
+
variables,
|
|
1555
|
+
customHeaders: job.customHeaders ?? {},
|
|
1556
|
+
profile: {
|
|
1557
|
+
name: profile.name,
|
|
1558
|
+
rank: profile.rank,
|
|
1559
|
+
model: profile.model,
|
|
1560
|
+
capabilities: profile.capabilities,
|
|
1561
|
+
},
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
const child = spawn(profile.command, {
|
|
1565
|
+
shell: true,
|
|
1566
|
+
// Run the shell in its own process group so the timeout handler can kill
|
|
1567
|
+
// the whole tree (shell + harness), not just the shell wrapper PID.
|
|
1568
|
+
detached: process.platform !== 'win32',
|
|
1569
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1570
|
+
env: {
|
|
1571
|
+
...process.env,
|
|
1572
|
+
AGENT_PROFILE: profile.name,
|
|
1573
|
+
AGENT_RANK: profile.rank,
|
|
1574
|
+
AGENT_MODEL: profile.model || '',
|
|
1575
|
+
AGENT_CAPABILITIES: (profile.capabilities || []).join(','),
|
|
1576
|
+
AGENT_JOB_TYPE: String(job.type ?? ''),
|
|
1577
|
+
},
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
const stdoutChunks = [];
|
|
1581
|
+
const stderrChunks = [];
|
|
1582
|
+
let stdoutBytes = 0;
|
|
1583
|
+
let stderrBytes = 0;
|
|
1584
|
+
let stdoutTruncated = false;
|
|
1585
|
+
let stderrTruncated = false;
|
|
1586
|
+
let settled = false;
|
|
1587
|
+
// Bound captured output (by BYTES, not string length) so a noisy/runaway
|
|
1588
|
+
// harness can't grow memory without limit and crash the worker.
|
|
1589
|
+
const MAX_CAPTURE_BYTES = 1_048_576; // 1 MiB per stream
|
|
1590
|
+
const finish = (result) => {
|
|
1591
|
+
if (settled) return;
|
|
1592
|
+
settled = true;
|
|
1593
|
+
if (timer) clearTimeout(timer);
|
|
1594
|
+
resolve(result);
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
// Kill (and fail) a child that runs longer than the job's timeout so it can
|
|
1598
|
+
// never permanently hold a worker slot or leak the process.
|
|
1599
|
+
const timer = timeoutMs && timeoutMs > 0
|
|
1600
|
+
? setTimeout(() => {
|
|
1601
|
+
killTree(child);
|
|
1602
|
+
finish({ ok: false, exitCode: null, stdout: joinCapped(stdoutChunks), stderr: joinCapped(stderrChunks), error: `timed out after ${timeoutMs}ms`, truncated: stdoutTruncated, stderrTruncated });
|
|
1603
|
+
}, timeoutMs)
|
|
1604
|
+
: null;
|
|
1605
|
+
|
|
1606
|
+
child.stdout.on('data', (d) => {
|
|
1607
|
+
const buf = Buffer.isBuffer(d) ? d : Buffer.from(d);
|
|
1608
|
+
const remaining = MAX_CAPTURE_BYTES - stdoutBytes;
|
|
1609
|
+
if (remaining <= 0) { stdoutTruncated = true; return; }
|
|
1610
|
+
if (buf.length > remaining) { stdoutChunks.push(buf.subarray(0, remaining)); stdoutBytes = MAX_CAPTURE_BYTES; stdoutTruncated = true; }
|
|
1611
|
+
else { stdoutChunks.push(buf); stdoutBytes += buf.length; }
|
|
1612
|
+
});
|
|
1613
|
+
child.stderr.on('data', (d) => {
|
|
1614
|
+
const buf = Buffer.isBuffer(d) ? d : Buffer.from(d);
|
|
1615
|
+
const remaining = MAX_CAPTURE_BYTES - stderrBytes;
|
|
1616
|
+
if (remaining <= 0) { stderrTruncated = true; return; }
|
|
1617
|
+
if (buf.length > remaining) { stderrChunks.push(buf.subarray(0, remaining)); stderrBytes = MAX_CAPTURE_BYTES; stderrTruncated = true; }
|
|
1618
|
+
else { stderrChunks.push(buf); stderrBytes += buf.length; }
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
child.on('error', (err) => {
|
|
1622
|
+
finish({ ok: false, exitCode: null, stdout: joinCapped(stdoutChunks), stderr: joinCapped(stderrChunks), error: err.message, truncated: stdoutTruncated, stderrTruncated });
|
|
1623
|
+
});
|
|
1624
|
+
child.on('close', (code, signal) => {
|
|
1625
|
+
finish({ ok: code === 0, exitCode: code, signal: signal ?? null, stdout: joinCapped(stdoutChunks), stderr: joinCapped(stderrChunks), truncated: stdoutTruncated, stderrTruncated });
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// The child may exit before reading stdin; swallow the async EPIPE so it
|
|
1629
|
+
// doesn't crash the whole worker process (only the `child` close/error
|
|
1630
|
+
// handlers above decide the job outcome).
|
|
1631
|
+
child.stdin.on('error', () => {});
|
|
1632
|
+
try {
|
|
1633
|
+
child.stdin.write(JSON.stringify(payload));
|
|
1634
|
+
child.stdin.end();
|
|
1635
|
+
} catch {
|
|
1636
|
+
// 'error' handler above resolves the promise on spawn failure.
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* work — turn a hire profile into live Nano job workers (one per job-type in
|
|
1643
|
+
* the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
|
|
1644
|
+
* Uses the c8ctl-provided SDK client (globalThis.c8ctl.createClient()).
|
|
1645
|
+
*/
|
|
1646
|
+
async function workAgent(req, flags) {
|
|
1647
|
+
const logger = getLogger();
|
|
1648
|
+
const name = flags?.name ? String(flags.name).trim() : req.positional[0];
|
|
1649
|
+
|
|
1650
|
+
if (!name) {
|
|
1651
|
+
const hires = readHires();
|
|
1652
|
+
const names = Object.keys(hires).sort();
|
|
1653
|
+
logger.error('Usage: c8ctl nano work <profileName>');
|
|
1654
|
+
if (names.length > 0) logger.info(`Profiles: ${names.join(', ')}`);
|
|
1655
|
+
else logger.info('No hires yet. Create one with: c8ctl nano hire');
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const stored = readHires()[name];
|
|
1660
|
+
if (!stored) {
|
|
1661
|
+
logger.error(`No hire named "${name}". List profiles with: c8ctl nano hire --list`);
|
|
1662
|
+
process.exit(1);
|
|
1663
|
+
}
|
|
1664
|
+
const normalized = normalizeStoredProfile(name, stored);
|
|
1665
|
+
if (normalized.error) {
|
|
1666
|
+
logger.error(`Cannot work "${name}": ${normalized.error}. Re-create it with: c8ctl nano hire`);
|
|
1667
|
+
process.exit(1);
|
|
1668
|
+
}
|
|
1669
|
+
const profile = normalized.profile;
|
|
1670
|
+
|
|
1671
|
+
if (!globalThis.c8ctl || typeof globalThis.c8ctl.createClient !== 'function') {
|
|
1672
|
+
logger.error('work requires the c8ctl runtime (createClient). Run it via the c8ctl CLI.');
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const intFlag = (v, dflt) => {
|
|
1677
|
+
const n = Number.parseInt(String(v ?? ''), 10);
|
|
1678
|
+
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
1679
|
+
};
|
|
1680
|
+
const maxParallelJobs = intFlag(flags?.['max-parallel'], 1);
|
|
1681
|
+
const jobTimeoutMs = intFlag(flags?.['job-timeout'], 5 * 60_000);
|
|
1682
|
+
|
|
1683
|
+
const matrix = jobTypeMatrix(profile.rank, profile.capabilities);
|
|
1684
|
+
const camunda = globalThis.c8ctl.createClient();
|
|
1685
|
+
|
|
1686
|
+
logger.info(`Putting "${name}" [${profile.rank}] to work → ${profile.command}`);
|
|
1687
|
+
logger.info(` model: ${profile.model || '(none)'}; capabilities: ${profile.capabilities.join(', ') || '(none)'}`);
|
|
1688
|
+
logger.info(` listening on ${matrix.length} job type(s): ${matrix.join(' ')}`);
|
|
1689
|
+
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobTimeoutMs}ms`);
|
|
1690
|
+
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
1691
|
+
|
|
1692
|
+
const workers = matrix.map((jobType) =>
|
|
1693
|
+
camunda.createJobWorker({
|
|
1694
|
+
jobType,
|
|
1695
|
+
workerName: `${name}:${jobType}`,
|
|
1696
|
+
maxParallelJobs,
|
|
1697
|
+
jobTimeoutMs,
|
|
1698
|
+
jobHandler: async (job) => {
|
|
1699
|
+
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${profile.command}`);
|
|
1700
|
+
const result = await runAgentJob(profile, job, jobTimeoutMs);
|
|
1701
|
+
if (result.ok) {
|
|
1702
|
+
logger.info(`[${jobType}] job ${job.jobKey} complete (exit 0)${result.truncated ? ' [output truncated]' : ''}`);
|
|
1703
|
+
return job.complete({ output: result.stdout, exitCode: 0, agent: profile.name, truncated: Boolean(result.truncated) });
|
|
1704
|
+
}
|
|
1705
|
+
const retries = Math.max(0, (Number(job.retries) || 1) - 1);
|
|
1706
|
+
const detail = result.error
|
|
1707
|
+
|| (result.stderr || '').trim() + (result.stderrTruncated && (result.stderr || '').trim() ? ' [stderr truncated]' : '')
|
|
1708
|
+
|| (result.signal ? `terminated by signal ${result.signal}` : `exit code ${result.exitCode}`);
|
|
1709
|
+
logger.warn(`[${jobType}] job ${job.jobKey} failed (${detail}); retries left ${retries}`);
|
|
1710
|
+
return job.fail({
|
|
1711
|
+
errorMessage: `agent "${profile.name}" failed: ${detail}`.slice(0, 2000),
|
|
1712
|
+
retries,
|
|
1713
|
+
});
|
|
1714
|
+
},
|
|
1715
|
+
}),
|
|
1716
|
+
);
|
|
1717
|
+
|
|
1718
|
+
// Keep the process alive until a stop signal, then drain gracefully.
|
|
1719
|
+
await new Promise((resolve) => {
|
|
1720
|
+
let stopping = false;
|
|
1721
|
+
const stop = async (signal) => {
|
|
1722
|
+
if (stopping) return;
|
|
1723
|
+
stopping = true;
|
|
1724
|
+
logger.info(`Received ${signal} — stopping ${workers.length} worker(s)...`);
|
|
1725
|
+
let stopFailures = 0;
|
|
1726
|
+
await Promise.all(
|
|
1727
|
+
workers.map(async (w) => {
|
|
1728
|
+
try {
|
|
1729
|
+
if (typeof w.stopGracefully === 'function') {
|
|
1730
|
+
await w.stopGracefully({ waitUpToMs: STOP_GRACE_MS });
|
|
1731
|
+
} else if (typeof w.stop === 'function') {
|
|
1732
|
+
await w.stop();
|
|
1733
|
+
}
|
|
1734
|
+
} catch {
|
|
1735
|
+
// best-effort: never let one worker's stop failure hang shutdown
|
|
1736
|
+
stopFailures += 1;
|
|
1737
|
+
}
|
|
1738
|
+
}),
|
|
1739
|
+
);
|
|
1740
|
+
if (stopFailures > 0) {
|
|
1741
|
+
logger.warn(`${stopFailures} of ${workers.length} worker(s) did not stop cleanly; some connections may still be open.`);
|
|
1742
|
+
} else {
|
|
1743
|
+
logger.info('All workers stopped.');
|
|
1744
|
+
}
|
|
1745
|
+
resolve();
|
|
1746
|
+
};
|
|
1747
|
+
process.once('SIGINT', () => { stop('SIGINT'); });
|
|
1748
|
+
process.once('SIGTERM', () => { stop('SIGTERM'); });
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1255
1752
|
// ---------------------------------------------------------------------------
|
|
1256
1753
|
// update — pull a new nanobpmn release onto a machine with an existing install.
|
|
1257
1754
|
// The plugin (and the bundled server binary, shipped via the matching platform
|
|
@@ -2507,6 +3004,10 @@ export const metadata = {
|
|
|
2507
3004
|
{ command: 'c8ctl nano config', description: 'Show current plugin configuration and paths' },
|
|
2508
3005
|
{ command: 'c8ctl nano update', description: 'Pull the latest published nano release (re-installs via npm)' },
|
|
2509
3006
|
{ command: 'c8ctl nano update --check', description: 'Check whether a newer nano release is available' },
|
|
3007
|
+
{ command: 'c8ctl nano hire', description: 'Interactively create a CLI agent worker profile (name, rank, command, model, capabilities)' },
|
|
3008
|
+
{ command: 'c8ctl nano hire --name reviewer --rank senior --command copilot --model gpt-5 --capabilities code-review,testing', description: 'Create a profile non-interactively' },
|
|
3009
|
+
{ command: 'c8ctl nano hire --list', description: 'List hired agent profiles' },
|
|
3010
|
+
{ command: 'c8ctl nano work reviewer', description: 'Spawn Nano job workers for the "reviewer" profile and poll for work' },
|
|
2510
3011
|
],
|
|
2511
3012
|
},
|
|
2512
3013
|
processos: {
|
|
@@ -2540,12 +3041,22 @@ export const commands = {
|
|
|
2540
3041
|
'in-memory': { type: 'boolean', description: 'start: run with NO on-disk journal/read-model (in-memory engine; state lost on restart). Alias: --no-journal' },
|
|
2541
3042
|
'no-journal': { type: 'boolean', description: 'start: alias for --in-memory' },
|
|
2542
3043
|
'history-max': { type: 'string', description: 'start: cap retained terminal instances in the read model (NANOBPMN_HISTORY_MAX_INSTANCES; 0/unset = unbounded)' },
|
|
3044
|
+
console: { type: 'string', description: 'start: runtime console profile off|observe|studio (NANOBPMN_CONSOLE; default studio). Alias: --profile' },
|
|
3045
|
+
profile: { type: 'string', description: 'start: alias for --console (off|observe|studio; default studio)' },
|
|
2543
3046
|
follow: { type: 'boolean', description: 'logs: stream output (tail -F)', short: 'f' },
|
|
2544
3047
|
purge: { type: 'boolean', description: 'stop/restart: also delete per-node engine data' },
|
|
2545
3048
|
force: { type: 'boolean', description: 'start: stop any existing cluster first' },
|
|
2546
3049
|
workspace: { type: 'boolean', description: 'clean: also delete the workspace (models + workers)' },
|
|
2547
3050
|
check: { type: 'boolean', description: 'update: only report whether a new release is available; do not install' },
|
|
2548
3051
|
binary: { type: 'string', description: 'Path to the nanobpmn server binary' },
|
|
3052
|
+
name: { type: 'string', description: 'hire/work: agent profile name (alt to positional arg)' },
|
|
3053
|
+
rank: { type: 'string', description: 'hire: agent rank (principal|senior|junior|decider)' },
|
|
3054
|
+
command: { type: 'string', description: 'hire: CLI command that runs the agent harness (e.g. copilot, claude, pi)' },
|
|
3055
|
+
model: { type: 'string', description: 'hire: model name passed to the harness (AGENT_MODEL)' },
|
|
3056
|
+
capabilities: { type: 'string', description: 'hire: comma-separated capability list' },
|
|
3057
|
+
list: { type: 'boolean', description: 'hire: list existing agent profiles instead of creating one' },
|
|
3058
|
+
'max-parallel': { type: 'string', description: 'work: max concurrent jobs per worker (default 1)' },
|
|
3059
|
+
'job-timeout': { type: 'string', description: 'work: max harness runtime per job in ms; the spawned process is killed past this (default 300000)' },
|
|
2549
3060
|
},
|
|
2550
3061
|
handler: async (args, flags) => {
|
|
2551
3062
|
const logger = getLogger();
|
|
@@ -2594,6 +3105,12 @@ export const commands = {
|
|
|
2594
3105
|
case 'update':
|
|
2595
3106
|
updatePlugin(req);
|
|
2596
3107
|
break;
|
|
3108
|
+
case 'hire':
|
|
3109
|
+
await hireWorker(req, flags);
|
|
3110
|
+
break;
|
|
3111
|
+
case 'work':
|
|
3112
|
+
await workAgent(req, flags);
|
|
3113
|
+
break;
|
|
2597
3114
|
}
|
|
2598
3115
|
} catch (error) {
|
|
2599
3116
|
logger.error(`nano ${req.subcommand} failed: ${error instanceof Error ? error.message : error}`);
|
|
@@ -2671,7 +3188,7 @@ export const commands = {
|
|
|
2671
3188
|
|
|
2672
3189
|
function printUsage() {
|
|
2673
3190
|
console.log('Usage:');
|
|
2674
|
-
console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--in-memory] [--history-max <n>] [--binary <path>]');
|
|
3191
|
+
console.log(' c8ctl nano start [<nodes>] [--port <basePort>] [--partitions <n>] [--rf <n>] [--raft] [--capture] [--in-memory] [--history-max <n>] [--console <profile>] [--binary <path>]');
|
|
2675
3192
|
console.log(' c8ctl nano status [--port <port>]');
|
|
2676
3193
|
console.log(' c8ctl nano stop [--purge]');
|
|
2677
3194
|
console.log(' c8ctl nano logs [<nodeId>] [--follow]');
|
|
@@ -2682,6 +3199,8 @@ function printUsage() {
|
|
|
2682
3199
|
console.log(' c8ctl nano set <bin|model-dir> <path>');
|
|
2683
3200
|
console.log(' c8ctl nano config');
|
|
2684
3201
|
console.log(' c8ctl nano update [--check]');
|
|
3202
|
+
console.log(' c8ctl nano hire [--name <n>] [--rank <r>] [--command <c>] [--model <m>] [--capabilities <a,b>] [--list]');
|
|
3203
|
+
console.log(' c8ctl nano work <profileName> [--max-parallel <n>] [--job-timeout <ms>]');
|
|
2685
3204
|
console.log('');
|
|
2686
3205
|
console.log('Subcommands:');
|
|
2687
3206
|
console.log(' start Spawn an N-node local cluster wired to talk to each other on localhost');
|
|
@@ -2695,6 +3214,8 @@ function printUsage() {
|
|
|
2695
3214
|
console.log(' set Persist a setting: "bin <path>" or "model-dir <path>"');
|
|
2696
3215
|
console.log(' config Show current configuration and on-disk locations');
|
|
2697
3216
|
console.log(' update Pull the latest published nano release (--check to only report)');
|
|
3217
|
+
console.log(' hire Create a CLI agent worker profile (rank + capabilities → job-type matrix)');
|
|
3218
|
+
console.log(' work Run a hired profile as Nano job workers, polling for work until Ctrl-C');
|
|
2698
3219
|
console.log('');
|
|
2699
3220
|
console.log('Options:');
|
|
2700
3221
|
console.log(' <nodes> Number of nodes to start (default 1)');
|
|
@@ -2705,6 +3226,7 @@ function printUsage() {
|
|
|
2705
3226
|
console.log(' --capture start: enable trace capture (recorded-input replay) on every node');
|
|
2706
3227
|
console.log(' --in-memory start: run with NO on-disk journal/read-model (alias --no-journal; state lost on restart)');
|
|
2707
3228
|
console.log(' --history-max <n> start: cap retained terminal instances in the read model (0/unset = unbounded)');
|
|
3229
|
+
console.log(' --console <profile> start: runtime console profile off|observe|studio (alias --profile; default studio)');
|
|
2708
3230
|
console.log(' --binary <path> Path to the nanobpmn server binary (overrides "set bin")');
|
|
2709
3231
|
console.log(' --purge stop: also delete per-node engine data');
|
|
2710
3232
|
console.log(' --force start: stop any existing cluster first');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"semantic-release": "^25.0.3"
|
|
50
50
|
},
|
|
51
51
|
"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.
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.9.0",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.9.0",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.9.0",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.9.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.9.0",
|
|
57
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.9.0",
|
|
58
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.9.0"
|
|
59
59
|
}
|
|
60
60
|
}
|