delegation-layer 0.1.2 → 0.1.4

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
@@ -1,129 +1,119 @@
1
1
  # Delegation Layer
2
2
 
3
- Delegation Layer runs one bounded turn of a supported AI command line tool as a
4
- durable, supervised local task. It records the request before submission,
5
- checks the executable and its capabilities at runtime, and publishes a
6
- validated result that can be collected later.
7
-
8
- > **Preview release**
9
- >
10
- > The project publishes checksum-verified CLI releases and an npm package with
11
- > installers for the CLI and integration skill. The provider CLI and Pueue
12
- > remain host prerequisites.
13
-
14
- ## What it does
15
-
16
- The `delegate` command accepts a finite brief, a canonical workspace, a
17
- permission mode, and a wall clock budget. It prepares a provider-specific
18
- launch profile, submits exactly one provider turn through Pueue, and stores
19
- immutable task records and output descriptors under a private state root.
20
-
21
- The companion `delegate-run` executable is started by the supervisor. It
22
- reconstructs the recorded profile, verifies that admission and launch still
23
- refer to the same executable, policy, workspace, and request, then captures and
24
- seals the provider result. A later `collect` call can recover a committed
25
- outcome without launching the provider again.
26
-
27
- ## Requirements
28
-
29
- - Go 1.27.1 is needed only for source builds and the Homebrew formula.
30
- - Pueue 4.0.4 and a running `pueued` daemon using a private configuration.
31
- - A signed-in installation of one or more supported provider CLIs: `agy`,
32
- `codex`, `claude`, `pi`, or `opencode`.
33
- - A Darwin or Linux host on amd64 or arm64 for the supplied build targets.
34
- Provider availability is profile-specific; see the provider guide for native
35
- platform prerequisites.
36
- - Node.js 18 or newer is needed for the npm and pnpm installers; Bun is needed
37
- for the Bun installer.
38
-
39
- The provider executable must be available to the process that dispatches the
40
- task. Delegation Layer runs the provider's version and help commands before
41
- admission and accepts any reported version that is valid and advertises every
42
- flag required by the adapter. It does not pin a provider release or predeclared
43
- binary hash. A valid version here means nonempty trimmed UTF-8 output without control
44
- characters; it is not required to match a semantic-version pattern. The
45
- observed version is diagnostic evidence, while capability flags, executable
46
- identity, and provider behavior decide compatibility. No web release lookup or
47
- semver allowlist is used.
3
+ <p align="center">
4
+ <a href="#install"><img src="docs/assets/delegation-layer-hero.jpg" alt="A lead AI model coordinating several specialist coding agents through Delegation Layer" width="1400"></a>
5
+ </p>
6
+
7
+ Give your lead AI agent a reliable way to delegate bounded coding work to the
8
+ provider that fits the task.
9
+
10
+ Delegation Layer is a local CLI and Agent Skill for supervising one finite turn
11
+ of a coding agent, checking its capabilities at runtime, and returning a
12
+ validated result with durable evidence. It works with the provider CLIs you
13
+ already use instead of replacing them.
14
+
15
+ ## Why use it?
16
+
17
+ - **One simple control layer** — your agent can discover providers and dispatch
18
+ work through one provider-neutral interface.
19
+ - **Bounded by design** — every task has an explicit workspace, permission mode,
20
+ and time budget.
21
+ - **Recoverable results** — a later `collect` call can read a finished task
22
+ without launching the provider again.
23
+ - **Runtime compatibility** — the selected provider's executable and advertised
24
+ capabilities are checked when the task is prepared.
25
+ - **Native authentication** — provider login stays with the provider CLI; task
26
+ state does not contain copied credentials.
27
+
28
+ ## How it works
29
+
30
+ 1. Install the Agent Skill so your harness knows how to use Delegation Layer.
31
+ 2. The lead agent discovers the available provider capabilities.
32
+ 3. Delegation Layer admits one bounded provider turn through the supervisor.
33
+ 4. The lead agent collects the sealed result and evidence.
48
34
 
49
35
  ## Install
50
36
 
51
- ### Install the CLI
37
+ ### Install the Agent Skill
52
38
 
53
- The shell installer downloads the latest published release, verifies its
54
- checksum, and installs both `delegate` and `delegate-run` in
55
- `$HOME/.local/bin`:
39
+ The standard Agent Skills installer works across popular coding agents. From
40
+ the project where you want the skill available, run:
56
41
 
57
42
  ```sh
58
- curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
43
+ npx --yes skills add hishamkaram/delegation-layer --skill agent-integration
59
44
  ```
