@detergent-software/conductor 3.0.0-dev.4 → 3.0.0-dev.40

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 (212) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/README.md +270 -123
  3. package/build/brief-cli.js +5 -12
  4. package/build/brief-cli.js.map +1 -1
  5. package/build/brief.js +4 -3
  6. package/build/brief.js.map +1 -1
  7. package/build/broker-cli.js +15 -10
  8. package/build/broker-cli.js.map +1 -1
  9. package/build/broker.js +1 -1
  10. package/build/broker.js.map +1 -1
  11. package/build/claude.js +48 -2
  12. package/build/claude.js.map +1 -1
  13. package/build/cli.js +221 -71
  14. package/build/cli.js.map +1 -1
  15. package/build/clones.js +92 -0
  16. package/build/clones.js.map +1 -0
  17. package/build/code-host-cli.js +158 -39
  18. package/build/code-host-cli.js.map +1 -1
  19. package/build/code-host-service.js +35 -2
  20. package/build/code-host-service.js.map +1 -1
  21. package/build/code-host.js +140 -16
  22. package/build/code-host.js.map +1 -1
  23. package/build/conditions.js +33 -16
  24. package/build/conditions.js.map +1 -1
  25. package/build/config.js +58 -28
  26. package/build/config.js.map +1 -1
  27. package/build/console.js +43 -27
  28. package/build/console.js.map +1 -1
  29. package/build/credentials.js +49 -36
  30. package/build/credentials.js.map +1 -1
  31. package/build/daemon.js +57 -66
  32. package/build/daemon.js.map +1 -1
  33. package/build/file-lock.js +35 -3
  34. package/build/file-lock.js.map +1 -1
  35. package/build/follow-ups.js +2 -2
  36. package/build/follow-ups.js.map +1 -1
  37. package/build/help.js +228 -82
  38. package/build/help.js.map +1 -1
  39. package/build/history.js +10 -4
  40. package/build/history.js.map +1 -1
  41. package/build/host/credential-manager.js +109 -0
  42. package/build/host/credential-manager.js.map +1 -0
  43. package/build/host/keychain.js +44 -0
  44. package/build/host/keychain.js.map +1 -0
  45. package/build/host/launchd.js +118 -0
  46. package/build/host/launchd.js.map +1 -0
  47. package/build/host/libsecret.js +40 -0
  48. package/build/host/libsecret.js.map +1 -0
  49. package/build/host/macos-terminal.js +63 -0
  50. package/build/host/macos-terminal.js.map +1 -0
  51. package/build/host/platform.js +4 -0
  52. package/build/host/platform.js.map +1 -0
  53. package/build/host/secrets.js +46 -0
  54. package/build/host/secrets.js.map +1 -0
  55. package/build/host/shell.js +26 -0
  56. package/build/host/shell.js.map +1 -0
  57. package/build/host/startup.js +26 -0
  58. package/build/host/startup.js.map +1 -0
  59. package/build/host/systemd.js +91 -0
  60. package/build/host/systemd.js.map +1 -0
  61. package/build/host/task-scheduler.js +78 -0
  62. package/build/host/task-scheduler.js.map +1 -0
  63. package/build/host/terminal.js +46 -0
  64. package/build/host/terminal.js.map +1 -0
  65. package/build/host/tmux.js +39 -0
  66. package/build/host/tmux.js.map +1 -0
  67. package/build/host/windows-terminal.js +46 -0
  68. package/build/host/windows-terminal.js.map +1 -0
  69. package/build/in-process.js +6 -5
  70. package/build/in-process.js.map +1 -1
  71. package/build/inbox.js +50 -34
  72. package/build/inbox.js.map +1 -1
  73. package/build/init.js +509 -0
  74. package/build/init.js.map +1 -0
  75. package/build/landing.js +86 -7
  76. package/build/landing.js.map +1 -1
  77. package/build/lib/providers/ado/code-host.js +5 -1
  78. package/build/lib/providers/ado/code-host.js.map +1 -1
  79. package/build/lib/providers/ado/pipelines.js +138 -26
  80. package/build/lib/providers/ado/pipelines.js.map +1 -1
  81. package/build/lib/providers/ado/trigger.js +5 -0
  82. package/build/lib/providers/ado/trigger.js.map +1 -1
  83. package/build/lib/providers/ado/writes.js +17 -2
  84. package/build/lib/providers/ado/writes.js.map +1 -1
  85. package/build/lib/providers/github/actions.js +90 -3
  86. package/build/lib/providers/github/actions.js.map +1 -1
  87. package/build/lib/providers/github/code-host.js +2 -0
  88. package/build/lib/providers/github/code-host.js.map +1 -1
  89. package/build/lib/providers/github/writes.js +25 -2
  90. package/build/lib/providers/github/writes.js.map +1 -1
  91. package/build/lib/providers/index.js +4 -3
  92. package/build/lib/providers/index.js.map +1 -1
  93. package/build/lib/providers/npm/stage.js +28 -5
  94. package/build/lib/providers/npm/stage.js.map +1 -1
  95. package/build/lib/providers/timing.js +10 -0
  96. package/build/lib/providers/timing.js.map +1 -0
  97. package/build/lib/providers/verdicts/pr.js +47 -6
  98. package/build/lib/providers/verdicts/pr.js.map +1 -1
  99. package/build/lib/providers/verdicts/runs.js +12 -2
  100. package/build/lib/providers/verdicts/runs.js.map +1 -1
  101. package/build/logins.js +1 -1
  102. package/build/logins.js.map +1 -1
  103. package/build/manager.js +131 -55
  104. package/build/manager.js.map +1 -1
  105. package/build/note.js +5 -15
  106. package/build/note.js.map +1 -1
  107. package/build/notes.js +130 -23
  108. package/build/notes.js.map +1 -1
  109. package/build/notify.js +104 -42
  110. package/build/notify.js.map +1 -1
  111. package/build/npm-reads.js +323 -0
  112. package/build/npm-reads.js.map +1 -0
  113. package/build/ownership.js +2 -2
  114. package/build/paths.js +44 -1
  115. package/build/paths.js.map +1 -1
  116. package/build/plugin.js +4 -2
  117. package/build/plugin.js.map +1 -1
  118. package/build/profile-fields.js +153 -0
  119. package/build/profile-fields.js.map +1 -0
  120. package/build/profile-layers.js +264 -0
  121. package/build/profile-layers.js.map +1 -0
  122. package/build/profile.js +107 -51
  123. package/build/profile.js.map +1 -1
  124. package/build/project-check-cli.js +19 -19
  125. package/build/project-check-cli.js.map +1 -1
  126. package/build/project-check.js +194 -108
  127. package/build/project-check.js.map +1 -1
  128. package/build/protocol.js +5 -21
  129. package/build/protocol.js.map +1 -1
  130. package/build/pushover.js +5 -5
  131. package/build/pushover.js.map +1 -1
  132. package/build/queue.js +4 -4
  133. package/build/queue.js.map +1 -1
  134. package/build/registry.js +195 -36
  135. package/build/registry.js.map +1 -1
  136. package/build/release-kinds.js +34 -0
  137. package/build/release-kinds.js.map +1 -0
  138. package/build/rule-actions.js +8 -6
  139. package/build/rule-actions.js.map +1 -1
  140. package/build/rules-service.js +104 -47
  141. package/build/rules-service.js.map +1 -1
  142. package/build/rules.js +23 -23
  143. package/build/rules.js.map +1 -1
  144. package/build/run.js +20 -7
  145. package/build/run.js.map +1 -1
  146. package/build/runs.js +218 -68
  147. package/build/runs.js.map +1 -1
  148. package/build/scheduler.js +89 -13
  149. package/build/scheduler.js.map +1 -1
  150. package/build/search.js +2 -1
  151. package/build/search.js.map +1 -1
  152. package/build/server.js +51 -14
  153. package/build/server.js.map +1 -1
  154. package/build/spend.js +1 -1
  155. package/build/spend.js.map +1 -1
  156. package/build/store.js +79 -20
  157. package/build/store.js.map +1 -1
  158. package/build/supervise.js +1 -1
  159. package/build/supervise.js.map +1 -1
  160. package/build/timeline.js +104 -0
  161. package/build/timeline.js.map +1 -0
  162. package/build/tracker-reads-cli.js +4 -5
  163. package/build/tracker-reads-cli.js.map +1 -1
  164. package/build/trackers.js +56 -10
  165. package/build/trackers.js.map +1 -1
  166. package/build/train-cli.js +6 -5
  167. package/build/train-cli.js.map +1 -1
  168. package/build/train.js +2 -2
  169. package/build/train.js.map +1 -1
  170. package/build/upgrade.js +8 -9
  171. package/build/upgrade.js.map +1 -1
  172. package/build/wait.js +10 -10
  173. package/build/wait.js.map +1 -1
  174. package/build/watchers/ado.js +42 -18
  175. package/build/watchers/ado.js.map +1 -1
  176. package/build/watchers/github.js +19 -4
  177. package/build/watchers/github.js.map +1 -1
  178. package/build/watchers/index.js +2 -2
  179. package/build/watchers/index.js.map +1 -1
  180. package/build/watchers/linear-queue.js +47 -14
  181. package/build/watchers/linear-queue.js.map +1 -1
  182. package/build/watchers/linear-writes.js +26 -12
  183. package/build/watchers/linear-writes.js.map +1 -1
  184. package/build/watchers/linear.js +2 -2
  185. package/build/watchers/linear.js.map +1 -1
  186. package/build/watchers/npm.js +6 -4
  187. package/build/watchers/npm.js.map +1 -1
  188. package/build/webhook.js +80 -0
  189. package/build/webhook.js.map +1 -0
  190. package/build/why.js +1 -1
  191. package/build/why.js.map +1 -1
  192. package/build/worktrees.js +56 -93
  193. package/build/worktrees.js.map +1 -1
  194. package/package.json +61 -61
  195. package/plugin/.claude-plugin/plugin.json +1 -1
  196. package/plugin/hooks/model.ts +46 -6
  197. package/plugin/hooks/register.tsx +41 -2
  198. package/plugin/skills/conductor-console/SKILL.md +26 -27
  199. package/plugin/skills/work-item/SKILL.md +57 -53
  200. package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
  201. package/plugin/skills/work-item/trackers/github-issues.md +1 -1
  202. package/plugin/skills/work-item/trackers/linear.md +2 -2
  203. package/plugin/types/index.d.ts +1 -1
  204. package/protocol/PROTOCOL.md +3 -9
  205. package/build/checkout-sync.js +0 -249
  206. package/build/checkout-sync.js.map +0 -1
  207. package/build/credential.js +0 -66
  208. package/build/credential.js.map +0 -1
  209. package/build/startup.js +0 -92
  210. package/build/startup.js.map +0 -1
  211. package/build/tab.js +0 -32
  212. package/build/tab.js.map +0 -1
package/README.md CHANGED
@@ -10,24 +10,30 @@ conduct start start the daemon detached and wait until it an
10
10
  conduct stop stop the running daemon
11
11
  conduct supervise [--fail-fast] run the daemon and restart it when it dies (see Restarts)
12
12
  conduct status [--json] the daemon's pid, port, projects, workers and held locks
13
- conduct project add <path> register the project whose checkout holds .claude/conductor.json
13
+ conduct init [<path>] [--name <name>] [--base <branch>] [--tracker <kind>] [--key <KEY>] [--team <key>]
14
+ [--query <query>] [--shared] [--yes]
15
+ onboard a repo: a starter profile, then project add and check (see Projects)
16
+ conduct project add <url | path> [--name <name>]
17
+ clone the project's remote into ~/.conductor/projects/<name>/repo and register it
14
18
  conduct project list [--json] the registered projects
