@detergent-software/conductor 3.0.0-dev.1 → 3.0.0-dev.11

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +47 -20
  3. package/build/broker-cli.js +19 -21
  4. package/build/broker-cli.js.map +1 -1
  5. package/build/broker.js +66 -92
  6. package/build/broker.js.map +1 -1
  7. package/build/claude.js +50 -7
  8. package/build/claude.js.map +1 -1
  9. package/build/cli.js +12 -22
  10. package/build/cli.js.map +1 -1
  11. package/build/code-host.js +10 -8
  12. package/build/code-host.js.map +1 -1
  13. package/build/config.js +10 -0
  14. package/build/config.js.map +1 -1
  15. package/build/console.js +26 -0
  16. package/build/console.js.map +1 -1
  17. package/build/daemon.js +13 -6
  18. package/build/daemon.js.map +1 -1
  19. package/build/help.js +10 -12
  20. package/build/help.js.map +1 -1
  21. package/build/in-process.js +3 -3
  22. package/build/in-process.js.map +1 -1
  23. package/build/inbox.js +39 -4
  24. package/build/inbox.js.map +1 -1
  25. package/build/lib/providers/index.js +1 -1
  26. package/build/lib/providers/index.js.map +1 -1
  27. package/build/lib/providers/npm/stage.js +14 -4
  28. package/build/lib/providers/npm/stage.js.map +1 -1
  29. package/build/local-files.js +95 -0
  30. package/build/local-files.js.map +1 -0
  31. package/build/manager.js +144 -27
  32. package/build/manager.js.map +1 -1
  33. package/build/npm-reads.js +194 -0
  34. package/build/npm-reads.js.map +1 -0
  35. package/build/plugin.js +10 -0
  36. package/build/plugin.js.map +1 -1
  37. package/build/profile.js +55 -19
  38. package/build/profile.js.map +1 -1
  39. package/build/project-check-cli.js +4 -1
  40. package/build/project-check-cli.js.map +1 -1
  41. package/build/project-check.js +69 -22
  42. package/build/project-check.js.map +1 -1
  43. package/build/rule-actions.js +3 -1
  44. package/build/rule-actions.js.map +1 -1
  45. package/build/rules-service.js +22 -4
  46. package/build/rules-service.js.map +1 -1
  47. package/build/rules.js +10 -1
  48. package/build/rules.js.map +1 -1
  49. package/build/scheduler.js +11 -1
  50. package/build/scheduler.js.map +1 -1
  51. package/build/server.js +0 -19
  52. package/build/server.js.map +1 -1
  53. package/build/spend.js +4 -0
  54. package/build/spend.js.map +1 -1
  55. package/build/store.js +36 -10
  56. package/build/store.js.map +1 -1
  57. package/build/tracker-reads-cli.js +2 -4
  58. package/build/tracker-reads-cli.js.map +1 -1
  59. package/build/upgrade.js +16 -1
  60. package/build/upgrade.js.map +1 -1
  61. package/build/watchers/ado.js +3 -2
  62. package/build/watchers/ado.js.map +1 -1
  63. package/build/watchers/github.js +3 -2
  64. package/build/watchers/github.js.map +1 -1
  65. package/build/watchers/npm.js +6 -4
  66. package/build/watchers/npm.js.map +1 -1
  67. package/build/worktrees.js +182 -38
  68. package/build/worktrees.js.map +1 -1
  69. package/package.json +2 -2
  70. package/plugin/skills/conductor-console/SKILL.md +1 -1
  71. package/plugin/skills/work-item/SKILL.md +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,85 @@
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
+ ## [3.0.0-dev.11](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.10&targetVersion=GTconductor-v3.0.0-dev.11) (2026-10-17)
6
+
7
+ ### Bug Fixes
8
+
9
+ * **conductor:** lift only the hold's own human label (ATK-575) ([0ad3a2a](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/0ad3a2a3340b9d1ade1a518c923f77e217e9a1c9))
10
+ * **conductor:** lift the pre-start hold's human label once the worker clears it (ATK-575) ([e23074f](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/e23074f2c60cf710ee1bddcab76a3cdaff412898))
11
+
12
+ ## [3.0.0-dev.10](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.9&targetVersion=GTconductor-v3.0.0-dev.10) (2026-10-17)
13
+
14
+ ### Bug Fixes
15
+
16
+ * **conductor:** follow a new session id once it stays listed 5 s (ATK-574) ([32b86ea](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/32b86ea40bc93e92736a9447aee3a1e0e9572879))
17
+ * **conductor:** follow a new session id only once two listings show it (ATK-574) ([5e8b65a](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/5e8b65ad625fdbfa57566bc1b2b7d4adaa55b186))
18
+
19
+ ## [3.0.0-dev.9](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.8&targetVersion=GTconductor-v3.0.0-dev.9) (2026-10-17)
20
+
21
+ ### Bug Fixes
22
+
23
+ * **conductor:** hold launches only for a profile that doesn't read (ATK-573) ([2032a41](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/2032a41daafc9fc85ccfcc594fd99520e8d38d49))
24
+ * **conductor:** name the conductor a profile needs and hold its launches (ATK-573) ([b8eb37e](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/b8eb37e13bdb94ab4c9ca1e9cf46495f65e13f7b))
25
+
26
+ ## [3.0.0-dev.8](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.7&targetVersion=GTconductor-v3.0.0-dev.8) (2026-10-17)
27
+
28
+ ### Bug Fixes
29
+
30
+ * **conductor:** open the console tab while CONDUCTOR_HOME is set (ATK-572) ([68e7131](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/68e713197bca1c45a93a1dc122293253ffe5c952))
31
+
32
+ ## [3.0.0-dev.7](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.6&targetVersion=GTconductor-v3.0.0-dev.7) (2026-10-17)
33
+
34
+ ### Features
35
+
36
+ * **conductor:** check Claude Code's workspace trust before a launch (ATK-571) ([fab9457](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/fab94576638b713f913198a9a394a1e08a5709ac))
37
+
38
+ ### Bug Fixes
39
+
40
+ * **conductor:** skip the trust probe behind a skills fail (ATK-571) ([a3614a5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/a3614a5f5c35c04991b002de6a459fdd3159d32e))
41
+ * **conductor:** trust probe review fixes (ATK-571) ([c9d1a11](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c9d1a11578dde0c92559bfce8c1b255a7f7ca8f7))
42
+
43
+ ## [3.0.0-dev.6](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.5&targetVersion=GTconductor-v3.0.0-dev.6) (2026-10-17)
44
+
45
+ ### Bug Fixes
46
+
47
+ * **conductor:** pick npm's proxy as npm does, and follow redirects (ATK-561) ([76db548](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/76db5481a3920fdf3e1ea97c773b158837249142))
48
+ * **conductor:** read npm with npm's own registry and proxy settings (ATK-561) ([cd5dacf](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/cd5dacf50c5a76c0a08fcec7d628210cd2637d42))
49
+
50
+ ## [3.0.0-dev.5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.4&targetVersion=GTconductor-v3.0.0-dev.5) (2026-10-17)
51
+
52
+ ### Bug Fixes
53
+
54
+ * **conductor:** start the console without the caller's Claude session (ATK-559) ([767279a](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/767279a80281dbc4774329b2f9d6aa06d0ccdf78))
55
+
56
+ ## [3.0.0-dev.4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.3&targetVersion=GTconductor-v3.0.0-dev.4) (2026-10-17)
57
+
58
+ ### Features
59
+
60
+ * **conductor:** make worktrees built in, with named ports (ATK-526) ([a899350](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/a899350fb4606f67a4de7af2a50d3e6d616ccead))
61
+ * **conductor:** take its ports in dev-ports, document built-in worktrees (ATK-526) ([1e19523](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/1e19523fa9ca8252f9cd5a518edd2b66b7899a2a))
62
+
63
+ ### Bug Fixes
64
+
65
+ * **conductor:** harden built-in worktrees from the review and probe (ATK-526) ([900f495](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/900f49504b3291083dd3fa5f64f7efc11d28ecc2))
66
+ * **conductor:** never delete a worktree that may hold work (ATK-526) ([f2114b1](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/f2114b1fb0e31c7ecad8d215fae6174ccfeee6b8))
67
+
68
+ ### Documentation
69
+
70
+ * **conductor:** describe the hardened worktree launch and retire (ATK-526) ([be45c3c](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/be45c3ca98c51f404d77279e4d34b03bedf3ea71))
71
+
72
+ ## [3.0.0-dev.3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.2&targetVersion=GTconductor-v3.0.0-dev.3) (2026-10-17)
73
+
74
+ ### Bug Fixes
75
+
76
+ * **conductor:** read the upgrade's dist-tag fresh from npm (ATK-558) ([4b31619](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/4b31619a6d3ebd6380751c993eada457259a1346))
77
+
78
+ ## [3.0.0-dev.2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v3.0.0-dev.1&targetVersion=GTconductor-v3.0.0-dev.2) (2026-10-17)
79
+
80
+ ### Documentation
81
+
82
+ * **conductor:** say the first console asks to trust its folder (ATK-525) ([8d9e37d](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/8d9e37de5b37807bfab5fb7a94903be868513d6a))
83
+
5
84
  ## [3.0.0-dev.1](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v2.2.0-dev.3&targetVersion=GTconductor-v3.0.0-dev.1) (2026-10-17)
