delegation-layer 0.1.1 → 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hesham Karm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,129 +1,110 @@
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
+ From the project where you want the skill available, run:
56
40
 
57
41
  ```sh
58
- curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
42
+ npx --yes delegation-layer install
59
43
  ```
60
44
 
61
- Set `DELEGATION_LAYER_INSTALL_DIR` if you want to install somewhere other than
62
- `$HOME/.local/bin`.
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.
63
48
 
64
- Homebrew installs the same two executables:
49
+ For scripts or a choice without prompts:
65
50
 
66
51
  ```sh
67
- brew install hishamkaram/tap/delegation-layer
52
+ npx --yes delegation-layer install \
53
+ --scope project \
54
+ --harness universal \
55
+ --yes
68
56
  ```
69
57
 
70
- The npm package also provides a checksum-verified CLI installer through npm,
71
- pnpm, and Bun:
58
+ The skill installer only installs guidance for your agent. It does not install
59
+ the CLI, change provider logins, or configure a supervisor.
60
+
61
+ ### Install the CLI
62
+
63
+ Use the prebuilt installer that fits your machine:
72
64
 
73
65
  ```sh
74
- npx --yes delegation-layer install-cli
75
- pnpm dlx delegation-layer install-cli
76
- bunx --bun delegation-layer install-cli
66
+ curl -fsSL https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/install.sh | sh
77
67
  ```
78
68
 
79
- For a source build, install both Go commands from the latest version:
69
+ ```sh
70
+ brew install hishamkaram/tap/delegation-layer
71
+ ```
80
72
 
81
73
  ```sh
82
- go install github.com/hishamkaram/delegation-layer/cmd/delegate@latest
83
- go install github.com/hishamkaram/delegation-layer/cmd/delegate-run@latest
74
+ npx --yes delegation-layer install-cli
75
+ pnpm dlx delegation-layer install-cli
76
+ bunx --bun delegation-layer install-cli
84
77
  ```
85
78
 
86
- Ensure the directory used by the installer or Go is on `PATH`.
79
+ The release installer verifies checksums and installs `delegate` and
80
+ `delegate-run` into `$HOME/.local/bin` by default. Set
81
+ `DELEGATION_LAYER_INSTALL_DIR` to use another directory.
87
82
 
88
- Confirm the installation:
83
+ Check the installation:
89
84
 
90
85
  ```sh
91
86
  delegate --help
92
87
  delegate providers --json
93
88
  ```
94
89
 
95
- ### Install the agent integration skill
90
+ ## What you need to run a task
96
91
 
97
- The provider-agnostic skill is separate from the CLI installation. Hermes can
98
- install the current skill directly:
92
+ Normal users do not need Go or Node after choosing an installation method.
93
+ Dispatching a task requires:
99
94
 
100
- ```sh
101
- hermes skills install \
102
- https://raw.githubusercontent.com/hishamkaram/delegation-layer/main/skills/agent-integration/SKILL.md
103
- ```
104
-
105
- Any harness with a writable skill directory can use the dependency-free npm,
106
- pnpm, or Bun installer:
107
-
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
- ```
95
+ - 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`;
99
+ - Darwin or Linux on amd64 or arm64 for the supplied release builds.
116
100
 
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.
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.
122
104
 
123
- ## Quick start
105
+ ## Try one bounded task
124
106
 
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:
107
+ Create a brief and a workspace outside the private state root:
127
108
 
128
109
  ```sh
129
110
  mkdir -p "$HOME/delegation-workspace"
@@ -139,83 +120,36 @@ delegate --pueue-config /absolute/path/to/pueue.yml dispatch \
139
120
  ```
140
121
 
141
122
  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`:
123
+ publication fields. Observe and collect it with:
144
124
 
145
125
  ```sh
146
126
  delegate status TASK_ID --json
147
127
  delegate collect TASK_ID --watch 5s --json