15
19
  conduct project remove <name> drop a registration (a renamed project's old name), resolving its inbox items
16
20
  conduct project check <name> [--no-worktree] [--json]
17
21
  whether the project can be conducted, before a launch finds out
18
- conduct launch <item> start a worker: worktree, background session, tab
22
+ conduct profile <project> [--json]
23
+ the project's profile, machine over repo, each field with its source (see Projects)
24
+ conduct launch <item> start a worker: worktree and background session (a tab with workers.tab)
19
25
  [--project <name>] [--probe] [--prompt <text>] [--conductor <name>]
20
26
  conduct retire <worker> [--force] stop a worker, remove its worktree and session
21
27
  conduct attach <worker> wake a parked worker (no event text) and open it in a tab
22
28
  conduct inbox [--json] what waits on you, with its commands and links (see Inbox)
23
29
  conduct approve <item> [--text <text>]
24
30
  approve a plan: post "approve" on the item as you, wake its worker
25
- conduct revise <item> <text> ask for another plan: post "revise: <text>", wake its worker
31
+ conduct revise <item> <text> ask for another plan, waiting or approved: post "revise: <text>", wake its worker
26
32
  conduct done <id> mark an inbox item done (a physical action, or one you handled)
27
33
  conduct notify [--json] the notification profile, quiet hours, each channel's state
28
34
  conduct notify profile [<name>] [--until <when>]
29
35
  switch where pushes go (see Notifications)
30
- conduct notify test [--kind <kind>] [--channel remote-control|pushover] [--raise]
36
+ conduct notify test [--kind <kind>] [--channel remote-control|pushover|webhook] [--raise]
31
37
  push a sample item now, or raise a real one with --raise
32
38
  conduct queue [--json] [--fresh] what the scheduler picks next and why the rest wait (see Scheduler)
33
39
  conduct pause [<project>] [--until <when>]
@@ -38,7 +44,7 @@ conduct focus <project> [--slots <n>] [--weight <n>] --until <when>
38
44
  conduct why <event id | item> [--json]
39
45
  the rule behind an action, and the events that led to it
40
46
  conduct console [--here]
41
- open the console in a Windows Terminal tab, or in this terminal (see Console)
47
+ open the console in a tab (Windows Terminal, iTerm or Terminal, tmux), or in this terminal (see Console)
42
48
  conduct escalations [--json] what rules escalated to the console and it hasn't handled
43
49
  conduct escalations done <id> [--ask] <text>
44
50
  the console's finding on an escalation; --ask puts it in the inbox
@@ -63,21 +69,29 @@ conduct mode --tier S|M|L --files <a,b>
63
69
  conduct wait <condition> wait for an outside event (see Waits)
64
70
  conduct state [--repo <repo>] the worktree's branch, changes and PR, and its other repos' (see Worker verbs)
65
71
  conduct branch <type> <slug> [--item <item>]
66
- rename worktree-ATK-n to <type>/atk-n-<slug>
72
+ rename worktree-ENG-n to <type>/eng-n-<slug>
67
73
  conduct checkout <owner>/<repo> <type> <slug> [--item <item>]
68
74
  clone another GitHub repo into .scratch/repos/<repo> for the item
69
75
  conduct pr create --title <title> --body-file <file> [--repo <repo>] [--item <item>] [--dry-run]
70
76
  check, push and open the worker's PR (see Worker verbs)
71
77
  conduct pr threads <pr> [--repo <repo>]
72
78
  the PR's threads, with the ones that wait on the worker
73
- conduct land <pr> --reason <why> [--mode auto|review] [--rearm] [--tier S|M|L] [--repo <repo>]
79
+ conduct pr abandon <pr> --reason <why> [--repo <repo>] [--force]
80
+ close the worker's PR unmerged, with a note saying why
81
+ conduct land <pr> --reason <why> [--mode auto|review] [--rearm] [--requeue <run>] [--tier S|M|L] [--repo <repo>]
74
82
  arm the worker's PR to land and give its first verdict (see Worker verbs)
75
83
  conduct reply <pr> [<thread>] --body-file <file> [--repo <repo>]
76
84
  answer on the worker's PR, headed as a note
77
85
  conduct runs <sha> [--for <pipeline>[,<pipeline>…]] [--repo <repo>]
78
86
  a merge's runs, their verdict, failed logs and npm stages (see Worker verbs)
87
+ conduct runs <sha> --for <pipeline>[,<pipeline>…] --grep <pattern> [--repo <repo>]
88
+ the same, with the lines of those runs' logs that match, whatever their result
79
89
  conduct runs <sha> --requeue <pipeline>
80
90
  queue the item's merge's cancelled run again on the base (see Worker verbs)
91
+ conduct timeline <run> [--task <regex>] [--grep <regex>] [--repo <repo>]
92
+ a run's tasks with their times, and the named tasks' log lines that match (see Worker verbs)
93
+ conduct timeline --for <pipeline> [--branch <branch>] [--count <n>] [--task <regex>] [--grep <regex>] [--repo <repo>]
94
+ the same for a pipeline's last finished runs on a branch
81
95
  conduct train [<project>] [--dry-run]
82
96
  merge the waiting skill publishes into one registry PR (see Skill trains)
83
97
  conduct ports [<path>] [--json] a conducted worktree's dev ports from the port broker, or every worktree's (see Broker)
@@ -85,13 +99,13 @@ conduct lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
85
99
  a lock every project shares, such as chrome (see Broker)
86
100
  conduct events [--since <id>] [--follow] [--console]
87
101
  the event log, one JSON event per line (see API)
88
- conduct install-startup start the supervised daemon at logon (Windows Task Scheduler)
89
- conduct install-startup --remove remove that logon task
102
+ conduct install-startup start the supervised daemon at every logon (Task Scheduler, launchd or systemd)
103
+ conduct install-startup --remove remove that task, agent or unit
90
104
  conduct upgrade [--tag dev|latest] [--allow-downgrade]
91
105
  stop the daemon, npm install the tag's newest version, start it again
92
106
  ```
93
107
 
94
- A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
108
+ A `<worker>` is its item (`ENG-309`) or its name (`probe-ENG-309`).
95
109
 
96
110
  `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"`.
97
111
 
@@ -101,38 +115,51 @@ The session manager owns the `claude --bg` sessions it launches, and only those:
101
115
 
102
116
  - **launch:** the worktree comes first, made by the daemon itself (design note v2, section 2; ATK-526), with no hooks:
103
117
  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;
118
+ 2. `git worktree add`, run in the project's clone (see Projects), makes `~/.conductor/projects/<project>/worktrees/<name>` on `worktree-<name>` from `origin/<base>`, fetched first (after `git worktree prune`, and on Windows with `core.longpaths` on). It doesn't fetch, pull, add worktrees to or change a checkout of yours (a `localFilesFrom` checkout is only read). 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
119
  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;
120
+ 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 (such as `pnpm install` and a script that writes the dev ports). The first that fails fails the launch, with the end of its output. So does a setup that leaves changes git sees, such as a committed lockfile that an install step rewrote: the worker would take them for its own;
107
121
  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
122
  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
123
 
110
124
  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
125
 
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.
126
+ 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`. Workers run headless. A machine whose `config.json` sets `"workers": { "tab": true }` also gets a tab attached to each session, `claude attach <session>`, in the terminal `workers.terminal` names (see Tabs); `conduct attach` opens one either way. A tab that fails to open is a `worker.tab-failed` event and never fails the launch.
113
127
  - **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.
114
128
  - **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.
115
129
  - **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.
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):
130
+ - **retire:** refuses a worker whose turn still runs or that is attached (open in a tab), unless `--force`, and never retires a session that is still starting. A refusal for an attach is logged once per attach, however often the scheduler tries again, and never raised in the inbox; the retire goes through once the attach is released (ATK-519). What happens to the worktree depends on the worker's PR (design note v2, section 2):
117
131
  - **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
132
  - **no PR, and nothing uncommitted or unpushed** (a canceled item, a worker retired before its first commit): as merged, with nothing to lose;
119
133
  - **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
134
  - **unread** (the code host didn't answer): it refuses, as a refusal that passes on its own, and the scheduler tries again.
121
135
 
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.
136
+ 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`. It works in the repository the worktree's own `.git` names, so a worker launched before the daemon owned clones retires from the checkout its worktree was made in; it removes only a linked worktree that repository lists, never a main checkout. With the directory gone already, it deletes the `worktree-<name>` branch in the project's clone only when none of its commits is unpushed. If the stop fails, nothing is removed. If the stop succeeds (or a fresh listing shows no process to stop) and a later step fails, `worker.retire-partial` says `parked`: the worker is parked with its waits closed: no sweep reads it as stopped unexpectedly, resumes it or compacts it, and the retire is tried again.
123
137
  - **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
124
138
  - **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`.
125
139
  - **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.
126
- - **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. `claude agents` names no command, so a permission stall reads the session's transcripts (its own and its subagents') for the call still open: its tool, the command or path, the subagent that made it, and the auto-mode classifier denials just before it. The inbox item and its push say them, for example "ATK-297 has waited 5 min on a permission prompt: Bash `git -C … diff …` in subagent general-purpose (Review ATK-297 diff vs plan), after 2 auto-mode denials". Auto mode asks a person after 3 classifier denials in a row (20 in a session), even in a background subagent, so that count is usually why the prompt came. An open item of a Handoff kind with nothing to watch (`abandoned`, `blocked`, `failed`, `human`, `operator`, `scope`) on the worker, or on its item with no worker, counts as a wait: its idle clock starts once that item resolves. A `merge`, `npm-otp` or `plan` item doesn't: those come with a wait, so a worker idle with only the item still stalls.
140
+ - **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. `claude agents` names no command, so a permission stall reads the session's transcripts (its own and its subagents') for the call still open: its tool, the command or path, the subagent that made it, and the auto-mode classifier denials just before it. The inbox item and its push say them, for example "ENG-312 has waited 5 min on a permission prompt: Bash `git -C … diff …` in subagent general-purpose (Review ENG-312 diff vs plan), after 2 auto-mode denials". Auto mode asks a person after 3 classifier denials in a row (20 in a session), even in a background subagent, so that count is usually why the prompt came. An open item of a Handoff kind with nothing to watch (`abandoned`, `blocked`, `failed`, `human`, `operator`, `scope`) on the worker, or on its item with no worker, counts as a wait: its idle clock starts once that item resolves. A `merge`, `npm-otp` or `plan` item doesn't: those come with a wait, so a worker idle with only the item still stalls.
127
141
  - **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. The spend `conduct status`, `GET /fleet` and the console's fleet pane show (`spend.today`, `spend.total`) and the `spend.*` rule fields count the same tokens: input, output and cache writes. Status shows the worker's cache reads beside them, labelled (`spend 417k today, 1.0M total, 25M cache reads`; `spend.cacheRead` in `GET /fleet`).
128
142
  - **context:** the session's context is its main transcript's newest model call: input, cache read, cache write and output. Subagents have contexts of their own, so theirs don't count. After a compaction it is unknown until the next call: the boundary counts only the conversation it kept, and the call adds the system prompt and tools back (a probe that kept 2.8k next read 68k). Status shows it as `context 312k` (`context` in `GET /fleet`), and the rules read it as `worker.context`.
129
143
  - **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.
130
144
 
131
145
  Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `failed`, `exited`. Every step is an event (`worker.launched`, `worker.parked`, `worker.resumed`, …) on `GET /events`.
132
146
 
147
+ ### Tabs
148
+
149
+ Workers are background sessions, and a tab is only a view of one: the session runs without it, and a tab closes when its worker parks. `~/.conductor/config.json` says whether each launch opens one and where tabs open:
150
+
151
+ ```jsonc
152
+ { "workers": { "tab": true, "terminal": "auto" } }
153
+ ```
154
+
155
+ - `tab` (false): open a tab on each launch. `conduct attach <worker>` opens one whatever it says. It was the profile's `workers.tab` until ATK-529: a profile may still set it, for older daemons, but it is ignored, and `conduct project check` warns about it. Set it in `config.json` on each machine that wants tabs.
156
+ - `terminal` (`auto`): `windows-terminal` (`wt.exe`, a window per project, "Conductor · <project>"), `iterm` (a tab in its front window), `terminal` (macOS Terminal, a window per tab) or `tmux` (a detached session per window, such as `conductor-web`, and a tmux window per tab). `auto` is Windows Terminal on Windows, iTerm on macOS where it is installed and Terminal where not, and tmux elsewhere.
157
+
158
+ A tmux tab runs where no desktop is, so `conduct attach` and `conduct console` print the `tmux attach -t <session>` that reaches it. With no terminal to open in (no tmux on a server), `conduct attach` prints the `claude attach <session>` to run yourself, and `conduct console` refuses with `conduct console --here --force` (`--force` because the refused start is claimed for a minute). A worker's tab whose `claude attach` fails stays open on the failure, until Enter, so you can read it. Tabs on macOS and tmux run their command through `sh -c`, whatever the login shell. macOS asks once whether the conductor may control Terminal or iTerm (Automation, in Privacy & Security). Terminal opens a window per tab, and closes it when the tab's shell exits only if the profile's "When the shell exits" says so (Settings, Profiles, Shell); otherwise a parked worker leaves its window behind.
159
+
133
160
  ## Notes
134
161
 
135
- 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. A PR opened note also records the worker's PR (`pr`, such as `ado:Emergent.AgenticToolkit#1850` or `github:<owner>/<repo>#458`) from its first PR link, replacing an earlier one; until then, the worker's first `pr:` wait records it. `conduct status` and the console's fleet pane show it. 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 or GitHub Issues `key`) is the item's key prefix.
162
+ A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-item · 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. A PR opened note also records the worker's PR (`pr`, such as `ado:Contoso.Web#1850` or `github:<owner>/<repo>#458`) from its first PR link, replacing an earlier one; until then, the worker's first `pr:` wait records it. `conduct status` and the console's fleet pane show it. 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 or GitHub Issues `key`) is the item's key prefix.
136
163
 
137
164
  **State follows the notes** (providers design note, section 5). A `Started`, `PR opened` or `Landed` note (not a Handoff) also moves the item, to the state its phase maps to in the profile's `tracker.transitions`:
138
165
 
@@ -146,7 +173,7 @@ A phase mapped to `null` moves nothing (`started` still assigns). A `Started` ne
146
173
 
147
174
  **Worker protocol 2.** The notes, their phases and kinds, the waits and the wake text are the package's spec, [`protocol/PROTOCOL.md`](protocol/PROTOCOL.md), held as data in `src/protocol.ts`. `conduct note` checks each note against it before anything is posted, on the daemon and with no daemon alike. It refuses one with an unknown phase, a Handoff without a kind and its line, a kind its phase doesn't carry (`jason` is `operator` since protocol 2), or a Plan without its `**Size tier**` and `**Landing:**` lines, and the error names the rule. The readers (watchers, briefs, history) still read older notes and people's comments.
148
175
 
149
- 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.
176
+ 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, the old name goes in `aliases` and items in flight keep their phases.
150
177
 
151
178
  | Exit | Means | The worker then |
152
179
  |---|---|---|
@@ -184,7 +211,7 @@ A brief is kept for 30 seconds. Anything the daemon sees happen to the item drop
184
211
 
185
212
  The rest of what a work skill and the console read from the tracker, so neither needs a tracker's MCP tools (design note v2, section 4). Each works for Linear, ADO Boards and GitHub Issues.
186
213
 
187
- - `conduct notes <item>` prints the item's phase notes with their whole text, oldest first, each with when and who. `--phase` and `--kind` keep the ones that match (case aside), and `--last` the newest of those: `conduct notes ATK-312 --phase Handoff --kind conductor --last` is the Handoff a console escalation is about.
214
+ - `conduct notes <item>` prints the item's phase notes with their whole text, oldest first, each with when and who. `--phase` and `--kind` keep the ones that match (case aside), and `--last` the newest of those: `conduct notes ENG-312 --phase Handoff --kind conductor --last` is the Handoff a console escalation is about.
188
215
  - `conduct comments <item>` prints every comment the same way, and whether an approver wrote it; `--since <time>` keeps the newer ones.
189
216
 
190
217
  Both read the item's brief (the comments now carry their author and their note heading), so they read as the daemon reads, else here, and refuse as `conduct brief` does. `--json` prints them as one object.
@@ -198,18 +225,29 @@ Protocol 2's verbs on git and the code host (providers design note, section 4),
198
225
  - `conduct state`, `conduct branch` and `conduct checkout` are git in the worktree, so they always run here. `state` reads the branch's PR as the project's account (see Accounts). `checkout` clones another GitHub repo into `.scratch/repos/<repo>`, keeps it out of the worktree's git status, and retire checks it for unpushed or uncommitted work (ATK-429).
199
226
  - `conduct pr create` checks everything before it pushes, `--dry-run` included: the title carries the item key, the branch carries it too and isn't the base, the description fits the host (ADO 4,000 characters, GitHub 65,536), no PR is open from the branch yet, nothing is uncommitted, the branch is ahead of its base, and commitlint passes (skipped in a clone without it installed, whose own CI lints the PR). Then it pushes from the checkout and has the daemon open the PR (`POST /workers/:id/prs`). The daemon checks ownership again from its own records and the worktree, never from the request: the worker is live, the branch the worktree is on now (or the `--repo` checkout's) carries the worker's item, and the title carries it. It records the PR on the worker (`pr`; another repo's only while the worker has none) and logs `code-host.pr-created`. `conduct branch` renames only the item's own branch, never the base, `main` or another item's.
200
227
  - `conduct land <pr>` sets the worker's PR up to land, as the helper's `land` did, and is safe to repeat. The PR must be the worker's (the same checks, and from the branch the checkout is on now, into its base). A brake already on it (a comment, a failed build, conflicts) is reported, and nothing is armed. A Landing note whose landing armed something, with that gone now, means someone cancelled it: `auto-merge-cancelled`, until `--rearm`. A PR ADO started completing in the last few minutes (`completionQueueTime`), which clears auto-complete, is `waiting` instead, and nothing is armed. `--mode` is held to the profile's landing rules (`work.landing`) for the PR's files as pushed (`origin/<base>...origin/<head>`, fetched first) and the plan's tier. The daemon reads the tier from the item's Plan note; in-process it takes `--tier`, and auto is refused without one. Without `--mode` the rules pick it. Then it arms the host's own landing behind its capability check (`landingCheck`). On ADO that is a +10 vote in auto mode, with auto-complete and noFastForward. On GitHub it is auto-merge with a merge commit in auto mode, where the repo allows it; in review mode nothing is armed. It posts the Landing note last, and a new one whenever the mode changes, then prints the verdict. A failed build comes with its log, and a merge with whether it has two parents. The daemon logs `code-host.landing-armed` with the mode.
228
+ - **`--requeue <run>`.** On a `build-failed` PR it queues that failed build again, for a flake no change caused, then arms as above, so it isn't safe to repeat. On ADO that is `PATCH policy/evaluations/<id>`, as keeping a landing armed does for an expired build. On GitHub (ATK-640) it is `POST actions/runs/<run>/rerun-failed-jobs` on the check's Actions run, for a PR the item opened from its `.scratch/repos` checkout. A PR a hook's publish opened has no checkout, so `land` refuses it. It checks the landing (`landingCheck`, and a cancelled landing is reported with nothing queued) and then posts a closed Requeue note, `## <skill> · Requeue · run: <run> (<build>) at <head>`, with `--reason` as the why. The note is the record, so it holds with or without the daemon: a build with a Requeue note on the PR's head commit is refused another requeue there, even for the new run's failure, which is fixed and pushed instead. Each build that failed on the head gets one, and a push gives the new head its own. The note is read from the comment that opened its thread, so a reply in it changes nothing. When queueing fails after the note, that build's requeue on the head is spent and the error says so. Until the PR shows the new run, the verdict (`conduct land`, `conduct wait pr`) waits on the build whose run a note names instead of reporting the old failure; after 15 minutes with the old run still there, it is `build-failed` again. A GitHub re-run keeps the run's id, so a failed check of that run that finished after the note (`finished`, the check's `completed_at`) is the new attempt's failure, `build-failed` at once. It refuses a PR that isn't `build-failed`, a run that isn't one of its failed builds, an ADO build with no policy evaluation, a GitHub run with a job still running (GitHub re-runs only a finished run), and a PR with a comment waiting for an answer (on GitHub the Requeue note would close it). The daemon logs `code-host.build-requeued` with the run and head.
201
229
  - `conduct wait pr <pr>` (no event word) is the verdict wait, `pr:<host>:<repo>#<n>:verdict`: it holds on the first verdict that isn't `waiting`, with a line such as `pr 2030 build-failed: <pipeline>`. While it waits on a PR whose landing the daemon recorded, the ADO watcher keeps it armed. It queues an expired build again (once per wait) and, in auto mode, restores a cleared vote (never over a negative one), logged as `code-host.kept-armed`. A local wait (no daemon) gives the same verdicts but keeps nothing armed.
202
230
  - `conduct pr create` ends the description with the tracker's reference when it has one, before the length check: `AB#<id>` for an ADO Boards item on a GitHub PR, so the Boards GitHub app links it, and `Refs <owner>/<repo>#<n>` for a GitHub Issues item on a GitHub PR. Linear has none.
203
- - `conduct file --title … --body-file … --area … [--related …] [--blocked-by …] [--human] [--waits-release]` files a follow-up of the checkout's item: in Backlog, in the item's project, with one Area (a label of the profile's Area group, a path below a board's `areaRoot`, or the name after a GitHub Issues `areaPrefix` label), the profile's `triageLabel`, its `humanLabel` with `--human`, and its `releaseWait` label (a tag on Boards) with `--waits-release` (the follow-up then waits until each Done blocker has shipped). It is related to the item and each `--related`, and waits on each `--blocked-by` (keys, repeated or comma-separated). A key of another project, an Area outside the group, or `--waits-release` without a `--blocked-by` or on a profile with no `tracker.releaseWait`, is refused before anything is written. On Linear a relation that fails after the issue exists is a warning; on Boards the item and its links are one request. On GitHub Issues the follow-up also carries the `queueLabel`, the related items are a `Related: #n` line in its body (GitHub has no related relation), and a dependency that fails after the issue exists is a warning. With `--waits-release`, Linear and GitHub Issues look up each `--blocked-by` first and refuse one they don't have. The daemon files it for the worker (`POST /workers/:id/follow-ups`) and logs `tracker.filed`. The worker searches for a duplicate first, with `conduct search`: this only files.
231
+ - `conduct file --title … --body-file … --area … [--related …] [--blocked-by …] [--human] [--waits-release]` files a follow-up of the checkout's item: in Backlog, in the item's project, with one Area (a label of the profile's Area group, a path below a board's `areaRoot`, or the name after a GitHub Issues `areaPrefix` label; none, and no `--area`, for a Linear team with no Area group), the profile's `triageLabel`, its `humanLabel` with `--human`, and its `releaseWait` label (a tag on Boards) with `--waits-release` (the follow-up then waits until each Done blocker has shipped). It is related to the item and each `--related`, and waits on each `--blocked-by` (keys, repeated or comma-separated). A key of another project, an Area outside the group, or `--waits-release` without a `--blocked-by` or on a profile with no `tracker.releaseWait`, is refused before anything is written. On Linear a relation that fails after the issue exists is a warning; on Boards the item and its links are one request. On GitHub Issues the follow-up also carries the `queueLabel`, the related items are a `Related: #n` line in its body (GitHub has no related relation), and a dependency that fails after the issue exists is a warning. With `--waits-release`, Linear and GitHub Issues look up each `--blocked-by` first and refuse one they don't have. The daemon files it for the worker (`POST /workers/:id/follow-ups`) and logs `tracker.filed`. The worker searches for a duplicate first, with `conduct search`: this only files.
204
232
  - `conduct mode --tier S|M|L --files <a,b>` gives the landing mode a plan states before its PR exists, with the reasons, by the rule `conduct land` holds the PR to (`work.landing`). It reads only the profile, so it runs here.
