cyberlegion 0.4.0 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.plugin/pins.json +1 -1
- package/bin/cyberlegion.mjs +18 -1
- package/dist/cli.mjs +267 -146
- package/dist/index.d.mts +19 -4
- package/dist/index.mjs +99 -15
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/readme.md +6 -3
- package/skills/dispatch-governance/SKILL.md +9 -4
- package/skills/dispatch-governance/scripts/cyberlegion.mjs +31 -0
- package/skills/init-cyberlegion/SKILL.md +16 -14
- package/skills/init-cyberlegion/scripts/cyberlegion.mjs +31 -0
- package/skills/legate/SKILL.md +14 -14
- package/skills/legate/scripts/cyberlegion.mjs +31 -0
- package/skills/manage-inbox/SKILL.md +27 -14
- package/skills/manage-inbox/scripts/cyberlegion.mjs +31 -0
- package/skills/relay-governance/README.md +2 -2
- package/skills/relay-governance/SKILL.md +7 -2
- package/skills/relay-governance/scripts/cyberlegion.mjs +31 -0
- package/skills/subagent-backend-governance/SKILL.md +6 -1
- package/skills/subagent-backend-governance/scripts/cyberlegion.mjs +31 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { callerPane, currentPane, herdrMuxAdapter, nodeExec, nudge, probeMultiplexer, probeMultiplexer as probeMultiplexer$1, tmuxMuxAdapter } from "cyber-mux";
|
|
2
2
|
import { assertDistinctFromPrimary, assertDistinctFromPrimary as assertDistinctFromPrimary$1, gitWorktreeAdapter, gitWorktreeAdapter as gitWorktreeAdapter$1, resolvePrimaryRoot, resolvePrimaryRoot as resolvePrimaryRoot$1 } from "cyber-mux/worktree";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
@@ -746,7 +746,10 @@ function spawn(ctx, input) {
|
|
|
746
746
|
const id = randomId();
|
|
747
747
|
const primaryRoot = resolvePrimaryRoot$1(exec);
|
|
748
748
|
const launch = input.command ?? LAUNCH_MAP[harness];
|
|
749
|
-
const
|
|
749
|
+
const launchLine = () => {
|
|
750
|
+
const shimDir = ctx.self ? writeSelfShim(paths.dataDir(ctx.store.root, id), ctx.self) : void 0;
|
|
751
|
+
return `${shimDir ? `PATH=${shellQuote$1(shimDir)}:"$PATH" ` : ""}${muxEnvPrefix(sessionAdapter.name)}${launch}`;
|
|
752
|
+
};
|
|
750
753
|
const from = callerPane(sessionAdapter, normalizedEnv);
|
|
751
754
|
let cwd;
|
|
752
755
|
let worktree;
|
|
@@ -759,7 +762,7 @@ function spawn(ctx, input) {
|
|
|
759
762
|
const at = input.at ?? "tab";
|
|
760
763
|
target = sessionAdapter.open(exec, {
|
|
761
764
|
cwd,
|
|
762
|
-
launch:
|
|
765
|
+
launch: launchLine(),
|
|
763
766
|
at,
|
|
764
767
|
from,
|
|
765
768
|
...labelFor(at, input, brief, id)
|
|
@@ -774,7 +777,7 @@ function spawn(ctx, input) {
|
|
|
774
777
|
primaryRoot,
|
|
775
778
|
branch,
|
|
776
779
|
path: worktreePath,
|
|
777
|
-
launch:
|
|
780
|
+
launch: launchLine(),
|
|
778
781
|
...labelFor(at, input, brief, id)
|
|
779
782
|
});
|
|
780
783
|
assertDistinctFromPrimary$1(opened.worktree.root, primaryRoot);
|
|
@@ -794,7 +797,7 @@ function spawn(ctx, input) {
|
|
|
794
797
|
worktree = added;
|
|
795
798
|
target = sessionAdapter.open(exec, {
|
|
796
799
|
cwd,
|
|
797
|
-
launch:
|
|
800
|
+
launch: launchLine(),
|
|
798
801
|
at,
|
|
799
802
|
from,
|
|
800
803
|
...labelFor(at, input, brief, id)
|
|
@@ -821,7 +824,7 @@ function spawn(ctx, input) {
|
|
|
821
824
|
};
|
|
822
825
|
saveAgent(ctx.store, rec);
|
|
823
826
|
ctx.store.putPaneIndex(target.id, id);
|
|
824
|
-
ctx.store.writeBrief(id, brief);
|
|
827
|
+
ctx.store.writeBrief(id, composeBrief(brief, input.briefInstructions));
|
|
825
828
|
return {
|
|
826
829
|
agent: rec,
|
|
827
830
|
pane: target.id,
|
|
@@ -888,6 +891,31 @@ function muxEnvPrefix(muxName) {
|
|
|
888
891
|
if (muxName === "herdr") return "CYBER_MUX=herdr ";
|
|
889
892
|
return "";
|
|
890
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Write `<dataDir>/bin/cyberlegion`, a POSIX shim that execs `self` with the caller's arguments,
|
|
896
|
+
* and return its directory. A spawned session gets that directory first on its PATH, so the
|
|
897
|
+
* `cyberlegion` a brief tells it to run is the install that spawned it: nothing is resolved from
|
|
898
|
+
* the registry or the session's own PATH at report time.
|
|
899
|
+
*/
|
|
900
|
+
function writeSelfShim(dataDir, self) {
|
|
901
|
+
const dir = join(dataDir, "bin");
|
|
902
|
+
mkdirSync(dir, { recursive: true });
|
|
903
|
+
const shim = join(dir, "cyberlegion");
|
|
904
|
+
writeFileSync(shim, `#!/bin/sh\nexec ${self.map(shellQuote$1).join(" ")} "$@"\n`);
|
|
905
|
+
chmodSync(shim, 493);
|
|
906
|
+
return dir;
|
|
907
|
+
}
|
|
908
|
+
/** Single-quote `s` for a POSIX shell, so any path survives word splitting and expansion. */
|
|
909
|
+
function shellQuote$1(s) {
|
|
910
|
+
return `'${s.replaceAll("'", "'\\''")}'`;
|
|
911
|
+
}
|
|
912
|
+
/** The brief file's text: the task alone, or — when the harness took no instructions on its command
|
|
913
|
+
* line — the def's instructions under their own heading ahead of it, so the peer can see they reach
|
|
914
|
+
* it as a user turn rather than a system prompt. */
|
|
915
|
+
function composeBrief(brief, instructions) {
|
|
916
|
+
if (!instructions) return brief;
|
|
917
|
+
return `## Agent instructions\n\n${instructions}\n\n## Brief\n\n${brief}`;
|
|
918
|
+
}
|
|
891
919
|
/** Resolve a spawn brief from --brief-file, --task -, or --task <text>; null if no source given. */
|
|
892
920
|
function resolveBrief(input, readStdin = () => readFileSync(0, "utf8")) {
|
|
893
921
|
if (input.briefFile) return readFileSync(input.briefFile, "utf8");
|
|
@@ -955,6 +983,14 @@ function toBool(v) {
|
|
|
955
983
|
if (v === "true") return true;
|
|
956
984
|
if (v === "false") return false;
|
|
957
985
|
}
|
|
986
|
+
/** The known harnesses are exactly those with a launch binary — one home for the set. */
|
|
987
|
+
const HARNESSES = Object.keys(LAUNCH_MAP);
|
|
988
|
+
/** A `harness` tag outside the known set has no launch binary, so a typo fails here, loudly, rather
|
|
989
|
+
* than surfacing later as an unlaunchable command. */
|
|
990
|
+
function toHarness(value, path) {
|
|
991
|
+
if (value === void 0 || HARNESSES.includes(value)) return value;
|
|
992
|
+
throw new Error(`agent def "${path}" has unknown harness "${value}" — expected one of ${HARNESSES.join(", ")}`);
|
|
993
|
+
}
|
|
958
994
|
function toAgentDef(path, text, fallbackName) {
|
|
959
995
|
const { fm, instructions } = parseAgentDefFile(text);
|
|
960
996
|
return {
|
|
@@ -962,7 +998,7 @@ function toAgentDef(path, text, fallbackName) {
|
|
|
962
998
|
description: fm.description,
|
|
963
999
|
model: fm.model,
|
|
964
1000
|
effort: fm.effort,
|
|
965
|
-
harness: fm.harness,
|
|
1001
|
+
harness: toHarness(fm.harness, path),
|
|
966
1002
|
warm: toBool(fm.warm),
|
|
967
1003
|
interactive: toBool(fm.interactive),
|
|
968
1004
|
instructions,
|
|
@@ -1015,19 +1051,67 @@ const DEFAULT_HARNESS = "claude";
|
|
|
1015
1051
|
function shellQuote(value) {
|
|
1016
1052
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1017
1053
|
}
|
|
1018
|
-
/**
|
|
1019
|
-
*
|
|
1020
|
-
|
|
1021
|
-
|
|
1054
|
+
/** Cursor carries effort as a bracket parameter on the model (`<model>[effort=<level>]`): merge it into
|
|
1055
|
+
* any bracket list the model already has, replacing an `effort=` already there. */
|
|
1056
|
+
function withCursorEffort(model, effort) {
|
|
1057
|
+
const open = model.lastIndexOf("[");
|
|
1058
|
+
if (open === -1 || !model.endsWith("]")) return `${model}[effort=${effort}]`;
|
|
1059
|
+
const params = model.slice(open + 1, -1).split(",").filter((p) => p !== "" && !p.startsWith("effort="));
|
|
1060
|
+
return `${model.slice(0, open)}[${[...params, `effort=${effort}`].join(",")}]`;
|
|
1061
|
+
}
|
|
1062
|
+
/** The model + effort arguments for one harness. No two harnesses spell effort alike: claude has
|
|
1063
|
+
* `--effort`, codex only a config override, cursor only a parameter on the model — so a cursor
|
|
1064
|
+
* effort with no model has nowhere to go and throws rather than launching at the default effort. */
|
|
1065
|
+
function modelAndEffortArgs(harness, model, effort) {
|
|
1066
|
+
if (harness === "cursor") {
|
|
1067
|
+
if (effort && !model) throw new Error(`cursor carries effort only as a parameter on the model; set a model to launch with effort "${effort}"`);
|
|
1068
|
+
if (!model) return [];
|
|
1069
|
+
return ["--model", shellQuote(effort ? withCursorEffort(model, effort) : model)];
|
|
1070
|
+
}
|
|
1071
|
+
const args = model ? ["--model", shellQuote(model)] : [];
|
|
1072
|
+
if (!effort) return args;
|
|
1073
|
+
if (harness === "codex") return [
|
|
1074
|
+
...args,
|
|
1075
|
+
"-c",
|
|
1076
|
+
shellQuote(`model_reasoning_effort="${effort}"`)
|
|
1077
|
+
];
|
|
1078
|
+
return [
|
|
1079
|
+
...args,
|
|
1080
|
+
"--effort",
|
|
1081
|
+
shellQuote(effort)
|
|
1082
|
+
];
|
|
1083
|
+
}
|
|
1084
|
+
/** A TOML basic string: JSON's escapes are all legal TOML escapes, and TOML also forbids a raw DEL. */
|
|
1085
|
+
function tomlBasicString(value) {
|
|
1086
|
+
return JSON.stringify(value).replace(/\x7f/g, "\\u007f");
|
|
1087
|
+
}
|
|
1088
|
+
/** The instruction arguments for one harness. Only claude has an append-to-system-prompt flag;
|
|
1089
|
+
* codex takes a `developer_instructions` config override, added on top of its own base
|
|
1090
|
+
* instructions. Cursor has no channel at all — its instructions go to the brief instead. */
|
|
1091
|
+
function instructionArgs(harness, instructions) {
|
|
1092
|
+
if (!instructions || harness === "cursor") return [];
|
|
1093
|
+
if (harness === "claude") return ["--append-system-prompt", shellQuote(instructions)];
|
|
1094
|
+
return ["-c", shellQuote(`developer_instructions=${tomlBasicString(instructions)}`)];
|
|
1095
|
+
}
|
|
1096
|
+
/** Build the harness launch invocation for a def — explicit `model`/`effort`/`harness` win over the
|
|
1097
|
+
* def's own tags, which win over the harness default. The def's effort and instructions each go
|
|
1098
|
+
* through the harness's own control (see `modelAndEffortArgs`, `instructionArgs`). */
|
|
1022
1099
|
function realizeLaunch(def, opts = {}) {
|
|
1023
1100
|
const harness = opts.harness ?? def.harness ?? DEFAULT_HARNESS;
|
|
1024
1101
|
const model = opts.model ?? def.model;
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1102
|
+
const effort = opts.effort ?? def.effort;
|
|
1103
|
+
const parts = [
|
|
1104
|
+
LAUNCH_MAP[harness],
|
|
1105
|
+
...modelAndEffortArgs(harness, model, effort),
|
|
1106
|
+
...instructionArgs(harness, def.instructions)
|
|
1107
|
+
];
|
|
1108
|
+
const briefInstructions = harness === "cursor" && def.instructions ? def.instructions : void 0;
|
|
1028
1109
|
return {
|
|
1029
1110
|
harness,
|
|
1030
|
-
command: parts.join(" ")
|
|
1111
|
+
command: parts.join(" "),
|
|
1112
|
+
model,
|
|
1113
|
+
effort,
|
|
1114
|
+
...briefInstructions ? { briefInstructions } : {}
|
|
1031
1115
|
};
|
|
1032
1116
|
}
|
|
1033
1117
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cyberlegion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Harness-agnostic agent session spawning and messaging over the filesystem",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.10.1",
|
|
47
|
+
"smol-toml": "^1.8.0",
|
|
47
48
|
"tsdown": "^0.22.0",
|
|
48
49
|
"tsx": "^4.22.3",
|
|
49
50
|
"typescript": "^7.0.2",
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
"scripts": {
|
|
60
61
|
"build": "tsdown",
|
|
61
62
|
"check:metaphor-free": "tsx src/check-metaphor-free.ts",
|
|
62
|
-
"dev": "tsx src/
|
|
63
|
+
"dev": "tsx src/dev.ts",
|
|
63
64
|
"test": "vitest run src",
|
|
64
65
|
"test:watch": "vitest",
|
|
65
66
|
"typecheck": "tsc --noEmit"
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "cyberlegion",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Agent session spawning, messaging, and dispatch — the Legion and its Legate.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
package/readme.md
CHANGED
|
@@ -154,9 +154,12 @@ npx skills add cyberuni/cyberplace --plugin cyberlegion --global
|
|
|
154
154
|
|
|
155
155
|
This package doubles as the plugin root: `plugin.json`, the generated `.claude-plugin/` and
|
|
156
156
|
`.codex-plugin/` manifests, `skills/`, and `agents/` all ship in the npm tarball. The bundle
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
the
|
|
157
|
+
Each skill that runs the CLI carries a launcher, `scripts/cyberlegion.mjs`, that runs the CLI
|
|
158
|
+
shipped in this same package (`bin/cyberlegion.mjs` over the committed `dist/cli.mjs`), so a skill
|
|
159
|
+
never fetches a copy through npx and always runs the version its own text describes. Where the
|
|
160
|
+
launcher cannot be resolved, a skill names one fallback pinned to the version that shipped it,
|
|
161
|
+
`npx -y cyberlegion@<version>`. `pnpm version` rewrites those pins and `.plugin/pins.json` at every
|
|
162
|
+
release, and the test suite fails on a stale one.
|
|
160
163
|
|
|
161
164
|
## License
|
|
162
165
|
|
|
@@ -14,10 +14,15 @@ realized headless by the `headless-legate` agent. Both loads run the exact same
|
|
|
14
14
|
**Input:** an intent to fulfill role `R` with brief `B`, optionally expecting a result that
|
|
15
15
|
satisfies verdict schema `V`.
|
|
16
16
|
|
|
17
|
+
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
18
|
+
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
19
|
+
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
20
|
+
> with the same arguments: `npx -y cyberlegion@0.5.0`.
|
|
21
|
+
|
|
17
22
|
## 1. Resolve the agent definition
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
|
|
25
|
+
node scripts/cyberlegion.mjs agent resolve <R> --format json
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
Read `model`, `effort`, `harness`, `warm`, `interactive` off the resolved def. `warm` marks a role
|
|
@@ -27,7 +32,7 @@ may need to grill the caller or converse over multiple rounds, not just return o
|
|
|
27
32
|
## 2. Probe the environment
|
|
28
33
|
|
|
29
34
|
```bash
|
|
30
|
-
|
|
35
|
+
node scripts/cyberlegion.mjs mux doctor --format json
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
Read whether a multiplexer was ancestry-discovered (tmux/herdr) — a **channel** strategy needs one
|
|
@@ -48,8 +53,8 @@ to open a pane in; its absence rules that strategy out regardless of what the ag
|
|
|
48
53
|
mint a thread id, weave it (and an instruction to reply on it) into brief `B`, then:
|
|
49
54
|
|
|
50
55
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
node scripts/cyberlegion.mjs unit spawn --agent <R> --brief-file <B> --at <placement>
|
|
57
|
+
node scripts/cyberlegion.mjs mail await --thread <the-minted-thread-id> [--timeout <ms>] [--max-wait <s>]
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
`unit spawn` launches the peer (its brief tells it what thread to reply on); `mail await` then
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the cyberlegion CLI this plugin ships, so the skill beside it never fetches a copy through npx.
|
|
3
|
+
// The package root is three levels up from this file (skills/<skill>/scripts/), resolved from the
|
|
4
|
+
// file's own location: the working directory is the repository the agent is working on, not the plugin.
|
|
5
|
+
// Arguments, output, and the exit code pass straight through the package's bin.
|
|
6
|
+
//
|
|
7
|
+
// A skill folder installed on its own has no package around it. Then this runs the published CLI of
|
|
8
|
+
// the version that shipped the skill instead, so the skill still works, only slower. The version sync
|
|
9
|
+
// run at release keeps FALLBACK at the package version.
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@0.5.0'
|
|
15
|
+
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
|
+
|
|
17
|
+
if (existsSync(fileURLToPath(bin))) {
|
|
18
|
+
await import(bin.href)
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`cyberlegion: no plugin package around this skill; running ${FALLBACK}\n`)
|
|
21
|
+
const [command, ...args] = FALLBACK.split(' ')
|
|
22
|
+
const res = spawnSync(command, [...args, ...process.argv.slice(2)], {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell: process.platform === 'win32',
|
|
25
|
+
})
|
|
26
|
+
if (res.error) {
|
|
27
|
+
process.stderr.write(`cyberlegion: could not run ${FALLBACK}: ${res.error.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
process.exit(res.status ?? 1)
|
|
31
|
+
}
|
|
@@ -12,26 +12,28 @@ It is a **thin wrapper**: every mechanic is a `cyberlegion` CLI call. The skill
|
|
|
12
12
|
and the judgment* — is this a root session? should we ask to bind? what does the environment look
|
|
13
13
|
like? — the CLI holds all the *mechanism*.
|
|
14
14
|
|
|
15
|
+
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
16
|
+
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
17
|
+
>
|
|
15
18
|
> **Version pin.** Resolve the CLI version **once, before the flow**, by reading the plugin's bundled
|
|
16
19
|
> map at `${CLAUDE_PLUGIN_ROOT}/.plugin/pins.json` — a flat `{ "<package>": "<version>" }` map the
|
|
17
|
-
>
|
|
20
|
+
> release version flow keeps equal to the shipped package version. Look up the `cyberlegion` key:
|
|
18
21
|
>
|
|
19
|
-
> - **A version is found** →
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
+
> - **A version is found** → pass it to the hook registration in step 2 as `init --pin <version>`
|
|
23
|
+
> so the installed surfacing hook is pinned to the same shipped version. If you cannot resolve
|
|
24
|
+
> `scripts/cyberlegion.mjs`, run `npx -y cyberlegion@<version>` in its place, with the same arguments.
|
|
22
25
|
> - **No `pins.json`, no `cyberlegion` key, or a malformed map** (an unbundled workspace checkout) →
|
|
23
|
-
>
|
|
24
|
-
> version number.**
|
|
26
|
+
> pass **no** `--pin`; if you cannot resolve `scripts/cyberlegion.mjs`, fall back to the unpinned
|
|
27
|
+
> `npx -y cyberlegion` form. **Never invent a version number.**
|
|
25
28
|
>
|
|
26
|
-
> Do not scrape the version from prose.
|
|
27
|
-
> is a later extraction CR; until then the npx pin is dormant and the local workspace bin serves.)
|
|
29
|
+
> Do not scrape the version from prose.
|
|
28
30
|
|
|
29
31
|
## Flow
|
|
30
32
|
|
|
31
33
|
### 1. Probe the environment
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
|
|
36
|
+
node scripts/cyberlegion.mjs mux doctor
|
|
35
37
|
```
|
|
36
38
|
|
|
37
39
|
Run this **before** touching the hook or any identity. It reports `harness`, `mux`, `pane`,
|
|
@@ -42,17 +44,17 @@ invent facts the probe did not report.
|
|
|
42
44
|
### 2. Register the surfacing hook
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
|
|
47
|
+
node scripts/cyberlegion.mjs init --pin <version>
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
Pass `--pin <version>` with the version resolved above so the installed hook is pinned to the shipped
|
|
49
|
-
version; **omit `--pin`**
|
|
51
|
+
version; **omit `--pin`** when the map yielded no version.
|
|
50
52
|
|
|
51
53
|
Auto-detect is the default — no `--agent` flag. Pass `--agent <name>` **only** when `mux doctor` could
|
|
52
54
|
not auto-detect the harness, or the user named one explicitly (it composes with `--pin`):
|
|
53
55
|
|
|
54
56
|
```bash
|
|
55
|
-
|
|
57
|
+
node scripts/cyberlegion.mjs init --pin <version> --agent <name>
|
|
56
58
|
```
|
|
57
59
|
|
|
58
60
|
This step is **idempotent**: if the hook is already registered, `init` reports `already present` —
|
|
@@ -84,8 +86,8 @@ Only a root session with no `legate` owner bound is offered the bind. Ask plainl
|
|
|
84
86
|
### 5. On an explicit yes — mint and bind
|
|
85
87
|
|
|
86
88
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
node scripts/cyberlegion.mjs unit register --standing --handle legate
|
|
90
|
+
node scripts/cyberlegion.mjs attach
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Run these **in this order** and only after the explicit yes: mint the durable, session-independent
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the cyberlegion CLI this plugin ships, so the skill beside it never fetches a copy through npx.
|
|
3
|
+
// The package root is three levels up from this file (skills/<skill>/scripts/), resolved from the
|
|
4
|
+
// file's own location: the working directory is the repository the agent is working on, not the plugin.
|
|
5
|
+
// Arguments, output, and the exit code pass straight through the package's bin.
|
|
6
|
+
//
|
|
7
|
+
// A skill folder installed on its own has no package around it. Then this runs the published CLI of
|
|
8
|
+
// the version that shipped the skill instead, so the skill still works, only slower. The version sync
|
|
9
|
+
// run at release keeps FALLBACK at the package version.
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@0.5.0'
|
|
15
|
+
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
|
+
|
|
17
|
+
if (existsSync(fileURLToPath(bin))) {
|
|
18
|
+
await import(bin.href)
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`cyberlegion: no plugin package around this skill; running ${FALLBACK}\n`)
|
|
21
|
+
const [command, ...args] = FALLBACK.split(' ')
|
|
22
|
+
const res = spawnSync(command, [...args, ...process.argv.slice(2)], {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell: process.platform === 'win32',
|
|
25
|
+
})
|
|
26
|
+
if (res.error) {
|
|
27
|
+
process.stderr.write(`cyberlegion: could not run ${FALLBACK}: ${res.error.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
process.exit(res.status ?? 1)
|
|
31
|
+
}
|
package/skills/legate/SKILL.md
CHANGED
|
@@ -10,10 +10,10 @@ It **classifies** the request and either runs the matching `cyberlegion` CLI cal
|
|
|
10
10
|
routing judgment to `dispatch-governance`. It is a **thin classifier**: it holds no production
|
|
11
11
|
logic, loads no other governance, and writes no state of its own.
|
|
12
12
|
|
|
13
|
-
> **
|
|
14
|
-
> The
|
|
15
|
-
>
|
|
16
|
-
>
|
|
13
|
+
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
14
|
+
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
15
|
+
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
16
|
+
> with the same arguments: `npx -y cyberlegion@0.5.0`.
|
|
17
17
|
|
|
18
18
|
## Doorbell vs mailbox
|
|
19
19
|
|
|
@@ -27,17 +27,17 @@ mailbox because a nudge already fired.
|
|
|
27
27
|
|
|
28
28
|
| User intent | Handling |
|
|
29
29
|
|---|---|
|
|
30
|
-
| Send a message to a named peer, "the pane on the right", a claude/cursor peer | `
|
|
31
|
-
| Check inbox / read unread mail | `
|
|
32
|
-
| Spawn a new peer session | `
|
|
33
|
-
| Close / tear down a peer session | `
|
|
34
|
-
| Wait for a threaded reply | `
|
|
35
|
-
| Watch mail as it streams in (observer, never acks) | `
|
|
36
|
-
| List addressable peers | `
|
|
37
|
-
| Sweep dead peers | `
|
|
30
|
+
| Send a message to a named peer, "the pane on the right", a claude/cursor peer | `node scripts/cyberlegion.mjs mail send --to <handle>` (or `unit nudge <ref>` first if the peer needs waking, then `mail send`) |
|
|
31
|
+
| Check inbox / read unread mail | `node scripts/cyberlegion.mjs mail inbox` — `mail read <msg-id>` to peek, `mail ack <msg-id>` once handled |
|
|
32
|
+
| Spawn a new peer session | `node scripts/cyberlegion.mjs unit spawn --agent <name> ...` — cyberlegion owns worktree creation; pass `--cwd <dir>` only when the caller already has a directory to spawn into (e.g. reusing an existing worktree) |
|
|
33
|
+
| Close / tear down a peer session | `node scripts/cyberlegion.mjs unit close <id>` |
|
|
34
|
+
| Wait for a threaded reply | `node scripts/cyberlegion.mjs mail await --thread <id>` |
|
|
35
|
+
| Watch mail as it streams in (observer, never acks) | `node scripts/cyberlegion.mjs mail watch` |
|
|
36
|
+
| List addressable peers | `node scripts/cyberlegion.mjs unit who` |
|
|
37
|
+
| Sweep dead peers | `node scripts/cyberlegion.mjs unit prune` |
|
|
38
38
|
| Onboard / set up cyberlegion (register the surfacing hook, bind the main owner pane, first-run setup) | **invoke the `init-cyberlegion` skill** — the interactive onboarding front door over `cyberlegion init` / `unit register --standing` / `attach` |
|
|
39
|
-
| Diagnose the environment (harness, multiplexer, pane, hub root) | `
|
|
40
|
-
| Register the surfacing hook by hand (low-level, explicit harness) | `
|
|
39
|
+
| Diagnose the environment (harness, multiplexer, pane, hub root) | `node scripts/cyberlegion.mjs mux doctor` |
|
|
40
|
+
| Register the surfacing hook by hand (low-level, explicit harness) | `node scripts/cyberlegion.mjs init --agent <harness>` — the `init-cyberlegion` skill wraps this; use directly only when scripting a known harness |
|
|
41
41
|
| Dispatch work to fulfill a role with a brief and expect a verdict back (routing judgment needed — which strategy, which agent def, warm vs cold) | **hand off to `dispatch-governance`** — do not compose `unit spawn`/`mail await`/the Task tool yourself |
|
|
42
42
|
| No user channel at all (unattended trigger, multi-unit fan-out) | **spawn the `headless-legate` agent** by name — it realizes this same gateway + `dispatch-governance` flow headless |
|
|
43
43
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the cyberlegion CLI this plugin ships, so the skill beside it never fetches a copy through npx.
|
|
3
|
+
// The package root is three levels up from this file (skills/<skill>/scripts/), resolved from the
|
|
4
|
+
// file's own location: the working directory is the repository the agent is working on, not the plugin.
|
|
5
|
+
// Arguments, output, and the exit code pass straight through the package's bin.
|
|
6
|
+
//
|
|
7
|
+
// A skill folder installed on its own has no package around it. Then this runs the published CLI of
|
|
8
|
+
// the version that shipped the skill instead, so the skill still works, only slower. The version sync
|
|
9
|
+
// run at release keeps FALLBACK at the package version.
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@0.5.0'
|
|
15
|
+
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
|
+
|
|
17
|
+
if (existsSync(fileURLToPath(bin))) {
|
|
18
|
+
await import(bin.href)
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`cyberlegion: no plugin package around this skill; running ${FALLBACK}\n`)
|
|
21
|
+
const [command, ...args] = FALLBACK.split(' ')
|
|
22
|
+
const res = spawnSync(command, [...args, ...process.argv.slice(2)], {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell: process.platform === 'win32',
|
|
25
|
+
})
|
|
26
|
+
if (res.error) {
|
|
27
|
+
process.stderr.write(`cyberlegion: could not run ${FALLBACK}: ${res.error.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
process.exit(res.status ?? 1)
|
|
31
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: manage-inbox
|
|
3
|
-
description: "Use this skill when the user wants to check, read, or clear the owner mailbox — the durable inbox where headless and cron-started agents send their reports. Triggers: 'check my inbox', 'any reports for me', 'what did the agents send', 'read that report', 'ack that', 'mark it read', 'clear my owner inbox', or acting on a surfaced owner-mail doorbell."
|
|
3
|
+
description: "Use this skill when the user wants to check, read, or clear the owner mailbox — the durable inbox where headless and cron-started agents send their reports. Triggers: 'check my inbox', 'any reports for me', 'what did the agents send', 'read that report', 'ack that', 'mark it read', 'clear my owner inbox', answering a question an agent left in a report, or acting on a surfaced owner-mail doorbell. Not for creating or binding the owner identity (init-cyberlegion) or a session's own mail (legate)."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# manage-inbox
|
|
@@ -10,27 +10,37 @@ owner identity holds, where frameless agents (cron-started, no parent frame) pus
|
|
|
10
10
|
wraps the `cyberlegion` CLI's owner-scoped mail commands so a human roaming across sessions manages
|
|
11
11
|
the one owner mailbox from wherever they are.
|
|
12
12
|
|
|
13
|
-
> **
|
|
14
|
-
> is
|
|
15
|
-
>
|
|
13
|
+
> **Running the CLI.** Every `node scripts/cyberlegion.mjs …` command below runs the `cyberlegion` CLI
|
|
14
|
+
> this plugin ships. The path is relative to this skill's own directory, not the working directory.
|
|
15
|
+
> If you cannot resolve it, run the published CLI of the version that shipped this skill instead,
|
|
16
|
+
> with the same arguments: `npx -y cyberlegion@0.5.0`.
|
|
16
17
|
|
|
17
18
|
## Resolve the owner handle
|
|
18
19
|
|
|
19
20
|
The owner mailbox is a **standing** identity. Find it first:
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
|
|
23
|
+
node scripts/cyberlegion.mjs unit register --standing # lists the standing owner record(s)
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
Pick the handle in this order, and scope every mail command below to it with `--owner <handle>`:
|
|
27
|
+
|
|
28
|
+
1. **`$CYBERLEGION_OWNER` is set** — use it as given; do not consult the standing list. If the CLI
|
|
29
|
+
answers that it is not a standing owner, report that error to the user and stop. Never retry with
|
|
30
|
+
a handle from the standing list: the variable named one owner, and a substitute is a guess.
|
|
31
|
+
2. **Unset, exactly one standing owner** — use that handle without asking.
|
|
32
|
+
3. **Unset, no standing owner** — tell the user there is no owner mailbox yet and stop. Do not run
|
|
33
|
+
`unit register --standing --handle` yourself: creating the owner identity is a deliberate act
|
|
34
|
+
that belongs to `init-cyberlegion`.
|
|
35
|
+
4. **Unset, more than one standing owner** — stop. Run no mail command with `--owner`. List every
|
|
36
|
+
standing handle, and tell the user to set `CYBERLEGION_OWNER` to the one whose mailbox they mean.
|
|
37
|
+
Never pick one: a wrong pick shows, or acks, another owner's mail.
|
|
28
38
|
|
|
29
39
|
## List — what is waiting
|
|
30
40
|
|
|
31
41
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
node scripts/cyberlegion.mjs mail inbox --owner <handle> # all owner mail, oldest-first
|
|
43
|
+
node scripts/cyberlegion.mjs mail inbox --owner <handle> --unread # only what is new
|
|
34
44
|
```
|
|
35
45
|
|
|
36
46
|
The aggregate line reports `<N> messages (<U> unread)`. This is a **pull** from any session — the
|
|
@@ -40,16 +50,16 @@ have seen it inline; listing is how you review deliberately.
|
|
|
40
50
|
## Read — peek without consuming
|
|
41
51
|
|
|
42
52
|
```bash
|
|
43
|
-
|
|
53
|
+
node scripts/cyberlegion.mjs mail read <msg-id> --owner <handle>
|
|
44
54
|
```
|
|
45
55
|
|
|
46
|
-
Prints the report body (sender, subject, id). **Read does not ack** — the message stays unread and
|
|
56
|
+
Prints the report body (sender, subject, id). Do not add `--ack`. **Read does not ack** — the message stays unread and
|
|
47
57
|
keeps surfacing until you explicitly clear it. Peeking is safe; it changes nothing.
|
|
48
58
|
|
|
49
59
|
## Ack — the only thing that clears it
|
|
50
60
|
|
|
51
61
|
```bash
|
|
52
|
-
|
|
62
|
+
node scripts/cyberlegion.mjs mail ack <msg-id> --owner <handle>
|
|
53
63
|
```
|
|
54
64
|
|
|
55
65
|
Ack is the sole read-state change and the sole signal that a report is handled — a surfaced message
|
|
@@ -58,13 +68,15 @@ acted on (or consciously dismissed) the report; acking an already-acked or unkno
|
|
|
58
68
|
than silently succeeding. Two concurrent acks of the same message resolve to exactly one success —
|
|
59
69
|
nothing is double-consumed.
|
|
60
70
|
|
|
71
|
+
If the ack exits nonzero, tell the user it failed and why; never report the message as cleared.
|
|
72
|
+
|
|
61
73
|
## Reply — answer a frameless agent's question
|
|
62
74
|
|
|
63
75
|
A report may be a **question** a frameless agent could not ask live. Reply on its thread so a later
|
|
64
76
|
tick (or the agent's next run) picks up the answer:
|
|
65
77
|
|
|
66
78
|
```bash
|
|
67
|
-
|
|
79
|
+
node scripts/cyberlegion.mjs mail send --to <agent-or-thread-origin> --thread <t> --body "<answer>"
|
|
68
80
|
```
|
|
69
81
|
|
|
70
82
|
The thread carries the state across the agent's stateless re-runs.
|
|
@@ -73,5 +85,6 @@ The thread carries the state across the agent's stateless re-runs.
|
|
|
73
85
|
|
|
74
86
|
- This skill only manages the **owner** mailbox (`--owner`). A session's own inbox is the plain
|
|
75
87
|
`mail inbox`/`read`/`ack` (no `--owner`) and is not this skill's concern.
|
|
88
|
+
- Creating or binding the owner identity is `init-cyberlegion`'s job; hand those requests to it.
|
|
76
89
|
- It is a thin CLI wrapper — it decides *nothing* about routing or dispatch (that is the Legate /
|
|
77
90
|
`dispatch-governance`), and writes no state beyond the ack/reply the human directs.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runs the cyberlegion CLI this plugin ships, so the skill beside it never fetches a copy through npx.
|
|
3
|
+
// The package root is three levels up from this file (skills/<skill>/scripts/), resolved from the
|
|
4
|
+
// file's own location: the working directory is the repository the agent is working on, not the plugin.
|
|
5
|
+
// Arguments, output, and the exit code pass straight through the package's bin.
|
|
6
|
+
//
|
|
7
|
+
// A skill folder installed on its own has no package around it. Then this runs the published CLI of
|
|
8
|
+
// the version that shipped the skill instead, so the skill still works, only slower. The version sync
|
|
9
|
+
// run at release keeps FALLBACK at the package version.
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
const FALLBACK = 'npx -y cyberlegion@0.5.0'
|
|
15
|
+
const bin = new URL('../../../bin/cyberlegion.mjs', import.meta.url)
|
|
16
|
+
|
|
17
|
+
if (existsSync(fileURLToPath(bin))) {
|
|
18
|
+
await import(bin.href)
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(`cyberlegion: no plugin package around this skill; running ${FALLBACK}\n`)
|
|
21
|
+
const [command, ...args] = FALLBACK.split(' ')
|
|
22
|
+
const res = spawnSync(command, [...args, ...process.argv.slice(2)], {
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell: process.platform === 'win32',
|
|
25
|
+
})
|
|
26
|
+
if (res.error) {
|
|
27
|
+
process.stderr.write(`cyberlegion: could not run ${FALLBACK}: ${res.error.message}\n`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
process.exit(res.status ?? 1)
|
|
31
|
+
}
|
|
@@ -19,8 +19,8 @@ ratification. Provenance over peer mail cannot be established, so a receiver act
|
|
|
19
19
|
can verify against its own loaded contract — a ratification it fetched from a peer is invalid.
|
|
20
20
|
|
|
21
21
|
**The ownership chain:** keyed on relationship, not on transport alone. A decision relayed by the
|
|
22
|
-
receiver's **own owner** **and** arriving on a **turn** in its own session — carrying the
|
|
23
|
-
verbatim words, where they were said, the relaying unit, and its scope (one action on one target) —
|
|
22
|
+
receiver's **own owner** **and** arriving on a **turn** in its own session — carrying the user-channel
|
|
23
|
+
holder's verbatim words, where they were said, the relaying unit, and its scope (one action on one target) —
|
|
24
24
|
is adoptable within that named scope and nothing adjacent, spent once acted on, attenuating at every
|
|
25
25
|
hop. Either half alone leaves it a peer steer. The four parts are a **well-formedness** requirement:
|
|
26
26
|
presence is checkable on its face, so a missing part gates adoption, and truth is not, so the parts
|