148
128
  ```
149
129
 
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.
130
+ Use `delegate providers --json` for the compiled provider catalog and
131
+ `delegate capabilities --provider PROFILE --json` for a provider-neutral
132
+ capability response. Dispatch still performs the host-specific runtime probe;
133
+ catalog discovery alone does not prove authentication or supervisor readiness.
195
134
 
196
135
  ## Documentation
197
136
 
198
- - [Getting started](docs/getting-started.md) — install, configure, and run a
199
- first task.
137
+ - [Getting started](docs/getting-started.md) — install and run a first task.
200
138
  - [CLI reference](docs/cli-reference.md) — commands, flags, responses, and
201
139
  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
140
+ - [Integration API](docs/api.md) — JSON responses and idempotent client
205
141
  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.
142
+ - [Provider guide](docs/providers.md) — profiles and runtime compatibility.
143
+ - [Architecture](docs/architecture.md) — lifecycle and ownership boundaries.
144
+ - [System components](diagrams/components.html) — interactive Archify view.
145
+ - [Task lifecycle](diagrams/lead-interface.html) — interactive lifecycle view.
209
146
  - [Security](docs/security.md) — isolation, policy, and result integrity.
210
147
  - [Troubleshooting](docs/troubleshooting.md) — common setup and runtime
211
148
  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.
149
+ - [Agent integration skill](skills/agent-integration/SKILL.md) — the
150
+ provider-neutral instructions installed into an agent harness.
151
+ - [Contributing](CONTRIBUTING.md) — development and review guidance.
216
152
 
217
153
  ## License
218
154
 
219
- No public license has been declared yet. Treat the source and release artifacts
220
- as proprietary and do not redistribute them without the project owner's
221
- permission.
155
+ This project is licensed under the [MIT License](LICENSE).
package/package.json CHANGED
@@ -1,12 +1,28 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.1.1",
4
- "description": "Delegation Layer CLI and provider-agnostic agent skill installers.",
3
+ "version": "0.1.3",
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": [
24
+ "LICENSE",
25
+ "docs/assets/delegation-layer-hero.jpg",
10
26
  "install.sh",
11
27
  "skills/agent-integration/SKILL.md",
12
28
  "scripts/install-agent-integration.mjs",
@@ -23,5 +39,5 @@
23
39
  "bugs": {
24
40
  "url": "https://github.com/hishamkaram/delegation-layer/issues"
25
41
  },
26
- "license": "UNLICENSED"
42
+ "license": "MIT"
27
43
  }
@@ -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 { createInterface } from "node:readline/promises";
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.\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,140 @@ 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 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;
295
+ }
296
+ if (!harnessByName.has(token)) {
297
+ throw new Error(`unknown harness ${token}`);
298
+ }
299
+ return token;
300
+ });
301
+ return [...new Set(names)];
302
+ }
303
+
304
+ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
305
+ const cwd = process.cwd();
306
+ const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
307
+ const detections = await detectHarnesses({ cwd, home });
308
+ const defaults = defaultHarnessNames(detections);
309
+ const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
310
+ const needsPrompt = !yes && !harnessNames && !scope && canPrompt;
311
+
312
+ if (!harnessNames) {
313
+ printDetections(detections);
314
+ }
315
+ if (!canPrompt && !yes && !harnessNames && !scope) {
316
+ console.log("No interactive terminal detected; using project scope and detected harnesses.");
317
+ }
318
+
319
+ 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();
334
+ }
335
+ }
336
+
337
+ return {
338
+ cwd,
339
+ force,
340
+ harnessNames: harnessNames ?? defaults,
341
+ home,
342
+ scope: scope ?? "project",
343
+ };
344
+ }
345
+
346
+ function targetFor(harness, scope, cwd, home) {
347
+ const root = scope === "global" ? home : cwd;
348
+ const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
349
+ return join(resolve(root, relativePath), "agent-integration");
350
+ }
351
+
67
352
  async function install({ force, target }) {
68
353
  const targetStat = await existingPath(target);
69
354
  if (targetStat?.isSymbolicLink()) {
@@ -109,8 +394,24 @@ if (argumentsList[0] === "install-cli") {
109
394
  console.error(`Error: ${error.message}`);
110
395
  process.exitCode = 1;
111
396
  }
397
+ } else if (argumentsList[0] === "install") {
398
+ const options = parseInstallArguments(argumentsList.slice(1));
399
+ if (options) {
400
+ try {
401
+ 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 });
407
+ }
408
+ } catch (error) {
409
+ console.error(`Error: ${error.message}`);
410
+ process.exitCode = 1;
411
+ }
412
+ }
112
413
  } else {
113
- const options = parseArguments(argumentsList);
414
+ const options = parseLegacyArguments(argumentsList);
114
415
  if (options) {
115
416
  try {
116
417
  await install(options);