205
233
  - `conduct pr threads <pr>` lists the PR's threads (on GitHub each comment and review) with `needsAnswer` (ATK-384). `conduct reply <pr> [<thread>]` answers on the worker's own PR: in the ADO thread named, or as a note on the PR. The text is headed `## <work skill> · Reply` unless it starts with a note heading. The daemon logs `code-host.replied`. `--repo` takes `github:<owner>/<repo>` (from its `.scratch/repos` checkout, into its default branch) or `ado:<repository>`; the profile's `codeHost` is the default.
234
+ - `conduct pr abandon <pr> --reason <why>` closes the worker's PR unmerged, such as one another item's PR made redundant (ATK-479): ADO sets its status to `abandoned` (`PATCH pullrequests/<n>`), GitHub closes it (`PATCH pulls/<n>`). Then it posts a closed `## <work skill> · Abandoned` note with the reason; a note that fails after the PR closed comes back as `noteError`, not an error. The PR must be the item's: its title and head branch carry the key, and it isn't from its base. It must also be from the branch the checkout is on now, unless `--force`, which takes the item's PR from another of its branches (an earlier one, a follow-up's), never another item's. A merged PR is refused. A closed one stays closed (`already: true`) and gets the note only when it has no Abandoned note yet, so it is safe to repeat, and a second run posts a note that failed the first time. The daemon abandons it for the worker (`POST /workers/:id/abandons`) and logs `code-host.pr-abandoned`.
206
235
  - `conduct runs <sha>` is one look at a merge's runs, as the helper's `runs-watch --once` and `stage` were (ATK-455). It is a read, so it runs here (except with `--requeue`).