6
85
 
7
86
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -80,8 +80,7 @@ conduct runs <sha> --requeue <pipeline>
80
80
  queue the item's merge's cancelled run again on the base (see Worker verbs)
81
81
  conduct train [<project>] [--dry-run]
82
82
  merge the waiting skill publishes into one registry PR (see Skill trains)
83
- conduct ports [<path>] [--prefer <slot>] [--json]
84
- a checkout's dev ports from the port broker, or every worktree's (see Broker)
83
+ conduct ports [<path>] [--json] a conducted worktree's dev ports from the port broker, or every worktree's (see Broker)
85
84
  conduct lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
86
85
  a lock every project shares, such as chrome (see Broker)
87
86
  conduct events [--since <id>] [--follow] [--console]
@@ -100,11 +99,27 @@ A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
100
99
 
101
100
  The session manager owns the `claude --bg` sessions it launches, and only those:
102
101
 
103
- - **launch:** the worktree comes first, through the project's `.claude/hooks/worktree-hooks.mjs`. In a repo with `.atk-lock.json`, the profile's work and verify skills must match the lockfile (`atk install` fixes them when it can). Then `claude --bg --worktree <name> -n <name> --plugin-dir <home>/plugin --disallowedTools EnterPlanMode ExitPlanMode --permission-mode auto -- <prompt>` runs from the main checkout (see Plugin). A work or verify skill named `conductor:<skill>` comes from the plugin, not the lockfile: the launch checks that the plugin holds it. A profile with `"workers": { "tab": true }` also gets a Windows Terminal tab attached to the session.
102
+ - **launch:** the worktree comes first, made by the daemon itself (design note v2, section 2; ATK-526), with no hooks:
103
+ 1. the broker gives it a port for each name in the profile's `ports` (see Broker);
104
+ 2. `git worktree add` makes `<root>/.claude/worktrees/<name>` on `worktree-<name>` from `origin/<base>`, fetched first (after `git worktree prune`, and on Windows with `core.longpaths` on). A worktree git still reads there is reused, its own changes and all; one with a `.git` git can't read is never deleted, and fails the launch. A `worktree-<name>` branch that is left is checked out again, with any commits it kept;
105
+ 3. with `localFilesFrom` (an absolute path to a checkout the operator keeps), the gitignored files there that match its `.worktreeinclude` are copied in, as they are now;
106
+ 4. the profile's `setup` commands run in order in the worktree, through the shell, with `CONDUCTOR_PORT_<NAME>` (such as `CONDUCTOR_PORT_WEB=41000`), `CONDUCTOR_ITEM`, `CONDUCTOR_PROJECT` and `CONDUCTOR_WORKTREE` set. They build its gitignored files from what the repo commits (ATK's: `pnpm install`, `atk setup --tool claude-code --yes`, `node scripts/dev-ports.mjs assign`, `atk install`). The first that fails fails the launch, with the end of its output. So does a setup that leaves changes git sees, such as an `.atk-lock.json` that `atk install` rewrote: the worker would take them for its own;
107
+ 5. the ports go into the worktree's `.claude/settings.local.json` as `env`, merged into what the file had. A `claude --bg` session doesn't inherit the environment it is started from, but Claude Code applies that file's `env` to every session in the folder and to each resume;
108
+ 6. the profile's work and verify skills must be there: a `conductor:<skill>` in the plugin, any other in the worktree's `.claude/skills` or `~/.claude/skills`.
109
+
110
+ A launch that fails removes the worktree it made when it is clean, with its branch while that still has the launch's name; a worktree that was there before the launch stays.
111
+
112
+ Then `claude --bg -n <name> --plugin-dir <home>/plugin --disallowedTools EnterPlanMode ExitPlanMode --permission-mode auto -- <prompt>` runs with its cwd in the worktree (see Plugin), never `--worktree`. A profile with `"workers": { "tab": true }` also gets a Windows Terminal tab attached to the session.
104
113
  - **park:** a worker with a wait (`POST /waits`) is stopped once `claude agents` shows its turn has ended. Its status becomes `parked`, and its tab closes.
