delegation-layer 0.1.3 → 0.1.5

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 CHANGED
@@ -36,17 +36,26 @@ already use instead of replacing them.
36
36
 
37
37
  ### Install the Agent Skill
38
38
 
39
- From the project where you want the skill available, run:
39
+ The standard Agent Skills installer works across popular coding agents. From
40
+ the project where you want the skill available, run:
40
41
 
41
42
  ```sh
42
- npx --yes delegation-layer install
43
+ npx --yes skills add hishamkaram/delegation-layer --skill agent-integration
43
44
  ```
44
45
 
45
- The installer detects supported Agent Skills harnesses, shows what it found,
46
- and asks whether to install for the current project or your user account. It
47
- offers the shared `.agents/skills` location when no specific harness is found.
46
+ The `--skill agent-integration` selection matters because this repository also
47
+ contains maintainer skills. The installer detects supported agents and lets you
48
+ choose the project or global scope. Add `--global` to install for all projects,
49
+ or `--agent codex` (and similar agent names) to choose a specific harness.
50
+
51
+ If you want the Delegation Layer guided installer instead, run:
52
+
53
+ ```sh
54
+ npx --yes delegation-layer install
55
+ ```
48
56
 
49
- For scripts or a choice without prompts:
57
+ It provides an interactive scope and harness picker, with global installation
58
+ selected by default. For scripts or a choice without prompts:
50
59
 
51
60
  ```sh
52
61
  npx --yes delegation-layer install \
@@ -56,7 +65,7 @@ npx --yes delegation-layer install \
56
65
  ```
57
66
 
58
67
  The skill installer only installs guidance for your agent. It does not install
59
- the CLI, change provider logins, or configure a supervisor.
68
+ the CLI, change provider logins, or run a task.
60
69
 
61
70
  ### Install the CLI
62
71
 
@@ -76,8 +85,9 @@ pnpm dlx delegation-layer install-cli
76
85
  bunx --bun delegation-layer install-cli