207
- - **Which runs.** On Azure Pipelines (the profile's `ci: ado-pipelines`, for its Azure DevOps repo), they are the pipelines whose YAML trigger the merge fires. The YAML and the merge's changed files are read in the worktree, which fetches the base when it hasn't got the commit. On GitHub Actions (`--repo github:<owner>/<repo>`, or `ci: github-actions` for a GitHub repo), they are the runs the merge's push started.
208
- - **The verdict.** It is `runsVerdict`: `succeeded`, `failed`, `cancelled`, `waiting-for-approval`, `missing`, `none` or `waiting`. On Azure Pipelines `none` comes at once when nothing is triggered. On GitHub it comes after 5 minutes with no run, and on Azure Pipelines a triggered pipeline gets 15 minutes to queue before it is `missing`.
209
- - **A cancelled run (ATK-547).** On Azure Pipelines a canceled build is `cancelled`, not `failed`. A later run on the base whose commit contains the merge stands in for it: the first that wasn't cancelled is the merge's run, with `supersedes` naming the cancelled one. So `cancelled` means no newer run carries the merge, and the note gives the `--requeue` line. GitHub reads runs by the merge's SHA alone, so its `cancelled` says nothing about newer runs.
210
- - **`--requeue <pipeline>`.** It queues that pipeline again on the base at its tip (`POST build/builds` with the tip as `sourceVersion`), after checking that the tip contains the merge. It is a write, for the item's own merge only: the checkout's branch carries the item's key, and that branch's PR merged as `<sha>`. It refuses a run that is waiting, running, succeeded or failed, so a run already listed is never queued twice. It also refuses a merge under 15 minutes old, since Azure DevOps can still list a newer run that carries it. A newer merge's batched run that ADO hasn't listed yet isn't caught. It refuses a pipeline the merge didn't trigger and a GitHub repo. The daemon queues it for the worker that owns the checkout (`POST /workers/:id/requeues`) and logs `code-host.run-requeued`; with no daemon it runs here. `conduct wait merge <sha>` then follows the new run, which carries the merge.
236
+ - **Which runs.** On Azure Pipelines (the profile's `ci: ado-pipelines`), they are the pipelines whose YAML trigger the merge fires. The YAML and the merge's changed files are read in the worktree, which fetches the base when it hasn't got the commit. On GitHub Actions (`--repo github:<owner>/<repo>`, or `ci: github-actions` for a GitHub repo), they are the runs the merge's push started.
237
+ - **CI apart from the code host (ATK-530).** Azure Pipelines can build the project's GitHub repo: `"ci": { "kind": "ado-pipelines", "organization": "https://dev.azure.com/<org>", "project": "<project>" }` names where its pipelines live (beside an Azure DevOps code host, both are refused: its pipelines are the repo's own project's). They are the definitions there whose repository is the GitHub repo, named `<owner>/<repo>` and of type `GitHub`. `code-host.ts`'s `ciOf` picks a repo's CI: the profile's for its own repo, its own host's for any other. The runs, `wait merge` (on the ADO source, as the project's ADO account, with the facts from the worktree's git), `wait run`, the grace period (by the CI: 15 minutes for Azure Pipelines) and `project check` (which also asks the ADO credential) follow it. Its PRs stay GitHub's. `--requeue` queues the code host's own CI, so it refuses a GitHub repo Azure Pipelines builds: queue the run in Azure Pipelines, and `conduct wait merge` follows it. A profile without the organization and project still reads, as it did before they existed, and its CI is refused where it is read.
238
+ - **The verdict.** It is `runsVerdict`: `succeeded`, `failed`, `cancelled`, `waiting-for-approval`, `missing`, `none` or `waiting`. On Azure Pipelines `none` comes at once when nothing is triggered. On GitHub it comes after 5 minutes with no run, and on Azure Pipelines a triggered pipeline gets 15 minutes to queue before it is `missing`. The note gives a `missing` pipeline's `--requeue` line.
239
+ - **A batch behind a run (ATK-434).** A `batch: true` pipeline holds a push while its run on the base still goes, and builds the newest commit when that run ends. So a merge with no run of such a pipeline yet reads that pipeline's newest builds on the base, and the newest that doesn't carry the merge is the run its batch queues after (`behind`: its `run`, and `finished`, null while it goes; state `not-queued`). The verdict is `waiting` for as long as that run goes and 2 minutes after it ends, while Azure DevOps lists the batch's run, past the 15 minutes too: a long deploy, or a `main` run waiting on its prod approval, can hold it longer. A `[skip ci]` release commit pushed in that time isn't in the deploy pipelines' batch (it touches only the released package's files), and the batch still runs.
240
+ - **A cancelled run (ATK-547).** On Azure Pipelines a canceled build is `cancelled`, not `failed`. A later run on the base whose commit contains the merge stands in for it: the first that wasn't cancelled is the merge's run, with `supersedes` naming the cancelled one. So `cancelled` means no newer run carries the merge, and the note gives the `--requeue` line. GitHub reads runs by the merge's SHA alone, so its `cancelled` says nothing about newer runs: its note says a newer run may carry the merge, and gives the line to re-run it while none does.
241
+ - **A failed run (ATK-638).** A failed run stands until a run queued by hand after it (`reason: manual`: `--requeue`, or a person's Run pipeline) contains the merge: that one is then the merge's run, with `supersedes` naming the failed one. A newer push's run never stands in for a failed one, nor for a run queued again for it and then cancelled, so another merge's pass doesn't hide a failure; each run queued again for a failure names it (`retryOf`). The note on `failed` gives the `--requeue` line, to run once when the log shows a flake that no change caused (another merge's real failure isn't one); it leaves it out for a run with `retryOf`, and gives the lines of any cancelled or missing pipelines beside it.
242
+ - **`--requeue <pipeline>`.** It queues that pipeline on the base at its tip (`POST build/builds` with the tip as `sourceVersion`), after checking that the tip contains the merge: a cancelled run no newer run carries, a failed one, or a missing one (`replaces: null`). It is a write, for the item's own merge only: the checkout's branch carries the item's key, and that branch's PR merged as `<sha>`. It refuses a run that is waiting, running or succeeded, so a run already listed is never queued twice, and a batch still behind a run, which queues when that run ends. A failed run is queued again once: it refuses a failed run that was itself queued again for a failed one (`retryOf`), so a real failure can't loop. The record is the merge's runs on Azure DevOps, so it holds with or without the daemon. A failed run needn't wait out the 15 minutes, since no push's run stands in for it. It also refuses a cancelled or missing run under 15 minutes after the merge, since Azure DevOps can still list a newer run that carries it. A newer merge's batched run that ADO hasn't listed yet isn't caught. It refuses a pipeline the merge didn't trigger. The daemon queues it for the worker that owns the checkout (`POST /workers/:id/requeues`) and logs `code-host.run-requeued`; with no daemon it runs here. `conduct wait merge <sha>` then follows the new run, which carries the merge.
243
+ - **`--requeue` on GitHub Actions (ATK-640).** For the project's GitHub repo, or one the item checked out under `.scratch/repos` (`--repo github:<owner>/<repo>`), it re-runs the merge's run itself. That is `POST actions/runs/<id>/rerun-failed-jobs` for a failed run and `…/rerun` for a cancelled one, so the run keeps its id. `runsForMerge` reads it by the merge's SHA, which gives its newest attempt (`attempt`, GitHub's `run_attempt` when above 1), so `conduct wait merge <sha> --repo …` follows it. The item's merge is checked as on Azure, from the checkout's branch and its PR. A run is re-run once: GitHub's `run_attempt` keeps the record, and an `attempt` above 1, failed or cancelled, is refused. A re-run builds the run's own commit, not the base's tip, so the adapter re-runs only the workflow's newest run on the base the PR merged into, of any event and whatever its status (a run someone started by hand counts). When a newer one exists, it refuses and names it: re-running an older merge's run after it could build or deploy older content. A missing run has nothing to re-run and is refused. Azure's 15-minute wait and tip check don't apply, since both come from queueing at the tip. The note on `failed` or `cancelled` gives the line with `--repo` for a run not yet re-run.
211
244
  - **`--for`.** It settles on the named pipelines alone, by YAML file name, path or name, and lists the rest under `others`. Azure Pipelines refuses a name no pipeline answers to.
212
- - **Logs and stages.** Each failed run comes with its failed tasks and log tail (`logs`). For the project's repo, each of the profile's `releases` whose run succeeded gives what it staged (`stages.<key>`): package, version, `stageId`, the `npm stage approve` line, and `published`.
245
+ - **`--grep <pattern>` (ATK-622).** With `--for`, it reads every task log of those pipelines' runs, succeeded or not (`Ci.taskLogs`: Azure Pipelines' timeline tasks under their stage, through the reader `failureLog` uses; GitHub Actions' `gh run view --log` steps under their job). It tests each line against a JavaScript regular expression, case-sensitive. `grep.runs` gives each run's `matches` by stage and task, at most 200 lines a task with `truncated` beyond that, and `searched`, each task with its line count. So a dev proof can show that a run never printed a line. Some runs carry a note saying why they were searched in part or not at all: a pipeline with no run yet (`run: null`), a run still queued or running, one waiting on an approval (its later stages haven't run), and a log the provider won't give. A transient failure or a lost login is thrown instead.
246
+ - **Logs and stages.** Each failed run comes with its failed tasks and log tail (`logs`). For the project's repo, each of the profile's `releases` whose run succeeded gives what it staged (`stages.<key>`): package, version, `stageId`, the `npm stage approve` line, and `published`. Each release has a `kind`, which says how its stage is read and which gate a person answers it by (`src/release-kinds.ts`, ATK-530). `npm-stage` is the only kind: the run's `npm stage publish` line, whether npm has the version, and the `npm-otp` gate. The profile's `gates` take `plan`, `merge` and each kind's gate, and `conduct project check` warns about a release whose gate they leave out.
247
+ - `conduct timeline` (ATK-623) reads when a run's tasks ran, so a worker can measure a step (a cache that has to pay for itself) without a merge commit. It is a read, so it runs here.
248
+ - **One run.** `conduct timeline <run>` gives the run's start, finish and `seconds`, and each task in the order it started, with its `stage`, `job`, `started`, `finished`, `seconds` (null until it has both times), and the host's `state` and `result` (`Ci.timeline`: Azure Pipelines' timeline `Task` records under their stage and job; GitHub Actions' jobs API steps under their job, with no stage).
249
+ - **A pipeline's last runs.** `conduct timeline --for <pipeline>` reads the last finished runs of one pipeline on a branch instead, newest first (`Ci.recentRuns`): the profile's base and 3 runs by default, `--branch` and `--count` (at most 20) to change them. The pipeline is named as `runs --for` names one, and a name no pipeline, or more than one, answers to is refused. A branch with fewer runs says so in a note.
250
+ - **`--task <regex>` and `--grep <regex>`.** `--task` keeps the tasks whose name matches, not case-sensitive, so `Cache Next build` also keeps its `Post-job: Cache Next build` save. One that matches nothing says so and lists the run's tasks. `--grep`, which needs `--task`, reads only those tasks' logs (`Ci.taskLogs` with `only`) and gives `grep` in `runs --grep`'s shape: `matches` and `searched` by task, with a note for a log the provider won't give. A run not finished says its tasks may be partial.
213
251
  - `conduct wait merge <sha> [--for …] [--repo …]` parks the worker until that verdict isn't `waiting` (see Waits), on either CI, so no worker polls with `wait time`. It absorbs ATK-430, ATK-246 and ATK-182.
214
252
 
215
253
  ## Writes
@@ -227,12 +265,14 @@ Every write the conductor makes outside its own store, with what triggers it and
227
265
  | A pushed branch | `conduct pr create` (a worker), from its own checkout | git's own credential helper |
228
266
  | A PR opened (ADO: noFastForward, delete the source branch on completion) | `conduct pr create` → `POST /workers/:id/prs` | the project's `az` identity, or its gh account |
229
267
  | ADO: a +10 vote (auto mode) and auto-complete; GitHub: auto-merge with a merge commit (auto mode). Then the Landing note | `conduct land` → `POST /workers/:id/landings` | the project's `az` identity, or its gh account |
268
+ | ADO: a PR's failed build queued again, once per build and head commit, after its Requeue note | `conduct land --requeue` → `POST /workers/:id/landings` | the project's `az` identity |
230
269
  | ADO: an expired build queued again, a cleared vote restored | a verdict wait on a landing `conduct land` armed (the ADO watcher, `code-host.kept-armed`) | the project's `az` identity |
231
270
  | A reply on the worker's PR | `conduct reply` → `POST /workers/:id/replies` | the project's `az` identity, or its gh account |
232
- | ADO: a pipeline queued again on the base, for the worker's merge whose run was cancelled | `conduct runs <sha> --requeue` → `POST /workers/:id/requeues` | the project's `az` identity |
271
+ | The worker's PR closed unmerged (ADO: abandoned), then its Abandoned note | `conduct pr abandon` → `POST /workers/:id/abandons` | the project's `az` identity, or its gh account |
272
+ | ADO: a pipeline queued on the base, for the worker's merge whose run was cancelled, failed or is missing | `conduct runs <sha> --requeue` → `POST /workers/:id/requeues` | the project's `az` identity |
233
273
  | A train: its branch, the members' merges into it, its PR, a note on each member | `conduct train` (you) | the project's gh account |
234
274
 
235
- Accounts are per project (see Accounts). Each code-host write is an event: `code-host.pr-created`, `code-host.landing-armed`, `code-host.kept-armed`, `code-host.replied`, `code-host.run-requeued`. So is each tracker write a worker causes: `tracker.transitioned` (or `tracker.transition-failed`) and `tracker.filed`. Rules still never change an item's state: only its phase notes do.
275
+ Accounts are per project (see Accounts). Each code-host write is an event: `code-host.pr-created`, `code-host.landing-armed`, `code-host.kept-armed`, `code-host.replied`, `code-host.pr-abandoned`, `code-host.run-requeued`, `code-host.build-requeued`. So is each tracker write a worker causes: `tracker.transitioned` (or `tracker.transition-failed`) and `tracker.filed`. Rules still never change an item's state: only its phase notes do.
236
276
 
237
277
  ## Inbox
238
278
 
@@ -244,11 +284,11 @@ The `inbox` table holds everything that waits on the operator, across projects.
244
284
  - the exact commands, and its links, each named (`{ label, href }`);
245
285
  - when it was raised.
246
286
 
247
- The console gives each link a button of its own, `open <label>`, and `conduct inbox` and `/inbox` print it as `<label>: <href>`. The daemon names the links it knows: a run is `run <n>`, a conflict's PR is `PR <n>`, a refused file is `rules` or `profile`, a checkout-sync item's root is `checkout`, a train's are `<repo> PRs` and `train PR <n>`. Every other link is named from its shape:
287
+ The console gives each link a button of its own, `open <label>`, and `conduct inbox` and `/inbox` print it as `<label>: <href>`. The daemon names the links it knows: a run is `run <n>`, a conflict's PR is `PR <n>`, a refused file is `rules` or `profile`, a train's are `<repo> PRs` and `train PR <n>`. Every other link is named from its shape:
248
288
 
249
289
  - A pull request is `PR <n>`, or `<repo> PR <n>` when it isn't the project's repo.
250
290
  - An Azure Pipelines or GitHub Actions run is `run <n>`.
251
- - A tracker item is its key: `ATK-217`, `CAD-1234` for a Boards work item, `PAR-12` for an issue of the project's GitHub Issues repo. A comment on an item or a PR adds ` note` (`ATK-217 note`, `PR 2078 note`). A work item or issue of another project is `work item <id>` or `<repo> #<n>`.
291
+ - A tracker item is its key: `ENG-312`, `CAD-1234` for a Boards work item, `PAR-12` for an issue of the project's GitHub Issues repo. A comment on an item or a PR adds ` note` (`ENG-312 note`, `PR 2078 note`). A work item or issue of another project is `work item <id>` or `<repo> #<n>`.
252
292
  - Anything else is its host, or a path's last segment.
253
293
 
254
294
  Labels longer than 40 characters are cut. Schema 9 named the links stored before this from their shape alone, with each item's project's profile, so a migrated checkout root reads as its folder's name rather than `checkout`.
@@ -259,7 +299,7 @@ An item resolves itself when its condition is seen, and records what resolved it
259
299
  |---|---|---|
260
300
  | `plan` | a `Plan` note (`conduct note`), or an `approval` wait; not a Plan an `auto-approve now` rule approves (unless its comment fails). An answer by someone who isn't an approver adds `(ignored approve from <name>: not an approver)` to its text | the approval wait resolving (an approver's `approve` or `revise:` comment), a `Plan approved` note, a newer Plan, or `conduct approve` / `revise` |
261
301
  | `merge` | a `Handoff · merge: <PR>` note; a `pr … merged` wait attaches to it. Held while its PR's checks are red, and held at once (no push) when the note names a PR whose checks are red: raised again as it was when a checks wait on the PR resolves passed | its PR wait resolving as merged, abandoned or closed, a `Landed` note, or the PR's checks failing (`pr.checks-failed`, resolved as `held: …`). A PR wait without the note raises nothing, because auto landing needs nobody |
262
- | `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 |
302
+ | `npm-otp` | a `Handoff · npm-otp` note (the `npm stage approve …` on its heading is the command), or an `npm` wait: the `npm-stage` release kind's gate | the version on its tag, or a `Resumed` or `Landed` note |
263
303
  | `operator`, `release`, `human`, `scope`, `blocked`, `abandoned`, `failed` | a Handoff note of that kind | any later note on the item (such as `Resumed`), or `conduct done` |
264
304
  | `permission`, `stall` | `worker.stalled` (no idle `stall` while the worker's Handoff item with nothing to watch is open; see Workers) | `worker.stall-cleared`, a park or resume, a new wait or Handoff (stall only), or a daemon restart (it is raised again if it persists) |
265
305
  | `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 |
@@ -269,37 +309,52 @@ An item resolves itself when its condition is seen, and records what resolved it
269
309
  | `login` | `provider.login-failed`: a project's provider login stopped working (see Accounts), once per provider and account however many projects and calls find it, with the command that signs in again | the login working again, found by the daemon's re-probe, any call, or a `login` wait (`provider.login-restored`), `conduct done`, or a daemon restart (the spell starts over) |
270
310
  | `runaway` | a worker passing `inbox.runawayTokens` (1.5M) input, output and cache-write tokens in all, once per worker | `conduct done`, or its retirement |
271
311
  | `context` | `worker.compact-failed`: the daemon couldn't compact a worker whose context passed the `compact-on-park` rule (see Workers), while one is open, with `conduct attach` (run `/compact` in it) and `conduct retire` | the session compacting (`worker.compacted`), `conduct done`, or its retirement |
272
- | `release`, `queue-drained` | the scheduler's default rules (see Scheduler): `ready-to-release` raises `release` with the profile's `release.handoff` text (ATK's says to run `/release-atk`) | `conduct done` |
312
+ | `release`, `queue-drained` | the scheduler's default rules (see Scheduler): `ready-to-release` raises `release` with the profile's `release.handoff` text | `conduct done` |
273
313
  | `triage` | the scheduler's default rule `triage`: an item with the profile's triage label, once per item (see Scheduler) | a read of its project that no longer shows the label on the item (removed, or the item closed or left the queue), in `scheduler.mode` `on`; or `conduct done`, after which it is raised again the next day while the label stays. Once resolved, a label put back raises it again at once |
274
314
  | `console` | the console's finding on an escalation, with `conduct escalations done <id> --ask` (see Console) | `conduct done` |
275
315
  | `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` |
276
316
 
277
- Worker protocol 2 renamed the kind `jason` to `operator` and merged `prod-approval` and `ready-to-release` into `release`; schema 10 moved the stored items. The daemon never approves prod itself. When a worker is retired, its open items resolve with it.
317
+ The daemon never approves prod itself. When a worker is retired, its open items resolve with it.
278
318
 
279
319
  For an item raised from a note:
280
320
 
281
321
  - The commands are the inline code on the note's heading and the lines of its shell-tagged fenced code blocks (`bash`, `sh`, `shell`, `powershell`, `pwsh`, `ps1`, `cmd`, `console`). An untagged block is output, not a command.
282
322
  - The links are the comment's URL, named `<key> note`, and every URL in the note. A URL a Markdown link gives (`[registry PR #539](…)`) takes that link's text as its label; the others are named from their shape.
283
323
 
284
- `conduct approve ATK-312` (or the inbox id written `#12`; a bare number is refused, since it could mean an issue) posts `approve` on the item through the tracker, or `approve: <text>` with `--text`. It uses the same key as notes, so the comment carries Jason's name and counts as his answer. `conduct revise ATK-312 "<text>"` posts `revise: <text>`; every word after the item is the text, quoted or not. Either one then:
324
+ `conduct approve ENG-312` (or the inbox id written `#12`; a bare number is refused, since it could mean an issue) posts `approve` on the item through the tracker, or `approve: <text>` with `--text`. It uses the same key as notes, so the comment carries the operator's name and counts as their answer. `conduct revise ENG-312 "<text>"` posts `revise: <text>`; every word after the item is the text, quoted or not. Either one then:
285
325
 
286
326
  1. resolves the plan item and the worker's open `approval` wait;
287
- 2. has the session manager resume a parked worker with `[conductor] event: approval ATK-312: approve HH:MM`. The worker reads the comment on the tracker before it acts.
327
+ 2. has the session manager resume a parked worker with `[conductor] event: approval ENG-312: approve HH:MM`. The worker reads the comment on the tracker before it acts.
288
328
 
289
329
  If the tracker refuses the comment, nothing is resolved. Only a plan takes approve or revise; `conduct done <id>` resolves any item.
290
330
 
331
+ An approved plan has no inbox item, so `conduct revise ENG-312 "<text>"` (and the console's `/revise`) with no plan of the item in the inbox revises the item's approved plan instead (ATK-664). The daemon posts `revise: <text>` the same way and then does at once what its 5-minute revise sweep would: it records the revise once for the Plan, and resolves every open wait of the item's parked worker with `plan ENG-312 revised: re-plan …`, so the worker resumes and re-plans. Once the PR has merged it wakes nobody: the revise is a follow-up PR, which the worker learns of at its next note. A running worker's next note is held back (exit 4), as for any revise of an approved plan. An item whose newest Plan isn't approved, or that has none the daemon saw, is refused before anything is posted. The daemon records only the revise the item's brief shows, which it reads up to three times, 2 s apart, since a tracker's read can lag its write. That way the worker's next note, which reads the brief too, is held back by the same revise. When the brief doesn't show the revise yet, can't be read, or the record fails, nothing is recorded, `conduct revise` says why, and the sweep records it on its next pass.
332
+
291
333
  ## Notifications
292
334
 
293
- The notifier routes each new inbox item to the phone, on two channels:
335
+ The notifier routes each new inbox item to the phone, or anywhere else that takes a webhook, on three channels:
294
336
 
295
337
  - **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.
296
- - **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. Only `~/.conductor` falls back to `atk-conductor-pushover`: a daemon on another `CONDUCTOR_HOME` (a scratch home for a test) reads a credential only when its `config.json` names `notify.pushover.credential`, so it pushes to your phone only with that or with `PUSHOVER_TOKEN` and `PUSHOVER_USER` in its environment (ATK-506).
338
+ - **Pushover:** sent from the daemon. The keys are `PUSHOVER_TOKEN` and `PUSHOVER_USER`, or else the credential `conductor-pushover` in the machine's secret store (`notify.pushover.credential`; see Accounts): its **user name is your Pushover user key** and its **password is the application's API token**. Save it with `cmdkey /generic:conductor-pushover /user:<user key> /pass` on Windows, `security add-generic-password -s conductor-pushover -a <user key> -w` on macOS, or `secret-tool store --label=conductor-pushover service conductor target conductor-pushover user <user key>` on Linux; each asks for the token. On macOS and Linux, delete an older item first (`conduct project check` prints the line that does). A credential saved while the daemon runs is read on the next push. The keys are never logged or stored. Only `~/.conductor` falls back to `conductor-pushover`: a daemon on another `CONDUCTOR_HOME` (a scratch home for a test) reads a credential only when its `config.json` names `notify.pushover.credential`, so it pushes to your phone only with that or with `PUSHOVER_TOKEN` and `PUSHOVER_USER` in its environment (ATK-506).
339
+ - **Webhook:** sent from the daemon, as one `POST` of JSON per push to `notify.webhook.url` in `config.json` (`http` or `https`). It is off until that is set: `conduct notify` shows it `inactive`, a test push to it fails with that reason, and `config.json` is refused while one of its profiles sends to it. `notify.webhook.credential` names a credential in the machine's secret store whose password goes out as `Authorization: Bearer <password>`, saved as Pushover's is (any user name); with one, the URL must be `https` or on this machine (`localhost`, `127.x.x.x`). `notify.webhook.timeoutSeconds` is 15 by default. Any 2xx counts as delivered; a redirect isn't followed, and counts as a refusal (set the URL it points to). A refusal or no answer marks the channel `failing`, and the push is tried again as a failed Pushover push is. Errors name only the URL's host, since a hook URL often carries its own secret. The body:
340
+
341
+ ```jsonc
342
+ {
343
+ "event": "raised", // raised, reminder, summary (quiet hours), rule (a rule's notify) or test
344
+ "title": "conductor · ENG-5 npm-otp",
345
+ "text": "ENG-5 · approve the npm stage", // the push in one line, as the other channels show it
346
+ "link": "https://…", // its first link, or null
347
+ "project": "web",
348
+ // the inbox item whole for raised and reminder, null otherwise
349
+ "inbox": { "id": 12, "kind": "npm-otp", "item": "ENG-5", "project": "web", "text": "…", "commands": ["npm stage approve … --otp=<code>"], "links": [{ "label": "run 7", "href": "https://…" }], "raisedAt": "2026-10-18T12:00:00.000Z", "worker": 3 }
350
+ }
351
+ ```
297
352
 
298
353
  **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:
299
354
 
300
355
  ```jsonc
301
356
  {
302
- "operator": { "name": "Jason" }, // the one person this daemon serves; messages name them ("the operator" by default)
357
+ "operator": { "name": "Sam" }, // the one person this daemon serves; messages name them ("the operator" by default)
303
358
  "quietHours": { "from": "22:00", "to": "07:00" }, // local; omit for none
304
359
  "notify": {
305
360
  "profile": "default", // the profile when none is switched to
@@ -308,7 +363,7 @@ The notifier routes each new inbox item to the phone, on two channels:
308
363
  "away": [{ "to": ["pushover"] }], // built in
309
364
  "focus": [{ "kinds": ["npm-otp", "permission", "failed", "conflict", "operator", "login"], "to": ["remote-control", "pushover"] }], // built in
310
365
  "workday": [
311
- { "projects": ["cadence"], "days": ["sat", "sun"], "to": [] }, // keep these off every channel
366
+ { "projects": ["portal"], "days": ["sat", "sun"], "to": [] }, // keep these off every channel
312
367
  { "kinds": ["npm-otp"], "to": ["pushover", "remote-control"], "remind": { "afterMinutes": 10, "times": 5 } },
313
368
  { "hours": "08:00-18:00", "to": ["remote-control"] }
314
369
  ]
@@ -319,12 +374,12 @@ The notifier routes each new inbox item to the phone, on two channels:
319
374
 
320
375
  A rule matches on `kinds` (inbox kinds), `projects`, `hours` (local, `HH:MM-HH:MM`, may run past midnight) and `days` (`sun` … `sat`, the day when the item is routed, so 01:00 on Saturday is `sat` even inside a Friday `22:00-02:00`); a rule with none of them matches everything. `conduct notify profile away` switches at once, without a restart, and the profile survives one. `--until 17:00` (the next 17:00), `--until 2026-10-12` (local midnight) or `--until 2026-10-12T09:00` hands back to the profile that was in force before. Switching to config.json's own `notify.profile` without `--until` hands the choice back to config.json. Each switch is a `notify.profile` event.
321
376
 
322
- - **Quiet hours** (`quietHours`): pushes are held (`notify.routed` with `outcome: "held"`). When they end, each channel gets one summary of the held items still open, such as `2 waiting since quiet hours: ATK-1 npm-otp, ATK-2 plan; 1 resolved meanwhile`, routed by the profile then in force. `conduct notify test` pushes at any hour.
377
+ - **Quiet hours** (`quietHours`): pushes are held (`notify.routed` with `outcome: "held"`). When they end, each channel gets one summary of the held items still open, such as `2 waiting since quiet hours: ENG-1 npm-otp, ENG-2 plan; 1 resolved meanwhile`, routed by the profile then in force. `conduct notify test` pushes at any hour.
323
378
  - **Dedupe:** the same project, item, kind and text pushed within `notify.dedupeMinutes` (30) to the channels it would go to now isn't pushed again; a newer note that replaces an item is the usual case. Its reminders carry on from the first push, on the newer item only. A copy that never reached the phone (held, or a failed push) gives way to the newer one.
324
379
  - **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.
325
- - **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`.
380
+ - **Events:** `notify.routed` (the profile, the rule, the channels, `sent`, `held`, `deduped` or `none`), `notify.requested`, `notify.sent` and `notify.failed` (Pushover and the webhook), `notify.channel` (a channel's state changed), `notify.profile`.
326
381
 
327
- `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.
382
+ `conduct notify` shows the profile, quiet hours and the last state of each channel: Pushover and the webhook from their own answers (the webhook is `inactive` with no `notify.webhook.url`), 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.
328
383
 
329
384
  ## Rules
330
385
 
@@ -341,14 +396,14 @@ Rules say what the conductor does when something happens (design note, section 9
341
396
  ]
342
397
  ```
343
398
 
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). 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), `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`.
399
+ - **Sources, in order:** the item's project's `rules`, its machine profile's (`~/.conductor/projects/<project>.json`, named `project.<project>.machine:<id>`) before its repo's (`.claude/conductor.json`, changed by PR, named `project.<project>:<id>`; see Projects), then `~/.conductor/rules.json`, then the built-in defaults: the conductor's own 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, and a machine-profile rule replaces the repo's rule of its id (`"do": "none"` switches it off).
400
+ - **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`.
346
401
  - **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.
347
402
  - **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).
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.
349
- - **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.
403
+ - **Reload:** saving `rules.json` or a project's machine profile reloads its rules, and so does a fetch of its clone that brings another `.claude/conductor.json` on the base branch. A profile changed either way, or 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 project keeps the name its folder gives it: the repo's `name` is only what `project add` takes when no `--name` is given, and a machine profile that names another project is refused. To rename one, `conduct project remove <old name>`, then `conduct project add <url> --name <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.
404
+ - **Checking:** `conduct rules` lists the rules in force (`project.web:#2`, `machine:runaway`, `default:launch`) and what replaces what. `conduct rules test <file>` dry-runs a sample, such as `{ "event": "pr.comment", "project": "web", "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.
350
405
 
351
- - **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.
406
+ - **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 ENG-312` does it for the item's newest actions.
352
407
 
353
408
  **How actions are carried out.** Every action is a `rule.fired` event that names its rule, then `rule.done` or `rule.failed`.
354
409
 
@@ -366,56 +421,87 @@ What each action does:
366
421
  - `escalate console` emits `console.escalated`, which wakes the console's model; the console reports back with `conduct escalations done` (see Console).
367
422
  - `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.
368
423
  - `auto-approve after N` pushes "auto-approves at HH:MM" where plans go, and the plan's inbox item says the same. 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. A deadline that falls in quiet hours (or a plan posted in them, or one that comes due in quiet hours set since) moves to their end plus N, so the quiet-hours summary reaches you first and you still get N to revise. If the comment can't be posted, the inbox item says the plan waits on you again.
369
- - `auto-approve now` approves the plan as its note arrives: no inbox item, no push. `conduct note` asks the rules before it records the Plan and puts the rule on `note.posted` (`autoApprove`), so the inbox raises nothing. The conductor then posts `approve: auto-approved by the conductor: rule "<id>", at once` as the key's owner and records `plan.auto-approved`, and the worker's approval wait resolves as soon as it is added (or as the comment lands). If the comment can't be posted (`plan.auto-approve-failed`), the plan's inbox item is raised and it waits on you. You can still write `revise: …` on the item: the worker's next `conduct note` reads the item, finds it after the approval, records `plan.revised`, posts nothing and exits 4 with your words, and the worker re-plans (see Notes). A Handoff note and a new Plan still go through, and an `approve` after the revise takes it back. Once you have revised a plan of an item, its later plans wait on you whatever the rules say, `auto-approve after` included. The same holds after a plan you approved yourself. A worker that posts no note meanwhile (parked, or only Handoffs) still has the revise counted: every 5 minutes the daemon reads the items whose newest Plan is approved and records `plan.revised` for one with a `revise:` after its approval. A worker parked on its PR sees the revise only when it wakes; past the PR, the brake is a PR comment. If the daemon stopped between recording the Plan and posting the approval, the approval wait raises nothing, and the next rules tick approves it once the note is 2 minutes old.
424
+ - `auto-approve now` approves the plan as its note arrives: no inbox item, no push. `conduct note` asks the rules before it records the Plan and puts the rule on `note.posted` (`autoApprove`), so the inbox raises nothing. The conductor then posts `approve: auto-approved by the conductor: rule "<id>", at once` as the key's owner and records `plan.auto-approved`, and the worker's approval wait resolves as soon as it is added (or as the comment lands). If the comment can't be posted (`plan.auto-approve-failed`), the plan's inbox item is raised and it waits on you. You can still write `revise: …` on the item: the worker's next `conduct note` reads the item, finds it after the approval, records `plan.revised`, posts nothing and exits 4 with your words, and the worker re-plans (see Notes). A Handoff note and a new Plan still go through, and an `approve` after the revise takes it back. Once you have revised a plan of an item, its later plans wait on you whatever the rules say, `auto-approve after` included. The same holds after a plan you approved yourself. A worker that posts no note meanwhile (parked, or only Handoffs) still has the revise counted: every 5 minutes the daemon reads the items whose newest Plan is approved and records `plan.revised` for one with a `revise:` after its approval. When that pass records one and the item's PR hasn't merged, it also resolves the open waits of the item's worker with `plan <item> revised …: re-plan`. The daemon then resumes a parked worker with it at once, so the worker re-plans instead of sleeping until its PR settles (ATK-462). After the merge it wakes nobody: the revise becomes a follow-up PR, which the worker learns of at its next note. The PR's auto-complete stays armed meanwhile: the brake on the PR itself is a PR comment. If the daemon stopped between recording the Plan and posting the approval, the approval wait raises nothing, and the next rules tick approves it once the note is 2 minutes old.
370
425
  - No default rule auto-approves. A Plan note's `**Size tier: S**` (or the older `**Size: S**`) gives `plan.tier` (and `item.tier` later), and its `**Landing:** auto|review` gives `plan.landing`: work-item lands its review paths (infra, pipelines, auth) and review tiers in `review`, so `plan.landing == auto` keeps them out of an auto-approve rule. A Plan that names neither fails the comparison and waits for you.
371
426
  - `launch` on `item.unblocked` brings the scheduler's next tick forward.
372
- - `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.
427
+ - `launch` and `retry` start sessions, so they wait for `scheduler.mode` `on`, as the scheduler's own launches do. In `off` they are logged, not done. Every other action acts in any mode, except those the scheduler's own tick fires (`item.closed`, `queue.*`), which `off` only logs on a queue read on demand.
373
428
 
374
429
  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, as every `spend.*` field does and as `conduct status` shows them. On that measure a worker's median to 2026-10-14 was 442k and its most 1.68M, and a busy day ran 16M to 26M (ATK-464).
375
430
 
376
431
  ## Scheduler
377
432
 
378
- 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.
433
+ The scheduler is the work loop's tick, in 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.
379
434
 
380
- 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.
381
- 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.
382
- - **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.
435
+ 1. **Read.** It reads each conducted project's queue from its tracker: for Linear, the team's projects with a priority (or the label of a label queue), the Area labels, every open item with its blockers, and the in-flight items (see Linear). 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.
436
+ 2. **Plan.** It works out the picks across projects, with an order and a reason for each item. A replay of a recorded Linear read is in the tests.
437
+ - **Order:** the tracker project's priority, then the conductor project's `weight` (profile, or `conduct focus --weight`), then the item's priority, then the tracker's own order within that priority (Linear's drag order, a Boards query's or a GitHub Issues queue's order), then when it was created.
383
438
  - **Areas:** one item per Area per project until its PR merges. An Area is held by every worker in flight and every started item, and by the Areas it covers or is covered by. An item whose Plan lands in `auto` holds it no more once its PR has merged, so the next item in its Area starts from a base branch that has its work, while it waits on the dev check or the npm OTP; `conduct queue` and `conduct why` name the item it shares the Area with. The profile's `areaRelease` sets when (ATK-555):
384
439
  - `merge` (the default): when the code host (Azure DevOps or GitHub) reports the PR merged, or at the item's `Landed` note. The PR is its worker's (from `conduct pr create` or its newest `PR opened` note), else, with no worker in flight, the one its newest `PR opened` note names. Each tick reads it in one request (the PR alone, without its checks or threads), and a merged PR isn't read again. A PR still open, one closed without merging, one the daemon can't read (logged once) or an item with no PR known keeps the Area held.
385
440
  - `pr`: as soon as its newest lifecycle note is `PR opened` or `Landed`, so the next item starts while the first PR still waits on CI and the merge (ATK-412).
386
441
 
387
442
  A `review` plan, or an item the daemon has no notes for, holds its Area until the item closes.
388
443
  - **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).
389
- - **Triage:** a profile's `tracker.triageLabel` (such as `triage`, ATK-277) marks a new item a worker filed that Jason hasn't looked at. It is never picked (`labelled triage: new, waits for Jason to triage it`, not on its own; the label matches in any case, as Boards tags do), an item it blocks waits as blocked by an item nobody works on, and `queue.triage` raises it in the inbox. Remove the label to queue it, or swap it for the human label. A rule's `label add triage` adds this label. Without the setting the label means nothing to the queue. A conductor older than the setting refuses a profile that has it (the profile is strict), so set it only once every daemon and `conduct` that reads the profile has been upgraded.
390
- - **Release waits:** a profile's `tracker.releaseWait: { "label": "waits-release", "shipped": "release-atk · Shipped" }` marks work that needs a dependency released, not only merged (a CLI change on npm `latest`, a prod deploy). It works on every tracker: the label is a Linear label, a Boards tag or a GitHub Issues label (matched without regard to case on the last two), and the blockers are Linear's blocking relations, Boards Predecessors or GitHub's issue dependencies. An item with that label waits, once its open blockers have closed, until every Done blocker has a comment whose first line starts with the `shipped` text, under any heading or bold (`waits for ATK-391 to ship`), and starts on the first tick after. On Boards that is a discussion comment, markdown or the web UI's HTML. A comment that only quotes the text doesn't count. A blocker still open in another team, or in a Boards state the profile doesn't map, waits the same way; a canceled one (Removed on Boards, closed as not planned or a duplicate on GitHub) holds nothing. A Boards Predecessor in another ADO project, or a GitHub blocker in another repo, has its comments read there. A blocker whose comments can't be read (a 403, a 404, a provider error) holds and is asked again next read, while the rest of the read goes on; so does a Boards Predecessor ADO doesn't return at all (deleted, or in a project the account can't read). A blocker that shipped before release-atk wrote its notes (2026-10-06) holds until you add the note or remove the label. With the label and no blocker at all it waits until you act (`labelled waits-release with no blocker to wait on`), for something outside the tracker such as a Claude Code release. Such an item doesn't keep its tracker project from `queue.ready-to-release`, since the release is what it waits for. On Linear the read asks about the Done blockers in one more request, only when a labelled item has one. On Boards and GitHub Issues it reads the comments of a Done blocker that has any, and again only once its comment count or revision moves (its comment count or update time on GitHub), as a new comment does; on GitHub a labelled issue also reads its blocker list every read, since GitHub's summary counts only open blockers. Without the setting, a Done blocker never holds anything.
391
- - **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.
444
+ - **Triage:** a profile's `tracker.triageLabel` (such as `triage`, ATK-277) marks a new item a worker filed that the operator hasn't looked at. It is never picked (`labelled triage: new, waits for the operator to triage it`, not on its own; the label matches in any case, as Boards tags do), an item it blocks waits as blocked by an item nobody works on, and `queue.triage` raises it in the inbox. Remove the label to queue it, or swap it for the human label. A rule's `label add triage` adds this label. Without the setting the label means nothing to the queue. A conductor older than the setting refuses a profile that has it (the profile is strict), so set it only once every daemon and `conduct` that reads the profile has been upgraded.
445
+ - **Release waits:** a profile's `tracker.releaseWait: { "label": "waits-release", "shipped": "release · Shipped" }` marks work that needs a dependency released, not only merged (a CLI change on npm `latest`, a prod deploy). It works on every tracker: the label is a Linear label, a Boards tag or a GitHub Issues label (matched without regard to case on the last two), and the blockers are Linear's blocking relations, Boards Predecessors or GitHub's issue dependencies. An item with that label waits, once its open blockers have closed, until every Done blocker has a comment whose first line starts with the `shipped` text, under any heading or bold (`waits for ENG-311 to ship`), and starts on the first tick after. On Boards that is a discussion comment, markdown or the web UI's HTML. A comment that only quotes the text doesn't count. A blocker still open in another team, or in a Boards state the profile doesn't map, waits the same way; a canceled one (Removed on Boards, closed as not planned or a duplicate on GitHub) holds nothing. A Boards Predecessor in another ADO project, or a GitHub blocker in another repo, has its comments read there. A blocker whose comments can't be read (a 403, a 404, a provider error) holds and is asked again next read, while the rest of the read goes on; so does a Boards Predecessor ADO doesn't return at all (deleted, or in a project the account can't read). A blocker that shipped with no such note holds until you add the note or remove the label. With the label and no blocker at all it waits until you act (`labelled waits-release with no blocker to wait on`), for something outside the tracker such as a Claude Code release. Such an item doesn't keep its tracker project from `queue.ready-to-release`, since the release is what it waits for. On Linear the read asks about the Done blockers in one more request, only when a labelled item has one. On Boards and GitHub Issues it reads the comments of a Done blocker that has any, and again only once its comment count or revision moves (its comment count or update time on GitHub), as a new comment does; on GitHub a labelled issue also reads its blocker list every read, since GitHub's summary counts only open blockers. Without the setting, a Done blocker never holds anything.
446
+ - **Slots:** `scheduler.slots` (4) across projects and the profile's `slots` per project, 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.
392
447
  - **Holds:** quiet hours and `conduct pause` turn picks into waits that say why.
393
448
  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; a partial retire is tried again after 15 minutes, and after 3 in a row it raises one inbox item that names the failed steps and any worktree left on disk, which the conductor never deletes, and is tried once a day), `queue.drained` and `queue.ready-to-release` → an inbox item once a day, `queue.triage` → a `triage` inbox item per item (default `triage`), resolved once the label goes. 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.
394
449
 
395
450
  `scheduler.mode` decides how far it goes:
396
451
  - **`on`** (the default): it acts.
397
- - **`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.
398
- - **`off`:** it doesn't run; `conduct queue --fresh` still works out a plan on request.
452
+ - **`off`:** no timed queue, so nothing is launched or retired and the queue raises no inbox items. A queue read on demand (`conduct queue --fresh`, or the first `conduct queue`) works out a plan and logs each action it would take once, and does none of them.
399
453
 
400
454
  `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: its picks, the number waiting, `waitingBy` (how many wait for each reason: `in-flight`, `human`, `blocked`, `release`, `area`, `held`, `slot`) and `areaHolders` (each Area that holds an item up, with its project, its holder and the items waiting on it). In `GET /queue`, each waiting item carries the same `kind`, and an Area wait its `holder`. 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.
401
455
 
402
- **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`.
456
+ **Overrides** end on their own: `conduct pause web --until 17:00`, `conduct pause --until tomorrow` (every project), `conduct unpause web`, `conduct focus web --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`.
403
457
 
404
- ## Skill bumps reach the main checkout
458
+ ## Projects
459
+
460
+ A conducted project is a clone the daemon owns (design note v2, sections 2 and 7; ATK-527). Everything of it lives under `~/.conductor/projects`:
461
+
462
+ | Path | Holds |
463
+ |---|---|
464
+ | `<name>.json` | its machine profile: what this machine says about the project, which wins over the repo's |
465
+ | `<name>/repo` | the clone of its remote, which the daemon fetches and adds worktrees to |
466
+ | `<name>/worktrees/<item>` | a worker's worktree (see Workers) |
405
467
 
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):
468
+ **Your first conducted project.** `conduct init` takes a repo from a checkout to a passing `conduct project check` with no hand-written JSON (ATK-528). The repo is on GitHub or Azure DevOps and has at least one commit on its base branch.
407
469
 
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.
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>`.
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.
470
+ 1. Install the conductor (see On PATH) and sign in to the code host and tracker (see Accounts).
471
+ 2. In a checkout of the repo, run `conduct init`. It shows what it detected:
472
+ - the code host from `origin`;
473
+ - CI as the code host's own (a hint when the pipeline files are another's; for a GitHub repo with Azure Pipelines files, how to set `ci` to them);
474
+ - the base branch from `origin/HEAD`;
475
+ - `setup` from the lockfile at the root (`pnpm install --frozen-lockfile`, `npm ci`, `yarn install`, `bun install`, `uv sync`, `poetry install` or `go mod download`);
476
+ - a `verify-*` skill in `.claude/skills`;
477
+ - the item key most recent commit subjects carry.
411
478
 
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`.
479
+ Then it asks for the rest, each detected value the default: the name, the base, the tracker (the code host's own issues or Boards, or Linear), its key, team or query, `setup` and the verify skill. `--yes` asks nothing and refuses, with each one's flag, what it couldn't detect. Without `--yes` it needs a terminal to ask in: node sees none in Git Bash's mintty, so use Windows Terminal there, or `--yes`.
480
+ 3. It writes a starter profile to `~/.conductor/projects/<name>.json`: the code host, CI, base, tracker, `conductor:work-item` and the verify skill, 2 slots, the plan and merge gates, a release handoff and `setup`, with the `conductor` minimum its fields need. `--shared` writes `.claude/conductor.json` in the checkout instead, which the conductor reads only on `origin/<base>`: commit it, push it and run `conduct init` again. A profile file that is there is never overwritten, and a repo profile on `origin/<base>` that names the project means nothing is written; one only in the working tree stops `init` until it is pushed.
481
+ 4. It runs `conduct project add` on the checkout. For a new clone it prints the trust step: run `claude` in the clone once and accept the prompt. In a terminal it waits for Enter; with `--yes` it stops there.
482
+ 5. It runs `conduct project check`. With no verify skill, the worktree check fails as a launch would, and `init` points at the conductor plugin's `/conductor:create-verification-skill`, which loads only with the plugin: run `claude --plugin-dir <the plugin>` in the checkout (`init` prints the folder), make the skill under the name the profile's `skills.verify` gives, commit it and push it.
483
+ 6. Run `conduct init` again after each step: it keeps what is done, fetches the clone and checks again. On a project that is set up, it writes nothing and only fetches and checks. Once the check passes, give the tracker's projects a priority (Linear) or label items `conduct` (GitHub Issues), and `conduct launch <item>`.
484
+
485
+ **Adding one.** `conduct project add <url | path> [--name <name>]` clones the remote into `<name>/repo`, in the CLI's own process, so its output shows and no API timeout applies. Given a path, it reads that checkout's `origin` and clones from the remote: the folder itself is never cloned, and nothing the conductor does touches it again. The name is `--name`, else the `name` the repo's `.claude/conductor.json` gives on its default branch; a repo with no profile needs `--name`. The clone is made in a staging folder first, so one that fails leaves nothing. Adding a project again fetches its clone (its origin must be the same remote) and updates the registration in place: its inbox, items and workers stay. Then the daemon registers it (or the store, with none running). Claude Code trusts a worktree by its repository, and nobody has opened a new clone in Claude Code yet, so `add` ends with the one step that trusts it: run `claude` in the clone once and accept the trust prompt, then `conduct project check <name>`.
486
+
487
+ **Its profile** comes in two layers, merged as the files are written, before the schema reads them:
488
+
489
+ - the **machine profile**, `<name>.json` (JSONC), optional beside a repo profile. A project with no repo profile keeps a whole profile here;
490
+ - the **repo profile**, `.claude/conductor.json` at `origin/<base>` in the clone, optional, changed by PR and shared by the team. `<base>` is the machine profile's `baseBranch`, else the remote's default branch (`origin/HEAD`); when the file there names another `baseBranch`, it is read once more at that one.
491
+
492
+ Objects merge key by key and the machine's value wins; arrays (`ports`, `setup`, `approvers`, `gates`, …) are replaced whole. `rules` stack instead: the machine profile's first, then the repo's, then `rules.json`, then the defaults, so a machine rule replaces the repo's rule of its id (see Rules). Each layer's `conductor` minimum is checked on its own, and `protocol` on the merge. `name` is the registered one: a machine profile may repeat it, not change it.
493
+
494
+ `conduct profile <name>` prints the merged profile from the files and the clone as fetched last, one line per field with its value and its source (`machine`, `repo` or `default`), then each rule with its layer; `--json` gives `{ profile, sources }`. It works with or without the daemon.
495
+
496
+ **Keeping it current.** When the daemon starts and every `clones.fetchMinutes` (10) after, it runs `git fetch --quiet --prune origin` in each clone and then reads the profile again, so a changed `.claude/conductor.json` on the base branch is in force as a saved machine profile is (a watch on `<name>.json` takes that at once). A worktree create waits out a fetch that is running, and the fetches pass a clone by while a create works in it, so the two never write the clone's refs at once. A fetch can't prompt for credentials (`GIT_TERMINAL_PROMPT=0`) and gives up after 2 minutes. A failed fetch is logged once a spell, and the profile stays as it was. Nothing is pulled, installed or fast-forwarded anywhere else: a worktree's `setup` installs what its branch pins.
497
+
498
+ **From a project registered by its checkout** (before ATK-527): the daemon conducts such a row no longer. Its profile is refused with the step that moves it, `conduct project add "<that checkout>"`, which clones the checkout's origin and keeps the registration, and the scheduler launches none of its items meanwhile. Workers in flight keep their worktrees in that checkout, and retire from it: that retire is the one time the conductor runs git there (`git worktree prune`, and `git branch -D` of the worker's branch). A profile refusal of the project, whichever file it named, resolves once the project's profile loads.
413
499
 
414
500
  ## Skill trains
415
501
 
416
- Each skill change ships as its own registry publish, and each publish is a GitHub PR that Jason merges. A train turns several of them into one merge (ATK-285). A profile turns it on with `"train": { "repo": "<owner>/<repo>", "minimum": 3, "maxHours": 24 }`.
502
+ Each skill change ships as its own registry publish, and each publish is a GitHub PR that the operator merges. A train turns several of them into one merge (ATK-285). A profile turns it on with `"train": { "repo": "<owner>/<repo>", "minimum": 3, "maxHours": 24 }`.
417
503
 
418
- 1. A worker publishes as before (`atk publish`, which opens registry PR #n). It then marks #n with a note headed `## <work skill> · Train`, or `## <work skill> · Train · urgent: <why>` to ask for a train now. It waits on #n's merge as before: parked, with its slot free.
504
+ 1. A worker publishes as before (its publish opens registry PR #n). It then marks #n with a note headed `## <work skill> · Train`, or `## <work skill> · Train · urgent: <why>` to ask for a train now. It waits on #n's merge as before: parked, with its slot free.
419
505
  2. Every 15 minutes, the first a minute after the start, the daemon reads the repo's open PRs and their comments. While the marked ones not yet on a train reach `minimum`, one is urgent, or the oldest has waited `maxHours`, one `operator` inbox item asks for `conduct train <project>`. While a train PR is open, a `merge` item asks you to merge it. Each resolves itself once it no longer holds.
420
506
  3. `conduct train` takes each marked PR that targets the default branch, has passed its checks, and changes no file another member changes. It creates `train/<yyyymmdd-hhmm>` (UTC) from the default branch and merges each member's head into it with GitHub's merges API, so nothing is checked out. It then opens one PR, which lists the members as `- #n …` lines, and notes `<work skill> · Train opened · merge: <url>` on each member. A merge GitHub refuses deletes the branch, and nothing is opened. `--dry-run` only reads. It lists what waits, what a train would take, and why the rest stay out.
421
507
  4. Merge the train with a merge commit. Each member's head is then on the default branch, so GitHub shows the member merged and its worker wakes. A member pushed to after the train opened stays open, and rides the next train.
@@ -428,20 +514,20 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
428
514
 
429
515
  | Verb | Full form | Holds when |
430
516
  |---|---|---|
431
- | `pr 1850` | `pr:ado:Emergent.AgenticToolkit#1850:verdict` | the PR's verdict isn't `waiting` (see Worker verbs); the daemon keeps a recorded landing armed meanwhile |
432
- | `pr 1850 merged` | `pr:ado:Emergent.AgenticToolkit#1850:merged` | the PR is completed (or abandoned: that resolves it too, as a fact) |
517
+ | `pr 1850` | `pr:ado:Contoso.Web#1850:verdict` | the PR's verdict isn't `waiting` (see Worker verbs); the daemon keeps a recorded landing armed meanwhile |
518
+ | `pr 1850 merged` | `pr:ado:Contoso.Web#1850:merged` | the PR is completed (or abandoned: that resolves it too, as a fact) |
433
519
  | `pr 1850 checks` | `…#1850:checks` | no blocking build of the PR's current commit is queued or running (passed or failed; a path-filtered build it doesn't touch counts as passed) |
434
520
  | `pr 1850 comment` | `…#1850:comment` | a thread is open whose last word isn't a work-skill note |
435
521
  | `pr 458 merged --repo <owner>/<repo>` | `pr:github:<owner>/<repo>#458:merged` | the same on GitHub; `comment` there is a person's comment or review after the wait was registered. Without `--repo`, a project whose `codeHost` is `{ "kind": "github", "owner", "repo" }` means its own repo |
436
522
  | `pr 1850 merged checks comment` | one wait per event | registers the three in one call; the first that holds wakes the worker. `verdict` can't be combined with other events |
437
- | `merge dda5b26c` / `--for deploy-api` | `merge:ado:Emergent.AgenticToolkit@dda5b26c:for=deploy-api` | the merge's runs settled as `conduct runs` reads them (its verdict isn't `waiting`). The wake names the outcome, the failed pipelines and each release's staged version with its approve line. The ADO watcher reads the merge's git in the worker's worktree, once per wait |
523
+ | `merge dda5b26c` / `--for deploy-api` | `merge:ado:Contoso.Web@dda5b26c:for=deploy-api` | the merge's runs settled as `conduct runs` reads them (its verdict isn't `waiting`). The wake names the outcome, the failed pipelines and each release's staged version with its approve line. The ADO watcher reads the merge's git in the worker's worktree, once per wait |
438
524
  | `merge 5426434c --repo <owner>/<repo>` | `merge:github:<owner>/<repo>@5426434c` | the same on GitHub Actions: the runs the merge's push started settled |