105
114
  - **resume:** when a wait resolves, the daemon lets the stop settle (`sessions.settleSeconds`). It checks the process has exited, then runs `claude --bg --resume <session> -- "[conductor] event: …"`. The session keeps its name, model, permission mode and disallowed tools. A resume of a session that still runs starts a copy; the daemon stops and removes the copy, then tries again.
106
115
  - **compact:** a parked worker with no resolved wait, whose context the rules find too big (the default `compact-on-park`: past `sessions.compactTokens`, 300k), is resumed with `/compact <what to keep>` once its stop has settled (`worker.compacting`). It runs until its transcript shows the compaction's boundary (`worker.compacted`, with the context before it and the conversation it kept), then parks again on its waits, which resume it as usual. Each turn of a session re-reads its whole context from cache, so a worker left to grow pays for it every turn (ATK-454 reached 883k). A resume that starts a copy is tried again on a later sweep (`worker.compact-deferred`), as a resume is. A compaction whose resume fails, or whose turn ends with no boundary 10 minutes on, logs `worker.compact-failed`, which raises a `context` item in the inbox. The worker still parks, and the daemon doesn't try it again until it restarts or the session compacts some other way: any boundary in its transcript, Claude Code's own included, logs `worker.compacted` and resolves that item. A worker whose context is unknown is never compacted, so even a rule with no threshold can't compact it on every park.
