delegation-layer 0.1.19 → 0.1.21
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 +13 -6
- package/package.json +1 -1
- package/plugins/delegation-layer/.claude-plugin/plugin.json +1 -1
- package/plugins/delegation-layer/.codex-plugin/plugin.json +1 -1
- package/plugins/delegation-layer/hooks/hooks.json +18 -0
- package/plugins/delegation-layer/skills/agent-integration/SKILL.md +70 -20
- package/plugins/delegation-layer/skills/agent-integration/evals/evals.json +40 -0
- package/plugins/delegation-layer/skills/agent-integration/evals/routing.json +47 -0
- package/plugins/delegation-layer/skills/agent-integration/examples/json-results.md +2 -2
- package/plugins/delegation-layer/skills/agent-integration/examples/read-only-task.md +1 -2
- package/plugins/delegation-layer/skills/agent-integration/examples/workspace-write-task.md +1 -2
- package/plugins/delegation-layer/skills/agent-integration/references/collaboration.md +77 -0
- package/plugins/delegation-layer/skills/agent-integration/references/command-reference.md +15 -4
- package/plugins/delegation-layer/skills/agent-integration/references/continuation.md +1 -1
- package/plugins/delegation-layer/skills/agent-integration/references/interaction.md +110 -0
- package/plugins/delegation-layer/skills/agent-integration/references/result-handling.md +4 -2
- package/plugins/delegation-layer/skills/agent-integration/references/routing-rule.json +1 -0
- package/plugins/delegation-layer/skills/agent-integration/scripts/delegate-routing-context.sh +26 -0
- package/scripts/install-agent-integration.mjs +212 -5
- package/skills/agent-integration/SKILL.md +70 -20
- package/skills/agent-integration/evals/evals.json +40 -0
- package/skills/agent-integration/evals/routing.json +47 -0
- package/skills/agent-integration/examples/json-results.md +2 -2
- package/skills/agent-integration/examples/read-only-task.md +1 -2
- package/skills/agent-integration/examples/workspace-write-task.md +1 -2
- package/skills/agent-integration/references/collaboration.md +77 -0
- package/skills/agent-integration/references/command-reference.md +15 -4
- package/skills/agent-integration/references/continuation.md +1 -1
- package/skills/agent-integration/references/interaction.md +110 -0
- package/skills/agent-integration/references/result-handling.md +4 -2
- package/skills/agent-integration/references/routing-rule.json +1 -0
- package/skills/agent-integration/scripts/delegate-routing-context.sh +26 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Questions and guidance during an active task
|
|
2
|
+
|
|
3
|
+
Use this reference when an external task may need clarification or guidance
|
|
4
|
+
before it finishes, including planning, consultation, and debate. Use only
|
|
5
|
+
Delegate commands; do not open a terminal session with the provider.
|
|
6
|
+
|
|
7
|
+
## Choose the transport
|
|
8
|
+
|
|
9
|
+
Read the selected provider's `interaction` capability and
|
|
10
|
+
`interaction.supported_modes`. `clarification` and `steering` are independent.
|
|
11
|
+
Add `--interactive` to dispatch when the chosen provider and permission mode
|
|
12
|
+
support the interaction you need. Keep the user's exact provider, model,
|
|
13
|
+
effort, workspace, permission, and budget.
|
|
14
|
+
|
|
15
|
+
Without a named provider, `dispatch --auto --interactive` selects among
|
|
16
|
+
providers that support interaction. If a named provider does not support it,
|
|
17
|
+
use ordinary bounded turns and `continue` for discussion when native
|
|
18
|
+
continuation is available. Explain that questions cannot be answered during
|
|
19
|
+
that provider's active turn. Never replace the requested provider silently.
|
|
20
|
+
|
|
21
|
+
In the worker brief, explicitly request **native structured clarification**:
|
|
22
|
+
"Use your native question tool to request clarification and await its answer.
|
|
23
|
+
Do not ask only in final text or a progress message and then sleep: the lead
|
|
24
|
+
can answer structured requests, not an unstructured prompt. If the question
|
|
25
|
+
tool is unavailable, report that blocker and finish instead of waiting."
|
|
26
|
+
Assign the actual work directly; the worker should not start another delegate.
|
|
27
|
+
|
|
28
|
+
No change is needed for self-contained tasks that do not need mid-turn input.
|
|
29
|
+
Continuation inherits interaction from its predecessor. `continue --task ID
|
|
30
|
+
--interactive` can enable it for a previously ordinary task.
|
|
31
|
+
|
|
32
|
+
## Respond to questions
|
|
33
|
+
|
|
34
|
+
1. Save the admitted task ID and state root. Run `collect TASK_ID --watch 30s
|
|
35
|
+
--json` with that same root. Exit 3 is pending, not completion. Collection
|
|
36
|
+
returns early when there is an outstanding native question.
|
|
37
|
+
2. Read `interaction.pending_requests`. Use the returned request ID, question
|
|
38
|
+
IDs, options, `multiple`, and `free_text`; do not invent or guess IDs.
|
|
39
|
+
3. Answer facts already supplied by the user or verified during the task.
|
|
40
|
+
Ask the user for missing preferences, business decisions, new scope, or
|
|
41
|
+
authorization. Do not guess an answer to keep the worker moving. A question
|
|
42
|
+
reply does not approve tool access or change the task's permission mode.
|
|
43
|
+
4. Write one JSON object mapping **every question ID in that request** to an
|
|
44
|
+
array of answer strings. A single-choice question takes one value. Use exact
|
|
45
|
+
option labels unless `free_text` permits another answer. Example:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"color": ["Blue"], "sections": ["Summary", "Evidence"]}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. Allocate a new 32-character lowercase hexadecimal message ID and save it
|
|
52
|
+
together with the exact answers. Submit once:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
delegate --root "${DELEGATE_ROOT:-$HOME/delegation-state}" reply "$task_id" \
|
|
56
|
+
--request "$request_id" --answers "$answers_file" \
|
|
57
|
+
--id "$message_id" --json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
6. Keep collecting. A receipt is not task completion. Only the task's
|
|
61
|
+
authoritative `outcome` establishes the final result.
|
|
62
|
+
|
|
63
|
+
Use the same root expression for dispatch, collection, reply, and steering.
|
|
64
|
+
If the user explicitly chose another root, use that exact path for all of
|
|
65
|
+
them. The other variables stand for actual values saved from this task;
|
|
66
|
+
reassign them in each shell invocation if your tool does not preserve variables.
|
|
67
|
+
A pending response without questions means keep collecting, not inspect OS
|
|
68
|
+
processes or dispatch a replacement task.
|
|
69
|
+
|
|
70
|
+
## Guide active work
|
|
71
|
+
|
|
72
|
+
When `interaction.capabilities.steering` is true and the task is active, put
|
|
73
|
+
additional guidance in a brief file and submit:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
delegate --root "${DELEGATE_ROOT:-$HOME/delegation-state}" steer "$task_id" \
|
|
77
|
+
--brief "$guidance_file" --id "$message_id" --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Steering is guidance to the existing turn. It does not change provider, model,
|
|
81
|
+
effort, workspace, permission, or budget. Once the turn ends, use `continue`
|
|
82
|
+
for another authorized turn instead. Do not cancel and redispatch simply to
|
|
83
|
+
send guidance.
|
|
84
|
+
|
|
85
|
+
## Interpret delivery precisely
|
|
86
|
+
|
|
87
|
+
| State | What it establishes |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `queued` | Delegate durably accepted the message; native delivery is pending. |
|
|
90
|
+
| `unknown` | Delivery was reserved or attempted, but its result is uncertain. |
|
|
91
|
+
| `sent` | The native input write completed; no native acknowledgement is established. |
|
|
92
|
+
| `acknowledged` | A correlated native response acknowledged the message. This does not prove the model followed guidance. |
|
|
93
|
+
| `rejected` | The message could not be delivered or the native protocol rejected it. Read the reason. |
|
|
94
|
+
|
|
95
|
+
If a submission response was lost, repeat the **same message ID and exact
|
|
96
|
+
content** to recover its receipt. Never allocate a fresh ID to retry an
|
|
97
|
+
`unknown` delivery. Delegate will not replay an ambiguous native write.
|
|
98
|
+
Changed content with an existing ID is a conflict. Observe the task instead
|
|
99
|
+
of guessing whether it consumed the message.
|
|
100
|
+
|
|
101
|
+
`events TASK_ID --after SEQUENCE --watch 30s --json` returns a bounded event
|
|
102
|
+
page. Save the returned `cursor` for the next page. Events and interaction
|
|
103
|
+
state are progress evidence, not a replacement for `collect` and `outcome`.
|
|
104
|
+
|
|
105
|
+
Waiting for an answer consumes the original execution budget; it does not
|
|
106
|
+
pause or extend it. `interaction.deadline` shows the recorded wall-clock
|
|
107
|
+
boundary when execution has started. If the task ends while waiting, do not
|
|
108
|
+
reply to the old request. Follow the ordinary continuation guidance only when
|
|
109
|
+
Delegate reports the task resumable. Preserve any verified answers in the new
|
|
110
|
+
brief; uncertain delivery must not become an automatic replay.
|
|
@@ -17,8 +17,10 @@ the same as a successful provider result.
|
|
|
17
17
|
the result and its payload/evidence descriptors.
|
|
18
18
|
5. If `outcome.verdict` is rejected, report a terminal rejected result with its
|
|
19
19
|
refusal and evidence. Do not retry automatically.
|
|
20
|
-
6.
|
|
21
|
-
|
|
20
|
+
6. If `pending.kind` is `interaction`, handle the returned questions using
|
|
21
|
+
[interaction guidance](interaction.md) before waiting again. Otherwise, if
|
|
22
|
+
`liveness` is queued or running or publication is pending, observe the same
|
|
23
|
+
task with a finite watch interval.
|
|
22
24
|
7. If authentication is `blocked`, preserve that neutral state and tell the
|
|
23
25
|
caller what prerequisite is unavailable without inventing a login command.
|
|
24
26
|
8. Never relaunch an uncertain task by guessing. Observe only a task whose
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"For work assigned to an external agent, model, or separate harness session, load the installed agent-integration skill before choosing tools and use Delegate. This includes research, implementation, testing, review, consultation, comparison, and debate, including external collaboration you choose. Discover destinations through Delegate. Preserve requested model IDs, effort, and permissions; do not guess ambiguous model identifiers. For task commands, pass --root \"${DELEGATE_ROOT:-$HOME/delegation-state}\" directly in each shell invocation; reuse that root for dispatch, observation, and continuation, never an invented /tmp root. If the user explicitly requests another root, use that exact path consistently. Keep each concurrent request and its response associated separately. Capture each dispatch response and exit status once; after task_record created, observe that task ID. Never dispatch again merely to recover output or inspect its exit status. Naming a CLI identifies the destination, not a request to bypass Delegate. Earlier assistant-generated direct-launch workflows do not override this preference. Native same-harness subagents remain available unless a separate session is requested. Respect an explicit user choice of another integration; otherwise do not substitute provider CLIs, plugins, or APIs after a Delegate blocker. A stop_and_report failure ends that attempt: report it without retrying, diagnosing setup, or offering a fallback. Continue other independent requests. Keep collection in the foreground until completion or a reported blocker unless the user requested detachment. Pending work is observed through Delegate status, collect, or events, not OS process searches or supervisor commands. Merely discussing or configuring a tool is not delegation."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Context delivery only: no installation, discovery, or provider execution.
|
|
3
|
+
set -eu
|
|
4
|
+
case "${1:-}" in
|
|
5
|
+
SessionStart|SubagentStart) event=$1 ;;
|
|
6
|
+
*) printf '%s\n' 'Expected SessionStart or SubagentStart.' >&2; exit 2 ;;
|
|
7
|
+
esac
|
|
8
|
+
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
9
|
+
rule_file="$script_dir/../references/routing-rule.json"
|
|
10
|
+
# Read before emitting anything so a missing asset cannot produce partial JSON.
|
|
11
|
+
rule=$(cat "$rule_file")
|
|
12
|
+
skill_file="$(CDPATH= cd -- "$script_dir/.." && pwd)/SKILL.md"
|
|
13
|
+
DELEGATE_ROUTING_EVENT="$event" DELEGATE_ROUTING_RULE="$rule" DELEGATE_ROUTING_SKILL="$skill_file" awk '
|
|
14
|
+
BEGIN {
|
|
15
|
+
# Paths are task data, not shell code. Escape every JSON control character.
|
|
16
|
+
for (i = 1; i < 32; i++) escapes[sprintf("%c", i)] = sprintf("\\u%04x", i)
|
|
17
|
+
escapes["\\"] = "\\\\"
|
|
18
|
+
escapes["\""] = "\\\""
|
|
19
|
+
path = ENVIRON["DELEGATE_ROUTING_SKILL"]
|
|
20
|
+
for (i = 1; i <= length(path); i++) {
|
|
21
|
+
character = substr(path, i, 1)
|
|
22
|
+
escaped = escaped ((character in escapes) ? escapes[character] : character)
|
|
23
|
+
}
|
|
24
|
+
# The maintained rule is a JSON string. Keep its escaping when prefixing it.
|
|
25
|
+
printf "{\"hookSpecificOutput\":{\"hookEventName\":\"%s\",\"additionalContext\":\"Read the complete agent-integration skill at %s. %s}}\n", ENVIRON["DELEGATE_ROUTING_EVENT"], escaped, substr(ENVIRON["DELEGATE_ROUTING_RULE"], 2)
|
|
26
|
+
}'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { accessSync, constants } from "node:fs";
|
|
4
|
-
import { copyFile, lstat, mkdir, readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { copyFile, lstat, mkdir, open, readdir, readFile, rename, unlink } from "node:fs/promises";
|
|
5
5
|
import { emitKeypressEvents } from "node:readline";
|
|
6
6
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -90,14 +90,14 @@ function usage(message, exitCode = 2) {
|
|
|
90
90
|
console.error(`Error: ${message}`);
|
|
91
91
|
}
|
|
92
92
|
console.error(
|
|
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]",
|
|
93
|
+
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force] [--routing default|skill-only]\n delegation-layer routing status|disable [--scope project|global] [--harness NAME[,NAME...]]\n delegation-layer --target ABSOLUTE_SKILL_DIR [--force]\n delegation-layer install-cli [--version VERSION] [--install-dir ABSOLUTE_DIR]",
|
|
94
94
|
);
|
|
95
95
|
process.exitCode = exitCode;
|
|
96
96
|
}
|
|
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.\nGlobal scope is the default; use --scope project for a project-local install.\nUse --harness universal when no specific harness is detected.",
|
|
100
|
+
"Usage:\n delegation-layer install [--scope project|global] [--harness NAME[,NAME...]] [--yes] [--force] [--routing default|skill-only]\n delegation-layer routing status|disable [--scope project|global] [--harness NAME[,NAME...]]\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
|
|
|
@@ -152,6 +152,7 @@ function parseInstallArguments(argumentsList) {
|
|
|
152
152
|
let harnessNames;
|
|
153
153
|
let scope;
|
|
154
154
|
let yes = false;
|
|
155
|
+
let routing;
|
|
155
156
|
|
|
156
157
|
for (let index = 0; index < argumentsList.length; index += 1) {
|
|
157
158
|
const argument = argumentsList[index];
|
|
@@ -163,6 +164,12 @@ function parseInstallArguments(argumentsList) {
|
|
|
163
164
|
yes = true;
|
|
164
165
|
continue;
|
|
165
166
|
}
|
|
167
|
+
if (argument === "--routing") {
|
|
168
|
+
routing = requiredValue(argumentsList, index, "--routing");
|
|
169
|
+
if (!routing) return null;
|
|
170
|
+
index += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
166
173
|
if (argument === "--scope") {
|
|
167
174
|
scope = requiredValue(argumentsList, index, "--scope");
|
|
168
175
|
if (!scope) {
|
|
@@ -201,7 +208,11 @@ function parseInstallArguments(argumentsList) {
|
|
|
201
208
|
usage(`unknown harness ${unknown}; choose from ${harnesses.map((harness) => harness.name).join(", ")}`);
|
|
202
209
|
return null;
|
|
203
210
|
}
|
|
204
|
-
|
|
211
|
+
if (routing && !["default", "skill-only"].includes(routing)) {
|
|
212
|
+
usage("--routing must be default or skill-only");
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return { force, harnessNames, scope, yes, routing };
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
async function existingPath(path) {
|
|
@@ -486,7 +497,7 @@ function pickOptions({ initialValues = [], multiple = false, options, title }) {
|
|
|
486
497
|
});
|
|
487
498
|
}
|
|
488
499
|
|
|
489
|
-
async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
|
|
500
|
+
async function chooseInstallOptions({ force, harnessNames, scope, yes, routing }) {
|
|
490
501
|
const cwd = process.cwd();
|
|
491
502
|
const home = resolve(process.env.DELEGATION_LAYER_HOME ?? homedir());
|
|
492
503
|
const detections = await detectHarnesses({ cwd, home });
|
|
@@ -536,7 +547,20 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
|
|
|
536
547
|
}
|
|
537
548
|
}
|
|
538
549
|
|
|
550
|
+
if (needsPrompt && !routing) {
|
|
551
|
+
routing = await pickOptions({
|
|
552
|
+
initialValues: ["default"],
|
|
553
|
+
options: [
|
|
554
|
+
{ value: "default", label: "Prefer Delegate", description: "Install collaboration context hooks for Claude and Codex; native hook trust still applies" },
|
|
555
|
+
{ value: "skill-only", label: "Skill only", description: "Install guidance without adding hooks; keep existing routing settings" },
|
|
556
|
+
],
|
|
557
|
+
title: "How should external collaboration be routed?",
|
|
558
|
+
});
|
|
559
|
+
if (!routing) return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
539
562
|
return {
|
|
563
|
+
routing: routing ?? "skill-only",
|
|
540
564
|
cwd,
|
|
541
565
|
force,
|
|
542
566
|
harnessNames: harnessNames ?? defaults,
|
|
@@ -546,6 +570,9 @@ async function chooseInstallOptions({ force, harnessNames, scope, yes }) {
|
|
|
546
570
|
}
|
|
547
571
|
|
|
548
572
|
function targetFor(harness, scope, cwd, home) {
|
|
573
|
+
if (scope === "global" && harness.name === "claude" && process.env.CLAUDE_CONFIG_DIR) {
|
|
574
|
+
return join(resolve(process.env.CLAUDE_CONFIG_DIR), "skills", "agent-integration");
|
|
575
|
+
}
|
|
549
576
|
const root = scope === "global" ? home : cwd;
|
|
550
577
|
const relativePath = scope === "global" ? harness.globalPath : harness.projectPath;
|
|
551
578
|
return join(resolve(root, relativePath), "agent-integration");
|
|
@@ -576,6 +603,165 @@ async function install({ force, target }) {
|
|
|
576
603
|
console.log(`Installed agent integration skill: ${join(target, "SKILL.md")}`);
|
|
577
604
|
}
|
|
578
605
|
|
|
606
|
+
function shellQuote(value) {
|
|
607
|
+
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function routingPath(name, { scope, cwd, home }) {
|
|
611
|
+
if (name === "claude") {
|
|
612
|
+
return scope === "global"
|
|
613
|
+
? join(resolve(process.env.CLAUDE_CONFIG_DIR || join(home, ".claude")), "settings.json")
|
|
614
|
+
: join(cwd, ".claude", "settings.local.json");
|
|
615
|
+
}
|
|
616
|
+
if (name === "codex") {
|
|
617
|
+
return join(scope === "global" ? resolve(process.env.CODEX_HOME || join(home, ".codex")) : join(cwd, ".codex"), "hooks.json");
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function jsonObject(value) {
|
|
623
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function validateHookConfig(config, path) {
|
|
627
|
+
if (!jsonObject(config) || (config.hooks !== undefined && !jsonObject(config.hooks))) {
|
|
628
|
+
throw new Error(`expected a JSON object with an optional hooks object: ${path}`);
|
|
629
|
+
}
|
|
630
|
+
for (const groups of Object.values(config.hooks ?? {})) {
|
|
631
|
+
if (!Array.isArray(groups) || groups.some((group) => !jsonObject(group)
|
|
632
|
+
|| !Array.isArray(group.hooks) || group.hooks.some((hook) => !jsonObject(hook) || typeof hook.type !== "string"))) {
|
|
633
|
+
throw new Error(`invalid hook configuration; no routing changes written: ${path}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function prepareRouting(selected, action) {
|
|
639
|
+
const plans = [];
|
|
640
|
+
for (const name of new Set(selected.harnessNames)) {
|
|
641
|
+
const path = routingPath(name, selected);
|
|
642
|
+
if (!path || process.platform === "win32") {
|
|
643
|
+
plans.push({ name, status: "unsupported-host" });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
await assertNoSymlinkComponents(path);
|
|
647
|
+
const stat = await existingPath(path);
|
|
648
|
+
if (stat && !stat.isFile()) throw new Error(`routing configuration must be a regular file: ${path}`);
|
|
649
|
+
const before = stat ? await readFile(path, "utf8") : null;
|
|
650
|
+
let config;
|
|
651
|
+
try {
|
|
652
|
+
config = before === null ? {} : JSON.parse(before);
|
|
653
|
+
} catch {
|
|
654
|
+
throw new Error(`invalid JSON; routing configuration left unchanged: ${path}`);
|
|
655
|
+
}
|
|
656
|
+
validateHookConfig(config, path);
|
|
657
|
+
const target = targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home);
|
|
658
|
+
const script = join(target, "scripts", "delegate-routing-context.sh");
|
|
659
|
+
const hooks = config.hooks ?? {};
|
|
660
|
+
let present = 0;
|
|
661
|
+
let changed = false;
|
|
662
|
+
for (const event of ["SessionStart", "SubagentStart"]) {
|
|
663
|
+
const command = `/bin/sh ${shellQuote(script)} ${event}`;
|
|
664
|
+
const groups = hooks[event] ?? [];
|
|
665
|
+
const matches = groups.flatMap((group) => group.hooks).filter((hook) => hook.type === "command" && hook.command === command);
|
|
666
|
+
if (matches.length) present += 1;
|
|
667
|
+
if (action === "disable") {
|
|
668
|
+
if (!matches.length) continue;
|
|
669
|
+
hooks[event] = groups.flatMap((group) => {
|
|
670
|
+
const remaining = group.hooks.filter((hook) => hook.type !== "command" || hook.command !== command);
|
|
671
|
+
if (remaining.length === group.hooks.length) return [group];
|
|
672
|
+
return remaining.length ? [{ ...group, hooks: remaining }] : [];
|
|
673
|
+
});
|
|
674
|
+
if (!hooks[event].length) delete hooks[event];
|
|
675
|
+
changed = true;
|
|
676
|
+
} else if (action === "enable" && !matches.length) {
|
|
677
|
+
hooks[event] = [...groups, { hooks: [{ type: "command", command }] }];
|
|
678
|
+
changed = true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (changed) config.hooks = hooks;
|
|
682
|
+
let assetsPresent = true;
|
|
683
|
+
if (action === "status" && present) {
|
|
684
|
+
for (const relative of ["SKILL.md", "scripts/delegate-routing-context.sh", "references/routing-rule.json"]) {
|
|
685
|
+
const asset = await existingPath(join(target, relative));
|
|
686
|
+
if (!asset?.isFile() || asset.isSymbolicLink()) assetsPresent = false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
plans.push({ name, path, target, before, after: changed ? JSON.stringify(config, null, 2) + "\n" : before,
|
|
690
|
+
mode: stat ? stat.mode & 0o777 : 0o600,
|
|
691
|
+
status: action === "disable" ? "disabled" : action === "enable" || (present === 2 && assetsPresent) ? "configured" : present ? "incomplete" : "not-configured" });
|
|
692
|
+
}
|
|
693
|
+
return plans;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function replaceRoutingConfig(plan, before, after) {
|
|
697
|
+
await assertNoSymlinkComponents(plan.path);
|
|
698
|
+
await mkdir(dirname(plan.path), { recursive: true });
|
|
699
|
+
const lockPath = plan.path + ".delegation-layer.lock";
|
|
700
|
+
let lock;
|
|
701
|
+
try {
|
|
702
|
+
lock = await open(lockPath, "wx", 0o600);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (error.code === "EEXIST") throw new Error(`routing configuration is locked: ${lockPath}; verify no other installer is running before removing a stale lock`);
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
708
|
+
const temporary = plan.path + `.delegation-layer-stage-${suffix}`;
|
|
709
|
+
try {
|
|
710
|
+
const stat = await existingPath(plan.path);
|
|
711
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw new Error(`routing configuration changed during installation: ${plan.path}`);
|
|
712
|
+
const current = stat ? await readFile(plan.path, "utf8") : null;
|
|
713
|
+
if (current !== before) throw new Error(`routing configuration changed during installation; retry: ${plan.path}`);
|
|
714
|
+
if (before !== null) {
|
|
715
|
+
const backup = await open(plan.path + `.delegation-layer-backup-${suffix}`, "wx", 0o600);
|
|
716
|
+
try { await backup.writeFile(before); await backup.sync(); } finally { await backup.close(); }
|
|
717
|
+
}
|
|
718
|
+
if (after === null) {
|
|
719
|
+
await unlink(plan.path);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const staged = await open(temporary, "wx", plan.mode);
|
|
723
|
+
try { await staged.writeFile(after); await staged.sync(); } finally { await staged.close(); }
|
|
724
|
+
await rename(temporary, plan.path);
|
|
725
|
+
} finally {
|
|
726
|
+
try {
|
|
727
|
+
try { await unlink(temporary); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
728
|
+
} finally {
|
|
729
|
+
try { await lock.close(); } finally { await unlink(lockPath); }
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function applyRouting(plans) {
|
|
735
|
+
const applied = [];
|
|
736
|
+
try {
|
|
737
|
+
for (const plan of plans) {
|
|
738
|
+
if (!plan.path || plan.before === plan.after) continue;
|
|
739
|
+
await replaceRoutingConfig(plan, plan.before, plan.after);
|
|
740
|
+
applied.push(plan);
|
|
741
|
+
}
|
|
742
|
+
} catch (error) {
|
|
743
|
+
const failures = [];
|
|
744
|
+
for (const plan of applied.reverse()) {
|
|
745
|
+
try { await replaceRoutingConfig(plan, plan.after, plan.before); }
|
|
746
|
+
catch (rollbackError) { failures.push(rollbackError.message); }
|
|
747
|
+
}
|
|
748
|
+
throw new Error(`${error.message}${failures.length ? `; rollback needs attention: ${failures.join("; ")}` : "; earlier routing changes rolled back"}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function printRouting(plan, action) {
|
|
753
|
+
if (!plan.path) {
|
|
754
|
+
console.log(`${plan.name}: unsupported-host for automatic routing; skill-only guidance remains available.`);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
console.log(`${plan.name}: ${plan.status} (${plan.path}).`);
|
|
758
|
+
if (plan.status === "configured") {
|
|
759
|
+
console.log("Activation not verified: start or resume a session and check the native hook output. Installation is not proof of model compliance.");
|
|
760
|
+
if (plan.name === "codex") console.log("Codex: review and trust the hooks using /hooks. New or changed definitions are skipped until trusted.");
|
|
761
|
+
}
|
|
762
|
+
if (action === "disable") console.log("Only this installer's matching hooks were removed; plugin hooks are managed by the host plugin manager.");
|
|
763
|
+
}
|
|
764
|
+
|
|
579
765
|
async function validateSourceTree() {
|
|
580
766
|
const files = await skillFiles(sourceDirectory);
|
|
581
767
|
if (!files.includes("SKILL.md")) {
|
|
@@ -595,6 +781,23 @@ if (argumentsList[0] === "install-cli") {
|
|
|
595
781
|
console.error(`Error: ${error.message}`);
|
|
596
782
|
process.exitCode = 1;
|
|
597
783
|
}
|
|
784
|
+
} else if (argumentsList[0] === "routing") {
|
|
785
|
+
const action = argumentsList[1];
|
|
786
|
+
const options = parseInstallArguments(argumentsList.slice(2));
|
|
787
|
+
if (!["status", "disable"].includes(action)) {
|
|
788
|
+
usage("routing requires status or disable");
|
|
789
|
+
} else if (options) {
|
|
790
|
+
try {
|
|
791
|
+
if (options.routing || options.force) throw new Error("routing status/disable do not accept --routing or --force");
|
|
792
|
+
const selected = await chooseInstallOptions({ ...options, yes: true, harnessNames: options.harnessNames ?? ["claude", "codex"] });
|
|
793
|
+
const plans = await prepareRouting(selected, action);
|
|
794
|
+
if (action === "disable") await applyRouting(plans);
|
|
795
|
+
for (const plan of plans) printRouting(plan, action);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error(`Error: ${error.message}`);
|
|
798
|
+
process.exitCode = 1;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
598
801
|
} else if (argumentsList[0] === "install") {
|
|
599
802
|
const options = parseInstallArguments(argumentsList.slice(1));
|
|
600
803
|
if (options) {
|
|
@@ -603,12 +806,16 @@ if (argumentsList[0] === "install-cli") {
|
|
|
603
806
|
if (!selected) {
|
|
604
807
|
console.log("Installation cancelled.");
|
|
605
808
|
} else {
|
|
809
|
+
const routingPlans = selected.routing === "default" ? await prepareRouting(selected, "enable") : [];
|
|
606
810
|
const targets = selected.harnessNames.map((name) => targetFor(harnessByName.get(name), selected.scope, selected.cwd, selected.home));
|
|
607
811
|
const uniqueTargets = [...new Set(targets)];
|
|
608
812
|
console.log(`Installing for ${selected.scope}: ${uniqueTargets.join(", ")}`);
|
|
609
813
|
for (const target of uniqueTargets) {
|
|
610
814
|
await install({ force: selected.force, target });
|
|
611
815
|
}
|
|
816
|
+
await applyRouting(routingPlans);
|
|
817
|
+
for (const plan of routingPlans) printRouting(plan, "enable");
|
|
818
|
+
if (selected.routing === "skill-only") console.log("Skill-only installation; existing routing settings were not changed.");
|
|
612
819
|
}
|
|
613
820
|
} catch (error) {
|
|
614
821
|
console.error(`Error: ${error.message}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-integration
|
|
3
|
-
description:
|
|
3
|
+
description: Coordinate external agent, model, and separate harness tasks through Delegate, including research, implementation, tests, reviews, consultations, comparisons, and debates. Use before choosing an external integration; ordinary native subagents and questions about tools remain local.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Delegation Layer
|
|
@@ -56,13 +56,27 @@ tasks may proceed; the failed attempt must remain reported as failed or blocked.
|
|
|
56
56
|
finite budget. Keep the state root outside the workspace. Reuse a supplied
|
|
57
57
|
`DELEGATE_ROOT` for all providers and tasks; do not invent per-provider or
|
|
58
58
|
per-task state roots. Task IDs separate work within that root.
|
|
59
|
-
4. Use `--json` and save
|
|
59
|
+
4. Use `--json` and save each response separately with its requested destination,
|
|
60
|
+
model, effort, workspace, and returned `task_id`. Never merge concurrent
|
|
61
|
+
responses into an unlabeled output stream. Read `admission`, `liveness`,
|
|
60
62
|
`publication`, `status`, `outcome`, `continuation`, `task_record`, `failure`,
|
|
61
63
|
and `error` as separate fields.
|
|
62
64
|
Delegate owns the supervisor lifecycle. Never install, configure, start,
|
|
63
65
|
inspect, or repair Pueue, and never poll a task unless the response says its
|
|
64
66
|
`task_record` is `created`.
|
|
65
|
-
5. Observe
|
|
67
|
+
5. Observe tasks only through Delegate `status`, `collect`, or `events`.
|
|
68
|
+
Do not run `ps`, `pgrep`, process-tree searches, or supervisor commands to
|
|
69
|
+
monitor delegated work. Pending output is normal startup or work in progress;
|
|
70
|
+
empty `pending_requests` means keep collecting, not diagnose the environment.
|
|
71
|
+
Environment diagnostics require a separately requested troubleshooting task.
|
|
72
|
+
Keep collection in the
|
|
73
|
+
foreground until all requested tasks reach a terminal result or an explicit
|
|
74
|
+
blocker. Do not end a headless turn promising a background notification.
|
|
75
|
+
Pending publication (exit 3) means keep waiting, not success. When the JSON
|
|
76
|
+
includes an outstanding `interaction.pending_requests` batch, follow
|
|
77
|
+
[active-task interaction](references/interaction.md) before waiting again. Only detach
|
|
78
|
+
when the user explicitly requests it, retaining the exact task IDs.
|
|
79
|
+
Collection may recover the
|
|
66
80
|
saved supervisor only to observe an already-requested budget stop; it never
|
|
67
81
|
launches a provider or retries a task.
|
|
68
82
|
6. If a task is timed out and its JSON says `continuation.resumable: true`, use
|
|
@@ -114,7 +128,7 @@ normal task. For an explicitly requested readiness check, see
|
|
|
114
128
|
[preflight guidance](references/command-reference.md#checking-readiness).
|
|
115
129
|
|
|
116
130
|
The catalog and `capabilities` response expose supported permission modes,
|
|
117
|
-
request options, continuation, and the read-only mechanism. A read-only
|
|
131
|
+
request options, continuation, native interaction, and the read-only mechanism. A read-only
|
|
118
132
|
capability with `containment: "provider-owned"` selects the provider's native
|
|
119
133
|
behavior but is not an independent delegate-owned sandbox. Static discovery
|
|
120
134
|
does not prove authentication or live acceptance; use the dispatch or live
|
|
@@ -129,28 +143,48 @@ name belongs to CCR; clarify only if discovery leaves the destination ambiguous.
|
|
|
129
143
|
|
|
130
144
|
Keep provider defaults when no model or effort was requested and neither is required. When an explicit
|
|
131
145
|
choice is needed, read [model and effort guidance](references/command-reference.md#choosing-models-and-effort).
|
|
132
|
-
Pass requested
|
|
133
|
-
fields; discovery is advisory, never an allowlist.
|
|
146
|
+
Pass exact requested model IDs and effort values unchanged through Delegate's
|
|
147
|
+
public `--model` and `--effort` fields; discovery is advisory, never an allowlist.
|
|
148
|
+
For a natural-language model name, resolve one exact ID through discovery. If
|
|
149
|
+
it remains ambiguous or discovery is unavailable, ask for the exact model ID;
|
|
150
|
+
do not turn a display name into a guessed alias. An explicit ID needs no such
|
|
151
|
+
clarification just because it is absent from discovery.
|
|
152
|
+
|
|
153
|
+
For planning, consultation, debate, or other external work that may need
|
|
154
|
+
answers or guidance while it runs, read [active-task interaction](references/interaction.md)
|
|
155
|
+
and use `--interactive` when supported. Answer known facts yourself; ask the
|
|
156
|
+
user for missing preferences, authorization, or scope. Keep ordinary native
|
|
157
|
+
subagents available under the same-harness rule above.
|
|
134
158
|
|
|
135
159
|
### 3. Prepare separate paths and a finite brief
|
|
136
160
|
|
|
137
161
|
Use an existing task brief when one is supplied. Otherwise create a bounded
|
|
138
162
|
brief file that contains the requested work and its acceptance criteria. Keep
|
|
139
163
|
the state root outside the workspace and provider-writable trees (which can
|
|
140
|
-
include the system temporary directory). Use `DELEGATE_ROOT` when supplied
|
|
164
|
+
include the system temporary directory). Use `DELEGATE_ROOT` when supplied.
|
|
141
165
|
|
|
142
166
|
Run path preparation and dispatch in **one shell/tool invocation**, as shown
|
|
143
167
|
below. Shell variables may disappear between tool calls. For later commands,
|
|
144
168
|
use the actual saved absolute paths or assign them again in that invocation;
|
|
145
169
|
never pass an empty `--root`, `--brief`, or `--cwd`.
|
|
146
170
|
|
|
147
|
-
|
|
171
|
+
Write the brief as the **worker’s own assignment**, not a copy of the
|
|
172
|
+
orchestration request. For “consult another agent about X”, tell the worker
|
|
173
|
+
“Investigate X and return your findings; you are the assigned worker, do not
|
|
174
|
+
delegate this assignment.” Keep dispatching, answering questions, and
|
|
175
|
+
collecting results in the lead session unless recursive delegation was
|
|
176
|
+
explicitly requested.
|
|
177
|
+
|
|
178
|
+
Replace the example brief below with that bounded worker assignment. Do not place the state
|
|
148
179
|
root inside the workspace or place secrets in either path’s task data. Keep it
|
|
149
180
|
outside provider configuration and runtime directories as well. If delegate
|
|
150
181
|
rejects a caller-supplied state root, report the placement error rather than
|
|
151
|
-
silently replacing that root.
|
|
152
|
-
|
|
153
|
-
root
|
|
182
|
+
silently replacing that root. For every task command, use
|
|
183
|
+
`--root "${DELEGATE_ROOT:-$HOME/delegation-state}"` as shown below. This reads the supplied root in each shell call and uses the
|
|
184
|
+
same default when none was supplied; no root setup or new directory choice is
|
|
185
|
+
needed. If the user explicitly requests another root, substitute that exact
|
|
186
|
+
absolute path consistently. Do not change provider settings or switch roots
|
|
187
|
+
when waiting, replying, steering, or continuing.
|
|
154
188
|
|
|
155
189
|
### 4. Dispatch one bounded turn
|
|
156
190
|
|
|
@@ -159,19 +193,22 @@ For a named harness or provider resolved in step 2, replace `--auto` with `--pro
|
|
|
159
193
|
combine the two:
|
|
160
194
|
|
|
161
195
|
```sh
|
|
162
|
-
STATE_ROOT="${DELEGATE_ROOT:-$HOME/delegation-state}"
|
|
163
196
|
WORKSPACE="$PWD"
|
|
164
197
|
BRIEF="$(mktemp)"
|
|
198
|
+
RESPONSE="$(mktemp)"
|
|
165
199
|
cat >"$BRIEF" <<'EOF'
|
|
166
200
|
Describe the delegated task, the files or behavior to change, and the checks
|
|
167
201
|
that must pass. Keep the response concise and report validation evidence.
|
|
168
202
|
EOF
|
|
169
|
-
delegate --root "$
|
|
203
|
+
delegate --root "${DELEGATE_ROOT:-$HOME/delegation-state}" dispatch --auto \
|
|
170
204
|
--brief "$BRIEF" \
|
|
171
205
|
--cwd "$WORKSPACE" \
|
|
172
206
|
--permission read-only \
|
|
173
207
|
--budget 30m \
|
|
174
|
-
--json
|
|
208
|
+
--json >"$RESPONSE"
|
|
209
|
+
dispatch_exit=$?
|
|
210
|
+
cat "$RESPONSE"
|
|
211
|
+
printf '\nDispatch exit: %s; saved response: %s\n' "$dispatch_exit" "$RESPONSE"
|
|
175
212
|
```
|
|
176
213
|
|
|
177
214
|
For an authorized code change, use `--permission workspace-write`; this requests
|
|
@@ -185,7 +222,16 @@ For independent work, the model may run several complete `dispatch` commands
|
|
|
185
222
|
concurrently. Keep each task's brief, workspace, permission, and task ID
|
|
186
223
|
independent. Do not configure Pueue or wait for one task before dispatching the
|
|
187
224
|
next solely to serialize ordinary work. Inspect each returned task separately
|
|
188
|
-
with `status` and `collect`.
|
|
225
|
+
with `status` and `collect`. Use independent tool calls or separate response
|
|
226
|
+
files, never background commands writing to the same stdout. Read
|
|
227
|
+
[parallel reviews and debates](references/collaboration.md) when coordinating
|
|
228
|
+
multiple participants or challenge rounds.
|
|
229
|
+
|
|
230
|
+
Capture the response and exit status in the **original** dispatch invocation,
|
|
231
|
+
as above. Read the saved response if you need it again. Never repeat dispatch
|
|
232
|
+
to obtain its exit code, recover output, or confirm that it worked: each call
|
|
233
|
+
can create new paid work. Once a response says `task_record: created`, save its
|
|
234
|
+
`task_id` and switch to observation. A missing exit code does not undo admission.
|
|
189
235
|
|
|
190
236
|
Save the exact `task_id` from the JSON response. Apply this decision separately
|
|
191
237
|
for **each attempt**, including requests issued together:
|
|
@@ -205,16 +251,20 @@ never retry that uncertain ID or run Pueue commands.
|
|
|
205
251
|
|
|
206
252
|
### 5. Observe and collect
|
|
207
253
|
|
|
208
|
-
Use the same state root and task ID
|
|
254
|
+
Use the same state root and task ID. Each command below resolves the root
|
|
255
|
+
in its own shell call, so it does not depend on a previous shell variable:
|
|
209
256
|
|
|
210
257
|
```sh
|
|
211
|
-
delegate --root "$
|
|
212
|
-
delegate --root "$
|
|
258
|
+
delegate --root "${DELEGATE_ROOT:-$HOME/delegation-state}" status TASK_ID --json
|
|
259
|
+
delegate --root "${DELEGATE_ROOT:-$HOME/delegation-state}" collect TASK_ID --watch 5s --json
|
|
213
260
|
```
|
|
214
261
|
|
|
215
262
|
Check `status` first: if it is `timed_out`, follow step 6 even when publication
|
|
216
|
-
is pending and no outcome is present.
|
|
217
|
-
|
|
263
|
+
is pending and no outcome is present. If `interaction.pending_requests` is
|
|
264
|
+
nonempty and the interaction is not closed, answer the questions using
|
|
265
|
+
[active-task interaction](references/interaction.md) before collecting again.
|
|
266
|
+
Otherwise, if `collect` exits with code `3` because publication is still
|
|
267
|
+
pending, wait a short interval and run
|
|
218
268
|
`collect` again. Do not dispatch again. Continue until
|
|
219
269
|
the response has a terminal `outcome` or a bounded operational error that
|
|
220
270
|
requires the caller’s attention.
|