439
- | `run atk-api-deploy 12345 done` | `run:ado:atk-api-deploy#12345:done` | the run completed, with its result |
440
- | `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, read through the `npm-stage` release adapter |
525
+ | `run web-api-deploy 12345 done` | `run:ado:web-api-deploy#12345:done` | the run completed, with its result |
526
+ | `npm 8.5.0-dev.2 --tag dev` | `npm:@contoso/cli@8.5.0-dev.2:dev` | the dist-tag points at the version, read through the `npm-stage` release adapter |
441
527
  | `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 |
442
- | `approval` | `approval:linear:ATK-310` (`approval:ado-boards:CAD-12` on Boards, `approval:github-issues:PAR-12` on GitHub Issues) | an `approve` / `revise:` comment by an approver after the item's newest `<work skill> · Plan` note: the Linear key's owner (on Boards, the `az login` identity; on GitHub Issues, the gh account), or an account the profile's `approvers` names by email (Linear), unique name (Boards: the UPN, usually the email) or login (GitHub). An answer by anyone else is ignored, and the plan's inbox item says whose it was (`approval.ignored`) |
443
- | `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; on GitHub Issues the state is `open`, `closed`, a phase label or its type |
444
- | `login ado` / `github` / `linear` | `login:ado:atk` | the project's login to the provider works again: a probe as its account (see Accounts) succeeds |
528
+ | `approval` | `approval:linear:ENG-312` (`approval:ado-boards:CAD-12` on Boards, `approval:github-issues:PAR-12` on GitHub Issues) | an `approve` / `revise:` comment by an approver after the item's newest `<work skill> · Plan` note: the Linear key's owner (on Boards, the `az login` identity; on GitHub Issues, the gh account), or an account the profile's `approvers` names by email (Linear), unique name (Boards: the UPN, usually the email) or login (GitHub). An answer by anyone else is ignored, and the plan's inbox item says whose it was (`approval.ignored`) |
529
+ | `item ENG-312 done` / `label human` / `note Landed` | `item:linear:ENG-312:state=done` | the state (name or type), a label (a tag on Boards), or a phase note; on GitHub Issues the state is `open`, `closed`, a phase label or its type |
530
+ | `login ado` / `github` / `linear` | `login:ado:web` | the project's login to the provider works again: a probe as its account (see Accounts) succeeds |
445
531
  | `time 2026-10-10T09:00` | `time:2026-10-10T14:00:00.000Z` | the time has passed (no zone means local time) |