107
- - **retire:** refuses uncommitted or unpushed work, and a worker whose turn still runs, unless `--force`. It never retires a session that is still starting. Then it stops the session, removes the worktree and runs `claude rm`. If the stop fails, nothing is removed.
116
+ - **retire:** refuses a worker whose turn still runs, unless `--force`, and never retires a session that is still starting. What happens to the worktree depends on the worker's PR (design note v2, section 2):
117
+ - **merged** (read from the code host): it refuses uncommitted or unpushed work unless `--force`, then stops the session, removes the worktree and deletes its local branch, and runs `claude rm`;
118
+ - **no PR, and nothing uncommitted or unpushed** (a canceled item, a worker retired before its first commit): as merged, with nothing to lose;
119
+ - **not merged** (abandoned or closed, or no PR with work in it): it stops and removes the session and frees the ports, and keeps the worktree and its branch, so nothing the PR didn't land is lost. `worker.retired` names it under `kept`, which raises one `failed` inbox item with the path, the branch and the commands that remove them. A PR the project's code host can't read at all counts as not merged;
120
+ - **unread** (the code host didn't answer): it refuses, as a refusal that passes on its own, and the scheduler tries again.
121
+
122
+ A probe's worktree and a `--force` retire's always go. The removal moves the worktree aside, has git forget it, deletes the branch and then the directory with Node, which unlinks pnpm's junctions without following them (`git worktree remove` leaves them on Windows). The delete is asynchronous, so the API goes on answering through a minute of `node_modules`. With the directory gone already, it deletes the `worktree-<name>` branch only when none of its commits is unpushed. If the stop fails, nothing is removed.
108
123
  - **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
109
124
  - **attach:** a parked worker is resumed with no event text, so it wakes idle and keeps running when its tab closes. An attached worker isn't parked until it registers a new wait, or until its turn has been over `sessions.idleMinutes` (`worker.detached`). The daemon can't see the tab, so this also delivers a wait that resolved while it was attached. A wait that resolves after the attach, on a worker whose transcript shows no turn since then, is delivered at once: the worker is detached (`reason: wait resolved`), parked, which closes its tab, and resumed with the event. A worker someone woke outside the daemon counts as attached since its stop, so the turn that woke it counts. One that took a turn since the attach (or has no transcript to tell), or whose wait resolved before the attach, keeps it until the idle release, logs `worker.resume-held` (`reason: attached`) once, and `conduct status` shows it as `idle, holding resolved waits for the attached session`.
110
125
  - **park failures:** a session whose process outlives `claude stop` by 30 s logs one `worker.park-failed` per spell, which raises a `stall` in the inbox (see Inbox), and the park is tried again every 5 minutes.
@@ -326,8 +341,8 @@ Rules say what the conductor does when something happens (design note, section 9
326
341
  ]
327
342
  ```
328
343
 
329
- - **Sources, in order:** the item's project's `rules` (in its `.claude/conductor.json`, changed by PR), then `~/.conductor/rules.json`, then the built-in defaults: conduct-atk's behavior written as rules (`launch`, `retire-closed`, `park-on-wait`, `resume-on-wait`, `compact-on-park`, `runaway`, `daily-cap`, `ready-to-release`, `queue-drained`, `triage`, which raises a new item that waits on the profile's triage label in the inbox, `conductor-handoff`, which escalates a worker's `Handoff · conductor` note to the console, and `hold-pre-start-handoff`, which adds the human label when a worker hands off as `human`, `operator`, `blocked` or `scope` before its `Started` note, so the scheduler never launches work no worker can do again; remove the label if the item should be picked later). A rule with an `id` replaces the rule with the same id further down, so a file holds only the differences.
330
- - **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`, `Started in item.phases`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed` (a failed launch, a session that stopped unexpectedly, or a PR's red checks: `pr.checks-failed`), `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `worker.parked` (a park; the session manager also asks it of each parked worker on a sweep, for `compact`), `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `provider.login-failed` (a project's login lost, once per spell; see Accounts), `queue.pick`, `queue.drained`, `queue.ready-to-release`, `queue.triage`. A rule without one is a state rule, checked on each scheduler tick against each wait, inbox item, worker or project, or once for the machine, by the fields it reads. Fields: `project`, `item.id/area/labels/tier/priority`, `item.phases` (the phases of every note the item has had, the one being posted too), `plan.tier/landing`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `worker.context` (the session's context in tokens, see Workers), `spend.worker/item/project/today` (input, output and cache-write tokens, without cache reads: the worker's and the item's in all, the project's and the machine's today), `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
344
+ - **Sources, in order:** the item's project's `rules` (in its `.claude/conductor.json`, changed by PR), then `~/.conductor/rules.json`, then the built-in defaults: conduct-atk's behavior written as rules (`launch`, `retire-closed`, `park-on-wait`, `resume-on-wait`, `compact-on-park`, `runaway`, `daily-cap`, `ready-to-release`, `queue-drained`, `triage`, which raises a new item that waits on the profile's triage label in the inbox, `conductor-handoff`, which escalates a worker's `Handoff · conductor` note to the console, and `hold-pre-start-handoff`, which adds the human label when a worker hands off as `human`, `operator`, `blocked` or `scope` before its `Started` note, so the scheduler never launches work no worker can do again; remove the label if the item should be picked later), and `release-pre-start-hold`, which takes that label off again when the worker clears the hold itself (its `Resumed`, or its `Started` after the hold), and leaves a human label the hold didn't add (ATK-575). A rule with an `id` replaces the rule with the same id further down, so a file holds only the differences.
345
+ - **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`, `Started in item.phases`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed` (a failed launch, a session that stopped unexpectedly, or a PR's red checks: `pr.checks-failed`), `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `worker.parked` (a park; the session manager also asks it of each parked worker on a sweep, for `compact`), `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `provider.login-failed` (a project's login lost, once per spell; see Accounts), `queue.pick`, `queue.drained`, `queue.ready-to-release`, `queue.triage`. A rule without one is a state rule, checked on each scheduler tick against each wait, inbox item, worker or project, or once for the machine, by the fields it reads. Fields: `project`, `item.id/area/labels/tier/priority`, `item.phases` (the phases of every note the item has had, the one being posted too), `item.labelled-by` (the ids of the rules whose label add still stands on the item: a label they added, not one it had already, that no rule removed since), `plan.tier/landing`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `worker.context` (the session's context in tokens, see Workers), `spend.worker/item/project/today` (input, output and cache-write tokens, without cache reads: the worker's and the item's in all, the project's and the machine's today), `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
331
346
  - **do:** `launch`, `retire`, `park`, `resume worker`, `retry`, `pause launches [in <project>] [until <when> | for <duration>]`, `pause all launches …`, `unpause launches`, `focus [<project>] weight|slots <n> until <when>`, `auto-approve now`, `auto-approve after <duration>`, `inbox <kind> ["<text>"]`, `remind operator`, `notify <channel> ["<text>"]`, `notify profile <name> [until <when>]`, `label add|remove human|triage|shipped-open`, `flag shipped-open`, `escalate console "<ask>"`, `none`. One string, or a list.
332
347
  - **Guardrails:** rules act inside Area locks, slots, quiet hours and config.json's limits. A rule that would go past one is refused when it loads: `focus … slots` over `scheduler.slots`, an override with no end or one over 14 days, `retire` on anything but `item.closed`, `launch` outside `queue.pick` / `item.unblocked`, `park` outside `worker.waiting`, `resume` outside `wait.resolved` / `pr.comment`, `compact` outside `worker.parked`, `retry` outside `run.failed`. `merge`, `push`, `publish`, `approve`, `delete` and `run` aren't in the vocabulary: rules never merge, push, publish, approve prod, delete or run commands, and the daemon writes to a code host only when the owning worker asks (see Writes).
333
348
  - **Reload:** saving `rules.json` or a project's profile reloads its rules. A profile save, or a profile changed while the daemon was down, is taken whole: the daemon stores it and logs `project.updated` with the fields that `changed`, and the scheduler, watchers and the rest use it from then on. A profile that renames the project is refused: a project keeps the name it was registered under. To rename one, `conduct project remove <old name>`, then `conduct project add` it under the new name (in that order: with both registered, both would read the same queue and could launch the same item). The remove drops the old registration and its queue read, resolves its open inbox items (the refusal's among them) and logs `project.removed`, on which the daemon drops its rules and its watch of the file. It is refused while the project has workers in flight, and works without the daemon. A file the conductor refuses leaves its last good rules (for a profile, the whole last good profile) in force, logs `rules.rejected` and raises a `failed` inbox item until it loads again (`rules.loaded`), including after a restart. `conduct project add` refuses a profile whose rules don't load, with or without the daemon.
@@ -388,13 +403,13 @@ The scheduler moves conduct-atk's tick into the daemon (design note, section 8).
388
403
 
389
404
  ## Skill bumps reach the main checkout
390
405
 
391
- A worker's worktree branches from `origin/<base>` and installs its own pins. The main checkout is different: it keeps the pins it was last pulled at, and the worktree hooks and the skills a new worktree copies come from there. The console's skill doesn't: it comes with the conductor plugin (see Plugin). So the daemon checks each conducted project that has an `.atk-lock.json`, at most every `checkout.intervalMinutes` (10):
406
+ A worker's worktree branches from `origin/<base>` and its `setup` installs its own pins. The main checkout is different: it keeps the pins it was last pulled at, for the sessions people start there. The console's skill doesn't: it comes with the conductor plugin (see Plugin). So the daemon checks each conducted project that has an `.atk-lock.json`, at most every `checkout.intervalMinutes` (10), until the daemon owns its clones (design note v2, step 6):
392
407
 
393
408
  1. It runs `git fetch origin <base>` in the main checkout and compares the pins (asset type, name and version) at `HEAD` with those at `origin/<base>`. When they match and nothing is left to install, nothing more happens.
394
409
  2. When they differ, it records the pins as pending (a `checkout.pending` event). With the main checkout on `<base>` and no local changes to `.atk-lock.json`, it then runs `git merge --ff-only origin/<base>`.
395
410
  3. Once `HEAD` has the pins, whether by this pull or yours, it runs `atk install` for the pending assets. A `checkout.updated` event lists each `name old → new`. An install that fails or leaves drift stays pending, and the next check tries it again, in this daemon or the next.
396
411
 
397
- The checkout isn't touched in these cases: it is on another branch, its lockfile has local changes, or git refuses the fast-forward. Instead one `operator` item gives the commands (`git -C <root> pull --ff-only`, `atk install -P <root>`). The same item stays open while an install leaves drift, and it resolves itself once the pins are installed. A worktree create waits for a check that is running, and a check waits while one of the project's workers is launching, so a new worktree never copies a half-updated checkout. A failed fetch is only logged. `checkout.mode` is `update` (the default), `notify` (the item only, with no git or atk run) or `off`.
412
+ The checkout isn't touched in these cases: it is on another branch, its lockfile has local changes, or git refuses the fast-forward. Instead one `operator` item gives the commands (`git -C <root> pull --ff-only`, `atk install -P <root>`). The same item stays open while an install leaves drift, and it resolves itself once the pins are installed. A worktree create waits for a check that is running, and a check waits while one of the project's workers is launching, so the two never run git in the main checkout at once. A failed fetch is only logged. `checkout.mode` is `update` (the default), `notify` (the item only, with no git or atk run) or `off`.
398
413
 
399
414
  ## Skill trains
400
415
 
@@ -542,14 +557,11 @@ Everything goes through the project's gh account (`projects.<name>.github.user`,
542
557
 
543
558
  The broker shares what every project on the machine uses (ATK-318).
544
559
 
545
- **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 }`.
546
- - A main checkout serves on the base ports (slot 0).
547
- - 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.
548
- - A retired worker's worktree frees its slot, and so does a worktree whose directory is gone (at the next assignment).
549
- - Ports are compared, not slots, so projects with different bases stay apart too.
550
- - 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 1 (`not brokered`): no daemon, an older one, or a checkout in no registered project.
551
- - 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.
552
- - A stored assignment follows its project's profile: when the base ports or `maxSlot` change, the worktree moves, its old slot first.
560
+ **Ports.** Each profile names the ports a worktree's dev servers need, such as ATK's `"ports": ["web", "api"]` (design note v2, section 2; ATK-526).
561
+ - A launch gives the worktree one port per name from the machine's pool, `ports.first` to `ports.last` in config.json (41000 to 41999): each the pool's first port that no other worktree of any project holds, nothing listens on (IPv4 or IPv6 loopback), and that binds, which also skips a range Windows reserves. They reach `setup` and the worker as `CONDUCTOR_PORT_<NAME>` (see Workers).
562
+ - The pool sits away from the usual dev ports and their slot schemes, such as ATK's 3000+N and 7071+N for a person's own worktrees, so those never take a port a worker was given.
563
+ - A worktree keeps its ports while the profile names the same ports; a changed list gives it new ones. Retiring the worker frees them, and so does a worktree whose directory is gone (at the next assignment).
564
+ - `conduct ports` lists the assignments, and `conduct ports <path>` gives one worktree's (`--json` for either). Only a launch assigns. `conduct project check` previews the ports a new worktree would get, saving nothing.
553
565
 
554
566
  **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).
555
567
  - The owner is `--owner`, else the item of the worker whose worktree this is.
@@ -662,7 +674,11 @@ conduct install-startup
662
674
  conduct project add <path to the project's main checkout>
663
675
  ```