60
45
 
61
- Set `DELEGATION_LAYER_INSTALL_DIR` if you want to install somewhere other than
62
- `$HOME/.local/bin`.
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.
63
50
 
64
- Homebrew installs the same two executables:
51
+ If you want the Delegation Layer guided installer instead, run:
65
52
 
66
53
  ```sh
67
- brew install hishamkaram/tap/delegation-layer
54
+ npx --yes delegation-layer install
68
55
  ```
69
56
 
70
- The npm package also provides a checksum-verified CLI installer through npm,
71
- pnpm, and Bun:
57
+ It provides an interactive scope and harness picker, with global installation
58
+ selected by default. For scripts or a choice without prompts:
72
59
 
73
60
  ```sh
74
- npx --yes delegation-layer install-cli
75
- pnpm dlx delegation-layer install-cli
76
- bunx --bun delegation-layer install-cli
61
+ npx --yes delegation-layer install \
62
+ --scope project \
63
+ --harness universal \
64
+ --yes
77
65
  ```
78
66
 
79
- For a source build, install both Go commands from the latest version:
67
+ The skill installer only installs guidance for your agent. It does not install
68
+ the CLI, change provider logins, or configure a supervisor.
69
+
70
+ ### Install the CLI
71
+
72
+ Use the prebuilt installer that fits your machine:
80
73
 
81
74
  ```sh
82
- go install github.com/hishamkaram/delegation-layer/cmd/delegate@latest
83
- go install github.com/hishamkaram/delegation-layer/cmd/delegate-run@latest
75
+ curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
84
76
  ```
85
77
 
86
- Ensure the directory used by the installer or Go is on `PATH`.
87
-
88
- Confirm the installation:
78
+ ```sh
79
+ brew install hishamkaram/tap/delegation-layer
80
+ ```
89
81
 
90
82
  ```sh
91
- delegate --help
92
- delegate providers --json
83
+ npx --yes delegation-layer install-cli
84
+ pnpm dlx delegation-layer install-cli
85
+ bunx --bun delegation-layer install-cli
93
86
  ```
94
87
 
95
- ### Install the agent integration skill
88
+ The release installer verifies checksums and installs `delegate` and
89
+ `delegate-run` into `$HOME/.local/bin` by default. Set
90
+ `DELEGATION_LAYER_INSTALL_DIR` to use another directory.
96
91
 
97
- The provider-agnostic skill is separate from the CLI installation. Hermes can
98
- install the current skill directly:
92
+ Check the installation:
99
93
 
100
94
  ```sh
101
- hermes skills install \
102
- https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/skills/agent-integration/SKILL.md
95
+ delegate --help
96
+ delegate providers --json
103
97
  ```
104
98
 
105
- Any harness with a writable skill directory can use the dependency-free npm,
106
- pnpm, or Bun installer:
99
+ ## What you need to run a task
107
100
 
108
- ```sh
109
- npx --yes delegation-layer \
110
- --target "$HOME/.hermes/skills/agent-integration"
111
- pnpm dlx delegation-layer \
112
- --target "$HOME/.hermes/skills/agent-integration"
113
- bunx --bun delegation-layer \
114
- --target "$HOME/.hermes/skills/agent-integration"
115
- ```
101
+ Normal users do not need Go or Node after choosing an installation method.
102
+ Dispatching a task requires:
103
+
104
+ - a released Delegation Layer CLI;
105
+ - Pueue 4.0.4 with a private, running `pueued` daemon;
106
+ - a signed-in supported provider CLI such as `agy`, `codex`, `claude`, `pi`,
107
+ or `opencode`;
108
+ - Darwin or Linux on amd64 or arm64 for the supplied release builds.
116
109
 
117
- The default package command copies only `SKILL.md` into the target directory.
118
- The package's explicit `install-cli` command installs the CLI separately; it
119
- does not change the skill or configure Pueue or a provider CLI. Release tags
120
- and `DELEGATION_LAYER_VERSION` remain available when a deployment needs a
121
- reproducible version.
110
+ Go is only needed for source builds and contributor work. Node.js 18 or newer
111
+ is needed for the npm-based installers; it is not a runtime requirement for the
112
+ released Go CLI. Provider authentication remains native to each provider.
122
113
 
123
- ## Quick start
114
+ ## Try one bounded task
124
115
 
125
- Create a brief and a workspace outside the state root. Use an absolute path to
126
- the Pueue configuration that controls the private daemon:
116
+ Create a brief and a workspace outside the private state root:
127
117
 
