@detergent-software/conductor 1.0.0-dev.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/README.md +277 -24
  3. package/build/brief-cli.js +68 -0
  4. package/build/brief-cli.js.map +1 -0
  5. package/build/brief.js +211 -0
  6. package/build/brief.js.map +1 -0
  7. package/build/broker-cli.js +126 -0
  8. package/build/broker-cli.js.map +1 -0
  9. package/build/broker.js +154 -0
  10. package/build/broker.js.map +1 -0
  11. package/build/cli.js +438 -132
  12. package/build/cli.js.map +1 -1
  13. package/build/conditions.js +15 -8
  14. package/build/conditions.js.map +1 -1
  15. package/build/config.js +26 -3
  16. package/build/config.js.map +1 -1
  17. package/build/console.js +176 -0
  18. package/build/console.js.map +1 -0
  19. package/build/daemon.js +136 -5
  20. package/build/daemon.js.map +1 -1
  21. package/build/events.js +2 -1
  22. package/build/events.js.map +1 -1
  23. package/build/help.js +447 -0
  24. package/build/help.js.map +1 -0
  25. package/build/history.js +637 -0
  26. package/build/history.js.map +1 -0
  27. package/build/inbox.js +117 -18
  28. package/build/inbox.js.map +1 -1
  29. package/build/manager.js +118 -43
  30. package/build/manager.js.map +1 -1
  31. package/build/notes.js +27 -43
  32. package/build/notes.js.map +1 -1
  33. package/build/notify.js +32 -3
  34. package/build/notify.js.map +1 -1
  35. package/build/overrides.js +83 -0
  36. package/build/overrides.js.map +1 -0
  37. package/build/paths.js +10 -1
  38. package/build/paths.js.map +1 -1
  39. package/build/profile.js +68 -2
  40. package/build/profile.js.map +1 -1
  41. package/build/queue.js +183 -0
  42. package/build/queue.js.map +1 -0
  43. package/build/registry.js +17 -10
  44. package/build/registry.js.map +1 -1
  45. package/build/rule-actions.js +444 -0
  46. package/build/rule-actions.js.map +1 -0
  47. package/build/rule-input.js +17 -0
  48. package/build/rule-input.js.map +1 -0
  49. package/build/rules-service.js +402 -0
  50. package/build/rules-service.js.map +1 -0
  51. package/build/rules.js +850 -0
  52. package/build/rules.js.map +1 -0
  53. package/build/scheduler.js +293 -0
  54. package/build/scheduler.js.map +1 -0
  55. package/build/serve.js +51 -0
  56. package/build/serve.js.map +1 -0
  57. package/build/server.js +230 -7
  58. package/build/server.js.map +1 -1
  59. package/build/spend.js +42 -0
  60. package/build/spend.js.map +1 -1
  61. package/build/store.js +266 -12
  62. package/build/store.js.map +1 -1
  63. package/build/trackers.js +15 -0
  64. package/build/trackers.js.map +1 -0
  65. package/build/upgrade.js +16 -14
  66. package/build/upgrade.js.map +1 -1
  67. package/build/wait.js +3 -9
  68. package/build/wait.js.map +1 -1
  69. package/build/watchers/ado-api.js +55 -0
  70. package/build/watchers/ado-api.js.map +1 -0
  71. package/build/watchers/ado.js +60 -52
  72. package/build/watchers/ado.js.map +1 -1
  73. package/build/watchers/boards-brief.js +63 -0
  74. package/build/watchers/boards-brief.js.map +1 -0
  75. package/build/watchers/boards-queue.js +120 -0
  76. package/build/watchers/boards-queue.js.map +1 -0
  77. package/build/watchers/boards-writes.js +83 -0
  78. package/build/watchers/boards-writes.js.map +1 -0
  79. package/build/watchers/boards.js +271 -0
  80. package/build/watchers/boards.js.map +1 -0
  81. package/build/watchers/conflicts.js +132 -0
  82. package/build/watchers/conflicts.js.map +1 -0
  83. package/build/watchers/github.js +5 -2
  84. package/build/watchers/github.js.map +1 -1
  85. package/build/watchers/index.js +21 -7
  86. package/build/watchers/index.js.map +1 -1
  87. package/build/watchers/linear-api.js +55 -0
  88. package/build/watchers/linear-api.js.map +1 -0
  89. package/build/watchers/linear-labels.js +43 -0
  90. package/build/watchers/linear-labels.js.map +1 -0
  91. package/build/watchers/linear-queue.js +96 -0
  92. package/build/watchers/linear-queue.js.map +1 -0
  93. package/build/watchers/linear.js +18 -16
  94. package/build/watchers/linear.js.map +1 -1
  95. package/build/watchers/source.js +3 -3
  96. package/build/watchers/source.js.map +1 -1
  97. package/build/why.js +78 -0
  98. package/build/why.js.map +1 -0
  99. package/mod/.claude-plugin/plugin.json +1 -1
  100. package/mod/README.md +2 -2
  101. package/mod/hooks/model.ts +135 -8
  102. package/mod/hooks/register.tsx +219 -19
  103. package/mod/types/index.d.ts +6 -0
  104. package/package.json +1 -1
package/README.md CHANGED
@@ -1,13 +1,14 @@
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), and step 9, the conductor mod for Claude Code in `mod/` (ATK-265).
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), step 12, the port broker (ATK-318), step 13, the ADO Boards tracker (ATK-319), and `conduct brief` for a worker's orient step (ATK-371).
4
4
 
5
5
  ## Commands
6
6
 