664
676
 
665
- `conduct project add` walks up from the path it is given to the directory that holds `.claude/conductor.json`, and maps a worktree to its main checkout. After that the daemon takes the whole profile from disk when it starts and each time the file is saved, so a `git pull` that changes `slots` or `skills.work` needs no `conduct project add`. An older daemon refuses a profile with fields it doesn't know, so upgrade the daemon before a profile that uses new ones lands. After an `--allow-downgrade` to a version without those fields, the stored copy no longer validates either: the project shows `the stored profile no longer validates` and isn't conducted until its file, saved without them, is read again.
677
+ `conduct project add` walks up from the path it is given to the directory that holds `.claude/conductor.json`, and maps a worktree to its main checkout. After that the daemon takes the whole profile from disk when it starts and each time the file is saved, so a `git pull` that changes `slots` or `skills.work` needs no `conduct project add`. An older daemon refuses a profile with fields it doesn't know, so upgrade the daemon before a profile that uses new ones lands.
678
+
679
+ A profile that uses new fields says which conductor reads them: `"conductor": "3.0.0-dev.9"`, the version that first read the newest of them (a `-dev.N` while it is only on `dev`; a stable minimum such as `3.0.0` is newer than every `3.0.0-dev.N`, as semver orders them). Every conductor from the one that knows this key (ATK-573) checks it before the rest of the profile, and an older one refuses the file with one line instead of a list of fields: `conductor: needs 3.0.0-dev.9 or newer; this is 3.0.0-dev.7: run \`conduct upgrade --tag dev\`` (`--tag latest` for a stable minimum). The worker verbs that read the profile in a worktree (`conduct state`, `conduct branch`, `conduct search`, a lock with no daemon) say the same and exit 1, and the daemon's inbox item carries the line and the upgrade command. While a project's profile on disk doesn't read (a minimum above this conductor, a schema problem, another protocol), the scheduler launches none of the project's items: each waits in `conduct queue` as `profile refused: <its first problem>`, since a new worktree would hold the same file and its verbs would refuse it. A profile that reads but whose rules are refused, or that renames the project, keeps the last good profile and launches as before. Workers already running carry on, and launches resume on the first tick after the file loads.
680
+
681
+ After an `--allow-downgrade` to a version without those fields, the stored copy no longer validates either: the project shows `the stored profile no longer validates` and isn't conducted until its file, saved without them, is read again.
666
682
 
667
683
  Then `conduct project check <name>` says whether the project can be conducted, before its first launch finds out. Each check prints one line, `ok`, `warn`, `fail` or `skip`, with a fix under a warn or a fail, and the command exits 1 when any check fails (`--json` prints the same as data):
668
684
 
@@ -670,13 +686,24 @@ Then `conduct project check <name>` says whether the project can be conducted, b
670
686
  - **auth:** which credential each provider the project uses signs in with (project token, machine token or CLI login; see Accounts), probed as the daemon would. For each that isn't the project's own token, a detail line gives the scopes, the `cmdkey` line and the config key that would scope it; that is advice, and the check passes. A token that is there but refused, or no credential at all, fails.
671
687
  - **tracker:** the tracker's login answers, and the queue reads (Linear: the team, and whether any of its projects has a priority; Boards: the saved query; GitHub Issues: the labelled issues).
672
688
  - **code-host** and **ci:** the code host's login answers and the base branch exists; each release's `pipeline` names exactly one of the repo's pipelines.