128
118
  ```sh
129
119
  mkdir -p "$HOME/delegation-workspace"
@@ -139,80 +129,35 @@ delegate --pueue-config /absolute/path/to/pueue.yml dispatch \
139
129
  ```
140
130
 
141
131
  The response contains a task ID and separate admission, liveness, and
142
- publication fields. Observe it with `status`, wait for a terminal publication,
143
- then read the validated result with `collect`:
132
+ publication fields. Observe and collect it with:
144
133
 
145
134
  ```sh
146
135
  delegate status TASK_ID --json
147
136
  delegate collect TASK_ID --watch 5s --json
148
137
  ```
149
138
 
150
- Use the exact task ID printed by `dispatch`. A continuation is a new task that
151
- names its predecessor with `--resume-task`; it never silently reuses the most
152
- recent conversation.
153
-
154
- ## Providers
155
-
156
- | Profile | Modes | Options |
157
- | --- | --- | --- |
158
- | `antigravity:print` | `workspace-write` | `continuation`, `native-timeout` |
159
- | `codex:exec` | `read-only`, `workspace-write` | `continuation` |
160
- | `claude:print` | `read-only`, `workspace-write` | `continuation` |
161
- | `pi:json` | `read-only` | `continuation`, `model`, `effort` |
162
- | `opencode:run` | `read-only`, `workspace-write` | `continuation`, `model`, `effort` |
163
-
164
- Run `delegate providers --json` for the compiled capability catalog. See
165
- [providers](docs/providers.md) for launch behavior, policy boundaries, and
166
- runtime compatibility checks.
167
- Use `delegate capabilities --provider PROFILE --json` for one provider's
168
- provider-neutral contract. Its catalog response is descriptive; dispatch is
169
- where the supervised runtime probe establishes host compatibility.
170
-
171
- When native logins are available, `make acceptance-native` runs the Pi and
172
- OpenCode live gates through a private supervisor. A pass exits `0`; an
173
- unavailable executable, supervisor, or login emits a sanitized `BLOCKED`
174
- receipt and exits `2`, which the aggregate target treats as neutral. Real
175
- behavior and evidence failures still fail the target.
176
-
177
- The catalog uses public request option names. The public `effort` option maps
178
- to Pi's native `--thinking` flag and OpenCode's native `--variant` flag.
179
-
180
- ## Safety model
181
-
182
- - Every task has one immutable request and one provider turn.
183
- - State, workspace, provider runtime, and temporary directories are canonical
184
- and kept disjoint where the selected policy requires it.
185
- - The supervisor owns process lifetime; the runner owns capture and sealing.
186
- - Admission and launch re-check executable identity, effective policy, and
187
- session identity.
188
- - Briefs and control responses are bounded. Sealed payloads and raw streams are
189
- described by validated paths, sizes, and SHA-256 digests.
190
- - Provider authentication remains native to the provider CLI. Delegation Layer
191
- does not copy credentials into task state or rewrite account settings.
192
-
193
- Read [security](docs/security.md) for the full boundary and its operational
194
- limitations.
139
+ Use `delegate providers --json` for the compiled provider catalog and
140
+ `delegate capabilities --provider PROFILE --json` for a provider-neutral
141
+ capability response. Dispatch still performs the host-specific runtime probe;
142
+ catalog discovery alone does not prove authentication or supervisor readiness.
195
143
 
196
144
  ## Documentation
197
145
 
198
- - [Getting started](docs/getting-started.md) — install, configure, and run a
199
- first task.
146
+ - [Getting started](docs/getting-started.md) — install and run a first task.
200
147
  - [CLI reference](docs/cli-reference.md) — commands, flags, responses, and
201
148
  exit codes.
202
- - [Integration API](docs/api.md) — JSON responses, state semantics, and
203
- idempotent client behavior.
204
- - [Provider guide](docs/providers.md) — supported profiles and compatibility
149
+ - [Integration API](docs/api.md) — JSON responses and idempotent client
205
150
  behavior.
206
- - [Architecture](docs/architecture.md) — task lifecycle and ownership.
207
- - [System components](diagrams/components.html) — current Archify component map.
208
- - [Task lifecycle](diagrams/lead-interface.html) — current Archify sequence view.
151
+ - [Provider guide](docs/providers.md) — profiles and runtime compatibility.
152
+ - [Architecture](docs/architecture.md) — lifecycle and ownership boundaries.
153
+ - [System components](diagrams/components.html) — interactive Archify view.
154
+ - [Task lifecycle](diagrams/lead-interface.html) — interactive lifecycle view.
209
155
  - [Security](docs/security.md) — isolation, policy, and result integrity.