446
532
 
447
533
  - **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 stored profile (the last good one read from disk), and refuses one it can't check, such as a `health` environment the profile has no URL for. A condition the worker already waits on (not resolved yet) is that wait, so registering every condition again after a wake adds no duplicates; `wait.added` says `existing: true` then.
@@ -459,8 +545,8 @@ Accounts belong to the person running the daemon, not to the repo, so each proje
459
545
 
460
546
  ```jsonc
461
547
  "projects": {
462
- "atk": { "ado": { "token": "conductor-atk-ado" }, "linear": { "credential": "conductor-atk-linear" } },
463
- "cadence": { "ado": { "tenant": "<tenant id or domain>" }, "github": { "user": "<gh account>" } }
548
+ "web": { "ado": { "token": "conductor-web-ado" }, "linear": { "credential": "conductor-web-linear" } },
549
+ "portal": { "ado": { "tenant": "<tenant id or domain>" }, "github": { "user": "<gh account>" } }
464
550
  }
465
551
  ```
466
552
 
@@ -468,11 +554,23 @@ Accounts belong to the person running the daemon, not to the repo, so each proje
468
554
 
469
555
  | Order | Source | Azure DevOps (PRs, runs, Boards) | GitHub (reads, and writes such as `conduct train`'s) | Linear (waits, notes, labels, queue, briefs) |
470
556
  |---|---|---|---|---|
471
- | 1 | **project token**: a generic credential (Credential Manager) the project names | `projects.<name>.ado.token`: a PAT, sent as Basic auth | `projects.<name>.github.token`: a token, as `GH_TOKEN` | `projects.<name>.linear.credential`: an API key |
472
- | 2 | **machine token**: a conductor-only env var, else a generic credential | `CONDUCTOR_ADO_TOKEN`, else `conductor-ado` | `CONDUCTOR_GITHUB_TOKEN`, else `conductor-github` | `CONDUCTOR_LINEAR_KEY` or `LINEAR_API_KEY`, else `atk-conductor-linear` (`ATK_CONDUCT_LINEAR_CREDENTIAL` names another) |
557
+ | 1 | **project token**: a credential in the machine's secret store that the project names | `projects.<name>.ado.token`: a PAT, sent as Basic auth | `projects.<name>.github.token`: a token, as `GH_TOKEN` | `projects.<name>.linear.credential`: an API key |
558
+ | 2 | **machine token**: a conductor-only env var, else a credential in the store | `CONDUCTOR_ADO_TOKEN`, else `conductor-ado` | `CONDUCTOR_GITHUB_TOKEN`, else `conductor-github` | `CONDUCTOR_LINEAR_KEY` or `LINEAR_API_KEY`, else `conductor-linear` (`CONDUCTOR_LINEAR_CREDENTIAL` names another) |
473
559
  | 3 | **CLI login**, last: it usually carries the widest permissions | `az account get-access-token --resource 499b84ac-…`, with `--tenant <tenant>` when `ado.tenant` names one | `gh auth token`, with `--user <user>` when `github.user` names one, so gh's active account doesn't change | none |
474
560
 
475
- Only the conductor's own env names count: another tool's `AZURE_DEVOPS_PAT` or `GITHUB_TOKEN` never changes what the conductor signs in as. Save a token with `cmdkey /generic:<target> /user:pat /pass` (Linear's with `/user:linear`); a token scoped to one project wants:
561
+ **The secret store** is the platform's own:
562
+
563
+ | Platform | Store | Save a token (each asks for the secret) |
564
+ |---|---|---|
565
+ | Windows | Credential Manager (generic credentials) | `cmdkey /generic:<target> /user:pat /pass` |
566
+ | macOS | the login keychain (generic passwords, the target as service) | `security delete-generic-password -s <target>; security add-generic-password -s <target> -a pat -w` |
567
+ | Linux | libsecret (the Secret Service: GNOME Keyring, KeePassXC …), attributes `service=conductor target=<target>` | `secret-tool clear service conductor target <target>; secret-tool store --label=<target> service conductor target <target> user pat` |
568
+
569
+ Linear's key is saved with the user name `linear` instead of `pat`. On macOS and Linux the old item is deleted first: an item there is kept per user name (macOS) or per set of attributes (Linux), so saving under another user name would leave two. `conduct project check` prints the line for its platform. **Env fallback:** where the store has no such credential, or can't be read (`secret-tool` missing, or a headless Linux box with no D-Bus session or keyring), the conductor reads `CONDUCTOR_SECRET_<TARGET>` instead: the target in upper snake case, so `conductor-web-ado` is `CONDUCTOR_SECRET_CONDUCTOR_WEB_ADO`, with its user name, when one is needed (Pushover), in the same name plus `_USER`. Targets that differ only in punctuation (`a.b`, `a-b`) share a variable, so give them names that differ in letters or digits.
570
+
571
+ **The names lost their ATK prefix in ATK-529:** the machine's Linear key is `conductor-linear` (it was `atk-conductor-linear`), Pushover's `conductor-pushover` (it was `atk-conductor-pushover`), and `CONDUCTOR_LINEAR_CREDENTIAL` replaces `ATK_CONDUCT_LINEAR_CREDENTIAL`, which is no longer read. On Windows, the first read of a new name that finds none copies the old Credential Manager entry to it, with its user name and secret, and leaves the old one in place for an older conductor; after that the new one is found, so the old name is never read again. Delete the old ones with `cmdkey /delete:atk-conductor-linear` and `cmdkey /delete:atk-conductor-pushover` once no older conductor runs.
572
+
573
+ Only the conductor's own env names count: another tool's `AZURE_DEVOPS_PAT` or `GITHUB_TOKEN` never changes what the conductor signs in as. A token scoped to one project wants:
476
574
  - **Azure DevOps:** Code (read & write) and Build (read & execute), plus Work Items (read & write) for Boards;
477
575
  - **GitHub:** a fine-grained token on the repo with Contents, Pull requests and Issues (read & write) and Actions (read);
478
576
  - **Linear:** an API key limited to the project's team.
@@ -481,7 +579,7 @@ Only the conductor's own env names count: another tool's `AZURE_DEVOPS_PAT` or `
481
579
 
482
580
  A project whose entry names its CLI account (`ado.tenant`, `github.user`) keeps that account ahead of the machine's token, which is another identity and may not reach the project's organization. One machine ADO PAT serves every other project, so a project in another organization than the PAT's gets a token of its own.
483
581
 
484
- Found means there: a project token that is named but missing, or any token that is there but refused, is a lost login (below), never a quiet fall back to a wider credential. A refused PAT is read again once, so a replaced one is tried; the same one refused again is the lost login. The machine credential is optional: one that `cmdkey /list` says isn't saved (no PowerShell started then), or that can't be read, is passed over.
582
+ Found means there: a project token that is named but missing, or any token that is there but refused, is a lost login (below), never a quiet fall back to a wider credential. A refused PAT is read again once, so a replaced one is tried; the same one refused again is the lost login. The machine credential is optional: one that the store says isn't saved (on Windows `cmdkey /list`, so no PowerShell starts), or that can't be read, is passed over.
485
583
 
486
584
  Each credential is kept per account: an az token 20 minutes, renewed when ADO answers 401 or 203; a token read again every 20 minutes, so a replaced or removed one is seen; a gh token asked again once on a 401; a Linear key until Linear answers 401, when it is read again once. Projects on the same account share it. The secrets are never logged, stored or put on a command line.
487
585
 
@@ -491,11 +589,39 @@ Each credential is kept per account: an az token 20 minutes, renewed when ADO an
491
589
  2. While the spell lasts, the account's calls fail at once, asking nothing. Every `watchers.login.intervalSeconds` (60) the daemon probes each lost account again: a fresh az token for ADO (a PAT is asked `_apis/connectionData` on the project's organization, since az can't vouch for it), the token and `gh api user` for GitHub, and the key and `{ viewer { id } }` for Linear. That is the spell's back-off.
492
590
  3. The first probe the provider answers (an answer other than a refusal; a timeout, a network error or a 5xx says nothing) logs `provider.login-restored`, which resolves the item, and calls go through again.
493
591
 
494
- `conduct wait login <provider>` waits on the same probe for the worktree's project (`login:<provider>:<project>`), within the `login` watcher's budget. A worker that can't go on without a login posts its note, waits on it and ends its turn; once Jason signs in, the daemon wakes it. **Exit code 5** ("login needed") is what the worker verbs (`state`, `pr`, `land`, `reply`) exit with when they hit a lost login, here or in the daemon.
592
+ `conduct wait login <provider>` waits on the same probe for the worktree's project (`login:<provider>:<project>`), within the `login` watcher's budget. A worker that can't go on without a login posts its note, waits on it and ends its turn; once the operator signs in, the daemon wakes it. **Exit code 5** ("login needed") is what the worker verbs (`state`, `pr`, `land`, `reply`) exit with when they hit a lost login, here or in the daemon.
593
+
594
+ ## Linear
595
+
596
+ A project whose items are a Linear team's issues sets `tracker.kind` to `linear`. Only the team is required; the rest are defaults written as data (ATK-530). A conductor before 3.0.0-dev.32 requires `areaLabelGroup`, `humanLabel` and `queue` and refuses a null `areaLabelGroup`, and the minimum check (`FIELD_SINCE`) sees only fields a profile writes, not ones it leaves out or sets to null. So keep them written, as `conduct init` does, until every daemon and `conduct` that reads the profile is on 3.0.0-dev.32 or newer.
597
+
598
+ ```jsonc
599
+ "tracker": {
600
+ "kind": "linear",
601
+ "team": "ENG", // items are ENG-<n>
602
+ "queue": "prioritized-projects", // the default: the team's projects with a priority are the queue
603
+ // "queue": { "label": "conduct", "priority": 3 }: the team's open issues carrying the label, in one queue
604
+ "areaLabelGroup": "Area", // the default: the label group whose labels are the Areas; null for none
605
+ "humanLabel": "human", // the default
606
+ "triageLabel": "triage", // optional (see Scheduler)
607
+ "transitions": { "landed": "Shipped" } // optional, over the state names in Notes
608
+ }
609
+ ```
610
+
611
+ | Tracker concept | On Linear |
612
+ |---|---|
613
+ | the queue | `prioritized-projects`: the open items of the team's projects that have a priority (1 urgent … 4 low), by that priority, then the projects' order. A label queue (`{ "label", "priority" }`, priority 3 by default; conductor 3.0.0-dev.32 or newer) is one tracker project named `<team> · <label>` at that priority, holding the open items that carry the label (in any case) and the in-flight ones whose label was taken off. As on GitHub Issues, it is ready to release whenever no open item carries the label. Either way every open item of the team is read, so one outside the queue still blocks and holds its Area. Within a priority the scheduler keeps the items' `prioritySortOrder`, the order a view ordered by priority shows, one value per item for the whole workspace (ATK-666). Linear puts a new item at the top of its priority, so items nobody dragged start newest first. |
614
+ | Area | the item's label in the `areaLabelGroup` group. With `null` the team has no Areas: no item holds one, as a Boards item at its root, and `conduct file` takes no `--area` (one given is dropped, with a warning). |
615
+ | blockers | "blocks" relations whose item is one of the team's open items (another team's open item holds only a release wait) |
616
+ | human, triage, labels | labels |
617
+ | states | read by Linear's status type (backlog, unstarted, started, completed, canceled), so a team's own state names need nothing; the writes move to the `transitions` names (Notes), which `tracker.transitions` overrides |
618
+ | phase notes, `approve` / `revise:` | comments, as the key's owner |
619
+
620
+ The tracker seams (the brief, labels, notes, the queue and the writes) are one `Tracker` per kind in `src/trackers.ts`, and `routedTracker` sends each call to the adapter of its project's tracker kind.
495
621
 
496
622
  ## ADO Boards
497
623
 
498
- A project whose work items live in Azure DevOps Boards (Cadence) sets `tracker.kind` to `ado-boards` (design note, section 12; ATK-319):
624
+ A project whose work items live in Azure DevOps Boards sets `tracker.kind` to `ado-boards` (design note, section 12; ATK-319):
499
625
 
500
626
  ```jsonc
501
627
  "tracker": {
@@ -503,10 +629,10 @@ A project whose work items live in Azure DevOps Boards (Cadence) sets `tracker.k
503
629
  "key": "CAD", // items are CAD-<work item id>: ADO's ids are numbers alone
504
630
  "query": "Shared Queries/Conduct", // the saved query that is the queue, by path or id
505
631
  "humanLabel": "human", // a tag here
506
- "triageLabel": "triage", // optional, a tag here too: new items Jason hasn't triaged (see Scheduler)
632
+ "triageLabel": "triage", // optional, a tag here too: new items the operator hasn't triaged (see Scheduler)
507
633
  "priority": 3, // the queue's priority against other projects' (1 urgent … 4 low)
508
- "areaRoot": "Emergent Software\\Cadence", // optional: an item's Area is its Area Path below it
509
- "states": { "Blocked": "started" } // optional, over the built-in state names
634
+ "areaRoot": "Contoso\\Portal", // optional: an item's Area is its Area Path below it
635
+ "states": { "Blocked": "started" } // optional, over the built-in state names
510
636
  // "organization", "project": the Azure DevOps code host's by default; required beside a GitHub one
511
637
  }
512
638
  ```
@@ -531,7 +657,7 @@ A project whose items are a GitHub repo's issues sets `tracker.kind` to `github-
531
657
  "kind": "github-issues",
532
658
  "key": "PAR", // items are PAR-<issue number>: issue numbers are numbers alone
533
659
  "humanLabel": "human", // a label
534
- "triageLabel": "triage", // optional, a label too: new items Jason hasn't triaged (see Scheduler)
660
+ "triageLabel": "triage", // optional, a label too: new items the operator hasn't triaged (see Scheduler)
535
661
  "queueLabel": "conduct", // the default: the open issues carrying it are the queue
536
662
  "areaPrefix": "area:", // the default: area:docs is the Area docs
537
663
  "priority": 3, // the queue's priority against other projects' (1 urgent … 4 low)
@@ -547,29 +673,29 @@ A project whose items are a GitHub repo's issues sets `tracker.kind` to `github-
547
673
  | blockers | GitHub's issue dependencies (`blocked by`) still open, inside the queue or not; one in another repo is listed as `<owner>/<repo>#<n>` |
548
674
  | human, triage, labels | labels, matched in any case |
549
675
  | states | open → unstarted; open with a `transitions` label → started (named after the label); closed as completed → completed; closed as not planned or a duplicate → canceled |
550
- | phase notes, `approve` / `revise:` | issue comments; the gh account's own comments count as Jason's, so the note heading tells them apart |
676
+ | phase notes, `approve` / `revise:` | issue comments; the gh account's own comments count as the operator's, so the note heading tells them apart |
551
677
 
552
678
  `Closes #n` would close an issue only on a merge to the default branch, so the conductor closes it on the `Landed` note instead, and a project on a `develop` branch works the same. Phase labels are added and taken off one at a time, never replaced as a set, so a label someone else added meanwhile stays. GitHub makes a label it doesn't have yet when a phase or rule first adds it; `conduct file` refuses an Area with no label.
553
679
 
554
- Everything goes through the project's gh account (`projects.<name>.github.user`, else gh's active account; see Accounts). The reads (the scheduler's queue and the watchers' waits) take from the `github` watcher's budget, as PR and merge waits on GitHub do. A wait pass reads each waited issue once, and its comments only when its update time or comment count moved. Notes, label edits and the tracker writes are outside the budget, as on Linear. That account's rate limit is shared with anything else it runs (ATK's prod API reads with Jason's), so the `github` watcher's `perMinute` bounds it.
680
+ Everything goes through the project's gh account (`projects.<name>.github.user`, else gh's active account; see Accounts). The reads (the scheduler's queue and the watchers' waits) take from the `github` watcher's budget, as PR and merge waits on GitHub do. A wait pass reads each waited issue once, and its comments only when its update time or comment count moved. Notes, label edits and the tracker writes are outside the budget, as on Linear. That account's rate limit is shared with anything else it runs, so the `github` watcher's `perMinute` bounds it.
555
681
 
556
682
  ## Broker
557
683
 
558
684
  The broker shares what every project on the machine uses (ATK-318).
559
685
 
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).
686
+ **Ports.** Each profile names the ports a worktree's dev servers need, such as `"ports": ["web", "api"]` (design note v2, section 2; ATK-526).
561
687
  - 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.