673
- - **worktree:** the launch's first steps on a throwaway `conduct-check-<time>`: the worktree hooks create it, its skills match `.atk-lock.json`, and it is removed, whatever happened. The one check that writes; `--no-worktree` skips it, since the create hook runs `pnpm install`.
689
+ - **worktree:** the launch's worktree steps on a throwaway `conduct-check-<time>`: `git worktree add` from `origin/<base>`, `localFilesFrom`'s files, the ports the broker would give, and `setup`. Then the profile's skills must be there after it, and a `warn` says when git doesn't ignore `.claude/settings.local.json`, where the ports go. A `claude --bg` with no prompt then checks that Claude Code would start a session there: it starts idle, costs no tokens and is removed at once. Claude Code keeps workspace trust per repository, so a root nobody has opened in Claude Code fails here with the one step that trusts it, `claude` run in the root once with its trust prompt accepted (ATK-571). It is removed with its branch, whatever happened. The one check that writes; `--no-worktree` skips it, since setup can take a while (ATK's runs `pnpm install`).
674
690
  - **skills:** `skills.work` is there and declares `protocol: 2` in its SKILL.md frontmatter (worker protocol 2), `skills.verify` is there, and `atk audit` finds the installed ones unchanged; a missing alias is a `warn`. A skill named `conductor:<skill>` is read from this package's plugin and isn't audited, and beside a plugin work skill the aliases only name older notes' headings, so they aren't checked; any other is read from the main checkout.
675
- - **ports:** the broker has a free slot for a new worktree, up to `ports.maxSlot`, saving nothing.
691
+ - **ports:** the machine's pool has a free port for each of the profile's `ports`, saving nothing; `skip` for a profile with none.
676
692
  - **health:** each `health` URL answers; a `warn` at most, since an environment can be down.
677
693
 
678
694
  With the daemon running it checks through the API, as the daemon's own accounts; without it, here, as the machine's.
679
695
 
696
+ A new worktree's local files come from the profile (design note v2, section 2; see Workers):
697
+
698
+ ```jsonc
699
+ // The dev ports a worktree needs, by name: CONDUCTOR_PORT_WEB and CONDUCTOR_PORT_API.
700
+ "ports": ["web", "api"],
701
+ // Run in order in each new worktree; they build its gitignored files from what the repo commits.
702
+ "setup": ["pnpm install --frozen-lockfile", "node scripts/dev-ports.mjs assign"],
703
+ // Optional: copy the .worktreeinclude files of a checkout you keep, for local files made by hand.
704
+ "localFilesFrom": "C:\\Users\\you\\src\\project"
705
+ ```
706
+
680
707
  The profile's optional `work` section is for the project's work skill; the daemon only checks it:
681
708
 
682
709
  ```jsonc
@@ -718,7 +745,7 @@ conduct console # a tab in Windows Terminal (window "conductor"), in ~/
718
745
  conduct console --here # the same session in this terminal
719
746
  ```
720
747
 
721
- It writes the plugin copy if none exists yet (see Plugin), then runs `claude --plugin-dir <home>/plugin -n conductor --permission-mode auto -- /conductor:conductor-console` with `CONDUCTOR_CONSOLE=1`. A background session started in that new folder ran with no trust prompt (ATK-525); the first interactive one may still ask. The skill starts the daemon if it isn't running (`conduct start`, detached, so it doesn't end with the session), then handles what waits.
748
+ It writes the plugin copy if none exists yet (see Plugin), then runs `claude --plugin-dir <home>/plugin -n conductor --permission-mode auto -- /conductor:conductor-console` with `CONDUCTOR_CONSOLE=1`. The first console in that folder asks Claude Code to trust it once, and the mod's band and the console's stream start only after you accept; background sessions there run with no prompt (ATK-525). The skill starts the daemon if it isn't running (`conduct start`, detached, so it doesn't end with the session), then handles what waits.
722
749
 
723
750
  **Escalations.** A rule's `escalate console "<ask>"` appends `console.escalated` (the default `conductor-handoff` does it for a worker's `Handoff · conductor` note). The mod wakes the model with `[conductor] escalation #<id> on <item>: <ask>`. The console does the job and reports back with `conduct escalations done <id> "<what it found or did>"`, which logs `console.handled` with the escalation as its cause, so `conduct why <that event>` leads to the rule. With `--ask`, the finding also goes in the inbox as a `console` item, pushed like any other, for something only you can do. `conduct escalations` lists those not yet handled, from the last 14 days, including the ones raised while no console was open; the console reads it when it starts.
724
751
 
@@ -7,7 +7,8 @@ import { itemFor } from "./code-host-cli.js";
7
7
  import { ConductorError } from "./errors.js";
8
8
  import { fileLock } from "./file-lock.js";
9
9
  import { usageError } from "./help.js";
10
- import { PROFILE_FILE, readProfile } from "./profile.js";
10
+ import { samePath } from "./paths.js";
11
+ import { PROFILE_FILE, profileProblem, readProfile } from "./profile.js";
11
12
  import { lockSchema, portAssignmentSchema } from "./store.js";
12
13
  import { daemonWorker, gitToplevel } from "./wait.js";
13
14
  // How often `conduct lock acquire --wait` asks again, as the work-issue helper's lock does.