210
156
  - [Troubleshooting](docs/troubleshooting.md) — common setup and runtime
211
157
  failures.
212
- - [Contributing](docs/contributing.md) — development setup and adapter
213
- extension guidance.
214
- - [Agent integration skill](skills/agent-integration/SKILL.md) — provider-neutral
215
- JSON and evidence handling guidance for agents.
158
+ - [Agent integration skill](skills/agent-integration/SKILL.md) — the
159
+ provider-neutral instructions installed into an agent harness.
160
+ - [Contributing](CONTRIBUTING.md) — development and review guidance.
216
161
 
217
162
  ## License
218
163
 
package/package.json CHANGED
@@ -1,13 +1,28 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.1.2",
4
- "description": "Delegation Layer CLI and provider-agnostic agent skill installers.",
3
+ "version": "0.1.4",
4
+ "description": "Multi-provider AI agent delegation CLI and Agent Skill for running bounded coding tasks across provider CLIs.",
5
+ "keywords": [
6
+ "ai-agents",
7
+ "agent-skills",
8
+ "ai-orchestration",
9
+ "coding-agents",
10
+ "multi-agent",
11
+ "llm-orchestration",
12
+ "model-routing",
13
+ "delegation",
14
+ "developer-tools",
15
+ "cli",
16
+ "automation",
17
+ "golang"
18
+ ],
5
19
  "type": "module",
6
20
  "bin": {
7
21
  "delegation-layer": "scripts/install-agent-integration.mjs"
8
22
  },