688
+ - The pool sits away from the usual dev ports and their slot schemes, such as 3000+N for a person's own worktrees, so those never take a port a worker was given.
563
689
  - 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
690
  - `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.
565
691
 
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).
692
+ **Locks.** `conduct lock acquire chrome` takes the lock every project's verify runs share. It answers JSON: `acquired`, `heldBy`, `since`, `refreshed`; `released` for a release; `verdict` for a status.
567
693
  - The owner is `--owner`, else the item of the worker whose worktree this is.
568
694
  - Acquiring again refreshes it. 30 minutes after its holder last acquired it, another owner may take it over.
569
695
  - `--wait <minutes>` asks again every 15 s until then.
570
696
  - Retiring a worker releases the locks its item holds (a probe's retire doesn't).
571
697
  - `conduct status` lists the locks held, each with its holder, its last acquire and whether it is stale.
572
- - With no daemon it takes a file lock every worktree of the checkout's repo shares; outside a project checkout it exits 1 (`not brokered`).
698
+ - With no daemon it takes a file lock every worktree of the checkout's repo shares; outside a project checkout it exits 1 (`not brokered`). On Windows a refresh can't replace a lock file something holds open (a virus scan, another worktree's read); after a second it answers `refreshed: false` with the lock still the owner's, going stale 30 minutes after `since` (ATK-652).
573
699
 
574
700
  Events: `ports.assigned`, `ports.freed`, `lock.acquired`, `lock.released`, `lock.taken-over`.
575
701
 
@@ -609,7 +735,8 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
609
735
 
610
736
  | File | Holds |
611
737
  |---|---|
612
- | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `inbox.runawayTokens` (1500000, 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), `compactTokens` (300000, the `compact-on-park` rule's number); `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) |
738
+ | `projects/` | each conducted project's machine profile, clone and worktrees (see Projects) |
739
+ | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `clones.fetchMinutes` (10, see Projects); `inbox.runawayTokens` (1500000, the `runaway` rule's number); `notify` and `quietHours` (see Notifications); `scheduler` holds `mode` (`on` 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), `compactTokens` (300000, the `compact-on-park` rule's number); `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); `workers` holds `tab` (false: workers run headless) and `terminal` (`auto`; see Tabs) |
613
740
  | `rules.json` | machine rules, JSONC, reloaded on save (see Rules) |
614
741
  | `token` | the API token, readable by this user only; every request sends `Authorization: Bearer <token>` |
615
742
  | `conductor.pid`, `port` | the single-instance lock and where the API listens (`127.0.0.1`) |
@@ -618,7 +745,7 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
618
745
 
619
746
  ## API
620
747
 
621
- - `GET /projects`: the registered projects. `POST /projects` with `{ "path": "<absolute path>" }` registers one. `DELETE /projects/<name>` removes one: `{ project, resolved }` (the inbox items it resolved), 404 for a name no project is registered under, or 409 naming its workers in flight. `POST /projects/<name>/check` with `{ "worktree": false }` or `{}` runs `conduct project check` as the daemon's accounts: `{ project, results: [{ check, status, summary, details, fix }] }`, or 404 for a name no project is registered under.
748
+ - `GET /projects`: the registered projects, each with its `clone`, `url`, profile and `sources`. `POST /projects` with `{ "name": "<name>" }` registers the clone `conduct project add` made at `~/.conductor/projects/<name>/repo` (400 when there is none, or its profile doesn't read). `DELETE /projects/<name>` removes one: `{ project, resolved }` (the inbox items it resolved), 404 for a name no project is registered under, or 409 naming its workers in flight. `POST /projects/<name>/check` with `{ "worktree": false }` or `{}` runs `conduct project check` as the daemon's accounts: `{ project, results: [{ check, status, summary, details, fix }] }`, or 404 for a name no project is registered under.
622
749
  - `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).
623
750
  - `POST /shutdown`: a clean stop (`conduct stop`).
624
751
  - `GET /console/escalations`: the open escalations, oldest first (see Console).
@@ -626,15 +753,16 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
626
753
  - `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.
627
754
  - `POST /plugin/seen` with `{ "sessionId", "version"? }`: the mod's report that the conductor plugin loaded in a session (see Plugin); 200 with `{ "worker": <name> | null }`, the worker that session belongs to.
628
755
  - `GET /fleet`: the workers in flight, with session activity, phase, PR, waits and spend (today, total).
629
- - `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.
756
+ - `POST /workers` with `{ "item": "ENG-312", "project"?, "probe"?, "prompt"?, "conductor"? }`: launch; answers 202 with the `launching` worker. `GET /workers/:id` shows it with its latest events.
630
757
  - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work (the worktree's, and each `.scratch/repos` checkout's under `checkouts`) when it refuses.
631
- - `POST /workers/:id/attach`: wake if parked, mark attached, open the tab.
758
+ - `POST /workers/:id/attach`: wake if parked, mark attached, open the tab; `tab` is why none opened, else null, and `hint` how to reach one off the desktop (`tmux attach -t <session>`).
632
759
  - `POST /workers/:id/prs` with `{ "title", "body", "repo"? }`: open the worker's PR after the ownership checks (see Worker verbs); 201 with the PR, 409 refused, 503 a lost login (with `login.provider` and `login.command`), 502 the host failed.
633
- - `POST /workers/:id/landings` with `{ "pr", "reason", "mode"?, "rearm"?, "repo"? }`: `conduct land` for the worker; 200 with the landing and its verdict. `POST /workers/:id/replies` with `{ "pr", "body", "thread"?, "repo"? }`: `conduct reply`; 201. Both answer 409, 503 and 502 as `/prs` does.
634
- - `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"`.
760
+ - `POST /workers/:id/landings` with `{ "pr", "reason", "mode"?, "rearm"?, "repo"?, "requeue"? }`: `conduct land` for the worker; 200 with the landing and its verdict. `POST /workers/:id/replies` with `{ "pr", "body", "thread"?, "repo"? }`: `conduct reply`; 201. Both answer 409, 503 and 502 as `/prs` does.
761
+ - `POST /items/:id/notes` with `{ "body": "## work-item · …", "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"`.
635
762
  - `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.
763
+ - `POST /items/:id/revise` with `{ "text" }`: `conduct revise` on an approved plan (see Inbox); 200 with `{ "revise": { comment, item, project, revised, woken, landed, worker, pending } }`. 409 when the item has no approved Plan (nothing posted); 502 when the tracker didn't take the comment, with `posted`.
636
764
  - `GET /inbox`: the open inbox items, oldest first.
637
- - `POST /inbox/:id/resolve`: Jason's answer (see Inbox). The body is `{ "action": "approve", "text"? }`, `{ "action": "revise", "text" }` or `{ "action": "done" }`.
765
+ - `POST /inbox/:id/resolve`: the operator's answer (see Inbox). The body is `{ "action": "approve", "text"? }`, `{ "action": "revise", "text" }` or `{ "action": "done" }`.
638
766
  - 200 with the item, the comment and the waits it resolved.
639
767
  - 404 when there is no such item.
640
768
  - 409 when the item is resolved already or isn't a plan.
@@ -647,43 +775,57 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
647
775
  - `GET /queue`: the scheduler's last plan; `?fresh=true` reads the trackers and plans now (see Scheduler).
648
776
  - `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`.
649
777
  - `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.
650
- - `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.
651
- - `POST /rules/test` with `{ "event"?: "pr.comment", "project"?: "atk", "fields"?: { … } }`: a dry run; 400 for a sample it can't read.
778
+ - `GET /why/:id`: the event's chain of causes and the rule behind it; `GET /why?item=ENG-312` the same for the item's newest actions.
779
+ - `POST /rules/test` with `{ "event"?: "pr.comment", "project"?: "web", "fields"?: { … } }`: a dry run; 400 for a sample it can't read.
652
780
  - `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.