@@ -69,13 +70,14 @@ export async function lockCommand(paths, action, name, options, io) {
69
70
  export function lockLine(lock) {
70
71
  return ` ${lock.name} ${lock.holder} since ${new Date(lock.since).toLocaleString()}${lock.verdict === 'stale' ? ' (stale: free to take over)' : ''}`;
71
72
  }
72
- // `conduct ports [<path>]`: the checkout's dev ports from the broker, or every assignment.
73
+ // `conduct ports [<checkout>]`: every worktree's dev ports from the broker, or one worktree's. Only a
74
+ // launch assigns them, so this only reads.
73
75
  export async function portsCommand(paths, path, options, io) {
76
+ const answer = await ask(paths, { method: 'GET', path: '/ports' });
77
+ if (!answer.ok)
78
+ return notBrokered(answer.why);
79
+ const assignments = z.object({ assignments: z.array(portAssignmentSchema) }).parse(answer.body).assignments;
74
80
  if (path === undefined) {
75
- const answer = await ask(paths, { method: 'GET', path: '/ports' });
76
- if (!answer.ok)
77
- return notBrokered(answer.why);
78
- const assignments = z.object({ assignments: z.array(portAssignmentSchema) }).parse(answer.body).assignments;
79
81
  if (options.json)
80
82
  io.out(JSON.stringify(assignments, null, 2));
81
83
  else if (assignments.length === 0)
@@ -84,23 +86,21 @@ export async function portsCommand(paths, path, options, io) {
84
86
  assignments.forEach((a) => io.out(portsLine(a)));
85
87
  return 0;
86
88
  }
87
- const answer = await ask(paths, {
88
- body: { worktree: resolve(io.cwd, path), ...(options.prefer === undefined ? {} : { prefer: options.prefer }) },
89
- method: 'POST',
90
- path: '/ports',
91
- });
92
- if (!answer.ok)
93
- return notBrokered(answer.why);
94
- const { assignment } = z.object({ assignment: portAssignmentSchema }).parse(answer.body);
89
+ const worktree = resolve(io.cwd, path);
90
+ const assignment = assignments.find((a) => samePath(a.worktree, worktree));
91
+ if (!assignment)
92
+ throw new ConductorError(`${worktree} has no ports from the conductor`, [
93
+ 'a conducted worktree gets them when its worker launches; `conduct ports` lists them all',
94
+ ]);
95
95
  io.out(options.json ? JSON.stringify(assignment, null, 2) : portsLine(assignment));
96
96
  return 0;
97
97
  }
98
- // One line per checkout: project, slot, each port, then where.
98
+ // One line per worktree: project, each port, then where.
99
99
  export function portsLine(a) {
100
100
  const ports = Object.entries(a.ports)
101
101
  .map(([name, port]) => `${name} ${port}`)
102
102
  .join(' · ');
103
- return `${a.project} slot ${a.slot} ${ports} ${a.worktree}`;
103
+ return `${a.project} ${ports} ${a.worktree}`;
104
104
  }
105
105
  // The body of a 200 answer, or why nothing was brokered.
106
106
  async function ask(paths, request) {
@@ -134,8 +134,8 @@ async function lockHere(action, name, options, io, why) {
134
134
  try {
135
135
  profile = readProfile(checkout).profile;
136
136
  }
137
- catch {
138
- return notBrokered(`${why}, and ${checkout} has no readable ${PROFILE_FILE}`);
137
+ catch (error) {
138
+ return notBrokered(`${why}, and ${checkout} has no readable ${PROFILE_FILE}: ${profileProblem(error)}`);
139
139
  }
140
140
  const given = options.owner?.trim();
141
141
  // Status needs no owner; the others take the checkout's item, and refuse without one.
@@ -158,9 +158,7 @@ async function lockHere(action, name, options, io, why) {
158
158
  io.out(JSON.stringify(answer, null, 2));
159
159
  return 0;
160
160
  }
161
- // Nothing brokered: none runs, an older one has no broker, or it refused (a checkout in no
162
- // registered project, no free slot). The caller keeps its own scheme, as ATK's
163
- // scripts/dev-ports.mjs does on any exit but 0.
161
+ // Nothing brokered: none runs, an older one has no broker, or it refused.
164
162
  function notBrokered(why) {
165
163
  throw new ConductorError(`not brokered: ${why}`, ['nothing was brokered']);
166
164
  }
@@ -1 +1 @@
1
- {"version":3,"file":"broker-cli.js","sourceRoot":"","sources":["../src/broker-cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"broker-cli.js","sourceRoot":"","sources":["../src/broker-cli.ts"],"names":[],"mappings":"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"}
package/build/broker.js CHANGED
@@ -1,83 +1,65 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { connect } from 'node:net';
2
+ import { connect, createServer } from 'node:net';
3
3
  import { ConductorError } from "./errors.js";
4
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.
5
+ import { ACTIVE_STATUSES } from "./store.js";
6
+ // The broker shares what every project on the machine uses (design note, sections 3 and 12; v2,
7
+ // section 2): dev server ports, so two worktrees of any projects never serve on the same port, and
8
+ // named locks such as chrome, so verify skills of different projects take turns with the one browser.
11
9
  // A holder keeps a lock by acquiring it again; one it hasn't acquired for this long is free to take
12
10
  // over. The same rule as the work-issue helper's file lock.
13
11
  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.
12
+ export function broker({ exists = existsSync, free = portFree, now = () => new Date(), pool, store, }) {
13
+ // One assignment at a time: each waits on port probes, and two at once could pick the same port.
17
14
  let assigning = Promise.resolve();
18
- // Frees the ports of worktrees whose directory is gone, such as one /worktree-gc removed.
15
+ const serially = (task) => {
16
+ const next = assigning.then(task);
17
+ assigning = next.catch(() => { });
18
+ return next;
19
+ };
20
+ // Frees the ports of worktrees whose directory is gone, such as one /worktree-gc removed. Not a
21
+ // worker's in flight: a launch takes its ports before `git worktree add` makes the directory.
19
22
  const sweep = () => {
20
23
  for (const held of store.portAssignments())
21
- if (!exists(held.worktree))
24
+ if (!live(held))
22
25
  freePorts(store, held.worktree, 'worktree gone');
23
26
  return store.portAssignments();
24
27
  };
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`, [
31
- '`conduct project add <path>` registers one',
32
- ]);
33
- const profile = profileOf(project);
34
- if (samePath(checkout, root))
35
- return { assignedAt: null, ports: basePorts(profile), project: project.name, slot: 0, worktree: checkout };
36
- let prefer = wanted;
37
- const held = sweep().find((a) => samePath(a.worktree, checkout));
28
+ const live = (held) => exists(held.worktree) ||
29
+ store.listWorkers(ACTIVE_STATUSES).some((w) => w.worktree !== null && samePath(w.worktree, held.worktree));
30
+ // A port per name: the pool's first ones that no assignment in `held` has and that are free now.
31
+ const pick = async (names, held) => {
32
+ const taken = new Set(held.flatMap((a) => Object.values(a.ports)));
33
+ const ports = {};
34
+ let port = pool.first;
35
+ for (const name of names) {
36
+ while (port <= pool.last && (taken.has(port) || !(await free(port))))
37
+ port++;
38
+ if (port > pool.last) {
39
+ throw new ConductorError(`no free port left in ${pool.first}-${pool.last} for "${name}"`, [
40
+ '`conduct ports` lists the assignments, and a removed worktree frees its ports; ports.first and ports.last in config.json set the pool',
41
+ ]);
42
+ }
43
+ ports[name] = port++;
44
+ }
45
+ return ports;
46
+ };
47
+ const assign = async (worktree, project, names) => {
48
+ const held = sweep().find((a) => samePath(a.worktree, worktree));
38
49
  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, slotPorts(profile, held.slot)))
50
+ if (sameNames(held.ports, names))
41
51
  return held;
42
52
  freePorts(store, held.worktree, 'profile changed');
43
- prefer ??= held.slot;
44
53
  }
45
- const free = await freeSlot(profile, prefer, store.portAssignments());
46
- if (free === null)
47
- throw noSlot(profile, checkout);
48
- const assignment = store.savePorts({ ...free, project: project.name, worktree: checkout });
49
- store.appendEvent({ data: { ...free, worktree: checkout }, kind: 'ports.assigned', project: project.name });
54
+ const ports = await pick(names, store.portAssignments());
55
+ if (names.length === 0)
56
+ return { assignedAt: now().toISOString(), ports, project, worktree };
57
+ const assignment = store.savePorts({ ports, project, worktree });
58
+ store.appendEvent({ data: { ports, worktree }, kind: 'ports.assigned', project });
50
59
  return assignment;
51
60
  };
52
- // The first slot of 1..maxSlot whose ports nobody has and nothing listens on, `prefer` first.
53
- // `held`: the worktrees' assignments.
54
- const freeSlot = async (profile, prefer, held) => {
55
- // Every main checkout's ports, and every worktree's.
56
- const taken = new Set([
57
- ...store.listProjects().flatMap((p) => (p.profile ? Object.values(basePorts(p.profile)) : [])),
58
- ...held.flatMap((a) => Object.values(a.ports)),
59
- ]);
60
- const slots = Array.from({ length: profile.ports.maxSlot }, (_, i) => i + 1);
61
- const order = prefer !== undefined && slots.includes(prefer) ? [prefer, ...slots.filter((s) => s !== prefer)] : slots;
62
- for (const slot of order) {
63
- const ports = slotPorts(profile, slot);
64
- const values = Object.values(ports);
65
- if (values.some((port) => port > MAX_PORT || taken.has(port)))
66
- continue;
67
- // The preferred slot is the one the checkout already serves on (its own scheme gave it), so
68
- // its own dev servers may be what listens there.
69
- if (slot !== prefer && (await Promise.all(values.map(listening))).some(Boolean))
70
- continue;
71
- return { ports, slot };
72
- }
73
- return null;
74
- };
75
61
  return {
76
- assignPorts: (path, prefer) => {
77
- const next = assigning.then(() => assign(path, prefer));
78
- assigning = next.catch(() => { });
79
- return next;
80
- },
62
+ assignPorts: (worktree, project, names) => serially(() => assign(worktree, project, names)),
81
63
  lock: (name, action, owner) => {
82
64
  const current = store.getLock(name);
83
65
  const verdict = lockVerdict(current, owner, now());
@@ -113,19 +95,9 @@ export function broker({ exists = existsSync, listening = portListening, now = (
113
95
  },
114
96
  locks: () => store.listLocks(),
115
97
  portAssignments: sweep,
116
- // After the assignments in flight, so it sees the slots they took. It writes nothing: a gone
98
+ // After the assignments in flight, so it sees the ports they took. It writes nothing: a gone
117
99
  // worktree's ports count as free, as the next sweep makes them.
118
- previewPorts: (profile) => {
119
- const next = assigning.then(async () => {
120
- const held = store.portAssignments().filter((a) => exists(a.worktree));
121
- const free = await freeSlot(profile, undefined, held);
122
- if (free === null)
123
- throw noSlot(profile, `a new ${profile.name} worktree`);
124
- return free;
125
- });
126
- assigning = next.catch(() => { });
127
- return next;
128
- },
100
+ previewPorts: (names) => serially(() => pick(names, store.portAssignments().filter(live))),
129
101
  };
130
102
  }
131
103
  // Frees the ports of a worktree (a retired worker's, or one whose directory is gone).
@@ -135,7 +107,7 @@ export function freePorts(store, worktree, reason) {
135
107
  continue;
136
108
  store.deletePorts(held.worktree);
137
109
  store.appendEvent({
138
- data: { ports: held.ports, reason, slot: held.slot, worktree: held.worktree },
110
+ data: { ports: held.ports, reason, worktree: held.worktree },
139
111
  kind: 'ports.freed',
140
112
  project: held.project,
141
113
  });
@@ -149,6 +121,24 @@ export function lockVerdict(lock, owner, now) {
149
121
  const since = Date.parse(lock.since);
150
122
  return Number.isNaN(since) || now.getTime() - since > LOCK_STALE_MS ? 'stale' : 'held';
151
123
  }
124
+ // True when the port can be bound on IPv4 loopback: not in use, and not in a range the OS reserves
125
+ // (Windows excludes ranges for Hyper-V and WSL, where a dev server would fail with EACCES).
126
+ export async function portBinds(port) {
127
+ return new Promise((done) => {
128
+ const server = createServer();
129
+ server.once('error', () => done(false));
130
+ server.listen({ exclusive: true, host: '127.0.0.1', port }, () => server.close(() => done(true)));
131
+ });
132
+ }
133
+ // The environment that hands a worktree its ports: CONDUCTOR_PORT_<NAME> for each, such as
134
+ // CONDUCTOR_PORT_WEB=41000.
135
+ export function portEnv(ports) {
136
+ return Object.fromEntries(Object.entries(ports).map(([name, port]) => [`CONDUCTOR_PORT_${name.toUpperCase()}`, String(port)]));
137
+ }
138
+ // Nothing listens on the port, on either loopback, and it binds.
139
+ export async function portFree(port) {
140
+ return !(await portListening(port)) && (await portBinds(port));
141
+ }
152
142
  // True when something accepts connections on the port, on IPv4 or IPv6 loopback.
153
143
  export async function portListening(port) {
154
144
  const probe = (host) => new Promise((done) => {
@@ -172,23 +162,7 @@ export function releaseLocks(store, holder, reason) {
172
162
  store.appendEvent({ data: { holder, reason }, kind: 'lock.released', subject: lock.name });
173
163
  }
174
164
  }
175
- function noSlot(profile, what) {
176
- return new ConductorError(`no free slot between 1 and ${profile.ports.maxSlot} for ${what}`, [
177
- '`conduct ports` lists the assignments; a removed worktree frees its slot',
178
- ]);
179
- }
180
- function profileOf(project) {
181
- if (project.profile)
182
- return project.profile;
183
- throw new ConductorError(`project ${project.name}'s stored profile is invalid: ${project.problem ?? 'unknown'}`, [
184
- `the daemon reads ${project.profilePath} again when it starts or the file is saved`,
185
- ]);
186
- }
187
- function samePorts(a, b) {
188
- return Object.keys(a).length === Object.keys(b).length && Object.entries(b).every(([name, port]) => a[name] === port);
189
- }
190
- // A worktree's ports at slot N: each of the profile's base ports + N.
191
- function slotPorts(profile, slot) {
192
- return Object.fromEntries(Object.entries(basePorts(profile)).map(([name, port]) => [name, port + slot]));
165
+ function sameNames(ports, names) {
166
+ return Object.keys(ports).length === names.length && names.every((name) => Object.hasOwn(ports, name));
193
167
  }
194
168
  //# sourceMappingURL=broker.js.map