77
86
  ```
78
87
 
79
- The release installer verifies checksums and installs `delegate` and
80
- `delegate-run` into `$HOME/.local/bin` by default. Set
88
+ The release installer verifies checksums and installs `delegate`,
89
+ `delegate-run`, `pueue`, and `pueued` into `$HOME/.local/bin` by default. The
90
+ CLI uses the bundled supervisor automatically. Set
81
91
  `DELEGATION_LAYER_INSTALL_DIR` to use another directory.
82
92
 
83
93
  Check the installation:
@@ -93,14 +103,17 @@ Normal users do not need Go or Node after choosing an installation method.
93
103
  Dispatching a task requires:
94
104
 
95
105
  - a released Delegation Layer CLI;
96
- - Pueue 4.0.4 with a private, running `pueued` daemon;
97
- - a signed-in supported provider CLI such as `agy`, `codex`, `claude`, `pi`,
98
- or `opencode`;
106
+ - a compatible provider CLI with an available native login;
107
+ - a supported provider CLI such as `agy`, `codex`, `claude`, `pi`, or `opencode`;
99
108
  - Darwin or Linux on amd64 or arm64 for the supplied release builds.
100
109
 
101
- Go is only needed for source builds and contributor work. Node.js 18 or newer
102
- is needed for the npm-based installers; it is not a runtime requirement for the
103
- released Go CLI. Provider authentication remains native to each provider.
110
+ The release contains the supervisor client and daemon and starts a private
111
+ instance under the state root, recovering it when a control command finds it
112
+ stopped. An explicit `--pueue-config` or
113
+ `DELEGATE_PUEUE_CONFIG` can still select an existing compatible supervisor for
114
+ advanced integrations. Node.js 18 or newer is needed only for the npm-based
115
+ installers; it is not a runtime requirement for the released Go CLI. Provider
116
+ authentication remains native to each provider.
104
117
 
105
118
  ## Try one bounded task
106
119
 
@@ -110,7 +123,7 @@ Create a brief and a workspace outside the private state root:
110
123
  mkdir -p "$HOME/delegation-workspace"
111
124
  printf '%s\n' 'Inspect the repository and summarize the current build status.' > brief.txt
112
125
 
113
- delegate --pueue-config /absolute/path/to/pueue.yml dispatch \
126
+ delegate --root "$HOME/delegation-state" dispatch \
114
127
  --provider codex:exec \
115
128
  --brief "$PWD/brief.txt" \
116
129
  --cwd "$HOME/delegation-workspace" \
package/install.sh CHANGED
@@ -65,12 +65,12 @@ fi
65
65
 
66
66
  tar -xzf "$ARCHIVE" -C "$TEMP_DIR" || fail "could not extract $ARCHIVE"
67
67
  mkdir -p "$INSTALL_DIR"
68
- for binary in delegate delegate-run; do
68
+ for binary in delegate delegate-run pueue pueued; do
69
69
  [ -f "$TEMP_DIR/$binary" ] || fail "release archive does not contain $binary"
70
70
  install -m 0755 "$TEMP_DIR/$binary" "$INSTALL_DIR/$binary"
71
71
  done
72
72
 
73
- printf 'Installed delegate and delegate-run %s in %s\n' "$VERSION" "$INSTALL_DIR"
73
+ printf 'Installed delegation-layer CLI and bundled supervisor %s in %s\n' "$VERSION" "$INSTALL_DIR"
74
74
  case ":${PATH:-}:" in
75
75
  *:"$INSTALL_DIR":*) ;;
76
76
  *) printf 'Add %s to PATH if it is not already there.\n' "$INSTALL_DIR" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Multi-provider AI agent delegation CLI and Agent Skill for running bounded coding tasks across provider CLIs.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { accessSync, constants } from "node:fs";
4
4
  import { copyFile, lstat, mkdir, readFile } from "node:fs/promises";
5
- import { createInterface } from "node:readline/promises";
5
+ import { emitKeypressEvents } from "node:readline";
6
6
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { fileURLToPath } from "node:url";
@@ -97,7 +97,7 @@ function usage(message, exitCode = 2) {
97
97
 
98
98
  function help() {
99
99
  console.log(
100
- "Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]\n\nThe install command detects supported agent harnesses and installs the skill for a project or user.\nUse --harness universal when no specific harness is detected.",
100
+ "Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]\n\nThe install command detects supported agent harnesses and installs the skill for a project or user.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.",
101
101
  );
102
102
  }
103
103
 
@@ -280,25 +280,123 @@ function printDetections(detections) {
280
280
  });
281
281
  }
282
282
 
283
- function parseHarnessSelection(value, detections) {
284
- const tokens = value
285
- .split(",")
286
- .map((token) => token.trim().toLowerCase())
287
- .filter(Boolean);
288
- const names = tokens.map((token) => {
289
- if (/^\d+$/.test(token)) {
290
- const detection = detections[Number(token) - 1];
291
- if (!detection) {
292
- throw new Error(`harness choice ${token} is out of range`);
293
- }
294
- return detection.harness.name;
283
+ function renderPicker({ error, index, multiple, options, selected, title }) {
284
+ const lines = [title, ""];
285
+ options.forEach((option, optionIndex) => {
286
+ const current = optionIndex === index;
287
+ const marker = multiple ? (selected.has(option.value) ? "[x]" : "[ ]") : "";
288
+ const cursor = current ? "❯" : " ";
289
+ const color = current ? "\u001b[36m" : "";
290
+ const reset = current ? "\u001b[0m" : "";
291
+ lines.push(`${color}${cursor} ${marker ? `${marker} ` : ""}${option.label}${reset}`);
292
+ if (option.description) {
293
+ lines.push(` ${option.description}`);
295
294
  }
296
- if (!harnessByName.has(token)) {
297
- throw new Error(`unknown harness ${token}`);
295
+ });
296
+ lines.push("");
297
+ lines.push(multiple ? "↑↓ move Space select Enter continue a all n none q cancel" : "↑↓ move Enter select q cancel");
298
+ if (error) {
299
+ lines.push(`\u001b[31m${error}\u001b[0m`);
300
+ }
301
+ process.stdout.write(`\u001b[2J\u001b[H${lines.join("\n")}\n`);
302
+ }
303
+
304
+ function pickOptions({ initialValues = [], multiple = false, options, title }) {
305
+ const input = process.stdin;
306
+ const output = process.stdout;
307
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
308
+ return null;
309
+ }
310
+
311
+ let index = 0;
312
+ const selected = new Set(initialValues);
313
+
314
+ return new Promise((resolve, reject) => {
315
+ let settled = false;
316
+ let error;
317
+
318
+ const cleanup = () => {
319
+ input.off("keypress", onKeypress);
320
+ try {
321
+ input.setRawMode(false);
322
+ } catch {
323
+ // The terminal may already be closed while handling cancellation.
324
+ }
325
+ input.pause();
326
+ output.write("\u001b[?25h\u001b[0m");
327
+ };
328
+
329
+ const finish = (value) => {
330
+ if (settled) {
331
+ return;
332
+ }
333
+ settled = true;
334
+ cleanup();
335
+ resolve(value);
336
+ };
337
+
338
+ const onKeypress = (character, key = {}) => {
339
+ if ((key.ctrl && key.name === "c") || key.name === "escape" || key.name === "q") {
340
+ finish(null);
341
+ return;
342
+ }
343
+ if (key.name === "up" || key.name === "k") {
344
+ index = (index + options.length - 1) % options.length;
345
+ error = undefined;
346
+ renderPicker({ error, index, multiple, options, selected, title });
347
+ return;
348
+ }
349
+ if (key.name === "down" || key.name === "j") {
350
+ index = (index + 1) % options.length;
351
+ error = undefined;
352
+ renderPicker({ error, index, multiple, options, selected, title });
353
+ return;
354
+ }
355
+ if (multiple && (key.name === "space" || character === " ")) {
356
+ const value = options[index].value;
357
+ if (selected.has(value)) {
358
+ selected.delete(value);
359
+ } else {
360
+ selected.add(value);
361
+ }
362
+ error = undefined;
363
+ renderPicker({ error, index, multiple, options, selected, title });
364
+ return;
365
+ }
366
+ if (multiple && key.name === "a") {
367
+ options.forEach((option) => selected.add(option.value));
368
+ error = undefined;
369
+ renderPicker({ error, index, multiple, options, selected, title });
370
+ return;
371
+ }
372
+ if (multiple && key.name === "n") {
373
+ selected.clear();
374
+ error = undefined;
375
+ renderPicker({ error, index, multiple, options, selected, title });
376
+ return;
377
+ }
378
+ if (key.name === "return" || key.name === "enter") {
379
+ if (multiple && selected.size === 0) {
380
+ error = "Select at least one harness, or press q to cancel.";
381
+ renderPicker({ error, index, multiple, options, selected, title });
382
+ return;
383
+ }
384
+ finish(multiple ? options.filter((option) => selected.has(option.value)).map((option) => option.value) : options[index].value);
385
+ }
386
+ };
387
+
388
+ try {
389
+ emitKeypressEvents(input);
390
+ input.setRawMode(true);
391
+ input.resume();
392
+ input.on("keypress", onKeypress);
393
+ output.write("\u001b[?25l");
394
+ renderPicker({ error, index, multiple, options, selected, title });
395
+ } catch (pickerError) {
396
+ cleanup();
397
+ reject(pickerError);
298
398
  }
299
- return token;
300
399
  });
301
- return [...new Set(names)];
302
400
  }
303
401
 
304
402
  async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
@@ -309,28 +407,45 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
309
407
  const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
310
408
  const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
311
409
 
312
- if (!harnessNames) {
410
+ if (!harnessNames && !needsPrompt) {
313
411
  printDetections(detections);
314
412
  }
315
413
  if (!canPrompt && !yes && !harnessNames && !scope) {
316
- console.log("No interactive terminal detected; using project scope and detected harnesses.");
414
+ console.log("No interactive terminal detected; using global scope and detected harnesses.");
317
415
  }
318
416
 
319
417
  if (needsPrompt) {
320
- const readline = createInterface({ input: process.stdin, output: process.stdout });
321
- try {
322
- const selectedScope = (await readline.question("Install for [project/global] (project): ")).trim().toLowerCase();
323
- scope = selectedScope || "project";
324
- if (scope !== "project" && scope !== "global") {
325
- throw new Error("scope must be project or global");
326
- }
327
- const defaultSelection = defaults
328
- .map((name) => detections.findIndex(({ harness }) => harness.name === name) + 1)
329
- .join(",");
330
- const selection = await readline.question(`Choose harnesses [${defaultSelection}]: `);
331
- harnessNames = selection.trim() ? parseHarnessSelection(selection, detections) : defaults;
332
- } finally {
333
- readline.close();
418
+ scope = await pickOptions({
419
+ initialValues: ["global"],
420
+ options: [
421
+ {
422
+ value: "global",
423
+ label: "Global",
424
+ description: `available to your agents from ${home}`,
425
+ },
426
+ {
427
+ value: "project",
428
+ label: "Project",
429
+ description: `available to this project from ${cwd}`,
430
+ },
431
+ ],
432
+ title: "Where should the agent skill be installed?",
433
+ });
434
+ if (!scope) {
435
+ return null;
436
+ }
437
+ harnessNames = await pickOptions({
438
+ initialValues: defaults,
439
+ multiple: true,
440
+ options: detections.map(({ harness, reasons }) => ({
441
+ value: harness.name,
442
+ label: `${harness.label}${formatDetected(reasons)}`,
443
+ description: targetFor(harness, scope, cwd, home),
444
+ })),
445
+ title: "Which harnesses should use the skill?",
446
+ });
447
+ if (!harnessNames) {
448
+ return null;
334
449
  }
335
450
  }
336
451
 
@@ -339,7 +454,7 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
339
454
  force,
340
455
  harnessNames: harnessNames ?? defaults,
341
456
  home,
342
- scope: scope ?? "project",
457
+ scope: scope ?? "global",
343
458
  };
344
459
  }
345
460
 
@@ -399,11 +514,15 @@ if (argumentsList[0] === "install-cli") {
399
514
  if (options) {
400
515
  try {
401
516
  const selected = await chooseInstallOptions(options);
402
- const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
403
- const uniqueTargets = [...new Set(targets)];
404
- console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
405
- for (const target of uniqueTargets) {
406
- await install({ force: selected.force, target });
517
+ if (!selected) {
518
+ console.log("Installation cancelled.");
519
+ } else {
520
+ const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
521
+ const uniqueTargets = [...new Set(targets)];
522
+ console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
523
+ for (const target of uniqueTargets) {
524
+ await install({ force: selected.force, target });
525
+ }
407
526
  }
408
527
  } catch (error) {
409
528
  console.error(`Error: ${error.message}`);
@@ -13,11 +13,12 @@ provider names, release versions, or native CLI syntax.
13
13
 
14
14
  ## Preconditions
15
15
 
16
- The host is responsible for installing and configuring the CLI and supervisor.
17
- This skill must not install, build, upgrade, or repair either one. It may check
18
- whether `delegate` is available, use a configured private supervisor
19
- configuration and socket, and create separate task and state directories that
20
- it is authorized to use.
16
+ The host is responsible for installing the CLI. This skill must not install,
17
+ build, upgrade, or repair it. It may check whether `delegate` is available and
18
+ create separate task and state directories that it is authorized to use. A
19
+ released CLI carries the compatible `pueue` and `pueued` supervisor binaries
20
+ and starts or recovers a private instance automatically; an explicit supervisor
21
+ config is only needed for an advanced integration.
21
22
  Authentication is a separate live gate; do not invent a provider-specific
22
23
  login check or classify a missing login as static incompatibility.
23
24
 
@@ -65,7 +66,6 @@ mkdir -p "${HOME}/delegation-workspace" "${HOME}/delegation-state"
65
66
  printf '%s\n' 'Inspect the workspace and return a short summary.' > "${HOME}/delegation-brief.txt"
66
67
 
67
68
  delegate --root "${HOME}/delegation-state" \
68
- --pueue-config /absolute/path/to/private-pueue.yml \
69
69
  dispatch \
70
70
  --provider PROFILE \
71
71
  --brief "${HOME}/delegation-brief.txt" \
@@ -75,11 +75,14 @@ delegate --root "${HOME}/delegation-state" \
75
75
  --json
76
76
  ```
77
77
 
78
- Use the host's configured private supervisor path (or its
79
- `DELEGATE_PUEUE_CONFIG` equivalent); the placeholder above is not a path to
80
- create. If the supervisor or provider executable is unavailable, stop on the
81
- CLI's structured error. For live acceptance, an unavailable authentication
82
- prerequisite is `blocked`; preserve that evidence and never relabel it as
78
+ The default release owns the private supervisor lifecycle under the state root
79
+ and recovers its bundled daemon when control commands find it stopped. Collection
80
+ stays observational and does not start a supervisor. If an existing compatible
81
+ supervisor must be used, pass its absolute
82
+ `--pueue-config` path or set `DELEGATE_PUEUE_CONFIG`. If the CLI or provider
83
+ executable is unavailable, stop on the CLI's structured error. For live
84
+ acceptance, an unavailable authentication prerequisite is `blocked`; preserve
85
+ that evidence and never relabel it as
83
86
  incompatible or passed.
84
87
 
85
88
  Save the returned `task_id`. Admission records one immutable request and at