7
7
  ```text
8
8
  conduct serve run the daemon in this process (one per machine)
9
+ conduct start start the daemon detached and wait until it answers (see Console)
9
10
  conduct stop stop the running daemon
10
- conduct status [--json] the daemon's pid, port, projects and workers
11
+ conduct status [--json] the daemon's pid, port, projects, workers and held locks
11
12
  conduct project add <path> register the project whose checkout holds .claude/conductor.json
12
13
  conduct project list [--json] the registered projects
13
14
  conduct launch <item> start a worker: worktree, background session, tab
@@ -24,10 +25,36 @@ conduct notify profile [<name>] [--until <when>]
24
25
  switch where pushes go (see Notifications)
25
26
  conduct notify test [--kind <kind>] [--channel remote-control|pushover] [--raise]
26
27
  push a sample item now, or raise a real one with --raise
28
+ conduct queue [--json] [--fresh] what the scheduler picks next and why the rest wait (see Scheduler)
29
+ conduct pause [<project>] [--until <when>]
30
+ no new launches in a project, or in all of them, until a time
31
+ conduct unpause [<project>] launch again
32
+ conduct focus <project> [--slots <n>] [--weight <n>] --until <when>
33
+ a project's slots or ordering weight, for a while (neither: it goes first)
34
+ conduct why <event id | item> [--json]
35
+ the rule behind an action, and the events that led to it
36
+ conduct console [--project <name>] [--here]
37
+ open the console in a Windows Terminal tab, or in this terminal (see Console)
38
+ conduct escalations [--json] what rules escalated to the console and it hasn't handled
39
+ conduct escalations done <id> [--ask] <text>
40
+ the console's finding on an escalation; --ask puts it in the inbox
41
+ conduct history [<range>] [--since <when>] [--until <when>] [--project <name>] [--json]
42
+ what a window of the log says: shipped, phases, waits, stalls, spend (see History)
43
+ conduct retro [<range>] [--project <name>]
44
+ the same as a Markdown report
45
+ conduct rules [--json] the rules in force, what replaces what, any file refused (see Rules)
46
+ conduct rules test <file> [--json]
47
+ dry-run the rules on a sample event; acts on nothing
27
48
  conduct note <item> <file> post a phase note on the item and record its phase (see Notes)
28
49
  [--worker <worker>]
50
+ conduct brief <item> [--json] [--fresh]
51
+ the item's status, links, phase notes and approval, from the tracker (see Briefs)
29
52
  conduct wait <condition> wait for an outside event (see Waits)
30
- conduct events [--since <id>] [--follow]
53
+ conduct ports [<path>] [--prefer <slot>] [--json]
54
+ a checkout's dev ports from the port broker, or every worktree's (see Broker)
55
+ conduct lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
56
+ a lock every project shares, such as chrome (see Broker)
57
+ conduct events [--since <id>] [--follow] [--console]
31
58
  the event log, one JSON event per line (see API)
32
59
  conduct install-startup start the daemon at logon (Windows Task Scheduler)
33
60
  conduct install-startup --remove remove that logon task
@@ -38,6 +65,8 @@ conduct upgrade [--tag dev|latest] [--allow-downgrade]
38
65
 
39
66
  A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
40
67
 
68
+ `conduct status --json` exits 1 with `running: false` when no daemon listens on its port, such as while it restarts or upgrades. It still lists the workers in flight, read from the store with `workersFrom: "store"` and activity `daemon not running`; a store it can't read leaves `workers` out. A running daemon's answer says `workersFrom: "daemon"`.
69
+
41
70
  ## Workers
42
71
 
43
72
  The session manager owns the `claude --bg` sessions it launches, and only those:
@@ -48,7 +77,7 @@ The session manager owns the `claude --bg` sessions it launches, and only those:
48
77
  - **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.
49
78
  - **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
50
79
  - **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.
51
- - **park failures:** a session whose process outlives `claude stop` by 30 s logs one `worker.park-failed` per spell, and the park is tried again every 5 minutes.
80
+ - **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.
52
81
  - **stalls:** a permission prompt open `sessions.permissionMinutes` (5), or idle `sessions.idleMinutes` (20) with no wait, logs one `worker.stalled` event per spell, and `worker.stall-cleared` when that spell ends.
53
82
  - **spend:** tokens from the session's transcript and its subagents' (`~/.claude/projects/*/<session>.jsonl`), each message counted once, go into `spend` per worker and local day. It keeps input, output, cache read and cache write; `tokens` is their sum. A worker whose tokens in all, not counting cache reads, pass `inbox.runawayTokens` logs `worker.runaway` once per daemon run. Cache reads are left out because they are nearly all of a session's tokens (97% on ATK-312) at a tenth of the price.
54
83
  - **probes:** `--probe` names the worktree and session `probe-<item>` and gives it a prompt that works no issue, so tests stay out of the live fleet.
@@ -57,7 +86,9 @@ Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `fail
57
86
 
58
87
  ## Notes
59
88
 
60
- A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-issue · PR opened`) with `conduct note <item> <file>`. The daemon posts the file as a comment on the item in the tracker, then records the phase at once: on the worker (`phase` in `GET /fleet` and `conduct status`), on the item, and as a `note.posted` event. The worker is `--worker <worker>`, else the one whose worktree is the current checkout, else the project's live worker on that item. The project is the worker's, else the one whose tracker team is the item's key prefix.
89
+ A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-issue · PR opened`) with `conduct note <item> <file>`. The daemon posts the file as a comment on the item in the tracker, then records the phase at once: on the worker (`phase` in `GET /fleet` and `conduct status`), on the item, and as a `note.posted` event. The worker is `--worker <worker>`, else the one whose worktree is the current checkout, else the project's live worker on that item. The project is the worker's, else the one whose tracker key (the Linear team, or the Boards `key`) is the item's key prefix.
90
+
91
+ The work skill is the profile's `skills.work`. A note headed with one of `skills.aliases` counts the same, there and wherever the daemon reads notes (approvals, `conduct wait item … note`, the review-thread checks of `conduct wait pr … comment`). So when a project renames its work skill (ATK's `work-issue` became `work-item`), the old name goes in `aliases` and items in flight keep their phases.
61
92
 
62
93
  | Exit | Means | The worker then |
63
94
  |---|---|---|
@@ -65,7 +96,29 @@ A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one li
65
96
  | 3 | nobody posted it: no daemon, one without notes, or the tracker refused it (such as a read-only key) | posts the note to the tracker itself, as before protocol v1 |
66
97
  | 1 | refused (no heading, no project) or unclear (the daemon or Linear didn't answer the write) | reads the message; when it says the note may be there, looks before posting again |
67
98
 
68
- On Linear the daemon uses the key the watchers read (below), so that key needs **Create comments** as well as Read. Comments it writes carry the key owner's name, as the Linear MCP's do.
99
+ On Linear the daemon uses the key the watchers read (below), so that key needs **Create comments** as well as Read. Comments it writes carry the key owner's name, as the Linear MCP's do. On ADO Boards it posts a markdown discussion comment as the `az login` identity.
100
+
101
+ ## Briefs
102
+
103
+ `conduct brief <item>` gives a worker the item's context for its orient step, read by the daemon from the tracker, so the worker needs no MCP call. It gives:
104
+ - the title, URL, status (name and type), the tracker's project, Area and labels;
105
+ - the blockers, the items it blocks and the related items, each with its status and title;
106
+ - its phase notes (time, phase, kind and line), the newest Plan, and the `approve` / `revise:` after it with its words;
107
+ - the description.
108
+
109
+ `--json` prints it as one object.
110
+
111
+ The queue's snapshot has no descriptions, relations or comments, so the daemon reads the item itself:
112
+ - **Linear:** one GraphQL request with the watchers' key, and more only for comments past the first 100.
113
+ - **ADO Boards:** the work item with its links, its linked items' titles and states, its discussion comments and the `az login` identity. Predecessor is a blocker, Successor an item it blocks.
114
+
115
+ A brief is kept for 30 seconds. Anything the daemon sees happen to the item drops it: a note it posted, an `approve` or `revise` it posted, a wait resolved. `--fresh` reads the tracker again, as a worker checking for an approval after a wake should.
116
+
117
+ | Exit | Means | The worker then |
118
+ |---|---|---|
119
+ | 0 | the brief | uses it |
120
+ | 3 | no daemon, one without the brief API, or one that couldn't read the tracker (no key, the tracker down) | reads the item from the tracker itself (the Linear MCP, or the work skill's helper) |
121
+ | 1 | refused: no project tracks the item, or the tracker has no such item | reads the message |
69
122
 
70
123
  ## Inbox
71
124
 
@@ -86,10 +139,15 @@ An item resolves itself when its condition is seen, and records what resolved it
86
139
  | `npm-otp` | a `Handoff · npm-otp` note (the `npm stage approve …` on its heading is the command), or an `npm` wait | the version on its tag, or a `Resumed` or `Landed` note |
87
140
  | `jason`, `human`, `scope`, `blocked`, `abandoned`, `failed` | a Handoff note of that kind | any later note on the item (such as `Resumed`), or `conduct done` |
88
141
  | `permission`, `stall` | `worker.stalled` | `worker.stall-cleared`, a park or resume, a new wait (stall only), or a daemon restart (it is raised again if it persists) |
142
+ | `stall` (park failing) | `worker.park-failed`, with `conduct attach` and `conduct retire` | a park, the session stopping unexpectedly (which raises `failed`), its retirement, or a daemon restart (the park is tried again, and fails again if it still does). A new wait doesn't resolve it. A retire stops the session the same way, so when the pid outlives `claude stop`, end that process: the next sweep finds the session gone |
89
143
  | `failed` | a failed launch, or a session that stopped unexpectedly | a new launch, the worker running again, or its retirement |
144
+ | `conflict` | `pr.conflict-stuck`: a watched PR has conflicted with its base for `watchers.conflictMinutes` (30) since its worker was woken for it, and the wake never reached the worker or a pass still sees the conflict; with `conduct attach` and the PR link | the PR merging cleanly again, merged or abandoned (`pr.conflict-cleared`), its retirement, or a daemon restart (the spell starts over) |
90
145
  | `runaway` | a worker passing `inbox.runawayTokens` (2M) input, output and cache-write tokens in all, once per worker | `conduct done`, or its retirement |
146
+ | `ready-to-release`, `queue-drained` | the scheduler's default rules (see Scheduler) | `conduct done` |
147
+ | `console` | the console's finding on an escalation, with `conduct escalations done <id> --ask` (see Console) | `conduct done` |
148
+ | `console` (no console open) | escalations wait and no console has followed the stream for 30 s, with `conduct console` and `conduct escalations` (see Console) | a console opening, the last escalation handled, or `conduct done` |
91
149
 
92
- Nothing raises `ready-to-release`, `queue-drained` or `prod-approval` yet. The scheduler (ATK-315) and `/release-atk` will raise them. When a worker is retired, its open items resolve with it.
150
+ Nothing raises `prod-approval` yet; `/release-atk` will. When a worker is retired, its open items resolve with it.
93
151
 
94
152
  For an item raised from a note:
95
153
 
@@ -107,7 +165,7 @@ If the tracker refuses the comment, nothing is resolved. Only a plan takes appro
107
165
 
108
166
  The notifier routes each new inbox item to the phone, on two channels:
109
167
 
110
- - **Remote Control:** the daemon appends `notify.requested` (`channel: "remote-control"`), and the conductor mod in the console pushes it with PushNotification. It reaches the phone only while the console is connected to Remote Control (see Mod).
168
+ - **Remote Control:** the daemon appends `notify.requested` (`channel: "remote-control"`), and the conductor mod in the console pushes it with PushNotification. It reaches the phone only while the console is connected to Remote Control (see Mod). On Claude Code 2.1.296 these pushes are reported sent but never reach the phone (ATK-359), so the built-in `default` and `focus` also send to Pushover until a Claude Code release fixes it.
111
169
  - **Pushover:** sent from the daemon. The keys are `PUSHOVER_TOKEN` and `PUSHOVER_USER`, or else the Windows generic credential `atk-conductor-pushover` (`notify.pushover.credential`): its **user name is your Pushover user key** and its **password is the application's API token**. Save it with `cmdkey /generic:atk-conductor-pushover /user:<user key> /pass:<app token>` or in Credential Manager. A credential saved while the daemon runs is read on the next push. The keys are never logged or stored.
112
170
 
113
171
  **Profiles** pick the channels. A profile is a list of rules, and the first rule that matches an item says where it goes; when none matches, the item stays in the inbox, unpushed. Three are built in, and `notify.profiles` in `config.json` replaces one by name or adds more:
@@ -118,9 +176,9 @@ The notifier routes each new inbox item to the phone, on two channels:
118
176
  "notify": {
119
177
  "profile": "default", // the profile when none is switched to
120
178
  "profiles": {
121
- "default": [{ "to": ["remote-control"] }], // built in
122
- "away": [{ "to": ["pushover"] }], // built in
123
- "focus": [{ "kinds": ["npm-otp", "permission", "failed"], "to": ["remote-control"] }], // built in
179
+ "default": [{ "to": ["remote-control", "pushover"] }], // built in
180
+ "away": [{ "to": ["pushover"] }], // built in
181
+ "focus": [{ "kinds": ["npm-otp", "permission", "failed", "conflict", "jason"], "to": ["remote-control", "pushover"] }], // built in
124
182
  "workday": [
125
183
  { "projects": ["cadence"], "days": ["sat", "sun"], "to": [] }, // keep these off every channel
126
184
  { "kinds": ["npm-otp"], "to": ["pushover", "remote-control"], "remind": { "afterMinutes": 10, "times": 5 } },
@@ -138,7 +196,74 @@ A rule matches on `kinds` (inbox kinds), `projects`, `hours` (local, `HH:MM-HH:M
138
196
  - **Reminders:** an open item is pushed again `notify.remind.afterMinutes` (30) after its last push, at most `notify.remind.times` (2) times, as `reminder 1/2: …`, on the channels the profile in force then gives it. A rule's own `remind` overrides that. A resolved item is never reminded. A first push that reached no channel (Pushover down, no credential) is tried again at the same interval without using up a reminder.
139
197
  - **Events:** `notify.routed` (the profile, the rule, the channels, `sent`, `held`, `deduped` or `none`), `notify.requested`, `notify.sent` and `notify.failed` (Pushover), `notify.channel` (a channel's state changed), `notify.profile`.
140
198
 
141
- `conduct notify` shows the profile, quiet hours and the last state of each channel: Pushover from its own answers, Remote Control from what the mod reports back from PushNotification after each push: `ok` (it reached the phone), `inactive` (Remote Control off, or mobile pushes off in Claude Code's config), `skipped` (not sent to the phone on purpose, such as while you are at the terminal: `user_present`) or `failing` (the call failed), each with its reason.
199
+ `conduct notify` shows the profile, quiet hours and the last state of each channel: Pushover from its own answers, Remote Control from what the mod reports back from PushNotification after each push: `ok` (Claude Code reported it sent; see ATK-359 above for why that doesn't prove it reached the phone), `inactive` (Remote Control off, or mobile pushes off in Claude Code's config), `skipped` (not sent to the phone on purpose, such as while you are at the terminal: `user_present`) or `failing` (the call failed), each with its reason.
200
+
201
+ ## Rules
202
+
203
+ Rules say what the conductor does when something happens (design note, section 9). Each is a `when` and a `do` in a fixed vocabulary, written in JSONC:
204
+
205
+ ```jsonc
206
+ // ~/.conductor/rules.json
207
+ [
208
+ { "when": "wait.kind == merge and wait.age > 2h", "do": "remind jason" },
209
+ { "when": "plan.tier == S and item.area in [docs, ops]", "do": "auto-approve after 15m" },
210
+ { "id": "runaway", "when": "spend.worker > 5M", "do": "inbox runaway" }, // replaces the default
211
+ { "id": "daily-cap", "do": "none" }, // switches a default off
212
+ { "when": "project == web and day == saturday", "do": "pause launches" }
213
+ ]
214
+ ```
215
+
216
+ - **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`, `runaway`, `daily-cap`, `ready-to-release`, `queue-drained`, and `conductor-handoff`, which escalates a worker's `Handoff · conductor` note to the console). A rule with an `id` replaces the rule with the same id further down, so a file holds only the differences.
217
+ - **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed`, `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `queue.pick`, `queue.drained`, `queue.ready-to-release`. 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`, `plan.tier`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `spend.worker/item/project/today`, `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
218
+ - **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 after <duration>`, `inbox <kind> ["<text>"]`, `remind jason`, `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.
219
+ - **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`, `retry` outside `run.failed`. `merge`, `publish`, `approve`, `delete` and `run` aren't in the vocabulary.
220
+ - **Reload:** saving `rules.json` or a project's profile reloads its rules. A profile save takes only the `rules`; the rest of a profile changes with `conduct project add`. A file the conductor refuses leaves its last good rules 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.
221
+ - **Checking:** `conduct rules` lists the rules in force (`project.atk:#2`, `machine:runaway`, `default:launch`) and what replaces what. `conduct rules test <file>` dry-runs a sample, such as `{ "event": "pr.comment", "project": "atk", "fields": { "wait.kind": "merge", "wait.age": "3h" } }` (leave out `event` for a state rule), and shows what fires, each action's guardrail, and why every other rule of the trigger didn't. Both read the files directly when the daemon isn't running.
222
+
223
+ - **Why:** every action the conductor takes is an event that names its rule, directly (`rule`) or through the event that caused it (`data.cause`). `conduct why <event id>` follows that chain back and shows the rule as it fired; `conduct why ATK-315` does it for the item's newest actions.
224
+
225
+ **How actions are carried out.** Every action is a `rule.fired` event that names its rule, then `rule.done` or `rule.failed`.
226
+
227
+ - **The scheduler** does `launch` on `queue.pick` and `retire` on `item.closed` (see Scheduler).
228
+ - **The session manager** asks the rules before each `park` and `resume`. A resume for a review comment is also a `pr.comment`.
229
+ - **Event rules** run on the daemon's events.
230
+ - **State rules** are checked each minute against every wait, inbox item, worker, project and the machine. Each fires once while it holds and again after it stops holding; a restart remembers which hold. A `pause launches` with no end, from a state rule, lifts when the rule stops holding (`rule.released`), and only the pause that rule set.
231
+
232
+ What each action does:
233
+ - `remind jason` pushes the subject's inbox item again: the wait's gate, or the worker's item.
234
+ - `notify <channel>` pushes once. `notify profile` switches the profile.
235
+ - `inbox <kind>` raises an item, once while it is open. `inbox runaway` is raised once per worker.
236
+ - `pause` / `unpause` / `focus` set overrides that name the rule.
237
+ - `label add|remove` edits one label on the item (the profile's human label for `human`), at most 20 times a day per item.
238
+ - `escalate console` emits `console.escalated`, which wakes the console's model; the console reports back with `conduct escalations done` (see Console).
239
+ - `retry` launches a failed launch again, or wakes a session that stopped unexpectedly with the fact. It does this once per item, whatever the rule says: a second failure stays in the inbox. `worker.retries` counts the earlier tries.
240
+ - `auto-approve after N` pushes "auto-approves at HH:MM" where plans go. At the deadline, if the plan still waits (no `revise:`, no newer Plan), it posts `approve: auto-approved by the conductor: rule "<id>", no revise by HH:MM` as the key's owner, which the worker's approval wait reads. No default rule auto-approves. A Plan note's `**Size tier: S**` gives `plan.tier` (and `item.tier` later).
241
+ - `launch` on `item.unblocked` brings the scheduler's next tick forward.
242
+ - `launch` and `retry` start sessions, so they wait for `scheduler.mode` `on`, as the scheduler's own launches do. In `observe` they are logged, not done; every other action acts in any mode.
243
+
244
+ The defaults `runaway` and `daily-cap` are the budget: a runaway inbox item past `inbox.runawayTokens` for one worker, and no new launches past `scheduler.dailyTokens` today until midnight. Both count tokens without cache reads.
245
+
246
+ ## Scheduler
247
+
248
+ The scheduler moves conduct-atk's tick into the daemon (design note, section 8). Every `scheduler.tickSeconds` (120), and 10 s after a phase note, a retire, a failed launch or a cleared inbox item, it does three things.
249
+
250
+ 1. **Read.** It reads each conducted project's queue from its tracker: for Linear, the team's projects with a priority, the Area labels, every open item with its blockers, and the in-flight items. Each read is one request (plus pages), within the Linear watcher's rate budget. For ADO Boards, the profile's saved query (see ADO Boards), within the ADO watcher's budget.
251
+ 2. **Plan.** It works out the picks across projects, with the same order and reasons as conduct-atk's `planQueue`. A replay of conduct-atk's own Linear read is in the tests.
252
+ - **Order:** the tracker project's priority, then the conductor project's `weight` (profile, or `conduct focus --weight`), then the item's priority, then when it was created.
253
+ - **Areas:** one in-flight item per Area per project. An Area is held by every worker in flight and every started item, and by the Areas it covers or is covered by.
254
+ - **Not picked:** items labelled with the profile's human label, and items whose blockers are still open, which wait (pending, or "not being worked" when nothing will clear them).
255
+ - **Slots:** `scheduler.slots` (4) across projects and the profile's `slots` per project (ATK's is 4), or `conduct focus --slots`, never more than the global ones. Slots count running workers only: a parked worker keeps its Area and frees its slot.
256
+ - **Holds:** quiet hours and `conduct pause` turn picks into waits that say why.
257
+ 3. **Act.** It runs the rules on what it found: `queue.pick` → `launch` (default `launch`), `item.closed` → `retire` (default `retire-closed`; a refused retire raises an inbox item once, except one refused while the worker's turn runs or its session starts, which is tried again each tick and raised only after 15 minutes), `queue.drained` and `queue.ready-to-release` → an inbox item once a day. Each action is a `rule.fired` event, then `rule.done` or `rule.failed`. A launch that failed isn't tried again while its inbox item is open.
258
+
259
+ `scheduler.mode` decides how far it goes:
260
+ - **`on`** (the default): it acts.
261
+ - **`observe`:** it reads, plans and logs each action once, and does none of them. Set it on a machine where conduct-atk's loop still launches.
262
+ - **`off`:** it doesn't run; `conduct queue --fresh` still works out a plan on request.
263
+
264
+ `conduct queue` shows the last plan (`--fresh` reads again): slots, holds, picks with their Area, and why each other item waits. A change of plan is a `scheduler.planned` event. A project whose read fails picks nothing that tick and logs `scheduler.read-failed` once, then `scheduler.read-recovered`. An item whose last open blocker closed is an `item.unblocked` event.
265
+
266
+ **Overrides** end on their own: `conduct pause atk --until 17:00`, `conduct pause --until tomorrow` (every project), `conduct unpause atk`, `conduct focus atk --slots 3 --until monday`. `--until` takes `17:00` (the next one), `tomorrow`, a day, or a date and time. Each is an `override.set` event, which `conduct why` attributes to the command, and they show in `conduct rules`.
142
267
 
143
268
  ## Waits
144
269
 
@@ -153,8 +278,8 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
153
278
  | `run atk-api-deploy 12345 done` | `run:ado:atk-api-deploy#12345:done` | the run completed, with its result |
154
279
  | `npm 8.5.0-dev.2 --tag dev` | `npm:@detergent-software/atk@8.5.0-dev.2:dev` | the dist-tag points at the version |
155
280
  | `health dev --version 1.2.3` / `--commit <sha>` | `health:dev:version=1.2.3` | the profile's `health.dev` URL answers with that version or commit |
156
- | `approval` | `approval:linear:ATK-310` | an `approve` / `revise:` comment by the Linear key's owner after the item's newest `<work skill> · Plan` note |
157
- | `item ATK-271 done` / `label human` / `note Landed` | `item:linear:ATK-271:state=done` | the state (name or type), a label, or a phase note |
281
+ | `approval` | `approval:linear:ATK-310` (`approval:ado-boards:CAD-12` on Boards) | an `approve` / `revise:` comment by the Linear key's owner (on Boards, the `az login` identity) after the item's newest `<work skill> · Plan` note |
282
+ | `item ATK-271 done` / `label human` / `note Landed` | `item:linear:ATK-271:state=done` | the state (name or type), a label (a tag on Boards), or a phase note |
158
283
  | `time 2026-10-10T09:00` | `time:2026-10-10T14:00:00.000Z` | the time has passed (no zone means local time) |
159
284
 
160
285
  - **With the daemon**, in a worker's worktree (or with `--worker <worker>`), `conduct wait` registers the wait and returns at once. The daemon parks the session when its turn ends and resumes it with `[conductor] event: <resolution> HH:MM` once the condition holds. The text is a fact, never an approval: the worker reads the source again. The daemon checks waits against the project's profile as registered (`conduct project add`), and refuses one it can't check, such as a `health` environment the profile has no URL for.
@@ -162,13 +287,96 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
162
287
 
163
288
  **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.
164
289
 
290
+ **PR conflicts.** Azure DevOps runs no build on a PR that conflicts with its base branch, so its `checks` never settle and it can't merge. The ADO watcher reads the PR's `mergeStatus` and target branch with the PR itself, at no extra request, and doesn't ask a conflicted PR's build evaluations, which can only be stale. The first pass that sees `conflicts` starts a spell for each worker waiting on the PR: it logs `pr.conflicted` and resolves the worker's waits on the PR with `pr <n> conflicts with <base>: merge origin/<base> into the branch (no rebase), resolve, rerun the checks, push and wait on the PR again` (after a wait's own line when it resolved on its own, such as a comment), which wakes the worker. Later passes in the spell wake nobody, unless the PR conflicts on a new source commit: the worker pushed and the base moved again, which starts a new spell. A pass that sees a clean merge, or the PR merged or abandoned, ends it (`pr.conflict-cleared`). A spell older than `watchers.conflictMinutes` (30) whose wake hasn't reached the worker, or whose conflict a pass still sees, logs `pr.conflict-stuck` once and raises a `conflict` inbox item. Spells are kept in memory, so after a restart a PR still in conflict wakes its worker once more. `conduct wait` without a daemon returns the same line while the PR conflicts.
291
+
292
+ ## ADO Boards
293
+
294
+ A project whose work items live in Azure DevOps Boards (Cadence) sets `tracker.kind` to `ado-boards` (design note, section 12; ATK-319):
295
+
296
+ ```jsonc
297
+ "tracker": {
298
+ "kind": "ado-boards",
299
+ "key": "CAD", // items are CAD-<work item id>: ADO's ids are numbers alone
300
+ "query": "Shared Queries/Conduct", // the saved query that is the queue, by path or id
301
+ "humanLabel": "human", // a tag here
302
+ "priority": 3, // the queue's priority against other projects' (1 urgent … 4 low)
303
+ "areaRoot": "Emergent Software\\Cadence", // optional: an item's Area is its Area Path below it
304
+ "states": { "Blocked": "started" } // optional, over the built-in state names
305
+ // "organization", "project": optional, the code host's by default
306
+ }
307
+ ```
308
+
309
+ | Tracker concept | On Boards |
310
+ |---|---|
311
+ | the queue | the saved query, a flat list, in its own order (by default the items tagged `conduct` that aren't Done or Removed, by Priority, then Backlog Priority; leaving closed items out keeps each read small). Its open items, and the in-flight ones, are one tracker project named after the query, with the profile's `priority`; within a priority the scheduler keeps the query's order. |
312
+ | Area | the Area Path below `areaRoot` (`Web/Forms`); an item at the root holds none |
313
+ | blockers | Predecessor links whose item is still open, inside the query or not |
314
+ | human, labels | tags |
315
+ | states | `New`, `Approved`, `To Do`, `Proposed` → unstarted; `Committed`, `Active`, `In Progress`, `Resolved` → started; `Done`, `Closed` → completed; `Removed` → canceled; `states` adds or overrides. A state neither names is never launched or retired. |
316
+ | phase notes, `approve` / `revise:` | discussion comments: notes are posted as markdown; a comment typed in the web UI (HTML) is read as its text |
317
+
318
+ Everything goes through the `az login` token. The reads (the scheduler's queue and the watchers' waits) take from the `ado` watcher's budget, as PR and pipeline waits do. A wait pass reads every waited item of a board in one batch, and an item's comments only when its rev or comment count moved. Notes and the rules' `label` edits (a patch of `System.Tags` alone, refused if the item changed since it was read) are writes, outside the budget, as on Linear.
319
+
320
+ ## Broker
321
+
322
+ The broker shares what every project on the machine uses (ATK-318).
323
+
324
+ **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 }`.
325
+ - A main checkout serves on the base ports (slot 0).
326
+ - 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.
327
+ - A retired worker's worktree frees its slot, and so does a worktree whose directory is gone (at the next assignment).
328
+ - Ports are compared, not slots, so projects with different bases stay apart too.
329
+ - 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.
330
+ - 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.
331
+ - A stored assignment follows its project's profile: when the base ports or `maxSlot` change, the worktree moves, its old slot first.
332
+
333
+ **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).
334
+ - The owner is `--owner`, else the item of the worker whose worktree this is.
335
+ - Acquiring again refreshes it. 30 minutes after its holder last acquired it, another owner may take it over.
336
+ - `--wait <minutes>` asks again every 15 s until then.
337
+ - Retiring a worker releases the locks its item holds (a probe's retire doesn't).
338
+ - `conduct status` lists the locks held, each with its holder, its last acquire and whether it is stale.
339
+ - Exit 3 when no daemon answers.
340
+
341
+ Events: `ports.assigned`, `ports.freed`, `lock.acquired`, `lock.released`, `lock.taken-over`.
342
+
343
+ ## History
344
+
345
+ `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).
346
+
347
+ - **Window.**
348
+ - `<range>` is `today`, `yesterday`, a date (`2026-10-10`, that day), `A..B` (`2026-10-10..2026-10-11`, through B), or a span back from now (`24h`, the default; `7d`; `90m`).
349
+ - Instead of a range, `--since` and `--until` take a time each, such as `2026-10-11T09:00`, `yesterday` or `6h`.
350
+ - Times are local, and a window never reaches past now.
351
+ - **What it measures:**
352
+ - **Shipped:** items with a Landed note in the window, with the time from their Started note.
353
+ - **Phases:** the time from each lifecycle note (Started, Done when or Repro, Plan, Plan approved, Verified, PR opened, Landed) to the item's next one, clipped to the window. Results are grouped by the tier on the item's newest Plan note (`?` without one). Handoff, Progress and Resumed notes fall inside a phase.
354
+ - **Plan latency:** from the newest Plan note to Plan approved, so a revision restarts the clock, plus the plans still waiting.
355
+ - **Waits:** the waits registered in the window, by what they held up: `plan`, `merge`, `checks`, `review`, `ci`, `npm-otp`, `time`, `item` and `health`.
356
+ - One PR event often resolves its merged, checks and comment waits together; they count once, under what happened.
357
+ - An open wait counts up to the window's end.
358
+ - Waits the manager closed itself (`worker retired`, `launch failed`) are counted apart.
359
+ - **Stalls and failures:**
360
+ - `worker.stalled` events, and Handoff notes by kind;
361
+ - failed launches, resumes and parks, workers stopped unexpectedly, refused or partial retires, failed rules and watchers;
362
+ - items launched more than once;
363
+ - unfinished items with their phase.
364
+ - **Inbox:** the items raised, by kind, with how long each took to resolve and how it was resolved: `conduct approve`/`revise`/`done`, a later note, the worker retired, or the condition seen.
365
+ - **Slots:** peak and average running workers, and running and parked hours by project and item.
366
+ - A slot is counted as the scheduler counts it, so a worker left attached and idle holds its slot the whole time.
367
+ - **Spend:** tokens by project and item. `spend` is kept per local day, so this covers the whole days the window touches.
368
+ - Tokens are input, output and cache writes; cache reads have their own column.
369
+ - **Rules:** how often each rule fired, and how its actions went.
370
+ - `conduct retro` prints the same as Markdown. The console can post it on a Linear issue, or it can be pasted anywhere.
371
+
165
372
  ## Files
166
373
 
167
374
  Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
168
375
 
169
376
  | File | Holds |
170
377
  |---|---|
171
- | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `inbox.runawayTokens` (2000000); `notify` and `quietHours` (see Notifications); `sessions` holds `pollSeconds` (10), `settleSeconds` (5), `permissionMinutes` (5), `idleMinutes` (20); `watchers` holds `ado`, `github`, `health`, `linear` and `npm`, each `{ intervalSeconds, perMinute }` (30/60, 30/30, 30/30, 30/10, 20/30), and `statsMinutes` (10) |
378
+ | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `inbox.runawayTokens` (2000000, the `runaway` rule's number); `notify` and `quietHours` (see Notifications); `scheduler` holds `mode` (`on`, `observe` or `off`), `slots` (4, across projects, and the most any rule can give one project), `dailyTokens` (20000000, the `daily-cap` rule's number) and `tickSeconds` (120); `sessions` holds `pollSeconds` (10), `settleSeconds` (5), `permissionMinutes` (5), `idleMinutes` (20); `watchers` holds `ado`, `github`, `health`, `linear` and `npm`, each `{ intervalSeconds, perMinute }` (30/60, 30/30, 30/30, 30/10, 20/30), `statsMinutes` (10) and `conflictMinutes` (30) |
379
+ | `rules.json` | machine rules, JSONC, reloaded on save (see Rules) |
172
380
  | `token` | the API token, readable by this user only; every request sends `Authorization: Bearer <token>` |
173
381
  | `conductor.pid`, `port` | the single-instance lock and where the API listens (`127.0.0.1`) |
174
382
  | `conductor.db` | SQLite state, including the append-only `events` log |
@@ -177,13 +385,17 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
177
385
  ## API
178
386
 
179
387
  - `GET /projects`: the registered projects. `POST /projects` with `{ "path": "<absolute path>" }` registers one.
180
- - `GET /events`: the event log as server-sent events. Add `?since=<id>` (or send `Last-Event-ID`) to replay from an id. Without either, the stream starts at the live end. `conduct events` prints it one JSON event per line: the events after `--since` (all of them without it), or with `--follow` everything from there on (live without `--since`). `--follow` reconnects with `Last-Event-ID` when the daemon goes away, so a restart loses nothing, and prints the stream's state as lines without an id: `{"kind":"conduct.connected"}` and, once per outage, `{"kind":"conduct.disconnected","error":…}`.
388
+ - `GET /events`: the event log as server-sent events. Add `?since=<id>` (or send `Last-Event-ID`) to replay from an id. Without either, the stream starts at the live end. `conduct events` prints it one JSON event per line: the events after `--since` (all of them without it), or with `--follow` everything from there on (live without `--since`). `--follow` reconnects with `Last-Event-ID` when the daemon goes away, so a restart loses nothing, and prints the stream's state as lines without an id: `{"kind":"conduct.connected"}` and, once per outage, `{"kind":"conduct.disconnected","error":…}`. `?console=1` (`conduct events --follow --console`, which the conductor mod runs, or `conduct events --follow` with `CONDUCTOR_CONSOLE=1`, as a mod from before `--console` runs it until `/reload-plugins`) marks the console's stream: while one is open, escalations have a console to wake (see Console).
181
389
  - `POST /shutdown`: a clean stop (`conduct stop`).
390
+ - `GET /console/escalations`: the open escalations, oldest first (see Console).
391
+ - `POST /console/escalations/:id` with `{ "text": "<finding>", "ask"?: true }`: the console's report on one; 201 with the escalation, the `console.handled` event's id and the inbox item `ask` raised. 404 when the id isn't an escalation, 409 when it is handled already or older than 14 days.
392
+ - `POST /console/cache` with `{ "action": "warm" | "cold" | "failed", "contextTokens", "usage": { "cacheRead", "cacheWrite", "input", "output" } | null, "warms" }` or `{ "action": "clear", "contextTokens", "reason" }`: the mod's report on the idle console's prompt cache (see Mod); 201 with the `console.cache` event's id.
182
393
  - `GET /fleet`: the workers in flight, with session activity, waits and spend (today, total).
183
394
  - `POST /workers` with `{ "item": "ATK-309", "project"?, "probe"?, "prompt"?, "conductor"? }`: launch; answers 202 with the `launching` worker. `GET /workers/:id` shows it with its latest events.
184
395
  - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work when it refuses.
185
396
  - `POST /workers/:id/attach`: wake if parked, mark attached, open the tab.
186
397
  - `POST /items/:id/notes` with `{ "body": "## work-issue · …", "worker"?: 1 }`: post a phase note (see Notes); 201 with the note and the worker. 409 when it won't post it; 502 when the tracker didn't take it, with `posted: "no"` or `"unknown"`.
398
+ - `GET /items/:id/brief` (`?fresh=true`): the item's brief (see Briefs), as `{ "brief": … }`. 404 when the tracker has no such item, 409 when no project tracks it, 502 when the tracker couldn't be read.
187
399
  - `GET /inbox`: the open inbox items, oldest first.
188
400
  - `POST /inbox/:id/resolve`: Jason's answer (see Inbox). The body is `{ "action": "approve", "text"? }`, `{ "action": "revise", "text" }` or `{ "action": "done" }`.
189
401
  - 200 with the item, the comment and the waits it resolved.
@@ -194,6 +406,14 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
194
406
  - `POST /notify/profile` with `{ "name": "away", "until"?: "<ISO time>" }`: switch; 409 for a profile that doesn't exist.
195
407
  - `POST /notify/test` with `{ "kind"?: "npm-otp", "channel"?: "pushover", "raise"?: false }`: push a sample now and answer each channel's result, or with `raise` put a real item in the inbox (201).
196
408
  - `POST /notify/report` with `{ "channel": "remote-control", "result": "<PushNotification's answer>", "state"?: "ok" | "inactive" }`: the mod's report of a push.
409
+ - `GET /rules`: the rules in force, each with its source, `ref`, `when`, `do` and the rules that replace it, plus any file refused and the live `overrides` (see Rules).
410
+ - `GET /queue`: the scheduler's last plan; `?fresh=true` reads the trackers and plans now (see Scheduler).
411
+ - `GET /overrides`, and `POST /overrides` with `{ "action": "pause", "project"?, "until"? }`, `{ "action": "unpause", "project"? }` or `{ "action": "focus", "project", "slots"?, "weight"?, "until" }` (and `by`, who set it): 201 with the overrides; 409 for an unknown project, a time past, or a focus over `scheduler.slots`.
412
+ - `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.
413
+ - `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.
414
+ - `POST /rules/test` with `{ "event"?: "pr.comment", "project"?: "atk", "fields"?: { … } }`: a dry run; 400 for a sample it can't read.
415
+ - `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.
416
+ - `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.
197
417
  - `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.
198
418
  - A refusal (a duplicate launch, a bad condition, a busy worker) is a 409 with `error` and `details`.
199
419
 
@@ -207,7 +427,20 @@ conduct install-startup
207
427
  conduct project add <path to the project's main checkout>
208
428
  ```
209
429
 
210
- `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.
430
+ `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. The daemon reloads a profile's `rules` when the file changes; every other change, such as a new `skills.work`, takes effect at the next `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. The same holds for the copy the daemon stored at `conduct project add`: after an `--allow-downgrade` to a version without those fields, the project shows `the stored profile no longer validates` and isn't conducted until a profile without them is added again.
431
+
432
+ The profile's optional `work` section is for the project's work skill; the daemon only checks it:
433
+
434
+ ```jsonc
435
+ "work": {
436
+ // Changes under these paths, and plans of these size tiers, land in review mode.
437
+ "landing": { "review": ["infra/", "pipelines/"], "reviewTiers": ["L"] },
438
+ // The pipelines that stage an npm package for a merge, by the key the work skill reports them under.
439
+ "releases": [{ "key": "cli", "pipeline": "pipelines/release-cli.yml", "package": "@detergent-software/atk" }],
440
+ // The project's own steps, which the work skill follows at its hook points.
441
+ "hooks": ".claude/work-hooks.md"
442
+ }
443
+ ```
211
444
 
212
445
  The logon task runs `node` on the `cli.js` that ran `install-startup`. A global install keeps that path across upgrades, so the task needs registering once. To upgrade:
213
446
 
@@ -228,24 +461,44 @@ conduct install-startup
228
461
 
229
462
  To switch from a linked build to the package: `conduct stop`, `npm rm --global @detergent-software/conductor` (removes the link), install the package as above, and run `conduct install-startup` again so the logon task points at the installed `cli.js`.
230
463
 
464
+ ## Console
465
+
466
+ The console (design note, section 11) is the one Claude session that stays open beside the daemon. The conductor mod in it does the plumbing in code (see Mod). Its model wakes only for an escalation or for you, and runs the project's console skill (`/conductor-console`, installed by ATK). It has no loop: no Monitor, no heartbeat, no ticks. Everything it knows comes from the daemon, so a new console after a close loses nothing.
467
+
468
+ ```bash
469
+ conduct console # a tab in Windows Terminal (window "conductor"), in the project's main checkout
470
+ conduct console --here # the same session in this terminal
471
+ conduct console --project cadence # with several projects registered: whose checkout it runs in
472
+ ```
473
+
474
+ It runs `claude -n conductor --permission-mode auto -- /conductor-console` with `CONDUCTOR_CONSOLE=1`. 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.
475
+
476
+ **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.
477
+
478
+ **With no console open.** The daemon counts the console's streams of `/events` (the mod's `conduct events --follow --console`). When escalations are open and no console stream has been open for 30 s, it raises one `console` inbox item, "escalations wait for the console and none is open: start one with conduct console", pushed like any other. The 30 s cover a console's own reconnects (the mod restarting `conduct events`, a daemon restart). The item resolves when a console opens or the last escalation is handled. The built-in `focus` profile doesn't push the `console` kind. The daemon never opens a console itself: that would wake a model on every escalation, unattended.
479
+
480
+ **Moving from conduct-atk.** Stop the conduct-atk session, `conduct upgrade` (the scheduler now launches by default), `atk install` in the main checkout, then `conduct console`. A machine that keeps conduct-atk's loop sets `"scheduler": { "mode": "observe" }` in `config.json`.
481
+
231
482
  ## Mod
232
483
 
233
484
  `mod/` is the conductor mod: a Claude Code plugin (a hooks module, code that costs no tokens) that does the console's plumbing. `conduct install-mod` installs it for this user from the package's own `mod/` folder, as a folder marketplace (`claude plugin install conductor --marketplace <package>/mod --scope user`). Claude Code reads a folder marketplace's plugin in place, so `conduct upgrade` replaces the mod along with the daemon; open sessions load the new version with `/reload-plugins`. `--remove` uninstalls it.
234
485
 
235
- - **Every session** shows a status line, `conductor 3▶ 2⏸ ✉2` (running, parked, inbox), from `GET /fleet` and `GET /inbox` every 30 s. It clears while no daemon answers.
236
- - **The console**, the session started with `CONDUCTOR_CONSOLE=1` (`$env:CONDUCTOR_CONSOLE=1; claude -n conductor` until the console skill, ATK-314, starts it), also gets:
486
+ - **Other sessions** get nothing from it: no status line, and no calls to the daemon.
487
+ - **The console**, the session started with `CONDUCTOR_CONSOLE=1` (`conduct console` starts it; see Console), reads `GET /fleet`, `GET /inbox` and `GET /notify` every 30 s and gets:
237
488
  - **the band** above the prompt while the inbox has items: one hotkey per item (a plan approves after a second press, `y`, since a digit typed into an empty prompt presses a band button; an npm OTP copies its `npm stage approve` line; the rest open the pane), and `o` for the pane;
238
- - **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, copy a command, open a link, done), the workers by project (a row expands to attach), and today's spend;
239
- - **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`;
240
- - **the stream**: it runs `node <package>/build/cli.js events --follow`, redraws on each event, toasts each new inbox item, and acts on three events:
489
+ - **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, copy a command, open a link, done), the workers by project (each project's rule has [pause] and [focus]; a row expands to attach and why), and today's spend;
490
+ - **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`, `/pause [<project>] [--until <when>]`, `/unpause [<project>]`, `/focus-project <project> [--until <when>]` (puts its items first, until midnight without `--until`; not `/focus`, which is Claude Code's own);
491
+ - **a check that `/conductor-console` ran**: a console started without it (by hand, as `claude -n conductor`) shows `console without its skill: run /conductor-console` in the band, and each prompt, Jason's or an escalation's, carries a line telling the model to load the skill before it answers. Running the skill (`/conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
492
+ - **a warm prompt cache while it idles** (ATK-386). The console's cache lasts an hour after its last use, and a wake after that writes the whole context again. So 55 minutes after the console's last turn, the mod sends one ping (`$.model.fork`, the console's own last request plus a one-word ask, no tools, no turn in the transcript). The API serves the ping from the cache for a tenth of a write's price, and the hour starts over. It pings again every 55 minutes until a turn comes, and each turn starts the timer over. At an idle point it runs `/clear` instead when the context is 150k or more, or after 12 pings in a row (the 13th idle point, about 12 hours after the last turn), and the band says `fresh console (cleared while idle: …)` until the next prompt loads the skill. A ping that read less than half the context from the cache (the cache was gone anyway, as under a 5-minute TTL) stops the pings until the next turn. Each ping and clear goes to `POST /console/cache`, which logs a `console.cache` event with the context size and what the ping cost. A fork isn't in the transcript, so that event is the only place its cost shows;
493
+ - **the stream**: it runs `node <package>/build/cli.js events --follow --console` (`--console` tells the daemon a console is open), redraws on each event, toasts each new inbox item, and acts on three events:
241
494
 
242
495
  | Event | Data | The mod |
243
496
  |---|---|---|
244
497
  | `notify.requested` | `channel: "remote-control"`, `message` | pushes it to the phone (PushNotification), with no model turn, and reports every outcome to `POST /notify/report` (see Notifications). When the push comes back "Remote Control inactive" (PushNotification's `disabledReason` `no_transport`), mobile pushes are off in Claude Code's config (`config_off`), or the call fails, it toasts once, saying which; it says so again only after a push has reached the phone. A push Claude Code holds back while you are at the terminal (`user_present`) is reported as `skipped`, with no toast |
245
- | `console.escalated` | `text` | wakes the console's model once with `[conductor] escalation on <item>: <text>`; nothing else wakes it |
498
+ | `console.escalated` | `text` | wakes the console's model once with `[conductor] escalation #<event id> on <item>: <text>`; nothing else wakes it |
246
499
  | `worker.message-requested` | `to`, `message` | sends the message to that worker's session |
247
500
 
248
- The notifier emits `notify.requested` (see Notifications). The rules engine (ATK-315) will emit the other two.
501
+ The notifier emits `notify.requested` (see Notifications), and the rules' `escalate console` action emits `console.escalated`. Nothing emits `worker.message-requested` yet.
249
502
 
250
503
  Its checks: `pnpm -C apps/conductor test` runs `mod/tests/model.spec.ts` (the logic, in CI). `claude plugin validate apps/conductor/mod` and `claude plugin test apps/conductor/mod` (the hooks against the engine, UI on the terminal, desktop, VS Code and mobile surfaces) need Claude Code, so they run locally. To try a change before installing it: `claude --plugin-dir apps/conductor/mod`.
251
504
 
@@ -0,0 +1,68 @@
1
+ import { z } from 'zod';
2
+ import { briefLines } from "./brief.js";
3
+ import { callApi, NotRunningError } from "./client.js";
4
+ import { ConductorError } from "./errors.js";
5
+ // Exit code for a brief no conductor gave (none running, one without the brief API, or one that
6
+ // couldn't read the tracker): the worker reads the item from the tracker itself. A refusal (no
7
+ // project tracks the item, the tracker has no such item) exits 1.
8
+ export const NOT_BRIEFED = 3;
9
+ // The daemon asks the tracker once or a few times, each within 20 s.
10
+ const BRIEF_TIMEOUT_MS = 60_000;
11
+ const link = z.object({ id: z.string(), status: z.string().nullable(), title: z.string().nullable() });
12
+ export const briefSchema = z.object({
13
+ approval: z.object({ at: z.string(), kind: z.enum(['approve', 'revise']), text: z.string() }).nullable(),
14
+ area: z.string().nullable(),
15
+ blockedBy: z.array(link),
16
+ blocks: z.array(link),
17
+ description: z.string(),
18
+ item: z.string(),
19
+ labels: z.array(z.string()),
20
+ notes: z.array(z.object({ at: z.string(), kind: z.string().nullable(), line: z.string().nullable(), phase: z.string() })),
21
+ plan: z.string().nullable(),
22
+ project: z.string(),
23
+ readAt: z.string(),
24
+ related: z.array(link),
25
+ status: z.object({ name: z.string(), type: z.string().nullable() }),
26
+ title: z.string(),
27
+ trackerProject: z.string().nullable(),
28
+ url: z.string().nullable(),
29
+ });
30
+ const refused = z.object({ details: z.array(z.string()).optional(), error: z.string() });
31
+ // `conduct brief <item>`: the item's context from the daemon, as text or JSON. Returns the exit
32
+ // code: 0, NOT_BRIEFED, or 1 (thrown) when the conductor refused it.
33
+ export async function briefCommand(paths, item, options, io) {
34
+ const notBriefed = (why) => {
35
+ io.err(`no brief: ${why}; read the item from the tracker yourself`);
36
+ return NOT_BRIEFED;
37
+ };
38
+ let answer;
39
+ try {
40
+ answer = await callApi(paths, {
41
+ method: 'GET',
42
+ path: `/items/${encodeURIComponent(item)}/brief${options.fresh ? '?fresh=true' : ''}`,
43
+ timeoutMs: BRIEF_TIMEOUT_MS,
44
+ });
45
+ }
46
+ catch (error) {
47
+ if (error instanceof NotRunningError)
48
+ return notBriefed('the conductor is not running');
49
+ throw error;
50
+ }
51
+ if (answer.status === 200) {
52
+ const { brief } = z.object({ brief: briefSchema }).parse(answer.body);
53
+ if (options.json)
54
+ io.out(JSON.stringify(brief, null, 2));
55
+ else
56
+ briefLines(brief).forEach((line) => io.out(line));
57
+ return 0;
58
+ }
59
+ const error = refused.safeParse(answer.body).data;
60
+ // A daemon from before the brief API answers its route's `not_found`; a missing item says which.
61
+ if (answer.status === 404 && error?.error === 'not_found')
62
+ return notBriefed('this conductor has no brief API (upgrade it)');
63
+ // The conductor couldn't read the tracker (no key, the tracker down): the worker still may.
64
+ if (answer.status === 502)
65
+ return notBriefed(`${error?.error ?? 'the conductor could not read the tracker'}${error?.details?.length ? ` (${error.details.join('; ')})` : ''}`);
66
+ throw new ConductorError(error?.error ?? `the API answered ${answer.status}`, error?.details ?? []);
67
+ }
68
+ //# sourceMappingURL=brief-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brief-cli.js","sourceRoot":"","sources":["../src/brief-cli.ts"],"names":[],"mappings":"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"}