9
23
  "files": [
10
24
  "LICENSE",
25
+ "docs/assets/delegation-layer-hero.jpg",
11
26
  "install.sh",
12
27
  "skills/agent-integration/SKILL.md",
13
28
  "scripts/install-agent-integration.mjs",
@@ -1,23 +1,116 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { accessSync, constants } from "node:fs";
3
4
  import { copyFile, lstat, mkdir, readFile } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { emitKeypressEvents } from "node:readline";
6
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { homedir } from "node:os";
5
8
  import { fileURLToPath } from "node:url";
6
9
 
7
10
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
11
  const sourcePath = join(packageRoot, "skills", "agent-integration", "SKILL.md");
12
+ const harnesses = [
13
+ {
14
+ name: "universal",
15
+ label: "Universal Agent Skills",
16
+ projectPath: join(".agents", "skills"),
17
+ globalPath: join(".agents", "skills"),
18
+ projectMarkers: [".agents"],
19
+ globalMarkers: [".agents"],
20
+ commands: [],
21
+ },
22
+ {
23
+ name: "codex",
24
+ label: "Codex",
25
+ projectPath: join(".agents", "skills"),
26
+ globalPath: join(".agents", "skills"),
27
+ projectMarkers: [".agents", ".codex"],
28
+ globalMarkers: [".agents", ".codex"],
29
+ commands: ["codex"],
30
+ },
31
+ {
32
+ name: "claude",
33
+ label: "Claude Code",
34
+ projectPath: join(".claude", "skills"),
35
+ globalPath: join(".claude", "skills"),
36
+ projectMarkers: [".claude"],
37
+ globalMarkers: [".claude"],
38
+ commands: ["claude"],
39
+ },
40
+ {
41
+ name: "cursor",
42
+ label: "Cursor",
43
+ projectPath: join(".cursor", "skills"),
44
+ globalPath: join(".cursor", "skills"),
45
+ projectMarkers: [".cursor"],
46
+ globalMarkers: [".cursor"],
47
+ commands: ["cursor"],
48
+ },
49
+ {
50
+ name: "gemini",
51
+ label: "Gemini CLI",
52
+ projectPath: join(".gemini", "skills"),
53
+ globalPath: join(".gemini", "skills"),
54
+ projectMarkers: [".gemini"],
55
+ globalMarkers: [".gemini"],
56
+ commands: ["gemini"],
57
+ },
58
+ {
59
+ name: "opencode",
60
+ label: "OpenCode",
61
+ projectPath: join(".opencode", "skills"),
62
+ globalPath: join(".config", "opencode", "skills"),
63
+ projectMarkers: [".opencode"],
64
+ globalMarkers: [join(".config", "opencode")],
65
+ commands: ["opencode"],
66
+ },
67
+ {
68
+ name: "pi",
69
+ label: "Pi",
70
+ projectPath: join(".pi", "skills"),
71
+ globalPath: join(".pi", "agent", "skills"),
72
+ projectMarkers: [".pi"],
73
+ globalMarkers: [join(".pi", "agent")],
74
+ commands: ["pi"],
75
+ },
76
+ {
77
+ name: "hermes",
78
+ label: "Hermes",
79
+ projectPath: join(".hermes", "skills"),
80
+ globalPath: join(".hermes", "skills"),
81
+ projectMarkers: [".hermes"],
82
+ globalMarkers: [".hermes"],
83
+ commands: ["hermes"],
84
+ },
85
+ ];
86
+ const harnessByName = new Map(harnesses.map((harness) => [harness.name, harness]));
9
87
 
10
- function usage(message) {
88
+ function usage(message, exitCode = 2) {
11
89
  if (message) {
12
90
  console.error(`Error: ${message}`);
13
91
  }
14
92
  console.error(
15
- "Usage:\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
93
+ "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]",
16
94
  );
17
- process.exitCode = 2;
95
+ process.exitCode = exitCode;
18
96
  }
19
97
 
20
- function parseArguments(argumentsList) {
98
+ function help() {
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.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.",
101
+ );
102
+ }
103
+
104
+ function requiredValue(argumentsList, index, option) {
105
+ const value = argumentsList[index + 1];
106
+ if (!value || value.startsWith("--")) {
107
+ usage(`${option} requires a value`);
108
+ return null;
109
+ }
110
+ return value;
111
+ }
112
+
113
+ function parseLegacyArguments(argumentsList) {
21
114
  let target;
22
115
  let force = false;
23
116
 
@@ -28,14 +121,15 @@ function parseArguments(argumentsList) {
28
121
  continue;
29
122
  }
30
123
  if (argument === "--target") {
31
- target = argumentsList[index + 1];
124
+ target = requiredValue(argumentsList, index, "--target");
125
+ if (!target) {
126
+ return null;
127
+ }
32
128
  index += 1;
33
129
  continue;
34
130
  }
35
131
  if (argument === "--help" || argument === "-h") {
36
- console.log(
37
- "Usage:\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
38
- );
132
+ help();
39
133
  process.exit(0);
40
134
  }
41
135
  usage(`unknown argument ${argument}`);
@@ -53,6 +147,63 @@ function parseArguments(argumentsList) {
53
147
  return { force, target: resolve(target) };
54
148
  }
55
149
 
150
+ function parseInstallArguments(argumentsList) {
151
+ let force = false;
152
+ let harnessNames;
153
+ let scope;
154
+ let yes = false;
155
+
156
+ for (let index = 0; index < argumentsList.length; index += 1) {
157
+ const argument = argumentsList[index];
158
+ if (argument === "--force") {
159
+ force = true;
160
+ continue;
161
+ }
162
+ if (argument === "--yes" || argument === "-y") {
163
+ yes = true;
164
+ continue;
165
+ }
166
+ if (argument === "--scope") {
167
+ scope = requiredValue(argumentsList, index, "--scope");
168
+ if (!scope) {
169
+ return null;
170
+ }
171
+ index += 1;
172
+ continue;
173
+ }
174
+ if (argument === "--harness") {
175
+ const value = requiredValue(argumentsList, index, "--harness");
176
+ if (!value) {
177
+ return null;
178
+ }
179
+ harnessNames = value.split(",").map((name) => name.trim().toLowerCase());
180
+ index += 1;
181
+ continue;
182
+ }
183
+ if (argument === "--help" || argument === "-h") {
184
+ help();
185
+ process.exit(0);
186
+ }
187
+ usage(`unknown argument ${argument}`);
188
+ return null;
189
+ }
190
+
191
+ if (scope && scope !== "project" && scope !== "global") {
192
+ usage("--scope must be project or global");
193
+ return null;
194
+ }
195
+ if (harnessNames?.length === 0 || harnessNames?.some((name) => !name)) {
196
+ usage("--harness requires at least one harness name");
197
+ return null;
198
+ }
199
+ if (harnessNames?.some((name) => !harnessByName.has(name))) {
200
+ const unknown = harnessNames.find((name) => !harnessByName.has(name));
201
+ usage(`unknown harness ${unknown}; choose from ${harnesses.map((harness) => harness.name).join(", ")}`);
202
+ return null;
203
+ }
204
+ return { force, harnessNames, scope, yes };
205
+ }
206
+
56
207
  async function existingPath(path) {
57
208
  try {
58
209
  return await lstat(path);
@@ -64,6 +215,255 @@ async function existingPath(path) {
64
215
  }
65
216
  }
66
217
 
218
+ function executableOnPath(command) {
219
+ const pathEntries = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
220
+ const candidates = process.platform === "win32" ? [command, `${command}.exe`, `${command}.cmd`] : [command];
221
+ return pathEntries.some((pathEntry) =>
222
+ candidates.some((candidate) => {
223
+ try {
224
+ accessSync(join(pathEntry, candidate), constants.X_OK);
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ }),
230
+ );
231
+ }
232
+
233
+ async function detectHarnesses({ cwd, home }) {
234
+ return Promise.all(
235
+ harnesses.map(async (harness) => {
236
+ const reasons = [];
237
+ for (const marker of harness.projectMarkers) {
238
+ const markerPath = resolve(cwd, marker);
239
+ if ((await existingPath(markerPath))?.isDirectory()) {
240
+ reasons.push(`project ${marker}`);
241
+ break;
242
+ }
243
+ }
244
+ for (const marker of harness.globalMarkers) {
245
+ const markerPath = resolve(home, marker);
246
+ if ((await existingPath(markerPath))?.isDirectory()) {
247
+ reasons.push(`global ${marker}`);
248
+ break;
249
+ }
250
+ }
251
+ for (const command of harness.commands) {
252
+ if (executableOnPath(command)) {
253
+ reasons.push(`${command} on PATH`);
254
+ break;
255
+ }
256
+ }
257
+ return { harness, reasons };
258
+ }),
259
+ );
260
+ }
261
+
262
+ function defaultHarnessNames(detections) {
263
+ const detected = detections
264
+ .filter(({ harness, reasons }) => harness.name !== "universal" && reasons.length > 0)
265
+ .map(({ harness }) => harness.name);
266
+ if (detected.length > 0) {
267
+ return detected;
268
+ }
269
+ return ["universal"];
270
+ }
271
+
272
+ function formatDetected(reasons) {
273
+ return reasons.length > 0 ? ` (detected: ${reasons.join(", ")})` : "";
274
+ }
275
+
276
+ function printDetections(detections) {
277
+ console.log("Detected harnesses:");
278
+ detections.forEach(({ harness, reasons }, index) => {
279
+ console.log(` ${index + 1}. ${harness.label}${formatDetected(reasons)}`);
280
+ });
281
+ }
282
+
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}`);
294
+ }
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);
398
+ }
399
+ });
400
+ }
401
+
402
+ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
403
+ const cwd = process.cwd();
404
+ const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
405
+ const detections = await detectHarnesses({ cwd, home });
406
+ const defaults = defaultHarnessNames(detections);
407
+ const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
408
+ const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
409
+
410
+ if (!harnessNames && !needsPrompt) {
411
+ printDetections(detections);
412
+ }
413
+ if (!canPrompt && !yes && !harnessNames && !scope) {
414
+ console.log("No interactive terminal detected; using global scope and detected harnesses.");
415
+ }
416
+
417
+ if (needsPrompt) {
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;
449
+ }
450
+ }
451
+
452
+ return {
453
+ cwd,
454
+ force,
455
+ harnessNames: harnessNames ?? defaults,
456
+ home,
457
+ scope: scope ?? "global",
458
+ };
459
+ }
460
+
461
+ function targetFor(harness, scope, cwd, home) {
462
+ const root = scope === "global" ? home : cwd;
463
+ const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
464
+ return join(resolve(root, relativePath), "agent-integration");
465
+ }
466
+
67
467
  async function install({ force, target }) {
68
468
  const targetStat = await existingPath(target);
69
469
  if (targetStat?.isSymbolicLink()) {
@@ -109,8 +509,28 @@ if (argumentsList[0] === "install-cli") {
109
509
  console.error(`Error: ${error.message}`);
110
510
  process.exitCode = 1;
111
511
  }
512
+ } else if (argumentsList[0] === "install") {
513
+ const options = parseInstallArguments(argumentsList.slice(1));
514
+ if (options) {
515
+ try {
516
+ const selected = await chooseInstallOptions(options);
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
+ }
526
+ }
527
+ } catch (error) {
528
+ console.error(`Error: ${error.message}`);
529
+ process.exitCode = 1;
530
+ }
531
+ }
112
532
  } else {
113
- const options = parseArguments(argumentsList);
533
+ const options = parseLegacyArguments(argumentsList);
114
534
  if (options) {
115
535
  try {
116
536
  await install(options);