653
- - `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.
654
- - `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.
781
+ - `GET /locks`: the locks held. `POST /locks/:name` with `{ "action": "acquire" | "release" | "status", "owner"? }`: 200 with the lock's JSON (see Broker); 409 for an acquire or release without an owner.
782
+ - `POST /waits` with `{ "worker": 1, "condition": "pr:ado:Contoso.Web#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.
655
783
  - A refusal (a duplicate launch, a bad condition, a busy worker) is a 409 with `error` and `details`.
656
784
 
657
785
  ## Restarts
658
786
 
659
- `conduct supervise` runs `conduct serve` as its child and starts it again when it dies: a crash, a kill, a start that fails. The logon task runs it, and `conduct start` and `conduct upgrade` start it detached.
787
+ `conduct supervise` runs `conduct serve` as its child and starts it again when it dies: a crash, a kill, a start that fails. The logon startup (see On PATH) runs it, and `conduct start` and `conduct upgrade` start it detached.
660
788
 
661
789
  - **Restart:** after 1 s, 5 s, 15 s and 30 s, then every minute, and every 5 minutes once it has pushed. A serve that stayed up 5 minutes ends the outage, so the next death starts at 1 s again. Each restarted daemon records `daemon.restarted` (`attempt`, the last serve's `code` or `signal`, `downSince`, `pushed`) after the `daemon.exited-uncleanly` its start finds.
662
790
  - **Push:** a daemon down 2 minutes sends one Pushover push (`conductor · the daemon is down`), with the last serve's stderr and the log's path. It uses `notify.pushover` from `config.json` and goes to Pushover whatever the notification profile: a `conduct notify profile` lives in the daemon's store, which may be what keeps it down. Quiet hours hold it until they end, if the daemon is still down. A `config.json` that doesn't load leaves the defaults, because a bad config is one way a daemon stays down. It tries once per outage; a refusal is logged.
663
- - **Stop:** a serve that exits 0 (`conduct stop`, `conduct upgrade`) ends the supervisor. While it waits between restarts nothing answers the API, so `conduct stop` and `conduct upgrade` write a request to `supervisor.stop` in the conductor home when no daemon answers (ATK-433). The supervisor checks for one every 250 ms while it waits, and after each serve exits. It ends, writes its pid into the request as its answer, and `stop` prints `stopped the conductor's supervisor (pid N), which was waiting to restart the daemon`. `stop` waits 10 s for that answer, and a daemon that comes up meanwhile is shut down as usual. With no answer it says the conductor isn't running. However it ends, `stop` removes its request. A supervisor heeds only a request written after it started and in the last 30 s, and drops any other as it starts. So a request left behind by a logoff never stops the next logon's supervisor, and a stale one never ends a later outage. `stop` only asks when `supervisor.pid`, which a supervisor writes as it starts, names a live process. With nothing running it says so at once. The file is a hint, never a lock, and nothing ends the process it names: one left by a logoff that names a reused pid costs a `stop` its 10 s wait and nothing more. After the answer, `stop` waits for the supervisor's process to end, so a `conduct start` right after it isn't ignored by the logon task. Ctrl+C or Ctrl+Break on a `conduct supervise` in a terminal stops its daemon as `conduct stop` does (the serve runs in a console of its own), or cuts short a wait between restarts; press it again if the first came while the daemon was still starting. A serve that finds another daemon holding the lock (exit 3) ends it too. `--fail-fast`, which `conduct start` passes, also ends it when the first serve exits before it says it listens, so `start` fails at once and points at the log, which has the serve's stderr; one that came up and then died is restarted.
791
+ - **Stop:** a serve that exits 0 (`conduct stop`, `conduct upgrade`) ends the supervisor. While it waits between restarts nothing answers the API, so `conduct stop` and `conduct upgrade` write a request to `supervisor.stop` in the conductor home when no daemon answers (ATK-433). The supervisor checks for one every 250 ms while it waits, and after each serve exits. It ends, writes its pid into the request as its answer, and `stop` prints `stopped the conductor's supervisor (pid N), which was waiting to restart the daemon`. `stop` waits 10 s for that answer, and a daemon that comes up meanwhile is shut down as usual. With no answer it says the conductor isn't running. However it ends, `stop` removes its request. A supervisor heeds only a request written after it started and in the last 30 s, and drops any other as it starts. So a request left behind by a logoff never stops the next logon's supervisor, and a stale one never ends a later outage. `stop` only asks when `supervisor.pid`, which a supervisor writes as it starts, names a live process. With nothing running it says so at once. The file is a hint, never a lock, and nothing ends the process it names: one left by a logoff that names a reused pid costs a `stop` its 10 s wait and nothing more. After the answer, `stop` waits for the supervisor's process to end, so a `conduct start` right after it isn't ignored by the Windows logon task. Ctrl+C or Ctrl+Break on a `conduct supervise` in a terminal stops its daemon as `conduct stop` does (the serve runs in a console of its own), or cuts short a wait between restarts; press it again if the first came while the daemon was still starting. A serve that finds another daemon holding the lock (exit 3) ends it too. `--fail-fast`, which `conduct start` passes, also ends it when the first serve exits before it says it listens, so `start` fails at once and points at the log, which has the serve's stderr; one that came up and then died is restarted.
664
792
  - **Logs:** the supervisor writes `the daemon exited; the supervisor restarts it` and its push results to `logs/conductor.log`.
665
- - **Not covered:** a logoff or reboot ends the supervisor with the session, and the logon task starts it again at the next logon. A daemon that runs but doesn't answer isn't restarted.
793
+ - **Not covered:** a logoff or reboot ends the supervisor with the session (a systemd unit with lingering on outlives a logoff), and the logon startup starts it again at the next logon. A daemon that runs but doesn't answer isn't restarted.
666
794
 
667
795
  ## On PATH
668
796
 
669
- Install the package globally, next to `atk`. `dev` follows `develop`; `latest` follows `main`:
797
+ Install the package globally. `dev` follows `develop`; `latest` follows `main`:
670
798
 
671
799
  ```bash
672
800
  npm install --global @detergent-software/conductor@dev
673
801
  conduct install-startup
674
- conduct project add <path to the project's main checkout>
802
+ conduct project add <the project's remote URL, or a checkout of it>
675
803
  ```
676
804
 
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. 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.
805
+ `conduct project add` clones the remote into `~/.conductor/projects/<name>/repo` (see Projects). After that the daemon reads the profile again when it starts, when the machine profile is saved and after each fetch of the clone, so a merge 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.
806
+
807
+ 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.
808
+
809
+ `src/profile-fields.ts` records the conductor that first read each field as it is written today (`FIELD_SINCE`, ATK-577), and its type makes a schema field without a record a typecheck error. Fields that 2.0.0 read, the first conductor that reads protocol 2, need no minimum: an older one refuses the profile on `protocol` first. `minimumProblems` reads a profile as written, so a field a default fills in doesn't count. It names each field the profile writes that is newer than its minimum, or newer than 2.0.0 when the profile sets none. A conductor test holds this repo's own `.claude/conductor.json` to it, and `atk-conductor-ci` runs on changes to that file. Any other project hears it from `conduct project check`, as a `warn` on its profile check.
810
+
811
+ A PR that adds or reshapes a field can't know the number semantic-release will give it, so it takes the next one:
812
+
813
+ 1. Find the newest conductor tag: `git tag -l 'conductor-v*' --sort=-v:refname` (after a `git fetch --tags`). On `develop` the next release is its `-dev.N` plus one, such as `3.0.0-dev.12` after `3.0.0-dev.11`, unless the PR's commits make a bigger bump than that series: after a stable release, the first `-dev.1` of the version its commit type makes (a fix's patch, a feature's minor), and for a breaking change the next major's `-dev.1`, as `2.2.0-dev.3` was followed by `3.0.0-dev.1`.
814
+ 2. Record that version for the field in `FIELD_SINCE`. If this repo's own profile writes the field in the same PR, raise its `conductor` to it too. Its launches are then held, with the upgrade line, until the daemon runs that release (`conduct upgrade --tag dev`, once a maintainer has approved its stage).
815
+ 3. After the merge, `conduct runs <merge>` gives the version `atk-conductor-release` staged. If another conductor PR took the number first, the record and the minimum are one too low: fix both in a follow-up. Too low lets the version before the field read the profile, and fail with the field list this key replaces. Too high holds launches until that version exists.
816
+
817
+ A `test`, `chore` or `docs` change stages no release and records nothing.
818
+
819
+ 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.
678
820
 
679
821
  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):
680
822
 
681
- - **profile:** the file reads with this build's schema and keeps the registered name; a `warn` lists the fields that differ from the daemon's stored copy, which it takes on the next save or start. The later checks use the file when it reads, else the stored copy; with neither they are `skip`.
682
- - **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.
823
+ - **profile:** the layers (see Projects) read with this build's schema; a `warn` lists the fields that differ from the daemon's stored copy, which it takes on the next save, fetch or start, and each field a file writes that is newer than its `conductor` minimum (`minimumProblems`, above), with the version to raise it to (ATK-580). A project still registered by a checkout fails, with the `conduct project add` that moves it to a clone. The later checks use the resolved profile when it reads, else the stored copy; with neither they are `skip`.
824
+ - **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 line that saves a token in the platform's store (`cmdkey`, `security` or `secret-tool`) 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.
683
825
  - **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).
684
826
  - **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.
685
- - **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. 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`).
686
- - **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.
827
+ - **worktree:** the launch's worktree steps on a throwaway `conduct-check-<time>` in the project's worktrees folder: `git worktree add` in the clone 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 clone nobody has opened in Claude Code fails here with the one step that trusts it, `claude` run in the clone 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 (such as a `pnpm install`).
828
+ - **skills:** a `conductor:<skill>` is in this package's plugin, and `skills.work` declares `protocol: 2` in its SKILL.md frontmatter (worker protocol 2). A project's own skills arrive through its `setup` (a command that installs them), not in the clone, so the worktree check looks for them.
687
829
  - **ports:** the machine's pool has a free port for each of the profile's `ports`, saving nothing; `skip` for a profile with none.
688
830
  - **health:** each `health` URL answers; a `warn` at most, since an environment can be down.
689
831
 
@@ -711,15 +853,23 @@ The profile's optional `work` section is for the project's work skill; the daemo
711
853
  }
712
854
  ```
713
855
 
714
- The logon task runs `conduct supervise` (see Restarts) with `node` on the `cli.js` that ran `install-startup`. A global install keeps that path across upgrades, so the task needs registering once. A task registered before ATK-417 runs the unsupervised `conduct serve`, and `conduct upgrade` says so: run `conduct install-startup` once more. To upgrade:
856
+ `conduct install-startup` registers the logon startup, which runs `conduct supervise` (see Restarts) with `node` on the `cli.js` that ran it. Its daemon uses `~/.conductor`, whatever `CONDUCTOR_HOME` says. Each platform has its own (`src/host/startup.ts`, ATK-529):
857
+
858
+ - **Windows:** a Task Scheduler task, `Conductor`, at logon, under `conhost --headless` so no window opens. Registering it starts nothing; `conduct start` runs it.
859
+ - **macOS:** a launchd agent, `~/Library/LaunchAgents/software.detergent.conductor.plist` (`RunAtLoad`), loaded into the login session with `launchctl bootstrap gui/<uid>`, which starts it; `conduct start` runs it with `launchctl kickstart`. Its output goes to `~/.conductor/logs/startup.log`. launchd only loads an agent for a logged-in user, so run `install-startup` in a terminal on the Mac, not over ssh alone.
860
+ - **Linux:** a systemd user unit, `conductor.service` in `~/.config/systemd/user` (`$XDG_CONFIG_HOME` when set), which `install-startup` enables and starts; `conduct start` runs `systemctl --user start conductor`. Its output goes to the journal (`journalctl --user -u conductor`). It starts when your user's service manager does: at login, or at boot once `loginctl enable-linger <you>` is on, which `install-startup` suggests when it isn't. That is how a server nobody logs in to runs the conductor headless.
861
+
862
+ The agent and the unit carry the `PATH` of the terminal that ran `install-startup`, because a login starts them with a short one that has no `claude`, `git` or `pnpm`: run it again after that changes. Neither restarts on its own, since `conduct supervise` does that and `conduct stop` must stick. Neither kills the processes the daemon started when it ends (`AbandonProcessGroup`, `KillMode=process`), so a stop or an upgrade leaves the workers and a tmux server alone, as on Windows. Running `install-startup` again replaces the definition: on macOS and Linux that restarts a daemon the agent or unit started. `--remove` deletes it and leaves a running daemon alone (a launchd agent stays loaded until logout).
863
+
864
+ A global install keeps the `cli.js` path across upgrades, so the startup needs registering once. A Windows task registered before ATK-417 runs the unsupervised `conduct serve`, and `conduct upgrade` says so: run `conduct install-startup` once more. To upgrade:
715
865
 
716
866
  ```bash
717
867
  conduct upgrade
718
868
  ```
719
869
 
720
- It takes the tag the installed version came from (`dev` for an `X.Y.Z-dev.N`, `latest` otherwise; `--tag` picks one), and does nothing when that tag's version is already installed. When the tag points at an older version (semver, so `1.2.0-dev.4` is older than `1.2.0`), it refuses before it stops anything and names that version; `--allow-downgrade` installs it anyway and reports `downgraded`. Otherwise it stops the daemon, runs `npm install --global @detergent-software/conductor@<the tag's version>` (the exact version it compared, in case the tag moves) and starts the daemon again: through the `Conductor` logon task when the task runs this `cli.js`, else as a detached `conduct supervise --fail-fast`. A supervisor waiting to restart a daemon that keeps dying is stopped the same way before npm runs (see Restarts), so it can't restart `serve` from the `cli.js` npm is replacing, and the conductor starts again on the new version. A daemon that wasn't running stays stopped. If npm fails, the daemon still starts again on whatever npm left in place, and `upgrade` exits 1 with npm's output. A linked build (below) refuses: its `cli.js` isn't under npm's global `node_modules`.
870
+ It takes the tag the installed version came from (`dev` for an `X.Y.Z-dev.N`, `latest` otherwise; `--tag` picks one), and does nothing when that tag's version is already installed. When the tag points at an older version (semver, so `1.2.0-dev.4` is older than `1.2.0`), it refuses before it stops anything and names that version; `--allow-downgrade` installs it anyway and reports `downgraded`. Otherwise it stops the daemon, runs `npm install --global @detergent-software/conductor@<the tag's version>` (the exact version it compared, in case the tag moves) and starts the daemon again: through the logon startup when it runs this `cli.js`, else as a detached `conduct supervise --fail-fast`. A supervisor waiting to restart a daemon that keeps dying is stopped the same way before npm runs (see Restarts), so it can't restart `serve` from the `cli.js` npm is replacing, and the conductor starts again on the new version. A daemon that wasn't running stays stopped. If npm fails, the daemon still starts again on whatever npm left in place, and `upgrade` exits 1 with npm's output. A linked build (below) refuses: its `cli.js` isn't under npm's global `node_modules`.
721
871
 
722
- **From a checkout instead (development).** `npm link` puts the main checkout's build on PATH, so every worktree and the logon task run that build. Run it from the main checkout, and upgrade by pulling `develop`, building, then `conduct stop` and `conduct serve`:
872
+ **From a checkout instead (development).** `npm link` puts a checkout's build on PATH, so every worktree and the logon startup run that build. Run it from your own checkout of the conductor's repo, and upgrade by pulling `develop`, building, then `conduct stop` and `conduct serve`:
723
873
 
724
874
  ```bash
725
875
  pnpm install
@@ -728,7 +878,7 @@ pnpm -C apps/conductor build
728
878
  conduct install-startup
729
879
  ```
730
880
 
731
- 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`.
881
+ 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 startup points at the installed `cli.js`.
732
882
 
733
883
  ## Console
734
884
 
@@ -737,7 +887,7 @@ The console (design note, section 11) is the one Claude session that stays open
737
887
  The console belongs to the machine, not to a project (design note v2, section 1): it runs in `<home>/console` (`~/.conductor/console`), a folder of the conductor's own, and opens with no project registered. It reads items through the `conduct` verbs, which find the project from the item while the daemon runs, and a subagent that reads a project's code or logs works in that project's checkout, from `conduct status`.
738
888
 
739
889
  ```bash
740
- conduct console # a tab in Windows Terminal (window "conductor"), in ~/.conductor/console
890
+ conduct console # a tab in ~/.conductor/console: Windows Terminal (window "conductor"), iTerm or Terminal, or tmux (session conductor)
741
891
  conduct console --here # the same session in this terminal
742
892
  ```
743
893
 
@@ -746,9 +896,6 @@ It writes the plugin copy if none exists yet (see Plugin), then runs `claude --p
746
896
  **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.
747
897
 
748
898
  **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.
749
-
750
- **Moving from conduct-atk.** Stop the conduct-atk session, `conduct upgrade` (the scheduler now launches by default), then `conduct console`. A machine that keeps conduct-atk's loop sets `"scheduler": { "mode": "observe" }` in `config.json`.
751
-
752
899
  ## Plugin
753
900
 
754
901
  `plugin/` is the conductor plugin (design note v2, section 4), one Claude Code plugin named `conductor` that holds:
@@ -777,9 +924,9 @@ The mod is the plugin's hooks module, code that costs no tokens, which does the
777
924
  - **Every session** sends its plugin report on start, and nothing else: no status line, and no other calls to the daemon.
778
925
  - **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:
779
926
  - **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;
780
- - **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; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]), and today's spend. The header counts the workers running and parked and the inbox in words. Each worker is one line in columns that line up across projects, cut to the pane's width so it never wraps: a glyph, the item, its phase, its PR, what it waits on in words (`PR #557 to merge (agentic-toolkit-registry)`, `npm 8.9.0-dev.1 on dev (atk)`, `login (ado)`) or else its activity, and the age. As the pane narrows, the age goes first, then the wait, then the PR and the phase (ATK-521). The glyphs are one cell wide: `●` in a turn, `◌` up between turns, `!` stuck on a prompt, `○` parked, `×` failed or gone, `·` starting. A row expands to attach and why, its tokens today, its whole PR ref and its raw wait keys;
927
+ - **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; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]), and today's spend. The header counts the workers running and parked and the inbox in words. Each worker is one line in columns that line up across projects, cut to the pane's width so it never wraps: a glyph, the item, its phase, its PR, what it waits on in words (`PR #557 to merge (skills-registry)`, `npm 8.9.0-dev.1 on dev (cli)`, `login (ado)`) or else its activity, and the age. As the pane narrows, the age goes first, then the wait, then the PR and the phase (ATK-521). The glyphs are one cell wide: `●` in a turn, `◌` up between turns, `!` stuck on a prompt, `○` parked, `×` failed or gone, `·` starting. A row expands to attach and why, its tokens today, its whole PR ref and its raw wait keys;
781
928
  - **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);
782
- - **a check that `/conductor:conductor-console` ran**: a console started without it (by hand, as `claude --plugin-dir … -n conductor`) shows `console without its skill: run /conductor: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:conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
929
+ - **a check that `/conductor:conductor-console` ran**: a console started without it (by hand, as `claude --plugin-dir … -n conductor`) shows `console without its skill: run /conductor:conductor-console` in the band, and each prompt, the operator's or an escalation's, carries a line telling the model to load the skill before it answers. Running the skill (`/conductor:conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
783
930
  - **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;
784
931
  - **the stream**: it runs `node <the cli.js that conductor.json names> 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:
785
932