@detergent-software/conductor 3.0.0-dev.3 → 3.0.0-dev.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +241 -0
- package/README.md +201 -63
- package/build/brief-cli.js +1 -1
- package/build/brief-cli.js.map +1 -1
- package/build/broker-cli.js +24 -25
- package/build/broker-cli.js.map +1 -1
- package/build/broker.js +66 -92
- package/build/broker.js.map +1 -1
- package/build/claude.js +50 -7
- package/build/claude.js.map +1 -1
- package/build/cli.js +143 -63
- package/build/cli.js.map +1 -1
- package/build/clones.js +92 -0
- package/build/clones.js.map +1 -0
- package/build/code-host-cli.js +116 -32
- package/build/code-host-cli.js.map +1 -1
- package/build/code-host-service.js +8 -0
- package/build/code-host-service.js.map +1 -1
- package/build/code-host.js +22 -11
- package/build/code-host.js.map +1 -1
- package/build/config.js +58 -14
- package/build/config.js.map +1 -1
- package/build/console.js +40 -24
- package/build/console.js.map +1 -1
- package/build/credentials.js +42 -29
- package/build/credentials.js.map +1 -1
- package/build/daemon.js +38 -22
- package/build/daemon.js.map +1 -1
- package/build/help.js +179 -42
- package/build/help.js.map +1 -1
- package/build/history.js +4 -2
- package/build/history.js.map +1 -1
- package/build/host/credential-manager.js +109 -0
- package/build/host/credential-manager.js.map +1 -0
- package/build/host/keychain.js +44 -0
- package/build/host/keychain.js.map +1 -0
- package/build/host/launchd.js +118 -0
- package/build/host/launchd.js.map +1 -0
- package/build/host/libsecret.js +40 -0
- package/build/host/libsecret.js.map +1 -0
- package/build/host/macos-terminal.js +63 -0
- package/build/host/macos-terminal.js.map +1 -0
- package/build/host/platform.js +4 -0
- package/build/host/platform.js.map +1 -0
- package/build/host/secrets.js +46 -0
- package/build/host/secrets.js.map +1 -0
- package/build/host/shell.js +26 -0
- package/build/host/shell.js.map +1 -0
- package/build/host/startup.js +26 -0
- package/build/host/startup.js.map +1 -0
- package/build/host/systemd.js +91 -0
- package/build/host/systemd.js.map +1 -0
- package/build/host/task-scheduler.js +78 -0
- package/build/host/task-scheduler.js.map +1 -0
- package/build/host/terminal.js +46 -0
- package/build/host/terminal.js.map +1 -0
- package/build/host/tmux.js +39 -0
- package/build/host/tmux.js.map +1 -0
- package/build/host/windows-terminal.js +46 -0
- package/build/host/windows-terminal.js.map +1 -0
- package/build/in-process.js +6 -5
- package/build/in-process.js.map +1 -1
- package/build/inbox.js +63 -17
- package/build/inbox.js.map +1 -1
- package/build/init.js +508 -0
- package/build/init.js.map +1 -0
- package/build/landing.js +86 -7
- package/build/landing.js.map +1 -1
- package/build/lib/providers/ado/code-host.js +5 -1
- package/build/lib/providers/ado/code-host.js.map +1 -1
- package/build/lib/providers/ado/pipelines.js +130 -22
- package/build/lib/providers/ado/pipelines.js.map +1 -1
- package/build/lib/providers/ado/trigger.js +5 -0
- package/build/lib/providers/ado/trigger.js.map +1 -1
- package/build/lib/providers/ado/writes.js +10 -1
- package/build/lib/providers/ado/writes.js.map +1 -1
- package/build/lib/providers/github/actions.js +90 -3
- package/build/lib/providers/github/actions.js.map +1 -1
- package/build/lib/providers/github/code-host.js +2 -0
- package/build/lib/providers/github/code-host.js.map +1 -1
- package/build/lib/providers/github/writes.js +24 -2
- package/build/lib/providers/github/writes.js.map +1 -1
- package/build/lib/providers/index.js +4 -3
- package/build/lib/providers/index.js.map +1 -1
- package/build/lib/providers/npm/stage.js +28 -5
- package/build/lib/providers/npm/stage.js.map +1 -1
- package/build/lib/providers/timing.js +10 -0
- package/build/lib/providers/timing.js.map +1 -0
- package/build/lib/providers/verdicts/pr.js +47 -6
- package/build/lib/providers/verdicts/pr.js.map +1 -1
- package/build/lib/providers/verdicts/runs.js +12 -2
- package/build/lib/providers/verdicts/runs.js.map +1 -1
- package/build/local-files.js +95 -0
- package/build/local-files.js.map +1 -0
- package/build/manager.js +171 -47
- package/build/manager.js.map +1 -1
- package/build/note.js +1 -1
- package/build/note.js.map +1 -1
- package/build/notify.js +101 -39
- package/build/notify.js.map +1 -1
- package/build/npm-reads.js +323 -0
- package/build/npm-reads.js.map +1 -0
- package/build/paths.js +44 -1
- package/build/paths.js.map +1 -1
- package/build/plugin.js +10 -0
- package/build/plugin.js.map +1 -1
- package/build/profile-fields.js +149 -0
- package/build/profile-fields.js.map +1 -0
- package/build/profile-layers.js +264 -0
- package/build/profile-layers.js.map +1 -0
- package/build/profile.js +70 -44
- package/build/profile.js.map +1 -1
- package/build/project-check-cli.js +18 -10
- package/build/project-check-cli.js.map +1 -1
- package/build/project-check.js +187 -98
- package/build/project-check.js.map +1 -1
- package/build/pushover.js +5 -5
- package/build/pushover.js.map +1 -1
- package/build/registry.js +195 -36
- package/build/registry.js.map +1 -1
- package/build/release-kinds.js +34 -0
- package/build/release-kinds.js.map +1 -0
- package/build/rule-actions.js +4 -2
- package/build/rule-actions.js.map +1 -1
- package/build/rules-service.js +101 -44
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +15 -15
- package/build/rules.js.map +1 -1
- package/build/run.js +20 -7
- package/build/run.js.map +1 -1
- package/build/runs.js +200 -45
- package/build/runs.js.map +1 -1
- package/build/scheduler.js +11 -1
- package/build/scheduler.js.map +1 -1
- package/build/server.js +6 -25
- package/build/server.js.map +1 -1
- package/build/spend.js +4 -0
- package/build/spend.js.map +1 -1
- package/build/store.js +82 -25
- package/build/store.js.map +1 -1
- package/build/timeline.js +104 -0
- package/build/timeline.js.map +1 -0
- package/build/tracker-reads-cli.js +4 -5
- package/build/tracker-reads-cli.js.map +1 -1
- package/build/train-cli.js +6 -5
- package/build/train-cli.js.map +1 -1
- package/build/upgrade.js +8 -9
- package/build/upgrade.js.map +1 -1
- package/build/wait.js +6 -6
- package/build/wait.js.map +1 -1
- package/build/watchers/ado.js +11 -3
- package/build/watchers/ado.js.map +1 -1
- package/build/watchers/github.js +15 -4
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/npm.js +6 -4
- package/build/watchers/npm.js.map +1 -1
- package/build/webhook.js +80 -0
- package/build/webhook.js.map +1 -0
- package/build/worktrees.js +199 -92
- package/build/worktrees.js.map +1 -1
- package/package.json +61 -61
- package/plugin/skills/conductor-console/SKILL.md +4 -4
- package/plugin/skills/work-item/SKILL.md +13 -11
- package/build/checkout-sync.js +0 -249
- package/build/checkout-sync.js.map +0 -1
- package/build/credential.js +0 -66
- package/build/credential.js.map +0 -1
- package/build/startup.js +0 -92
- package/build/startup.js.map +0 -1
- package/build/tab.js +0 -32
- package/build/tab.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,12 +10,18 @@ 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
|
|
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
|
|
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
|
|
@@ -27,7 +33,7 @@ conduct done <id> mark an inbox item done (a physical action, or
|
|
|
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
|
|
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
|
|
@@ -70,24 +76,29 @@ conduct pr create --title <title> --body-file <file> [--repo <repo>] [--item <it
|
|
|
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 land <pr> --reason <why> [--mode auto|review] [--rearm] [--requeue <run>] [--tier S|M|L] [--repo <repo>]
|
|
74
80
|
arm the worker's PR to land and give its first verdict (see Worker verbs)
|
|
75
81
|
conduct reply <pr> [<thread>] --body-file <file> [--repo <repo>]
|
|
76
82
|
answer on the worker's PR, headed as a note
|
|
77
83
|
conduct runs <sha> [--for <pipeline>[,<pipeline>…]] [--repo <repo>]
|
|
78
84
|
a merge's runs, their verdict, failed logs and npm stages (see Worker verbs)
|
|
85
|
+
conduct runs <sha> --for <pipeline>[,<pipeline>…] --grep <pattern> [--repo <repo>]
|
|
86
|
+
the same, with the lines of those runs' logs that match, whatever their result
|
|
79
87
|
conduct runs <sha> --requeue <pipeline>
|
|
80
88
|
queue the item's merge's cancelled run again on the base (see Worker verbs)
|
|
89
|
+
conduct timeline <run> [--task <regex>] [--grep <regex>] [--repo <repo>]
|
|
90
|
+
a run's tasks with their times, and the named tasks' log lines that match (see Worker verbs)
|
|
91
|
+
conduct timeline --for <pipeline> [--branch <branch>] [--count <n>] [--task <regex>] [--grep <regex>] [--repo <repo>]
|
|
92
|
+
the same for a pipeline's last finished runs on a branch
|
|
81
93
|
conduct train [<project>] [--dry-run]
|
|
82
94
|
merge the waiting skill publishes into one registry PR (see Skill trains)
|
|
83
|
-
conduct ports [<path>] [--
|
|
84
|
-
a checkout's dev ports from the port broker, or every worktree's (see Broker)
|
|
95
|
+
conduct ports [<path>] [--json] a conducted worktree's dev ports from the port broker, or every worktree's (see Broker)
|
|
85
96
|
conduct lock acquire|release|status <name> [--owner <id>] [--wait <minutes>]
|
|
86
97
|
a lock every project shares, such as chrome (see Broker)
|
|
87
98
|
conduct events [--since <id>] [--follow] [--console]
|
|
88
99
|
the event log, one JSON event per line (see API)
|
|
89
|
-
conduct install-startup start the supervised daemon at logon (
|
|
90
|
-
conduct install-startup --remove remove that
|
|
100
|
+
conduct install-startup start the supervised daemon at every logon (Task Scheduler, launchd or systemd)
|
|
101
|
+
conduct install-startup --remove remove that task, agent or unit
|
|
91
102
|
conduct upgrade [--tag dev|latest] [--allow-downgrade]
|
|
92
103
|
stop the daemon, npm install the tag's newest version, start it again
|
|
93
104
|
```
|
|
@@ -100,11 +111,27 @@ A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
|
|
|
100
111
|
|
|
101
112
|
The session manager owns the `claude --bg` sessions it launches, and only those:
|
|
102
113
|
|
|
103
|
-
- **launch:** the worktree comes first,
|
|
114
|
+
- **launch:** the worktree comes first, made by the daemon itself (design note v2, section 2; ATK-526), with no hooks:
|
|
115
|
+
1. the broker gives it a port for each name in the profile's `ports` (see Broker);
|
|
116
|
+
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;
|
|
117
|
+
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;
|
|
118
|
+
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;
|
|
119
|
+
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;
|
|
120
|
+
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`.
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
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.
|
|
104
125
|
- **park:** a worker with a wait (`POST /waits`) is stopped once `claude agents` shows its turn has ended. Its status becomes `parked`, and its tab closes.
|
|
105
126
|
- **resume:** when a wait resolves, the daemon lets the stop settle (`sessions.settleSeconds`). It checks the process has exited, then runs `claude --bg --resume <session> -- "[conductor] event: …"`. The session keeps its name, model, permission mode and disallowed tools. A resume of a session that still runs starts a copy; the daemon stops and removes the copy, then tries again.
|
|
106
127
|
- **compact:** a parked worker with no resolved wait, whose context the rules find too big (the default `compact-on-park`: past `sessions.compactTokens`, 300k), is resumed with `/compact <what to keep>` once its stop has settled (`worker.compacting`). It runs until its transcript shows the compaction's boundary (`worker.compacted`, with the context before it and the conversation it kept), then parks again on its waits, which resume it as usual. Each turn of a session re-reads its whole context from cache, so a worker left to grow pays for it every turn (ATK-454 reached 883k). A resume that starts a copy is tried again on a later sweep (`worker.compact-deferred`), as a resume is. A compaction whose resume fails, or whose turn ends with no boundary 10 minutes on, logs `worker.compact-failed`, which raises a `context` item in the inbox. The worker still parks, and the daemon doesn't try it again until it restarts or the session compacts some other way: any boundary in its transcript, Claude Code's own included, logs `worker.compacted` and resolves that item. A worker whose context is unknown is never compacted, so even a rule with no threshold can't compact it on every park.
|
|
107
|
-
- **retire:** refuses
|
|
128
|
+
- **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):
|
|
129
|
+
- **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`;
|
|
130
|
+
- **no PR, and nothing uncommitted or unpushed** (a canceled item, a worker retired before its first commit): as merged, with nothing to lose;
|
|
131
|
+
- **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;
|
|
132
|
+
- **unread** (the code host didn't answer): it refuses, as a refusal that passes on its own, and the scheduler tries again.
|
|
133
|
+
|
|
134
|
+
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.
|
|
108
135
|
- **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
|
|
109
136
|
- **attach:** a parked worker is resumed with no event text, so it wakes idle and keeps running when its tab closes. An attached worker isn't parked until it registers a new wait, or until its turn has been over `sessions.idleMinutes` (`worker.detached`). The daemon can't see the tab, so this also delivers a wait that resolved while it was attached. A wait that resolves after the attach, on a worker whose transcript shows no turn since then, is delivered at once: the worker is detached (`reason: wait resolved`), parked, which closes its tab, and resumed with the event. A worker someone woke outside the daemon counts as attached since its stop, so the turn that woke it counts. One that took a turn since the attach (or has no transcript to tell), or whose wait resolved before the attach, keeps it until the idle release, logs `worker.resume-held` (`reason: attached`) once, and `conduct status` shows it as `idle, holding resolved waits for the attached session`.
|
|
110
137
|
- **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.
|
|
@@ -115,6 +142,19 @@ The session manager owns the `claude --bg` sessions it launches, and only those:
|
|
|
115
142
|
|
|
116
143
|
Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `failed`, `exited`. Every step is an event (`worker.launched`, `worker.parked`, `worker.resumed`, …) on `GET /events`.
|
|
117
144
|
|
|
145
|
+
### Tabs
|
|
146
|
+
|
|
147
|
+
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:
|
|
148
|
+
|
|
149
|
+
```jsonc
|
|
150
|
+
{ "workers": { "tab": true, "terminal": "auto" } }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `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.
|
|
154
|
+
- `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-atk`, 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.
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
118
158
|
## Notes
|
|
119
159
|
|
|
120
160
|
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.
|
|
@@ -183,6 +223,7 @@ Protocol 2's verbs on git and the code host (providers design note, section 4),
|
|
|
183
223
|
- `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).
|
|
184
224
|
- `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.
|
|
185
225
|
- `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.
|
|
226
|
+
- **`--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.
|
|
186
227
|
- `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.
|
|
187
228
|
- `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.
|
|
188
229
|
- `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.
|
|
@@ -190,11 +231,19 @@ Protocol 2's verbs on git and the code host (providers design note, section 4),
|
|
|
190
231
|
- `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.
|
|
191
232
|
- `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`).
|
|
192
233
|
- **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.
|
|
193
|
-
- **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`.
|
|
194
|
-
- **A
|
|
195
|
-
-
|
|
234
|
+
- **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.
|
|
235
|
+
- **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 `apps/cli/` or `apps/conductor/`), and the batch still runs.
|
|
236
|
+
- **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.
|
|
237
|
+
- **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.
|
|
238
|
+
- **`--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.
|
|
239
|
+
- **`--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.
|
|
196
240
|
- **`--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.
|
|
197
|
-
-
|
|
241
|
+
- **`--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.
|
|
242
|
+
- **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.
|
|
243
|
+
- `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.
|
|
244
|
+
- **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).
|
|
245
|
+
- **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.
|
|
246
|
+
- **`--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.
|
|
198
247
|
- `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.
|
|
199
248
|
|
|
200
249
|
## Writes
|
|
@@ -212,12 +261,13 @@ Every write the conductor makes outside its own store, with what triggers it and
|
|
|
212
261
|
| A pushed branch | `conduct pr create` (a worker), from its own checkout | git's own credential helper |
|
|
213
262
|
| 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 |
|
|
214
263
|
| 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 |
|
|
264
|
+
| 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 |
|
|
215
265
|
| 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 |
|
|
216
266
|
| A reply on the worker's PR | `conduct reply` → `POST /workers/:id/replies` | the project's `az` identity, or its gh account |
|
|
217
|
-
| ADO: a pipeline queued
|
|
267
|
+
| 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 |
|
|
218
268
|
| A train: its branch, the members' merges into it, its PR, a note on each member | `conduct train` (you) | the project's gh account |
|
|
219
269
|
|
|
220
|
-
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.
|
|
270
|
+
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`, `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.
|
|
221
271
|
|
|
222
272
|
## Inbox
|
|
223
273
|
|
|
@@ -229,7 +279,7 @@ The `inbox` table holds everything that waits on the operator, across projects.
|
|
|
229
279
|
- the exact commands, and its links, each named (`{ label, href }`);
|
|
230
280
|
- when it was raised.
|
|
231
281
|
|
|
232
|
-
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
|
|
282
|
+
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:
|
|
233
283
|
|
|
234
284
|
- A pull request is `PR <n>`, or `<repo> PR <n>` when it isn't the project's repo.
|
|
235
285
|
- An Azure Pipelines or GitHub Actions run is `run <n>`.
|
|
@@ -244,7 +294,7 @@ An item resolves itself when its condition is seen, and records what resolved it
|
|
|
244
294
|
|---|---|---|
|
|
245
295
|
| `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` |
|
|
246
296
|
| `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 |
|
|
247
|
-
| `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 |
|
|
297
|
+
| `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 |
|
|
248
298
|
| `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` |
|
|
249
299
|
| `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) |
|
|
250
300
|
| `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 |
|
|
@@ -275,10 +325,23 @@ If the tracker refuses the comment, nothing is resolved. Only a plan takes appro
|
|
|
275
325
|
|
|
276
326
|
## Notifications
|
|
277
327
|
|
|
278
|
-
The notifier routes each new inbox item to the phone, on
|
|
328
|
+
The notifier routes each new inbox item to the phone, or anywhere else that takes a webhook, on three channels:
|
|
279
329
|
|
|
280
330
|
- **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.
|
|
281
|
-
- **Pushover:** sent from the daemon. The keys are `PUSHOVER_TOKEN` and `PUSHOVER_USER`, or else the
|
|
331
|
+
- **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).
|
|
332
|
+
- **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:
|
|
333
|
+
|
|
334
|
+
```jsonc
|
|
335
|
+
{
|
|
336
|
+
"event": "raised", // raised, reminder, summary (quiet hours), rule (a rule's notify) or test
|
|
337
|
+
"title": "conductor · ATK-5 npm-otp",
|
|
338
|
+
"text": "ATK-5 · approve the npm stage", // the push in one line, as the other channels show it
|
|
339
|
+
"link": "https://…", // its first link, or null
|
|
340
|
+
"project": "atk",
|
|
341
|
+
// the inbox item whole for raised and reminder, null otherwise
|
|
342
|
+
"inbox": { "id": 12, "kind": "npm-otp", "item": "ATK-5", "project": "atk", "text": "…", "commands": ["npm stage approve … --otp=<code>"], "links": [{ "label": "run 7", "href": "https://…" }], "raisedAt": "2026-10-18T12:00:00.000Z", "worker": 3 }
|
|
343
|
+
}
|
|
344
|
+
```
|
|
282
345
|
|
|
283
346
|
**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:
|
|
284
347
|
|
|
@@ -307,9 +370,9 @@ A rule matches on `kinds` (inbox kinds), `projects`, `hours` (local, `HH:MM-HH:M
|
|
|
307
370
|
- **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.
|
|
308
371
|
- **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.
|
|
309
372
|
- **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.
|
|
310
|
-
- **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`.
|
|
373
|
+
- **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`.
|
|
311
374
|
|
|
312
|
-
`conduct notify` shows the profile, quiet hours and the last state of each channel: Pushover from
|
|
375
|
+
`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.
|
|
313
376
|
|
|
314
377
|
## Rules
|
|
315
378
|
|
|
@@ -326,11 +389,11 @@ Rules say what the conductor does when something happens (design note, section 9
|
|
|
326
389
|
]
|
|
327
390
|
```
|
|
328
391
|
|
|
329
|
-
- **Sources, in order:** the item's project's `rules
|
|
330
|
-
- **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`, `Started in item.phases`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed` (a failed launch, a session that stopped unexpectedly, or a PR's red checks: `pr.checks-failed`), `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `worker.parked` (a park; the session manager also asks it of each parked worker on a sweep, for `compact`), `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `provider.login-failed` (a project's login lost, once per spell; see Accounts), `queue.pick`, `queue.drained`, `queue.ready-to-release`, `queue.triage`. A rule without one is a state rule, checked on each scheduler tick against each wait, inbox item, worker or project, or once for the machine, by the fields it reads. Fields: `project`, `item.id/area/labels/tier/priority`, `item.phases` (the phases of every note the item has had, the one being posted too), `plan.tier/landing`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `worker.context` (the session's context in tokens, see Workers), `spend.worker/item/project/today` (input, output and cache-write tokens, without cache reads: the worker's and the item's in all, the project's and the machine's today), `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
|
|
392
|
+
- **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: conduct-atk's behavior written as rules (`launch`, `retire-closed`, `park-on-wait`, `resume-on-wait`, `compact-on-park`, `runaway`, `daily-cap`, `ready-to-release`, `queue-drained`, `triage`, which raises a new item that waits on the profile's triage label in the inbox, `conductor-handoff`, which escalates a worker's `Handoff · conductor` note to the console, and `hold-pre-start-handoff`, which adds the human label when a worker hands off as `human`, `operator`, `blocked` or `scope` before its `Started` note, so the scheduler never launches work no worker can do again; remove the label if the item should be picked later), and `release-pre-start-hold`, which takes that label off again when the worker clears the hold itself (its `Resumed`, or its `Started` after the hold), and leaves a human label the hold didn't add (ATK-575). A rule with an `id` replaces the rule with the same id further down, so a file holds only the differences, and a machine-profile rule replaces the repo's rule of its id (`"do": "none"` switches it off).
|
|
393
|
+
- **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`, `Started in item.phases`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed` (a failed launch, a session that stopped unexpectedly, or a PR's red checks: `pr.checks-failed`), `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `worker.parked` (a park; the session manager also asks it of each parked worker on a sweep, for `compact`), `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `provider.login-failed` (a project's login lost, once per spell; see Accounts), `queue.pick`, `queue.drained`, `queue.ready-to-release`, `queue.triage`. A rule without one is a state rule, checked on each scheduler tick against each wait, inbox item, worker or project, or once for the machine, by the fields it reads. Fields: `project`, `item.id/area/labels/tier/priority`, `item.phases` (the phases of every note the item has had, the one being posted too), `item.labelled-by` (the ids of the rules whose label add still stands on the item: a label they added, not one it had already, that no rule removed since), `plan.tier/landing`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `worker.context` (the session's context in tokens, see Workers), `spend.worker/item/project/today` (input, output and cache-write tokens, without cache reads: the worker's and the item's in all, the project's and the machine's today), `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
|
|
331
394
|
- **do:** `launch`, `retire`, `park`, `resume worker`, `retry`, `pause launches [in <project>] [until <when> | for <duration>]`, `pause all launches …`, `unpause launches`, `focus [<project>] weight|slots <n> until <when>`, `auto-approve now`, `auto-approve after <duration>`, `inbox <kind> ["<text>"]`, `remind operator`, `notify <channel> ["<text>"]`, `notify profile <name> [until <when>]`, `label add|remove human|triage|shipped-open`, `flag shipped-open`, `escalate console "<ask>"`, `none`. One string, or a list.
|
|
332
395
|
- **Guardrails:** rules act inside Area locks, slots, quiet hours and config.json's limits. A rule that would go past one is refused when it loads: `focus … slots` over `scheduler.slots`, an override with no end or one over 14 days, `retire` on anything but `item.closed`, `launch` outside `queue.pick` / `item.unblocked`, `park` outside `worker.waiting`, `resume` outside `wait.resolved` / `pr.comment`, `compact` outside `worker.parked`, `retry` outside `run.failed`. `merge`, `push`, `publish`, `approve`, `delete` and `run` aren't in the vocabulary: rules never merge, push, publish, approve prod, delete or run commands, and the daemon writes to a code host only when the owning worker asks (see Writes).
|
|
333
|
-
- **Reload:** saving `rules.json` or a project's profile reloads its rules.
|
|
396
|
+
- **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.
|
|
334
397
|
- **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.
|
|
335
398
|
|
|
336
399
|
- **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.
|
|
@@ -386,15 +449,47 @@ The scheduler moves conduct-atk's tick into the daemon (design note, section 8).
|
|
|
386
449
|
|
|
387
450
|
**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`.
|
|
388
451
|
|
|
389
|
-
##
|
|
452
|
+
## Projects
|
|
453
|
+
|
|
454
|
+
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`:
|
|
455
|
+
|
|
456
|
+
| Path | Holds |
|
|
457
|
+
|---|---|
|
|
458
|
+
| `<name>.json` | its machine profile: what this machine says about the project, which wins over the repo's |
|
|
459
|
+
| `<name>/repo` | the clone of its remote, which the daemon fetches and adds worktrees to |
|
|
460
|
+
| `<name>/worktrees/<item>` | a worker's worktree (see Workers) |
|
|
461
|
+
|
|
462
|
+
**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.
|
|
463
|
+
|
|
464
|
+
1. Install the conductor (see On PATH) and sign in to the code host and tracker (see Accounts).
|
|
465
|
+
2. In a checkout of the repo, run `conduct init`. It shows what it detected:
|
|
466
|
+
- the code host from `origin`;
|
|
467
|
+
- CI as the code host's own (a hint when the pipeline files are another's);
|
|
468
|
+
- the base branch from `origin/HEAD`;
|
|
469
|
+
- `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`);
|
|
470
|
+
- a `verify-*` skill in `.claude/skills`;
|
|
471
|
+
- the item key most recent commit subjects carry.
|
|
472
|
+
|
|
473
|
+
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`.
|
|
474
|
+
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.
|
|
475
|
+
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.
|
|
476
|
+
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.
|
|
477
|
+
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, ATK among them, 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>`.
|
|
478
|
+
|
|
479
|
+
**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>`.
|
|
480
|
+
|
|
481
|
+
**Its profile** comes in two layers, merged as the files are written, before the schema reads them:
|
|
482
|
+
|
|
483
|
+
- the **machine profile**, `<name>.json` (JSONC), optional beside a repo profile. A project with no repo profile keeps a whole profile here;
|
|
484
|
+
- 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.
|
|
390
485
|
|
|
391
|
-
|
|
486
|
+
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.
|
|
392
487
|
|
|
393
|
-
|
|
394
|
-
2. When they differ, it records the pins as pending (a `checkout.pending` event). With the main checkout on `<base>` and no local changes to `.atk-lock.json`, it then runs `git merge --ff-only origin/<base>`.
|
|
395
|
-
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.
|
|
488
|
+
`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.
|
|
396
489
|
|
|
397
|
-
|
|
490
|
+
**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.
|
|
491
|
+
|
|
492
|
+
**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.
|
|
398
493
|
|
|
399
494
|
## Skill trains
|
|
400
495
|
|
|
@@ -453,11 +548,23 @@ Accounts belong to the person running the daemon, not to the repo, so each proje
|
|
|
453
548
|
|
|
454
549
|
| Order | Source | Azure DevOps (PRs, runs, Boards) | GitHub (reads, and writes such as `conduct train`'s) | Linear (waits, notes, labels, queue, briefs) |
|
|
455
550
|
|---|---|---|---|---|
|
|
456
|
-
| 1 | **project token**: a
|
|
457
|
-
| 2 | **machine token**: a conductor-only env var, else a
|
|
551
|
+
| 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 |
|
|
552
|
+
| 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) |
|
|
458
553
|
| 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 |
|
|
459
554
|
|
|
460
|
-
|
|
555
|
+
**The secret store** is the platform's own:
|
|
556
|
+
|
|
557
|
+
| Platform | Store | Save a token (each asks for the secret) |
|
|
558
|
+
|---|---|---|
|
|
559
|
+
| Windows | Credential Manager (generic credentials) | `cmdkey /generic:<target> /user:pat /pass` |
|
|
560
|
+
| 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` |
|
|
561
|
+
| 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` |
|
|
562
|
+
|
|
563
|
+
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-atk-ado` is `CONDUCTOR_SECRET_CONDUCTOR_ATK_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.
|
|
564
|
+
|
|
565
|
+
**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.
|
|
566
|
+
|
|
567
|
+
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:
|
|
461
568
|
- **Azure DevOps:** Code (read & write) and Build (read & execute), plus Work Items (read & write) for Boards;
|
|
462
569
|
- **GitHub:** a fine-grained token on the repo with Contents, Pull requests and Issues (read & write) and Actions (read);
|
|
463
570
|
- **Linear:** an API key limited to the project's team.
|
|
@@ -466,7 +573,7 @@ Only the conductor's own env names count: another tool's `AZURE_DEVOPS_PAT` or `
|
|
|
466
573
|
|
|
467
574
|
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.
|
|
468
575
|
|
|
469
|
-
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
|
|
576
|
+
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.
|
|
470
577
|
|
|
471
578
|
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.
|
|
472
579
|
|
|
@@ -542,14 +649,11 @@ Everything goes through the project's gh account (`projects.<name>.github.user`,
|
|
|
542
649
|
|
|
543
650
|
The broker shares what every project on the machine uses (ATK-318).
|
|
544
651
|
|
|
545
|
-
**Ports.** Each profile
|
|
546
|
-
- A
|
|
547
|
-
-
|
|
548
|
-
- A
|
|
549
|
-
-
|
|
550
|
-
- A project's dev scripts read the assignment with `--json`, as ATK's `scripts/dev-ports.mjs assign` does. They keep their own scheme when it exits 1 (`not brokered`): no daemon, an older one, or a checkout in no registered project.
|
|
551
|
-
- The broker knows only the slots it gave out. A slot a script picked on its own while no daemon answered stays unknown until that worktree asks the broker; until then only the listening probe keeps the slot clear. So `--prefer` is the slot the script would pick itself (`dev-ports.mjs` passes the checkout's own slot or its own free-slot scan's). The preferred slot is taken without the listening probe, because the checkout's own servers may be what listens there.
|
|
552
|
-
- A stored assignment follows its project's profile: when the base ports or `maxSlot` change, the worktree moves, its old slot first.
|
|
652
|
+
**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).
|
|
653
|
+
- 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).
|
|
654
|
+
- 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.
|
|
655
|
+
- 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).
|
|
656
|
+
- `conduct ports` lists the assignments, and `conduct ports <path>` gives one worktree's (`--json` for either). Only a launch assigns. `conduct project check` previews the ports a new worktree would get, saving nothing.
|
|
553
657
|
|
|
554
658
|
**Locks.** `conduct lock acquire chrome` takes the lock every project's verify runs share. It answers the work-issue helper's JSON (`acquired`, `heldBy`, `since`, `refreshed`; `released` for a release; `verdict` for a status).
|
|
555
659
|
- The owner is `--owner`, else the item of the worker whose worktree this is.
|
|
@@ -597,7 +701,8 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
|
|
|
597
701
|
|
|
598
702
|
| File | Holds |
|
|
599
703
|
|---|---|
|
|
600
|
-
| `
|
|
704
|
+
| `projects/` | each conducted project's machine profile, clone and worktrees (see Projects) |
|
|
705
|
+
| `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`, `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); `workers` holds `tab` (false: workers run headless) and `terminal` (`auto`; see Tabs) |
|
|
601
706
|
| `rules.json` | machine rules, JSONC, reloaded on save (see Rules) |
|
|
602
707
|
| `token` | the API token, readable by this user only; every request sends `Authorization: Bearer <token>` |
|
|
603
708
|
| `conductor.pid`, `port` | the single-instance lock and where the API listens (`127.0.0.1`) |
|
|
@@ -606,7 +711,7 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
|
|
|
606
711
|
|
|
607
712
|
## API
|
|
608
713
|
|
|
609
|
-
- `GET /projects`: the registered projects
|
|
714
|
+
- `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.
|
|
610
715
|
- `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).
|
|
611
716
|
- `POST /shutdown`: a clean stop (`conduct stop`).
|
|
612
717
|
- `GET /console/escalations`: the open escalations, oldest first (see Console).
|
|
@@ -616,9 +721,9 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
|
|
|
616
721
|
- `GET /fleet`: the workers in flight, with session activity, phase, PR, waits and spend (today, total).
|
|
617
722
|
- `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.
|
|
618
723
|
- `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.
|
|
619
|
-
- `POST /workers/:id/attach`: wake if parked, mark attached, open the tab.
|
|
724
|
+
- `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>`).
|
|
620
725
|
- `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.
|
|
621
|
-
- `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.
|
|
726
|
+
- `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.
|
|
622
727
|
- `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"`.
|
|
623
728
|
- `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.
|
|
624
729
|
- `GET /inbox`: the open inbox items, oldest first.
|
|
@@ -644,13 +749,13 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
|
|
|
644
749
|
|
|
645
750
|
## Restarts
|
|
646
751
|
|
|
647
|
-
`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
|
|
752
|
+
`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.
|
|
648
753
|
|
|
649
754
|
- **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.
|
|
650
755
|
- **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.
|
|
651
|
-
- **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.
|
|
756
|
+
- **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.
|
|
652
757
|
- **Logs:** the supervisor writes `the daemon exited; the supervisor restarts it` and its push results to `logs/conductor.log`.
|
|
653
|
-
- **Not covered:** a logoff or reboot ends the supervisor with the session, and the logon
|
|
758
|
+
- **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.
|
|
654
759
|
|
|
655
760
|
## On PATH
|
|
656
761
|
|
|
@@ -659,24 +764,49 @@ Install the package globally, next to `atk`. `dev` follows `develop`; `latest` f
|
|
|
659
764
|
```bash
|
|
660
765
|
npm install --global @detergent-software/conductor@dev
|
|
661
766
|
conduct install-startup
|
|
662
|
-
conduct project add <
|
|
767
|
+
conduct project add <the project's remote URL, or a checkout of it>
|
|
663
768
|
```
|
|
664
769
|
|
|
665
|
-
`conduct project add`
|
|
770
|
+
`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.
|
|
771
|
+
|
|
772
|
+
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.
|
|
773
|
+
|
|
774
|
+
`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 ATK's `.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.
|
|
775
|
+
|
|
776
|
+
A PR that adds or reshapes a field can't know the number semantic-release will give it, so it takes the next one:
|
|
777
|
+
|
|
778
|
+
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`.
|
|
779
|
+
2. Record that version for the field in `FIELD_SINCE`. If ATK's 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 Jason has approved its stage).
|
|
780
|
+
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.
|
|
781
|
+
|
|
782
|
+
A `test`, `chore` or `docs` change stages no release and records nothing.
|
|
783
|
+
|
|
784
|
+
After an `--allow-downgrade` to a version without those fields, the stored copy no longer validates either: the project shows `the stored profile no longer validates` and isn't conducted until its file, saved without them, is read again.
|
|
666
785
|
|
|
667
786
|
Then `conduct project check <name>` says whether the project can be conducted, before its first launch finds out. Each check prints one line, `ok`, `warn`, `fail` or `skip`, with a fix under a warn or a fail, and the command exits 1 when any check fails (`--json` prints the same as data):
|
|
668
787
|
|
|
669
|
-
- **profile:** the
|
|
670
|
-
- **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`
|
|
788
|
+
- **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`.
|
|
789
|
+
- **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.
|
|
671
790
|
- **tracker:** the tracker's login answers, and the queue reads (Linear: the team, and whether any of its projects has a priority; Boards: the saved query; GitHub Issues: the labelled issues).
|
|
672
791
|
- **code-host** and **ci:** the code host's login answers and the base branch exists; each release's `pipeline` names exactly one of the repo's pipelines.
|
|
673
|
-
- **worktree:** the launch's
|
|
674
|
-
- **skills:** `
|
|
675
|
-
- **ports:** the
|
|
792
|
+
- **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 (ATK's runs `pnpm install`).
|
|
793
|
+
- **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` (ATK's runs `atk install`), not in the clone, so the worktree check looks for them.
|
|
794
|
+
- **ports:** the machine's pool has a free port for each of the profile's `ports`, saving nothing; `skip` for a profile with none.
|
|
676
795
|
- **health:** each `health` URL answers; a `warn` at most, since an environment can be down.
|
|
677
796
|
|
|
678
797
|
With the daemon running it checks through the API, as the daemon's own accounts; without it, here, as the machine's.
|
|
679
798
|
|
|
799
|
+
A new worktree's local files come from the profile (design note v2, section 2; see Workers):
|
|
800
|
+
|
|
801
|
+
```jsonc
|
|
802
|
+
// The dev ports a worktree needs, by name: CONDUCTOR_PORT_WEB and CONDUCTOR_PORT_API.
|
|
803
|
+
"ports": ["web", "api"],
|
|
804
|
+
// Run in order in each new worktree; they build its gitignored files from what the repo commits.
|
|
805
|
+
"setup": ["pnpm install --frozen-lockfile", "node scripts/dev-ports.mjs assign"],
|
|
806
|
+
// Optional: copy the .worktreeinclude files of a checkout you keep, for local files made by hand.
|
|
807
|
+
"localFilesFrom": "C:\\Users\\you\\src\\project"
|
|
808
|
+
```
|
|
809
|
+
|
|
680
810
|
The profile's optional `work` section is for the project's work skill; the daemon only checks it:
|
|
681
811
|
|
|
682
812
|
```jsonc
|
|
@@ -688,15 +818,23 @@ The profile's optional `work` section is for the project's work skill; the daemo
|
|
|
688
818
|
}
|
|
689
819
|
```
|
|
690
820
|
|
|
691
|
-
|
|
821
|
+
`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):
|
|
822
|
+
|
|
823
|
+
- **Windows:** a Task Scheduler task, `Conductor`, at logon, under `conhost --headless` so no window opens. Registering it starts nothing; `conduct start` runs it.
|
|
824
|
+
- **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.
|
|
825
|
+
- **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.
|
|
826
|
+
|
|
827
|
+
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).
|
|
828
|
+
|
|
829
|
+
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:
|
|
692
830
|
|
|
693
831
|
```bash
|
|
694
832
|
conduct upgrade
|
|
695
833
|
```
|
|
696
834
|
|
|
697
|
-
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
|
|
835
|
+
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`.
|
|
698
836
|
|
|
699
|
-
**From a checkout instead (development).** `npm link` puts
|
|
837
|
+
**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`:
|
|
700
838
|
|
|
701
839
|
```bash
|
|
702
840
|
pnpm install
|
|
@@ -705,7 +843,7 @@ pnpm -C apps/conductor build
|
|
|
705
843
|
conduct install-startup
|
|
706
844
|
```
|
|
707
845
|
|
|
708
|
-
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
|
|
846
|
+
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`.
|
|
709
847
|
|
|
710
848
|
## Console
|
|
711
849
|
|
|
@@ -714,7 +852,7 @@ The console (design note, section 11) is the one Claude session that stays open
|
|
|
714
852
|
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`.
|
|
715
853
|
|
|
716
854
|
```bash
|
|
717
|
-
conduct console # a tab in Windows Terminal (window "conductor"),
|
|
855
|
+
conduct console # a tab in ~/.conductor/console: Windows Terminal (window "conductor"), iTerm or Terminal, or tmux (session conductor)
|
|
718
856
|
conduct console --here # the same session in this terminal
|
|
719
857
|
```
|
|
720
858
|
|