delegation-layer 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Multi-provider AI agent delegation CLI and Agent Skill for running bounded coding tasks across provider CLIs.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
3
  "displayName": "Delegation Layer",
4
- "version": "0.3.1",
4
+ "version": "0.4.0",
5
5
  "description": "Consult external AI harnesses and CCR-configured models for reviews, research, implementation, and debate through the resumable Delegate CLI.",
6
6
  "author": {
7
7
  "name": "Hesham Karam",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delegation-layer",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Consult external AI harnesses and CCR-configured models for reviews, research, implementation, and debate through the resumable Delegate CLI.",
5
5
  "author": {
6
6
  "name": "Hesham Karam",
@@ -295,6 +295,32 @@ Missing task files and corrupt evidence are distinct; see
295
295
  [scheduled result handling](result-handling.md#scheduled-execution-results)
296
296
  for freshness, no-result states, response bounds, exit codes and recovery.
297
297
 
298
+ ## Supervisor owner control
299
+
300
+ ```sh
301
+ delegate supervisor stop-owner --json
302
+ ```
303
+
304
+ Use only when the installed build advertises this command. Preserve the explicit
305
+ root for work bound to a custom or historical root. This command contacts one
306
+ existing infrastructure owner; it does not initialize, repair or start one.
307
+ It leaves Pueue and admitted tasks running. Startup, accepted operations and
308
+ unresolved cleanup can make the owner busy.
309
+
310
+ The `supervisor_owner` response separates `state` (`serving_released`,
311
+ `not_initialized`, `busy` or `unknown`) from `process_closure` (`closed`,
312
+ `pending` or `unknown`). A released owner can remain alive as the sole waiter for
313
+ its daemon; serving release is not proof of process exit or root quiescence.
314
+ The optional `process` value (`pid`, `start_token`) identifies the validated
315
+ selected `startup`; it does not establish liveness. It is omitted when no owner
316
+ was selected or validated. Observe that exact incarnation after daemon shutdown;
317
+ a new root-selected stop can address a newer owner.
318
+ An absent root reports `not_initialized` with unknown process closure.
319
+ Exit `0` reports the observed release or uninitialized state, `3` reports busy,
320
+ and `1` reports a control or observation fault. Preserve an uncertain result;
321
+ the command does not resend an uncertain stop request. Human output exposes
322
+ the same separate observations.
323
+
298
324
  ## Scheduler runtime
299
325
 
300
326
  Check `delegate help` for the installed build's command surface. Use `serve`,
@@ -487,9 +513,11 @@ Inspection-backed admission can return `status: "ready"` with
487
513
  `verification: "runtime"` after its probe. Managed runtime-only admission
488
514
  keeps the catalog projection; its supervised worker checks compatibility before
489
515
  provider execution. Collect the created task to establish the result.
490
- Optional protocol members `version` and `executable_sha256` are diagnostics,
491
- not launch authority. Live acceptance remains separate (`not_run`, `passed`,
492
- or `blocked`); a blocked prerequisite is unresolved.
516
+ Provider versions are optional diagnostics. Historical executable-digest members
517
+ remain readable as recorded data. Do not depend on these fields or calculate new
518
+ executable fingerprints for diagnostics or admission. Required behavior and the
519
+ selected installed executable govern compatibility. Live acceptance remains separate (`not_run`,
520
+ `passed`, or `blocked`); a blocked prerequisite is unresolved.
493
521
 
494
522
  ## Choosing models and effort
495
523
 
@@ -5,11 +5,11 @@ case "${1:-}" in
5
5
  SessionStart|SubagentStart) event=$1 ;;
6
6
  *) printf '%s\n' 'Expected SessionStart or SubagentStart.' >&2; exit 2 ;;
7
7
  esac
8
- script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
8
+ script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
9
9
  rule_file="$script_dir/../references/routing-rule.json"
10
10
  # Read before emitting anything so a missing asset cannot produce partial JSON.
11
11
  rule=$(cat "$rule_file")
12
- skill_file="$(CDPATH= cd -- "$script_dir/.." && pwd)/SKILL.md"
12
+ skill_file="$(CDPATH='' cd -- "$script_dir/.." && pwd)/SKILL.md"
13
13
  DELEGATE_ROUTING_EVENT="$event" DELEGATE_ROUTING_RULE="$rule" DELEGATE_ROUTING_SKILL="$skill_file" awk '
14
14
  BEGIN {
15
15
  # Paths are task data, not shell code. Escape every JSON control character.
@@ -5,7 +5,7 @@ import { assertNoSymlinkComponents, existingPath } from "./filesystem.mjs";
5
5
  import { countRoutingEvents, createRoutingPlan, routingPath, validateHookConfig } from "./routing-rules.mjs";
6
6
  import { targetFor } from "./selection.mjs";
7
7
 
8
- export async function prepareRouting(selected, action, { environment, platform, sourceDirectory }) {
8
+ export async function prepareRouting(selected, action, { environment, platform }) {
9
9
  const plans = [];
10
10
  for (const name of new Set(selected.harnessNames)) {
11
11
  const path = routingPath(name, {
@@ -55,85 +55,118 @@ export async function prepareRouting(selected, action, { environment, platform,
55
55
  return plans;
56
56
  }
57
57
 
58
- async function replaceRoutingConfig(plan, before, after) {
59
- await assertNoSymlinkComponents(plan.path);
60
- await mkdir(dirname(plan.path), { recursive: true });
58
+ const routingFileOperations = Object.freeze({ open, mkdir, readFile, rename, unlink, existingPath, assertNoSymlinkComponents });
59
+
60
+ function errorMessage(error) {
61
+ return error instanceof Error ? error.message : String(error);
62
+ }
63
+
64
+ function combinedFailure(failures) {
65
+ if (failures.length === 1) return failures[0];
66
+ return new AggregateError(failures, failures.map(errorMessage).join("; "), { cause: failures[0] });
67
+ }
68
+
69
+ async function writeAndClose(file, contents) {
70
+ const failures = [];
71
+ try {
72
+ await file.writeFile(contents);
73
+ await file.sync();
74
+ } catch (error) {
75
+ failures.push(error);
76
+ }
77
+ try {
78
+ await file.close();
79
+ } catch (error) {
80
+ failures.push(error);
81
+ }
82
+ if (failures.length) throw combinedFailure(failures);
83
+ }
84
+
85
+ async function cleanupReplacement(lock, temporary, lockPath, operations) {
86
+ const failures = [];
87
+ try {
88
+ await operations.unlink(temporary);
89
+ } catch (error) {
90
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) failures.push(error);
91
+ }
92
+ try {
93
+ await lock.close();
94
+ } catch (error) {
95
+ failures.push(error);
96
+ }
97
+ try {
98
+ await operations.unlink(lockPath);
99
+ } catch (error) {
100
+ failures.push(error);
101
+ }
102
+ return failures;
103
+ }
104
+
105
+ async function replaceRoutingConfig(plan, before, after, operations, onCommitted) {
106
+ await operations.assertNoSymlinkComponents(plan.path);
107
+ await operations.mkdir(dirname(plan.path), { recursive: true });
61
108
  const lockPath = plan.path + ".delegation-layer.lock";
62
109
  let lock;
63
110
  try {
64
- lock = await open(lockPath, "wx", 0o600);
111
+ lock = await operations.open(lockPath, "wx", 0o600);
65
112
  } catch (error) {
66
113
  if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
67
- throw new Error(`routing configuration is locked: ${lockPath}; verify no other installer is running before removing a stale lock`);
114
+ throw new Error(`routing configuration is locked: ${lockPath}; verify no other installer is running before removing a stale lock`, { cause: error });
68
115
  }
69
116
  throw error;
70
117
  }
71
118
  const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
72
119
  const temporary = plan.path + `.delegation-layer-stage-${suffix}`;
120
+ const failures = [];
73
121
  try {
74
- const stat = await existingPath(plan.path);
122
+ const stat = await operations.existingPath(plan.path);
75
123
  if (stat && (!stat.isFile() || stat.isSymbolicLink())) {
76
124
  throw new Error(`routing configuration changed during installation: ${plan.path}`);
77
125
  }
78
- const current = stat ? await readFile(plan.path, "utf8") : null;
126
+ const current = stat ? await operations.readFile(plan.path, "utf8") : null;
79
127
  if (current !== before) throw new Error(`routing configuration changed during installation; retry: ${plan.path}`);
80
128
  if (before !== null) {
81
- const backup = await open(plan.path + `.delegation-layer-backup-${suffix}`, "wx", 0o600);
82
- try {
83
- await backup.writeFile(before);
84
- await backup.sync();
85
- } finally {
86
- await backup.close();
87
- }
129
+ const backup = await operations.open(plan.path + `.delegation-layer-backup-${suffix}`, "wx", 0o600);
130
+ await writeAndClose(backup, before);
88
131
  }
89
132
  if (after === null) {
90
- await unlink(plan.path);
91
- return;
92
- }
93
- const staged = await open(temporary, "wx", plan.mode);
94
- try {
95
- await staged.writeFile(after);
96
- await staged.sync();
97
- } finally {
98
- await staged.close();
99
- }
100
- await rename(temporary, plan.path);
101
- } finally {
102
- try {
103
- try {
104
- await unlink(temporary);
105
- } catch (error) {
106
- if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) throw error;
107
- }
108
- } finally {
109
- try {
110
- await lock.close();
111
- } finally {
112
- await unlink(lockPath);
113
- }
133
+ await operations.unlink(plan.path);
134
+ } else {
135
+ const staged = await operations.open(temporary, "wx", plan.mode);
136
+ await writeAndClose(staged, after);
137
+ await operations.rename(temporary, plan.path);
114
138
  }
139
+ onCommitted?.();
140
+ } catch (error) {
141
+ failures.push(error);
115
142
  }
143
+ failures.push(...await cleanupReplacement(lock, temporary, lockPath, operations));
144
+ if (failures.length) throw combinedFailure(failures);
116
145
  }
117
146
 
118
- export async function applyRouting(plans) {
147
+ export async function applyRouting(plans, fileOperations = {}) {
148
+ const operations = { ...routingFileOperations, ...fileOperations };
149
+ // This transaction owns committed mutations, including commits followed by a cleanup failure.
119
150
  const applied = [];
120
151
  try {
121
152
  for (const plan of plans) {
122
153
  if (!plan.path || plan.before === plan.after) continue;
123
- await replaceRoutingConfig(plan, plan.before, plan.after);
124
- applied.push(plan);
154
+ await replaceRoutingConfig(plan, plan.before, plan.after, operations, () => applied.push(plan));
125
155
  }
126
156
  } catch (error) {
127
157
  const failures = [];
128
158
  for (const plan of applied.reverse()) {
129
159
  try {
130
- await replaceRoutingConfig(plan, plan.after, plan.before);
160
+ await replaceRoutingConfig(plan, plan.after, plan.before, operations);
131
161
  } catch (rollbackError) {
132
- failures.push(rollbackError instanceof Error ? rollbackError.message : String(rollbackError));
162
+ failures.push(rollbackError);
133
163
  }
134
164
  }
135
- const reason = error instanceof Error ? error.message : String(error);
136
- throw new Error(`${reason}${failures.length ? `; rollback needs attention: ${failures.join("; ")}` : "; earlier routing changes rolled back"}`);
165
+ const reason = errorMessage(error);
166
+ if (failures.length) {
167
+ throw new AggregateError([error, ...failures], `${reason}; rollback needs attention: ${failures.map(errorMessage).join("; ")}`, { cause: error });
168
+ }
169
+ throw new Error(`${reason}; earlier routing changes rolled back`, { cause: error });
137
170
  }
138
171
  }
139
172
 
@@ -295,6 +295,32 @@ Missing task files and corrupt evidence are distinct; see
295
295
  [scheduled result handling](result-handling.md#scheduled-execution-results)
296
296
  for freshness, no-result states, response bounds, exit codes and recovery.
297
297
 
298
+ ## Supervisor owner control
299
+
300
+ ```sh
301
+ delegate supervisor stop-owner --json
302
+ ```
303
+
304
+ Use only when the installed build advertises this command. Preserve the explicit
305
+ root for work bound to a custom or historical root. This command contacts one
306
+ existing infrastructure owner; it does not initialize, repair or start one.
307
+ It leaves Pueue and admitted tasks running. Startup, accepted operations and
308
+ unresolved cleanup can make the owner busy.
309
+
310
+ The `supervisor_owner` response separates `state` (`serving_released`,
311
+ `not_initialized`, `busy` or `unknown`) from `process_closure` (`closed`,
312
+ `pending` or `unknown`). A released owner can remain alive as the sole waiter for
313
+ its daemon; serving release is not proof of process exit or root quiescence.
314
+ The optional `process` value (`pid`, `start_token`) identifies the validated
315
+ selected `startup`; it does not establish liveness. It is omitted when no owner
316
+ was selected or validated. Observe that exact incarnation after daemon shutdown;
317
+ a new root-selected stop can address a newer owner.
318
+ An absent root reports `not_initialized` with unknown process closure.
319
+ Exit `0` reports the observed release or uninitialized state, `3` reports busy,
320
+ and `1` reports a control or observation fault. Preserve an uncertain result;
321
+ the command does not resend an uncertain stop request. Human output exposes
322
+ the same separate observations.
323
+
298
324
  ## Scheduler runtime
299
325
 
300
326
  Check `delegate help` for the installed build's command surface. Use `serve`,
@@ -487,9 +513,11 @@ Inspection-backed admission can return `status: "ready"` with
487
513
  `verification: "runtime"` after its probe. Managed runtime-only admission
488
514
  keeps the catalog projection; its supervised worker checks compatibility before
489
515
  provider execution. Collect the created task to establish the result.
490
- Optional protocol members `version` and `executable_sha256` are diagnostics,
491
- not launch authority. Live acceptance remains separate (`not_run`, `passed`,
492
- or `blocked`); a blocked prerequisite is unresolved.
516
+ Provider versions are optional diagnostics. Historical executable-digest members
517
+ remain readable as recorded data. Do not depend on these fields or calculate new
518
+ executable fingerprints for diagnostics or admission. Required behavior and the
519
+ selected installed executable govern compatibility. Live acceptance remains separate (`not_run`,
520
+ `passed`, or `blocked`); a blocked prerequisite is unresolved.
493
521
 
494
522
  ## Choosing models and effort
495
523
 
@@ -5,11 +5,11 @@ case "${1:-}" in
5
5
  SessionStart|SubagentStart) event=$1 ;;
6
6
  *) printf '%s\n' 'Expected SessionStart or SubagentStart.' >&2; exit 2 ;;
7
7
  esac
8
- script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
8
+ script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
9
9
  rule_file="$script_dir/../references/routing-rule.json"
10
10
  # Read before emitting anything so a missing asset cannot produce partial JSON.
11
11
  rule=$(cat "$rule_file")
12
- skill_file="$(CDPATH= cd -- "$script_dir/.." && pwd)/SKILL.md"
12
+ skill_file="$(CDPATH='' cd -- "$script_dir/.." && pwd)/SKILL.md"
13
13
  DELEGATE_ROUTING_EVENT="$event" DELEGATE_ROUTING_RULE="$rule" DELEGATE_ROUTING_SKILL="$skill_file" awk '
14
14
  BEGIN {
15
15
  # Paths are task data, not shell code. Escape every JSON control character.