@detergent-software/conductor 1.0.0-dev.17 → 1.0.0-dev.19
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/CHANGELOG.md +20 -0
- package/README.md +31 -2
- package/build/broker-cli.js +125 -0
- package/build/broker-cli.js.map +1 -0
- package/build/broker.js +154 -0
- package/build/broker.js.map +1 -0
- package/build/cli.js +39 -2
- package/build/cli.js.map +1 -1
- package/build/daemon.js +2 -0
- package/build/daemon.js.map +1 -1
- package/build/manager.js +6 -0
- package/build/manager.js.map +1 -1
- package/build/paths.js +9 -1
- package/build/paths.js.map +1 -1
- package/build/profile.js +13 -1
- package/build/profile.js.map +1 -1
- package/build/registry.js +14 -9
- package/build/registry.js.map +1 -1
- package/build/server.js +38 -0
- package/build/server.js.map +1 -1
- package/build/store.js +46 -0
- package/build/store.js.map +1 -1
- package/build/wait.js +1 -7
- package/build/wait.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
|
|
4
4
|
|
|
5
|
+
## [1.0.0-dev.19](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.18&targetVersion=GTconductor-v1.0.0-dev.19) (2026-10-11)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **conductor:** show the broker's locks in conduct status (ATK-372) ([77a3f54](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/77a3f547e15a653a9e4dbc8a1308e5118ac5492d))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **conductor:** keep the daemon's reason when GET /locks fails (ATK-372) ([79f3e69](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/79f3e6979c2475612ff6b4a8780cb1dd01fa9718))
|
|
14
|
+
|
|
15
|
+
## [1.0.0-dev.18](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.17&targetVersion=GTconductor-v1.0.0-dev.18) (2026-10-11)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **conductor:** port broker and shared locks across projects (ATK-318) ([90a46d0](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/90a46d0827dd805668fc98c11b4082487f9bd806))
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **conductor:** review fixes for the port broker (ATK-318) ([db0bf0d](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/db0bf0de8d0404d155ad2d95ac32923ab5d0fb7f))
|
|
24
|
+
|
|
5
25
|
## [1.0.0-dev.17](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.16&targetVersion=GTconductor-v1.0.0-dev.17) (2026-10-11)
|
|
6
26
|
|
|
7
27
|
### Features
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Conductor
|
|
2
2
|
|
|
3
|
-
`@detergent-software/conductor`: the local daemon that runs the work loop for every conducted project on this machine, and its `conduct` CLI. The design, decisions and build order are in the Linear document *Conductor: design note* (project *Conductor daemon*). This package has build step 1, the skeleton (ATK-308), step 2, the session manager (ATK-309), step 3, the watchers and `conduct wait` (ATK-310), the daemon's side of step 4, worker protocol v1's `conduct note` (ATK-311), step 5, the inbox with `conduct approve` / `revise` (ATK-312), step 6, the notifier (ATK-313), step 9, the conductor mod for Claude Code in `mod/` (ATK-265), step 8, the scheduler, settings and rules (ATK-315),
|
|
3
|
+
`@detergent-software/conductor`: the local daemon that runs the work loop for every conducted project on this machine, and its `conduct` CLI. The design, decisions and build order are in the Linear document *Conductor: design note* (project *Conductor daemon*). This package has build step 1, the skeleton (ATK-308), step 2, the session manager (ATK-309), step 3, the watchers and `conduct wait` (ATK-310), the daemon's side of step 4, worker protocol v1's `conduct note` (ATK-311), step 5, the inbox with `conduct approve` / `revise` (ATK-312), step 6, the notifier (ATK-313), step 9, the conductor mod for Claude Code in `mod/` (ATK-265), step 8, the scheduler, settings and rules (ATK-315), the daemon's side of step 7, the console (ATK-314), and step 12, the port broker (ATK-318).
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
conduct serve run the daemon in this process (one per machine)
|
|
9
9
|
conduct start start the daemon detached and wait until it answers (see Console)
|
|
10
10
|
conduct stop stop the running daemon
|
|
11
|
-
conduct status [--json] the daemon's pid, port, projects and
|
|
11
|
+
conduct status [--json] the daemon's pid, port, projects, workers and held locks
|
|
12
12
|
conduct project add <path> register the project whose checkout holds .claude/conductor.json
|
|
13
13
|
conduct project list [--json] the registered projects
|
|
14
14
|
conduct launch <item> start a worker: worktree, background session, tab
|
|
@@ -48,6 +48,10 @@ conduct rules test <file> [--json]
|
|
|
48
48
|
conduct note <item> <file> post a phase note on the item and record its phase (see Notes)
|
|
49
49
|
[--worker <worker>]
|
|
50
50
|
conduct wait <condition> wait for an outside event (see Waits)
|
|
51
|
+
conduct ports [<path>] [--prefer <slot>] [--json]
|
|
52
|
+
a checkout's dev ports from the port broker, or every worktree's (see Broker)
|
|
53
|
+
conduct lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
|
|
54
|
+
a lock every project shares, such as chrome (see Broker)
|
|
51
55
|
conduct events [--since <id>] [--follow]
|
|
52
56
|
the event log, one JSON event per line (see API)
|
|
53
57
|
conduct install-startup start the daemon at logon (Windows Task Scheduler)
|
|
@@ -254,6 +258,29 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
|
|
|
254
258
|
|
|
255
259
|
**Watchers.** One per API: Linear (GraphQL with `LINEAR_API_KEY` or the `atk-conductor-linear` credential, ported from conduct-atk), Azure DevOps (REST with the `az login` token), GitHub (`gh api`), npm (the registry's dist-tags, no cache) and `/health`. Time waits are the session manager's sweep. An API is asked nothing while no wait needs it. Otherwise it's asked once per `watchers.<api>.intervalSeconds`, within `perMinute` requests in any rolling minute shared by every project. Each pass covers all of that API's waits in as few requests as it can; Linear's is one request, filtered on `updatedAt` (with a 2-minute overlap) after the first. A pass that runs out of budget stops, and the next one starts a wait further on. A failing API waits twice as long each time (up to 10 minutes) and logs `watcher.failed` once, then `watcher.recovered`. A wait that can't be checked logs `wait.check-failed` once and stays open. Every `watchers.statsMinutes` (10) in which anything waited, a `watcher.stats` event records each API's passes, requests, budget skips, failures and waits.
|
|
256
260
|
|
|
261
|
+
## Broker
|
|
262
|
+
|
|
263
|
+
The broker shares what every project on the machine uses (ATK-318).
|
|
264
|
+
|
|
265
|
+
**Ports.** Each profile declares its dev servers' base ports by name, such as ATK's `"ports": { "web": 3000, "api": 7071, "maxSlot": 50 }` or `{ "web": 3000 }`.
|
|
266
|
+
- A main checkout serves on the base ports (slot 0).
|
|
267
|
+
- Each worktree gets a slot from 1 to `maxSlot` (default 50) and serves on base + slot. `conduct ports <path>` gives it the first slot whose ports no other checkout of any registered project has and nothing listens on, `--prefer` first. It's the same answer each time after that.
|
|
268
|
+
- A retired worker's worktree frees its slot, and so does a worktree whose directory is gone (at the next assignment).
|
|
269
|
+
- Ports are compared, not slots, so projects with different bases stay apart too.
|
|
270
|
+
- A project's dev scripts read the assignment with `--json`, as ATK's `scripts/dev-ports.mjs assign` does. They keep their own scheme when it exits 3: no daemon, an older one, or a checkout in no registered project.
|
|
271
|
+
- The broker knows only the slots it gave out. A slot a script picked on its own while no daemon answered stays unknown until that worktree asks the broker; until then only the listening probe keeps the slot clear. So `--prefer` is the slot the script would pick itself (`dev-ports.mjs` passes the checkout's own slot or its own free-slot scan's). The preferred slot is taken without the listening probe, because the checkout's own servers may be what listens there.
|
|
272
|
+
- A stored assignment follows its project's profile: when the base ports or `maxSlot` change, the worktree moves, its old slot first.
|
|
273
|
+
|
|
274
|
+
**Locks.** `conduct lock acquire chrome` takes the lock every project's verify runs share. It answers the work-issue helper's JSON (`acquired`, `heldBy`, `since`, `refreshed`; `released` for a release; `verdict` for a status).
|
|
275
|
+
- The owner is `--owner`, else the item of the worker whose worktree this is.
|
|
276
|
+
- Acquiring again refreshes it. 30 minutes after its holder last acquired it, another owner may take it over.
|
|
277
|
+
- `--wait <minutes>` asks again every 15 s until then.
|
|
278
|
+
- Retiring a worker releases the locks its item holds (a probe's retire doesn't).
|
|
279
|
+
- `conduct status` lists the locks held, each with its holder, its last acquire and whether it is stale.
|
|
280
|
+
- Exit 3 when no daemon answers.
|
|
281
|
+
|
|
282
|
+
Events: `ports.assigned`, `ports.freed`, `lock.acquired`, `lock.released`, `lock.taken-over`.
|
|
283
|
+
|
|
257
284
|
## History
|
|
258
285
|
|
|
259
286
|
`conduct history` and `conduct retro` read a window of the log and say what happened in it, so a review of a conducted run needs no transcripts. Both are queries over `conductor.db`, so they work without a daemon too (or with one older than this build).
|
|
@@ -324,6 +351,8 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
|
|
|
324
351
|
- `GET /history?since=<ISO time>&until=<ISO time>&project=<name>`: the report for that window (see History). Every parameter is optional; the default window is the 24 hours before `until` (default: now). 400 for a time it can't read, a window whose start isn't before its end, or an unknown parameter.
|
|
325
352
|
- `GET /why/:id`: the event's chain of causes and the rule behind it; `GET /why?item=ATK-315` the same for the item's newest actions.
|
|
326
353
|
- `POST /rules/test` with `{ "event"?: "pr.comment", "project"?: "atk", "fields"?: { … } }`: a dry run; 400 for a sample it can't read.
|
|
354
|
+
- `GET /ports`: every worktree's port assignment. `POST /ports` with `{ "worktree": "<absolute path>", "prefer"?: 3 }`: the checkout's (see Broker); 200 with `assignment`, 409 when the path is in no registered project or no slot is free.
|
|
355
|
+
- `GET /locks`: the locks held. `POST /locks/:name` with `{ "action": "acquire" | "release" | "status", "owner"? }`: 200 with the work-issue helper's lock JSON; 409 for an acquire or release without an owner.
|
|
327
356
|
- `POST /waits` with `{ "worker": 1, "condition": "pr:ado:Emergent.AgenticToolkit#1850:merged" }`: a wait in its full form (see Waits), stored in its normal form; 201 with the wait. `GET /waits/:id` shows one, with its resolution once it has one.
|
|
328
357
|
- A refusal (a duplicate launch, a bad condition, a busy worker) is a 409 with `error` and `details`.
|
|
329
358
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { lockVerdict } from "./broker.js";
|
|
4
|
+
import { callApi, NotRunningError } from "./client.js";
|
|
5
|
+
import { ConductorError } from "./errors.js";
|
|
6
|
+
import { lockSchema, portAssignmentSchema } from "./store.js";
|
|
7
|
+
import { daemonWorker, gitToplevel } from "./wait.js";
|
|
8
|
+
// Exit code when no daemon brokered the request: none runs, an older one has no broker, or it
|
|
9
|
+
// refused (a checkout in no registered project, no free slot). The caller keeps its own scheme,
|
|
10
|
+
// as ATK's scripts/dev-ports.mjs and the work-issue helper's file lock do.
|
|
11
|
+
export const NOT_BROKERED = 3;
|
|
12
|
+
// How often `conduct lock acquire --wait` asks again, as the work-issue helper's lock does.
|
|
13
|
+
const LOCK_POLL_MS = 15_000;
|
|
14
|
+
const errorBody = z.object({ details: z.array(z.string()).optional(), error: z.string() });
|
|
15
|
+
const lockAnswer = z.looseObject({ acquired: z.boolean().optional(), lock: z.string() });
|
|
16
|
+
// The broker's locks for `conduct status`, or null from a daemon older than the broker.
|
|
17
|
+
export async function heldLocks(paths, now = new Date()) {
|
|
18
|
+
const answer = await callApi(paths, { method: 'GET', path: '/locks' });
|
|
19
|
+
if (answer.status === 404)
|
|
20
|
+
return null;
|
|
21
|
+
if (answer.status !== 200) {
|
|
22
|
+
const refusal = errorBody.safeParse(answer.body).data;
|
|
23
|
+
throw new ConductorError(refusal?.error ?? `the API answered ${answer.status} for the locks`, refusal?.details ?? []);
|
|
24
|
+
}
|
|
25
|
+
return z
|
|
26
|
+
.object({ locks: z.array(lockSchema) })
|
|
27
|
+
.parse(answer.body)
|
|
28
|
+
.locks.map((lock) => ({ ...lock, verdict: lockVerdict(lock, null, now) === 'stale' ? 'stale' : 'held' }));
|
|
29
|
+
}
|
|
30
|
+
// `conduct lock <acquire|release|status> <name>`: the broker's named lock, such as chrome, which
|
|
31
|
+
// verify skills of every project take turns with. Prints the work-issue helper's JSON. The owner
|
|
32
|
+
// is --owner, else the item of the worker whose worktree this is.
|
|
33
|
+
export async function lockCommand(paths, action, name, options, io) {
|
|
34
|
+
if (action !== 'acquire' && action !== 'release' && action !== 'status')
|
|
35
|
+
throw new ConductorError('usage: conduct lock <acquire|release|status> <name> [--owner <id>] [--wait <minutes>]');
|
|
36
|
+
if (!/^[\w-]+$/.test(name))
|
|
37
|
+
throw new ConductorError(`a lock name is letters, digits, _ and -, not "${name}"`);
|
|
38
|
+
let owner = options.owner?.trim() || null;
|
|
39
|
+
if (owner === null) {
|
|
40
|
+
const worker = await daemonWorker(paths, await gitToplevel(io.cwd), undefined);
|
|
41
|
+
if (worker === 'no-daemon')
|
|
42
|
+
return notBrokered(io, 'the conductor is not running');
|
|
43
|
+
if (worker === 'unreachable')
|
|
44
|
+
throw new ConductorError("the conductor didn't answer");
|
|
45
|
+
owner = worker?.item ?? null;
|
|
46
|
+
if (owner === null && action !== 'status')
|
|
47
|
+
throw new ConductorError('no worker of the conductor works in this checkout: pass --owner <id>');
|
|
48
|
+
}
|
|
49
|
+
const until = Date.now() + (options.waitMinutes ?? 0) * 60_000;
|
|
50
|
+
const sleep = io.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
51
|
+
for (;;) {
|
|
52
|
+
const answer = await ask(paths, { body: { action, ...(owner === null ? {} : { owner }) }, method: 'POST', path: `/locks/${name}` });
|
|
53
|
+
if (!answer.ok)
|
|
54
|
+
return notBrokered(io, answer.why);
|
|
55
|
+
const body = lockAnswer.parse(answer.body);
|
|
56
|
+
if (action !== 'acquire' || body.acquired !== false || Date.now() >= until) {
|
|
57
|
+
io.out(JSON.stringify(body, null, 2));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
await sleep(LOCK_POLL_MS);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// One line per lock: name, holder, and its last acquire.
|
|
64
|
+
export function lockLine(lock) {
|
|
65
|
+
return ` ${lock.name} ${lock.holder} since ${new Date(lock.since).toLocaleString()}${lock.verdict === 'stale' ? ' (stale: free to take over)' : ''}`;
|
|
66
|
+
}
|
|
67
|
+
// `conduct ports [<path>]`: the checkout's dev ports from the broker, or every assignment.
|
|
68
|
+
export async function portsCommand(paths, path, options, io) {
|
|
69
|
+
if (path === undefined) {
|
|
70
|
+
const answer = await ask(paths, { method: 'GET', path: '/ports' });
|
|
71
|
+
if (!answer.ok)
|
|
72
|
+
return notBrokered(io, answer.why);
|
|
73
|
+
const assignments = z.object({ assignments: z.array(portAssignmentSchema) }).parse(answer.body).assignments;
|
|
74
|
+
if (options.json)
|
|
75
|
+
io.out(JSON.stringify(assignments, null, 2));
|
|
76
|
+
else if (assignments.length === 0)
|
|
77
|
+
io.out('no worktree has ports from the conductor');
|
|
78
|
+
else
|
|
79
|
+
assignments.forEach((a) => io.out(portsLine(a)));
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const answer = await ask(paths, {
|
|
83
|
+
body: { worktree: resolve(io.cwd, path), ...(options.prefer === undefined ? {} : { prefer: options.prefer }) },
|
|
84
|
+
method: 'POST',
|
|
85
|
+
path: '/ports',
|
|
86
|
+
});
|
|
87
|
+
if (!answer.ok)
|
|
88
|
+
return notBrokered(io, answer.why);
|
|
89
|
+
const { assignment } = z.object({ assignment: portAssignmentSchema }).parse(answer.body);
|
|
90
|
+
io.out(options.json ? JSON.stringify(assignment, null, 2) : portsLine(assignment));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
// One line per checkout: project, slot, each port, then where.
|
|
94
|
+
export function portsLine(a) {
|
|
95
|
+
const ports = Object.entries(a.ports)
|
|
96
|
+
.map(([name, port]) => `${name} ${port}`)
|
|
97
|
+
.join(' · ');
|
|
98
|
+
return `${a.project} slot ${a.slot} ${ports} ${a.worktree}`;
|
|
99
|
+
}
|
|
100
|
+
// The body of a 200 answer, or why nothing was brokered.
|
|
101
|
+
async function ask(paths, request) {
|
|
102
|
+
let answer;
|
|
103
|
+
try {
|
|
104
|
+
answer = await callApi(paths, request);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof NotRunningError)
|
|
108
|
+
return { ok: false, why: 'the conductor is not running' };
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (answer.status === 200)
|
|
112
|
+
return { body: answer.body, ok: true };
|
|
113
|
+
// A daemon from before the broker has no such route.
|
|
114
|
+
if (answer.status === 404)
|
|
115
|
+
return { ok: false, why: 'this conductor has no port broker (upgrade it)' };
|
|
116
|
+
const refusal = errorBody.safeParse(answer.body).data;
|
|
117
|
+
if (answer.status === 409 && refusal)
|
|
118
|
+
return { ok: false, why: [refusal.error, ...(refusal.details ?? [])].join('; ') };
|
|
119
|
+
throw new ConductorError(refusal?.error ?? `the API answered ${answer.status}`, refusal?.details ?? []);
|
|
120
|
+
}
|
|
121
|
+
function notBrokered(io, why) {
|
|
122
|
+
io.err(`not brokered: ${why}`);
|
|
123
|
+
return NOT_BROKERED;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=broker-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker-cli.js","sourceRoot":"","sources":["../src/broker-cli.ts"],"names":[],"mappings":"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"}
|
package/build/broker.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { connect } from 'node:net';
|
|
3
|
+
import { ConductorError } from "./errors.js";
|
|
4
|
+
import { samePath } from "./paths.js";
|
|
5
|
+
import { basePorts } from "./profile.js";
|
|
6
|
+
import { checkoutOf } from "./registry.js";
|
|
7
|
+
import {} from "./store.js";
|
|
8
|
+
// The broker shares what every project on the machine uses (design note, sections 3 and 12): dev
|
|
9
|
+
// server ports, so two projects' worktrees never serve on the same port, and named locks such as
|
|
10
|
+
// chrome, so verify skills of different projects take turns with the one browser.
|
|
11
|
+
// A holder keeps a lock by acquiring it again; one it hasn't acquired for this long is free to take
|
|
12
|
+
// over. The same rule as the work-issue helper's file lock.
|
|
13
|
+
export const LOCK_STALE_MS = 30 * 60_000;
|
|
14
|
+
const MAX_PORT = 65_535;
|
|
15
|
+
export function broker({ exists = existsSync, listening = portListening, now = () => new Date(), store }) {
|
|
16
|
+
// One assignment at a time: each waits on port probes, and two at once could pick the same slot.
|
|
17
|
+
let assigning = Promise.resolve();
|
|
18
|
+
// Frees the ports of worktrees whose directory is gone, such as one /worktree-gc removed.
|
|
19
|
+
const sweep = () => {
|
|
20
|
+
for (const held of store.portAssignments())
|
|
21
|
+
if (!exists(held.worktree))
|
|
22
|
+
freePorts(store, held.worktree, 'worktree gone');
|
|
23
|
+
return store.portAssignments();
|
|
24
|
+
};
|
|
25
|
+
const assign = async (path, wanted) => {
|
|
26
|
+
const { checkout, root } = checkoutOf(path);
|
|
27
|
+
const projects = store.listProjects();
|
|
28
|
+
const project = projects.find((p) => samePath(p.root, root));
|
|
29
|
+
if (!project)
|
|
30
|
+
throw new ConductorError(`${checkout} is in no registered project`, ['`conduct project add <path>` registers one']);
|
|
31
|
+
const profile = profileOf(project);
|
|
32
|
+
const base = basePorts(profile);
|
|
33
|
+
if (samePath(checkout, root))
|
|
34
|
+
return { assignedAt: null, ports: base, project: project.name, slot: 0, worktree: checkout };
|
|
35
|
+
const portsOf = (slot) => Object.fromEntries(Object.entries(base).map(([name, port]) => [name, port + slot]));
|
|
36
|
+
let prefer = wanted;
|
|
37
|
+
const held = sweep().find((a) => samePath(a.worktree, checkout));
|
|
38
|
+
if (held) {
|
|
39
|
+
// Kept while the profile still gives its slot those ports; otherwise it moves, its old slot first.
|
|
40
|
+
if (held.slot <= profile.ports.maxSlot && samePorts(held.ports, portsOf(held.slot)))
|
|
41
|
+
return held;
|
|
42
|
+
freePorts(store, held.worktree, 'profile changed');
|
|
43
|
+
prefer ??= held.slot;
|
|
44
|
+
}
|
|
45
|
+
// Every main checkout's ports, and every worktree's.
|
|
46
|
+
const taken = new Set([
|
|
47
|
+
...projects.flatMap((p) => (p.profile ? Object.values(basePorts(p.profile)) : [])),
|
|
48
|
+
...store.portAssignments().flatMap((a) => Object.values(a.ports)),
|
|
49
|
+
]);
|
|
50
|
+
const slots = Array.from({ length: profile.ports.maxSlot }, (_, i) => i + 1);
|
|
51
|
+
const order = prefer !== undefined && slots.includes(prefer) ? [prefer, ...slots.filter((s) => s !== prefer)] : slots;
|
|
52
|
+
for (const slot of order) {
|
|
53
|
+
const ports = portsOf(slot);
|
|
54
|
+
const values = Object.values(ports);
|
|
55
|
+
if (values.some((port) => port > MAX_PORT || taken.has(port)))
|
|
56
|
+
continue;
|
|
57
|
+
// The preferred slot is the one the checkout already serves on (its own scheme gave it), so
|
|
58
|
+
// its own dev servers may be what listens there.
|
|
59
|
+
if (slot !== prefer && (await Promise.all(values.map(listening))).some(Boolean))
|
|
60
|
+
continue;
|
|
61
|
+
const assignment = store.savePorts({ ports, project: project.name, slot, worktree: checkout });
|
|
62
|
+
store.appendEvent({ data: { ports, slot, worktree: checkout }, kind: 'ports.assigned', project: project.name });
|
|
63
|
+
return assignment;
|
|
64
|
+
}
|
|
65
|
+
throw new ConductorError(`no free slot between 1 and ${profile.ports.maxSlot} for ${checkout}`, [
|
|
66
|
+
'`conduct ports` lists the assignments; a removed worktree frees its slot',
|
|
67
|
+
]);
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
assignPorts: (path, prefer) => {
|
|
71
|
+
const next = assigning.then(() => assign(path, prefer));
|
|
72
|
+
assigning = next.catch(() => { });
|
|
73
|
+
return next;
|
|
74
|
+
},
|
|
75
|
+
lock: (name, action, owner) => {
|
|
76
|
+
const current = store.getLock(name);
|
|
77
|
+
const verdict = lockVerdict(current, owner, now());
|
|
78
|
+
if (action === 'status')
|
|
79
|
+
return { lock: name, verdict, ...(current ? { heldBy: current.holder, since: current.since } : {}) };
|
|
80
|
+
if (owner === null)
|
|
81
|
+
throw new ConductorError(`an owner is needed to ${action} a lock`);
|
|
82
|
+
if (action === 'release') {
|
|
83
|
+
if (verdict === 'mine') {
|
|
84
|
+
store.deleteLock(name);
|
|
85
|
+
store.appendEvent({ data: { holder: owner }, kind: 'lock.released', subject: name });
|
|
86
|
+
}
|
|
87
|
+
return { lock: name, released: verdict === 'mine', ...(current && verdict !== 'mine' ? { heldBy: current.holder } : {}) };
|
|
88
|
+
}
|
|
89
|
+
if (current && verdict === 'held')
|
|
90
|
+
return { acquired: false, heldBy: current.holder, lock: name, since: current.since };
|
|
91
|
+
store.saveLock(name, owner);
|
|
92
|
+
if (verdict === 'stale' && current) {
|
|
93
|
+
store.appendEvent({ data: { holder: owner, previous: current.holder, since: current.since }, kind: 'lock.taken-over', subject: name });
|
|
94
|
+
}
|
|
95
|
+
else if (verdict === 'free') {
|
|
96
|
+
store.appendEvent({ data: { holder: owner }, kind: 'lock.acquired', subject: name });
|
|
97
|
+
}
|
|
98
|
+
return { acquired: true, lock: name, owner, ...(verdict === 'mine' ? { refreshed: true } : {}) };
|
|
99
|
+
},
|
|
100
|
+
locks: () => store.listLocks(),
|
|
101
|
+
portAssignments: sweep,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Frees the ports of a worktree (a retired worker's, or one whose directory is gone).
|
|
105
|
+
export function freePorts(store, worktree, reason) {
|
|
106
|
+
for (const held of store.portAssignments()) {
|
|
107
|
+
if (!samePath(held.worktree, worktree))
|
|
108
|
+
continue;
|
|
109
|
+
store.deletePorts(held.worktree);
|
|
110
|
+
store.appendEvent({ data: { ports: held.ports, reason, slot: held.slot, worktree: held.worktree }, kind: 'ports.freed', project: held.project });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function lockVerdict(lock, owner, now) {
|
|
114
|
+
if (!lock)
|
|
115
|
+
return 'free';
|
|
116
|
+
if (owner !== null && lock.holder === owner)
|
|
117
|
+
return 'mine';
|
|
118
|
+
const since = Date.parse(lock.since);
|
|
119
|
+
return Number.isNaN(since) || now.getTime() - since > LOCK_STALE_MS ? 'stale' : 'held';
|
|
120
|
+
}
|
|
121
|
+
// True when something accepts connections on the port, on IPv4 or IPv6 loopback.
|
|
122
|
+
export async function portListening(port) {
|
|
123
|
+
const probe = (host) => new Promise((done) => {
|
|
124
|
+
const socket = connect({ host, port });
|
|
125
|
+
const finish = (open) => {
|
|
126
|
+
socket.destroy();
|
|
127
|
+
done(open);
|
|
128
|
+
};
|
|
129
|
+
socket.setTimeout(300, () => finish(false));
|
|
130
|
+
socket.once('connect', () => finish(true));
|
|
131
|
+
socket.once('error', () => finish(false));
|
|
132
|
+
});
|
|
133
|
+
return (await Promise.all(['127.0.0.1', '::1'].map(probe))).some(Boolean);
|
|
134
|
+
}
|
|
135
|
+
// Releases every lock the holder has, such as a retired worker's item.
|
|
136
|
+
export function releaseLocks(store, holder, reason) {
|
|
137
|
+
for (const lock of store.listLocks()) {
|
|
138
|
+
if (lock.holder !== holder)
|
|
139
|
+
continue;
|
|
140
|
+
store.deleteLock(lock.name);
|
|
141
|
+
store.appendEvent({ data: { holder, reason }, kind: 'lock.released', subject: lock.name });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function profileOf(project) {
|
|
145
|
+
if (project.profile)
|
|
146
|
+
return project.profile;
|
|
147
|
+
throw new ConductorError(`project ${project.name}'s stored profile is invalid: ${project.problem ?? 'unknown'}`, [
|
|
148
|
+
'`conduct project add <path>` reads it again',
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
function samePorts(a, b) {
|
|
152
|
+
return Object.keys(a).length === Object.keys(b).length && Object.entries(b).every(([name, port]) => a[name] === port);
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=broker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker.js","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"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"}
|
package/build/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolve, sep } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { heldLocks, lockCommand, lockLine, portsCommand } from "./broker-cli.js";
|
|
8
9
|
import { callApi, NotRunningError, ping, readEndpoint } from "./client.js";
|
|
9
10
|
import { loadConfig } from "./config.js";
|
|
10
11
|
import { CONSOLE_ARGS, CONSOLE_WINDOW, consoleProject, consoleTabArgs, escalationLines, escalationSchema, handledSchema } from "./console.js";
|
|
@@ -38,7 +39,7 @@ const USAGE = `Usage: conduct <command>
|
|
|
38
39
|
start start the daemon detached (the logon task, else a detached serve) and
|
|
39
40
|
wait until it answers; nothing when it runs already
|
|
40
41
|
stop stop the running daemon
|
|
41
|
-
status [--json] the daemon's pid, port, projects and
|
|
42
|
+
status [--json] the daemon's pid, port, projects, workers and held locks
|
|
42
43
|
project add <path> register the project whose checkout holds .claude/conductor.json
|
|
43
44
|
project list [--json] the registered projects
|
|
44
45
|
launch <item> start a worker: its worktree, a background session, a tab
|
|
@@ -103,6 +104,16 @@ const USAGE = `Usage: conduct <command>
|
|
|
103
104
|
time <when> such as 17:00 (the next one) or 2026-10-10T09:00 (local)
|
|
104
105
|
<full form> such as pr:ado:Emergent.AgenticToolkit#1850:merged
|
|
105
106
|
[--local] [--timeout <minutes, local only; 29>] [--worker <worker>] [--item <item>]
|
|
107
|
+
ports [<path>] [--prefer <slot>] [--json]
|
|
108
|
+
the checkout's dev ports from the port broker: its project's base ports
|
|
109
|
+
+ its slot, which no other project's worktree shares (a main checkout:
|
|
110
|
+
slot 0); --prefer asks for a slot first. Without <path>, every worktree's.
|
|
111
|
+
Exit 3 when no conductor brokered it, so the caller keeps its own ports
|
|
112
|
+
lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
|
|
113
|
+
a lock every project shares, such as chrome for verify runs; stale 30 min
|
|
114
|
+
after its holder last acquired it (acquire again to keep it). The owner is
|
|
115
|
+
this checkout's worker's item unless given; --wait polls. Prints JSON;
|
|
116
|
+
exit 3 when no conductor answers
|
|
106
117
|
events [--since <id>] the event log after <id> (all of it without), one JSON event per line
|
|
107
118
|
events --follow [--since <id>]
|
|
108
119
|
keep following it (live without --since), reconnecting when the
|
|
@@ -218,6 +229,10 @@ async function awaitDaemon(paths) {
|
|
|
218
229
|
}
|
|
219
230
|
throw new ConductorError(`the conductor did not answer within ${START_WAIT_MS / 1000} s`, [`see ${paths.log}`]);
|
|
220
231
|
}
|
|
232
|
+
// What `conduct ports` and `conduct lock` print to and run in.
|
|
233
|
+
function brokerIo() {
|
|
234
|
+
return { cwd: process.cwd(), err: (line) => process.stderr.write(`${line}\n`), out };
|
|
235
|
+
}
|
|
221
236
|
// `conduct console`: the console (design note, section 11), the Claude session the conductor mod
|
|
222
237
|
// serves, started with CONDUCTOR_CONSOLE=1 in the project's main checkout, where the console skill
|
|
223
238
|
// is installed. It keeps no state of its own, so a new one after a close loses nothing.
|
|
@@ -442,6 +457,8 @@ async function main(argv) {
|
|
|
442
457
|
here: { type: 'boolean' },
|
|
443
458
|
json: { type: 'boolean' },
|
|
444
459
|
kind: { type: 'string' },
|
|
460
|
+
owner: { type: 'string' },
|
|
461
|
+
prefer: { type: 'string' },
|
|
445
462
|
probe: { type: 'boolean' },
|
|
446
463
|
project: { type: 'string' },
|
|
447
464
|
prompt: { type: 'string' },
|
|
@@ -453,6 +470,7 @@ async function main(argv) {
|
|
|
453
470
|
text: { type: 'string' },
|
|
454
471
|
until: { type: 'string' },
|
|
455
472
|
version: { short: 'v', type: 'boolean' },
|
|
473
|
+
wait: { type: 'string' },
|
|
456
474
|
weight: { type: 'string' },
|
|
457
475
|
worker: { type: 'string' },
|
|
458
476
|
},
|
|
@@ -502,6 +520,15 @@ async function main(argv) {
|
|
|
502
520
|
project: values.project,
|
|
503
521
|
prompt: values.prompt,
|
|
504
522
|
});
|
|
523
|
+
case 'lock': {
|
|
524
|
+
if (sub === undefined || arg === undefined)
|
|
525
|
+
throw new ConductorError('usage: conduct lock <acquire|release|status> <name> [--owner <id>] [--wait <minutes>]');
|
|
526
|
+
const waitMinutes = values.wait === undefined ? undefined : Number(values.wait);
|
|
527
|
+
if (waitMinutes !== undefined && !(waitMinutes >= 0))
|
|
528
|
+
throw new ConductorError('--wait takes minutes, such as --wait 30');
|
|
529
|
+
process.exitCode = await lockCommand(paths, sub, arg, { owner: values.owner, waitMinutes }, brokerIo());
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
505
532
|
case 'note':
|
|
506
533
|
if (sub === undefined || arg === undefined)
|
|
507
534
|
throw new ConductorError('usage: conduct note <item> <file> [--worker <worker>]');
|
|
@@ -537,6 +564,9 @@ async function main(argv) {
|
|
|
537
564
|
});
|
|
538
565
|
case 'pause':
|
|
539
566
|
return override(paths, { action: 'pause', ...(sub === undefined ? {} : { project: sub }), ...(values.until === undefined ? {} : { until: untilOf(values.until) }) });
|
|
567
|
+
case 'ports':
|
|
568
|
+
process.exitCode = await portsCommand(paths, sub, { json: values.json === true, ...(values.prefer === undefined ? {} : { prefer: wholeNumber('--prefer', values.prefer) }) }, brokerIo());
|
|
569
|
+
return;
|
|
540
570
|
case 'queue':
|
|
541
571
|
return queue(paths, values.json === true, values.fresh === true);
|
|
542
572
|
case 'rules':
|
|
@@ -823,15 +853,22 @@ async function status(paths, json) {
|
|
|
823
853
|
const { workers } = fleetBody.parse(await expectOk(callApi(paths, { method: 'GET', path: '/fleet' })));
|
|
824
854
|
// A linked build's CLI can be newer than the daemon still running, which has no inbox (null).
|
|
825
855
|
const items = await openItems(paths);
|
|
856
|
+
// Null from a daemon older than the broker.
|
|
857
|
+
const locks = await heldLocks(paths);
|
|
826
858
|
const pid = readPid(paths.pid);
|
|
827
859
|
const port = readEndpoint(paths)?.port ?? null;
|
|
828
860
|
if (json)
|
|
829
|
-
return out(JSON.stringify({ home: paths.home, inbox: items, pid, port, projects, running: true, workers }, null, 2));
|
|
861
|
+
return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers }, null, 2));
|
|
830
862
|
out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
|
|
831
863
|
out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
|
|
832
864
|
projects.forEach((p) => out(projectLine(p)));
|
|
833
865
|
out(workers.length === 0 ? 'no workers' : 'workers:');
|
|
834
866
|
workers.forEach((w) => out(workerLine(w)));
|
|
867
|
+
if (locks === null)
|
|
868
|
+
out('locks: not in this daemon (restart it to run this build)');
|
|
869
|
+
else
|
|
870
|
+
out(locks.length === 0 ? 'locks: none held' : 'locks:');
|
|
871
|
+
locks?.forEach((l) => out(lockLine(l)));
|
|
835
872
|
if (items === null)
|
|
836
873
|
out('inbox: not in this daemon (restart it to run this build)');
|
|
837
874
|
else
|