@detergent-software/conductor 1.0.0-dev.1 → 1.0.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +175 -9
  3. package/build/cli.js +368 -21
  4. package/build/cli.js.map +1 -1
  5. package/build/clock.js +17 -0
  6. package/build/clock.js.map +1 -0
  7. package/build/conditions.js +5 -6
  8. package/build/conditions.js.map +1 -1
  9. package/build/config.js +92 -1
  10. package/build/config.js.map +1 -1
  11. package/build/credential.js +52 -0
  12. package/build/credential.js.map +1 -0
  13. package/build/daemon.js +56 -1
  14. package/build/daemon.js.map +1 -1
  15. package/build/events.js +106 -0
  16. package/build/events.js.map +1 -0
  17. package/build/inbox.js +385 -0
  18. package/build/inbox.js.map +1 -0
  19. package/build/manager.js +26 -3
  20. package/build/manager.js.map +1 -1
  21. package/build/mod.js +51 -0
  22. package/build/mod.js.map +1 -0
  23. package/build/note.js +71 -0
  24. package/build/note.js.map +1 -0
  25. package/build/notes.js +188 -0
  26. package/build/notes.js.map +1 -0
  27. package/build/notify.js +393 -0
  28. package/build/notify.js.map +1 -0
  29. package/build/paths.js +1 -0
  30. package/build/paths.js.map +1 -1
  31. package/build/profile.js +7 -0
  32. package/build/profile.js.map +1 -1
  33. package/build/pushover.js +67 -0
  34. package/build/pushover.js.map +1 -0
  35. package/build/registry.js +3 -1
  36. package/build/registry.js.map +1 -1
  37. package/build/rule-input.js +17 -0
  38. package/build/rule-input.js.map +1 -0
  39. package/build/rules-service.js +391 -0
  40. package/build/rules-service.js.map +1 -0
  41. package/build/rules.js +842 -0
  42. package/build/rules.js.map +1 -0
  43. package/build/server.js +140 -0
  44. package/build/server.js.map +1 -1
  45. package/build/spend.js +8 -0
  46. package/build/spend.js.map +1 -1
  47. package/build/startup.js +30 -0
  48. package/build/startup.js.map +1 -1
  49. package/build/store.js +196 -0
  50. package/build/store.js.map +1 -1
  51. package/build/token.js +3 -3
  52. package/build/token.js.map +1 -1
  53. package/build/upgrade.js +105 -0
  54. package/build/upgrade.js.map +1 -0
  55. package/build/wait.js +32 -32
  56. package/build/wait.js.map +1 -1
  57. package/build/watchers/linear-key.js +6 -45
  58. package/build/watchers/linear-key.js.map +1 -1
  59. package/build/watchers/linear.js +16 -7
  60. package/build/watchers/linear.js.map +1 -1
  61. package/mod/.claude-plugin/marketplace.json +6 -0
  62. package/mod/.claude-plugin/plugin.json +7 -0
  63. package/mod/README.md +17 -0
  64. package/mod/hooks/hooks.json +1 -0
  65. package/mod/hooks/model.ts +395 -0
  66. package/mod/hooks/register.tsx +505 -0
  67. package/mod/types/index.d.ts +57 -0
  68. package/package.json +6 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,93 @@
2
2
 
3
3
  Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
4
4
 
5
+ ## [1.0.0-dev.10](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.9&targetVersion=GTconductor-v1.0.0-dev.10) (2026-10-11)
6
+
7
+ ### Features
8
+
9
+ * **conductor:** rules engine, settings and conduct rules (ATK-315) ([dd7110d](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/dd7110dfe48a2323ed73dac0f4a1d7334635f09d))
10
+
11
+ ## [1.0.0-dev.9](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.8&targetVersion=GTconductor-v1.0.0-dev.9) (2026-10-11)
12
+
13
+ ### Bug Fixes
14
+
15
+ * **conductor:** read a bare HH:MM in conduct wait time (ATK-356) ([c988e8b](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c988e8b457ea8614eeb5fd078b4c033bac5ed54f))
16
+
17
+ ## [1.0.0-dev.8](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.7&targetVersion=GTconductor-v1.0.0-dev.8) (2026-10-11)
18
+
19
+ ### Bug Fixes
20
+
21
+ * **conductor:** report every Remote Control push outcome (ATK-313) ([f4d1e3c](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/f4d1e3c762f635789c6a5871eef99eddcc8d157b))
22
+
23
+ ## [1.0.0-dev.7](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.6&targetVersion=GTconductor-v1.0.0-dev.7) (2026-10-11)
24
+
25
+ ### Features
26
+
27
+ * **conductor:** route inbox items through a notifier (ATK-313) ([cec93e4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/cec93e4dd6a286c9a8d4a47a642eb84b28ba899f))
28
+ * **conductor:** switch notification profiles from the console (ATK-313) ([e011463](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/e011463f763736a1665be8ee1b7164395be331f9))
29
+
30
+ ### Bug Fixes
31
+
32
+ * **conductor:** harden the notifier after review (ATK-313) ([0da9596](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/0da959630e50ccd820e9e625085943b1a245da1c))
33
+ * **conductor:** route reminders by the profile in force (ATK-313) ([987c2cc](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/987c2cc72bc3c6b5d0c2de73f9d7dd3e5a25ef33))
34
+
35
+ ### Documentation
36
+
37
+ * **conductor:** say which day a notify rule's days match (ATK-313) ([6d175d3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6d175d3083b0f9276535758043c3b3bfeadca741))
38
+
39
+ ## [1.0.0-dev.6](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.5&targetVersion=GTconductor-v1.0.0-dev.6) (2026-10-11)
40
+
41
+ ### Features
42
+
43
+ * **conductor:** add conduct events, notify.requested and install-mod ([c0665ac](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c0665ac8be2854f73903605a49bf8283e6425703))
44
+ * **conductor:** add the conductor mod for Claude Code ([6f89cd4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/6f89cd4ade58c9d10cc8882cac6890edc6d68258))
45
+
46
+ ### Bug Fixes
47
+
48
+ * **conductor:** address review findings on the mod and its CLI ([c35c2da](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c35c2da943ff25bd3ff0854965a545fd17031768))
49
+
50
+ ## [1.0.0-dev.5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.4&targetVersion=GTconductor-v1.0.0-dev.5) (2026-10-11)
51
+
52
+ ### Features
53
+
54
+ * **conductor:** add the inbox and conduct approve / revise (ATK-312) ([b187b8d](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/b187b8d5101568ee6907bd577ef243c9aa6fdb5e))
55
+
56
+ ### Bug Fixes
57
+
58
+ * **conductor:** address code review of the inbox (ATK-312) ([de75a51](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/de75a51e9d60a2873c54e169236552ff9f92e7ef))
59
+ * **conductor:** address review of the inbox (ATK-312) ([fa5fb30](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/fa5fb30bbfe7c3ada49b5d84b7776ce3727db0c7))
60
+
61
+ ## [1.0.0-dev.4](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.3&targetVersion=GTconductor-v1.0.0-dev.4) (2026-10-11)
62
+
63
+ ### Features
64
+
65
+ * **conductor:** add conduct note, protocol v1's phase notes (ATK-311) ([c89342b](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/c89342bb1eaaf23ae9041d529e131256a2f0cf62))
66
+
67
+ ### Bug Fixes
68
+
69
+ * **conductor:** address code review of conduct note (ATK-311) ([9253da3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/9253da3bb673e1131f70fe666069e6307a40d599))
70
+
71
+ ## [1.0.0-dev.3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.2&targetVersion=GTconductor-v1.0.0-dev.3) (2026-10-11)
72
+
73
+ ### Features
74
+
75
+ * **conductor:** conduct upgrade refuses a downgrade (ATK-343) ([bb6dfdd](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/bb6dfdd2e86ad36cb3c0730407e7d3ac006abeba))
76
+
77
+ ### Bug Fixes
78
+
79
+ * **conductor:** install the exact version upgrade compared (ATK-343) ([124def0](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/124def059730b2a6570fdbebbb303211c906201c))
80
+
81
+ ## [1.0.0-dev.2](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.0.0-dev.1&targetVersion=GTconductor-v1.0.0-dev.2) (2026-10-11)
82
+
83
+ ### Features
84
+
85
+ * **conductor:** conduct upgrade stops, installs and restarts (ATK-340) ([76e6fa9](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/76e6fa9596e5992ddd0ef55e2b7ebbb863b8bbac))
86
+
87
+ ### Bug Fixes
88
+
89
+ * **conductor:** make conduct upgrade's restart robust (ATK-340) ([eabe05f](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/eabe05fdd0a7435b09eadbd5f4bdd4c83b517a14))
90
+ * **conductor:** start the upgraded daemon in its home (ATK-340) ([bc0d46a](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/bc0d46a516348123eff48e0856e00320649cf0fd))
91
+
5
92
  ## 1.0.0-dev.1 (2026-10-10)
6
93
 
7
94
  ### Features
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Conductor
2
2
 
3
- `@detergent-software/conductor`: the local daemon that runs the work loop for every conducted project on this machine, and its `conduct` CLI. The design, decisions and build order are in the Linear document *Conductor: design note* (project *Conductor daemon*). This package has build step 1, the skeleton (ATK-308), step 2, the session manager (ATK-309), and step 3, the watchers and `conduct wait` (ATK-310).
3
+ `@detergent-software/conductor`: the local daemon that runs the work loop for every conducted project on this machine, and its `conduct` CLI. The design, decisions and build order are in the Linear document *Conductor: design note* (project *Conductor daemon*). This package has build step 1, the skeleton (ATK-308), step 2, the session manager (ATK-309), step 3, the watchers and `conduct wait` (ATK-310), the daemon's side of step 4, worker protocol v1's `conduct note` (ATK-311), step 5, the inbox with `conduct approve` / `revise` (ATK-312), step 6, the notifier (ATK-313), step 9, the conductor mod for Claude Code in `mod/` (ATK-265), and the first part of step 8, the rules engine (ATK-315; the scheduler that acts on the rules follows).
4
4
 
5
5
  ## Commands
6
6
 
@@ -14,9 +14,29 @@ conduct launch <item> start a worker: worktree, background session,
14
14
  [--project <name>] [--probe] [--prompt <text>] [--conductor <name>]
15
15
  conduct retire <worker> [--force] stop a worker, remove its worktree and session
16
16
  conduct attach <worker> wake a parked worker (no event text) and open it in a tab
17
+ conduct inbox [--json] what waits on you, with its commands and links (see Inbox)
18
+ conduct approve <item> [--text <text>]
19
+ approve a plan: post "approve" on the item as you, wake its worker
20
+ conduct revise <item> <text> ask for another plan: post "revise: <text>", wake its worker
21
+ conduct done <id> mark an inbox item done (a physical action, or one you handled)
22
+ conduct notify [--json] the notification profile, quiet hours, each channel's state
23
+ conduct notify profile [<name>] [--until <when>]
24
+ switch where pushes go (see Notifications)
25
+ conduct notify test [--kind <kind>] [--channel remote-control|pushover] [--raise]
26
+ push a sample item now, or raise a real one with --raise
27
+ conduct rules [--json] the rules in force, what replaces what, any file refused (see Rules)
28
+ conduct rules test <file> [--json]
29
+ dry-run the rules on a sample event; acts on nothing
30
+ conduct note <item> <file> post a phase note on the item and record its phase (see Notes)
31
+ [--worker <worker>]
17
32
  conduct wait <condition> wait for an outside event (see Waits)
33
+ conduct events [--since <id>] [--follow]
34
+ the event log, one JSON event per line (see API)
18
35
  conduct install-startup start the daemon at logon (Windows Task Scheduler)
19
36
  conduct install-startup --remove remove that logon task
37
+ conduct install-mod [--remove] install the conductor mod in Claude Code (see Mod)
38
+ conduct upgrade [--tag dev|latest] [--allow-downgrade]
39
+ stop the daemon, npm install the tag's newest version, start it again
20
40
  ```
21
41
 
22
42
  A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
@@ -32,12 +52,121 @@ The session manager owns the `claude --bg` sessions it launches, and only those:
32
52
  - **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
33
53
  - **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.
34
54
  - **park failures:** a session whose process outlives `claude stop` by 30 s logs one `worker.park-failed` per spell, and the park is tried again every 5 minutes.
35
- - **stalls:** a permission prompt open `sessions.permissionMinutes` (5), or idle `sessions.idleMinutes` (20) with no wait, logs one `worker.stalled` event per spell.
36
- - **spend:** tokens from the session's transcript and its subagents' (`~/.claude/projects/*/<session>.jsonl`), each message counted once, go into `spend` per worker and local day. It keeps input, output, cache read and cache write; `tokens` is their sum.
55
+ - **stalls:** a permission prompt open `sessions.permissionMinutes` (5), or idle `sessions.idleMinutes` (20) with no wait, logs one `worker.stalled` event per spell, and `worker.stall-cleared` when that spell ends.
56
+ - **spend:** tokens from the session's transcript and its subagents' (`~/.claude/projects/*/<session>.jsonl`), each message counted once, go into `spend` per worker and local day. It keeps input, output, cache read and cache write; `tokens` is their sum. A worker whose tokens in all, not counting cache reads, pass `inbox.runawayTokens` logs `worker.runaway` once per daemon run. Cache reads are left out because they are nearly all of a session's tokens (97% on ATK-312) at a tenth of the price.
37
57
  - **probes:** `--probe` names the worktree and session `probe-<item>` and gives it a prompt that works no issue, so tests stay out of the live fleet.
38
58
 
39
59
  Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `failed`, `exited`. Every step is an event (`worker.launched`, `worker.parked`, `worker.resumed`, …) on `GET /events`.
40
60
 
61
+ ## Notes
62
+
63
+ A worker writes each phase note (`## <work skill> · <Phase>[ · <kind>: <one line>]`, such as `## work-issue · PR opened`) with `conduct note <item> <file>`. The daemon posts the file as a comment on the item in the tracker, then records the phase at once: on the worker (`phase` in `GET /fleet` and `conduct status`), on the item, and as a `note.posted` event. The worker is `--worker <worker>`, else the one whose worktree is the current checkout, else the project's live worker on that item. The project is the worker's, else the one whose tracker team is the item's key prefix.
64
+
65
+ | Exit | Means | The worker then |
66
+ |---|---|---|
67
+ | 0 | posted | goes on |
68
+ | 3 | nobody posted it: no daemon, one without notes, or the tracker refused it (such as a read-only key) | posts the note to the tracker itself, as before protocol v1 |
69
+ | 1 | refused (no heading, no project) or unclear (the daemon or Linear didn't answer the write) | reads the message; when it says the note may be there, looks before posting again |
70
+
71
+ On Linear the daemon uses the key the watchers read (below), so that key needs **Create comments** as well as Read. Comments it writes carry the key owner's name, as the Linear MCP's do.
72
+
73
+ ## Inbox
74
+
75
+ The `inbox` table holds everything that waits on Jason, across projects. Each item records:
76
+
77
+ - its kind;
78
+ - the item and worker it is about;
79
+ - one line of text;
80
+ - the exact commands and links;
81
+ - when it was raised.
82
+
83
+ An item resolves itself when its condition is seen, and records what resolved it. `inbox.raised` and `inbox.resolved` events go on `GET /events`. The notifier pushes each new item to the phone (see Notifications).
84
+
85
+ | Kind | Raised by | Resolved by |
86
+ |---|---|---|
87
+ | `plan` | a `Plan` note (`conduct note`), or an `approval` wait | the approval wait resolving (an `approve` or `revise:` comment), a `Plan approved` note, a newer Plan, or `conduct approve` / `revise` |
88
+ | `merge` | a `Handoff · merge: <PR>` note; a `pr … merged` wait attaches to it | its PR wait resolving as merged, abandoned or closed, or a `Landed` note. A PR wait without the note raises nothing, because auto landing needs nobody |
89
+ | `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 |
90
+ | `jason`, `human`, `scope`, `blocked`, `abandoned`, `failed` | a Handoff note of that kind | any later note on the item (such as `Resumed`), or `conduct done` |
91
+ | `permission`, `stall` | `worker.stalled` | `worker.stall-cleared`, a park or resume, a new wait (stall only), or a daemon restart (it is raised again if it persists) |
92
+ | `failed` | a failed launch, or a session that stopped unexpectedly | a new launch, the worker running again, or its retirement |
93
+ | `runaway` | a worker passing `inbox.runawayTokens` (2M) input, output and cache-write tokens in all, once per worker | `conduct done`, or its retirement |
94
+
95
+ Nothing raises `ready-to-release`, `queue-drained` or `prod-approval` yet. The scheduler (ATK-315) and `/release-atk` will raise them. When a worker is retired, its open items resolve with it.
96
+
97
+ For an item raised from a note:
98
+
99
+ - The commands are the inline code on the note's heading and the lines of its shell-tagged fenced code blocks (`bash`, `sh`, `shell`, `powershell`, `pwsh`, `ps1`, `cmd`, `console`). An untagged block is output, not a command.
100
+ - The links are the comment's URL and every URL in the note.
101
+
102
+ `conduct approve ATK-312` (or the inbox id written `#12`; a bare number is refused, since it could mean an issue) posts `approve` on the item through the tracker, or `approve: <text>` with `--text`. It uses the same key as notes, so the comment carries Jason's name and counts as his answer. `conduct revise ATK-312 "<text>"` posts `revise: <text>`; every word after the item is the text, quoted or not. Either one then:
103
+
104
+ 1. resolves the plan item and the worker's open `approval` wait;
105
+ 2. has the session manager resume a parked worker with `[conductor] event: approval ATK-312: approve HH:MM`. The worker reads the comment on the tracker before it acts.
106
+
107
+ If the tracker refuses the comment, nothing is resolved. Only a plan takes approve or revise; `conduct done <id>` resolves any item.
108
+
109
+ ## Notifications
110
+
111
+ The notifier routes each new inbox item to the phone, on two channels:
112
+
113
+ - **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).
114
+ - **Pushover:** sent from the daemon. The keys are `PUSHOVER_TOKEN` and `PUSHOVER_USER`, or else the Windows generic credential `atk-conductor-pushover` (`notify.pushover.credential`): its **user name is your Pushover user key** and its **password is the application's API token**. Save it with `cmdkey /generic:atk-conductor-pushover /user:<user key> /pass:<app token>` or in Credential Manager. A credential saved while the daemon runs is read on the next push. The keys are never logged or stored.
115
+
116
+ **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:
117
+
118
+ ```jsonc
119
+ {
120
+ "quietHours": { "from": "22:00", "to": "07:00" }, // local; omit for none
121
+ "notify": {
122
+ "profile": "default", // the profile when none is switched to
123
+ "profiles": {
124
+ "default": [{ "to": ["remote-control"] }], // built in
125
+ "away": [{ "to": ["pushover"] }], // built in
126
+ "focus": [{ "kinds": ["npm-otp", "permission", "failed"], "to": ["remote-control"] }], // built in
127
+ "workday": [
128
+ { "projects": ["cadence"], "days": ["sat", "sun"], "to": [] }, // keep these off every channel
129
+ { "kinds": ["npm-otp"], "to": ["pushover", "remote-control"], "remind": { "afterMinutes": 10, "times": 5 } },
130
+ { "hours": "08:00-18:00", "to": ["remote-control"] }
131
+ ]
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ A rule matches on `kinds` (inbox kinds), `projects`, `hours` (local, `HH:MM-HH:MM`, may run past midnight) and `days` (`sun` … `sat`, the day when the item is routed, so 01:00 on Saturday is `sat` even inside a Friday `22:00-02:00`); a rule with none of them matches everything. `conduct notify profile away` switches at once, without a restart, and the profile survives one. `--until 17:00` (the next 17:00), `--until 2026-10-12` (local midnight) or `--until 2026-10-12T09:00` hands back to the profile that was in force before. Switching to config.json's own `notify.profile` without `--until` hands the choice back to config.json. Each switch is a `notify.profile` event.
138
+
139
+ - **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.
140
+ - **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.
141
+ - **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.
142
+ - **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`.
143
+
144
+ `conduct notify` shows the profile, quiet hours and the last state of each channel: Pushover from its own answers, Remote Control from what the mod reports back from PushNotification after each push: `ok` (it reached the phone), `inactive` (Remote Control off, or mobile pushes off in Claude Code's config), `skipped` (not sent to the phone on purpose, such as while you are at the terminal: `user_present`) or `failing` (the call failed), each with its reason.
145
+
146
+ ## Rules
147
+
148
+ Rules say what the conductor does when something happens (design note, section 9). Each is a `when` and a `do` in a fixed vocabulary, written in JSONC:
149
+
150
+ ```jsonc
151
+ // ~/.conductor/rules.json
152
+ [
153
+ { "when": "wait.kind == merge and wait.age > 2h", "do": "remind jason" },
154
+ { "when": "plan.tier == S and item.area in [docs, ops]", "do": "auto-approve after 15m" },
155
+ { "id": "runaway", "when": "spend.worker > 5M", "do": "inbox runaway" }, // replaces the default
156
+ { "id": "daily-cap", "do": "none" }, // switches a default off
157
+ { "when": "project == web and day == saturday", "do": "pause launches" }
158
+ ]
159
+ ```
160
+
161
+ - **Sources, in order:** the item's project's `rules` (in its `.claude/conductor.json`, changed by PR), then `~/.conductor/rules.json`, then the built-in defaults: conduct-atk's behavior written as rules (`launch`, `retire-closed`, `park-on-wait`, `resume-on-wait`, `runaway`, `daily-cap`, `ready-to-release`, `queue-drained`). A rule with an `id` replaces the rule with the same id further down, so a file holds only the differences.
162
+ - **when:** `and`, `or`, `not` and parentheses over comparisons (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in [a, b]`, `<label> in item.labels`) and events. An event rule fires when one of its events happens: `note`, `plan`, `wait.resolved`, `run.failed`, `worker.idle`, `worker.exited`, `worker.permission`, `worker.waiting`, `inbox.raised`, `pr.comment`, `item.unblocked`, `item.closed`, `queue.pick`, `queue.drained`, `queue.ready-to-release`. A rule without one is a state rule, checked on each scheduler tick against each wait, inbox item, worker or project, or once for the machine, by the fields it reads. Fields: `project`, `item.id/area/labels/tier/priority`, `plan.tier`, `note.phase/kind`, `wait.kind/age`, `inbox.kind/age`, `worker.retries/phase`, `spend.worker/item/project/today`, `time`, `day`, `notify.profile`. Values carry units: `15m`, `2h`, `500k`, `2M`, `22:00`, `saturday`.
163
+ - **do:** `launch`, `retire`, `park`, `resume worker`, `retry`, `pause launches [in <project>] [until <when> | for <duration>]`, `pause all launches …`, `unpause launches`, `focus [<project>] weight|slots <n> until <when>`, `auto-approve after <duration>`, `inbox <kind> ["<text>"]`, `remind jason`, `notify <channel> ["<text>"]`, `notify profile <name> [until <when>]`, `label add|remove human|triage|shipped-open`, `flag shipped-open`, `escalate console "<ask>"`, `none`. One string, or a list.
164
+ - **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`. `merge`, `publish`, `approve`, `delete` and `run` aren't in the vocabulary.
165
+ - **Reload:** saving `rules.json` or a project's profile reloads its rules. A profile save takes only the `rules`; the rest of a profile changes with `conduct project add`. A file the conductor refuses leaves its last good rules in force, logs `rules.rejected` and raises a `failed` inbox item until it loads again (`rules.loaded`), including after a restart. `conduct project add` refuses a profile whose rules don't load, with or without the daemon.
166
+ - **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.
167
+
168
+ In this build the rules load, reload, refuse and dry-run; nothing acts on them yet. The scheduler (`scheduler.mode`, `observe` until conduct-atk's loop retires) and the actions follow in the next parts of ATK-315.
169
+
41
170
  ## Waits
42
171
 
43
172
  A worker waits by posting its note, running `conduct wait …` and ending the turn (worker protocol v1). The verbs take the repo, host and package from the worktree's `.claude/conductor.json`; the full form is accepted too, and is what the store keeps:
@@ -66,7 +195,8 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
66
195
 
67
196
  | File | Holds |
68
197
  |---|---|
69
- | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `sessions` holds `pollSeconds` (10), `settleSeconds` (5), `permissionMinutes` (5), `idleMinutes` (20); `watchers` holds `ado`, `github`, `health`, `linear` and `npm`, each `{ intervalSeconds, perMinute }` (30/60, 30/30, 30/30, 30/10, 20/30), and `statsMinutes` (10) |
198
+ | `config.json` | machine settings, JSONC (comments allowed), validated at startup; `inbox.runawayTokens` (2000000); `notify` and `quietHours` (see Notifications); `scheduler` holds `mode` (`observe`, `on` or `off`), `slots` (4, across projects, and the most any rule can give one project), `dailyTokens` (20000000, the `daily-cap` rule's number) and `tickSeconds` (120); `sessions` holds `pollSeconds` (10), `settleSeconds` (5), `permissionMinutes` (5), `idleMinutes` (20); `watchers` holds `ado`, `github`, `health`, `linear` and `npm`, each `{ intervalSeconds, perMinute }` (30/60, 30/30, 30/30, 30/10, 20/30), and `statsMinutes` (10) |
199
+ | `rules.json` | machine rules, JSONC, reloaded on save (see Rules) |
70
200
  | `token` | the API token, readable by this user only; every request sends `Authorization: Bearer <token>` |
71
201
  | `conductor.pid`, `port` | the single-instance lock and where the API listens (`127.0.0.1`) |
72
202
  | `conductor.db` | SQLite state, including the append-only `events` log |
@@ -75,12 +205,25 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
75
205
  ## API
76
206
 
77
207
  - `GET /projects`: the registered projects. `POST /projects` with `{ "path": "<absolute path>" }` registers one.
78
- - `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.
208
+ - `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":…}`.
79
209
  - `POST /shutdown`: a clean stop (`conduct stop`).
80
210
  - `GET /fleet`: the workers in flight, with session activity, waits and spend (today, total).
81
211
  - `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.
82
212
  - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work when it refuses.
83
213
  - `POST /workers/:id/attach`: wake if parked, mark attached, open the tab.
214
+ - `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"`.
215
+ - `GET /inbox`: the open inbox items, oldest first.
216
+ - `POST /inbox/:id/resolve`: Jason's answer (see Inbox). The body is `{ "action": "approve", "text"? }`, `{ "action": "revise", "text" }` or `{ "action": "done" }`.
217
+ - 200 with the item, the comment and the waits it resolved.
218
+ - 404 when there is no such item.
219
+ - 409 when the item is resolved already or isn't a plan.
220
+ - 502 with `posted` when the tracker didn't take the comment.
221
+ - `GET /notify`: the profile (and until when), the profiles, quiet hours, the held count and each channel's state.
222
+ - `POST /notify/profile` with `{ "name": "away", "until"?: "<ISO time>" }`: switch; 409 for a profile that doesn't exist.
223
+ - `POST /notify/test` with `{ "kind"?: "npm-otp", "channel"?: "pushover", "raise"?: false }`: push a sample now and answer each channel's result, or with `raise` put a real item in the inbox (201).
224
+ - `POST /notify/report` with `{ "channel": "remote-control", "result": "<PushNotification's answer>", "state"?: "ok" | "inactive" }`: the mod's report of a push.
225
+ - `GET /rules`: the rules in force, each with its source, `ref`, `when`, `do` and the rules that replace it, plus any file refused (see Rules).
226
+ - `POST /rules/test` with `{ "event"?: "pr.comment", "project"?: "atk", "fields"?: { … } }`: a dry run; 400 for a sample it can't read.
84
227
  - `POST /waits` with `{ "worker": 1, "condition": "pr:ado:Emergent.AgenticToolkit#1850:merged" }`: a wait in its full form (see Waits), stored in its normal form; 201 with the wait. `GET /waits/:id` shows one, with its resolution once it has one.
85
228
  - A refusal (a duplicate launch, a bad condition, a busy worker) is a 409 with `error` and `details`.
86
229
 
@@ -96,14 +239,14 @@ conduct project add <path to the project's main checkout>
96
239
 
97
240
  `conduct project add` walks up from the path it is given to the directory that holds `.claude/conductor.json`, and maps a worktree to its main checkout.
98
241
 
99
- The logon task runs `node` on the `cli.js` that ran `install-startup`. A global install keeps that path across upgrades, so the task needs registering once. To upgrade, stop the daemon, install, and start it again (or sign out and back in):
242
+ The logon task runs `node` on the `cli.js` that ran `install-startup`. A global install keeps that path across upgrades, so the task needs registering once. To upgrade:
100
243
 
101
244
  ```bash
102
- conduct stop
103
- npm install --global @detergent-software/conductor@dev
104
- conduct serve
245
+ conduct upgrade
105
246
  ```
106
247
 
248
+ It takes the tag the installed version came from (`dev` for an `X.Y.Z-dev.N`, `latest` otherwise; `--tag` picks one), and does nothing when that tag's version is already installed. When the tag points at an older version (semver, so `1.2.0-dev.4` is older than `1.2.0`), it refuses before it stops anything and names that version; `--allow-downgrade` installs it anyway and reports `downgraded`. Otherwise it stops the daemon, runs `npm install --global @detergent-software/conductor@<the tag's version>` (the exact version it compared, in case the tag moves) and starts the daemon again: through the `Conductor` logon task when the task runs this `cli.js`, else as a detached `conduct serve`. 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`.
249
+
107
250
  **From a checkout instead (development).** `npm link` puts the main checkout's build on PATH, so every worktree and the logon task run that build. Run it from the main checkout, and upgrade by pulling `develop`, building, then `conduct stop` and `conduct serve`:
108
251
 
109
252
  ```bash
@@ -115,6 +258,27 @@ conduct install-startup
115
258
 
116
259
  To switch from a linked build to the package: `conduct stop`, `npm rm --global @detergent-software/conductor` (removes the link), install the package as above, and run `conduct install-startup` again so the logon task points at the installed `cli.js`.
117
260
 
261
+ ## Mod
262
+
263
+ `mod/` is the conductor mod: a Claude Code plugin (a hooks module, code that costs no tokens) that does the console's plumbing. `conduct install-mod` installs it for this user from the package's own `mod/` folder, as a folder marketplace (`claude plugin install conductor --marketplace <package>/mod --scope user`). Claude Code reads a folder marketplace's plugin in place, so `conduct upgrade` replaces the mod along with the daemon; open sessions load the new version with `/reload-plugins`. `--remove` uninstalls it.
264
+
265
+ - **Every session** shows a status line, `conductor 3▶ 2⏸ ✉2` (running, parked, inbox), from `GET /fleet` and `GET /inbox` every 30 s. It clears while no daemon answers.
266
+ - **The console**, the session started with `CONDUCTOR_CONSOLE=1` (`$env:CONDUCTOR_CONSOLE=1; claude -n conductor` until the console skill, ATK-314, starts it), also gets:
267
+ - **the band** above the prompt while the inbox has items: one hotkey per item (a plan approves after a second press, `y`, since a digit typed into an empty prompt presses a band button; an npm OTP copies its `npm stage approve` line; the rest open the pane), and `o` for the pane;
268
+ - **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, copy a command, open a link, done), the workers by project (a row expands to attach), and today's spend;
269
+ - **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`;
270
+ - **the stream**: it runs `node <package>/build/cli.js events --follow`, redraws on each event, toasts each new inbox item, and acts on three events:
271
+
272
+ | Event | Data | The mod |
273
+ |---|---|---|
274
+ | `notify.requested` | `channel: "remote-control"`, `message` | pushes it to the phone (PushNotification), with no model turn, and reports every outcome to `POST /notify/report` (see Notifications). When the push comes back "Remote Control inactive" (PushNotification's `disabledReason` `no_transport`), mobile pushes are off in Claude Code's config (`config_off`), or the call fails, it toasts once, saying which; it says so again only after a push has reached the phone. A push Claude Code holds back while you are at the terminal (`user_present`) is reported as `skipped`, with no toast |
275
+ | `console.escalated` | `text` | wakes the console's model once with `[conductor] escalation on <item>: <text>`; nothing else wakes it |
276
+ | `worker.message-requested` | `to`, `message` | sends the message to that worker's session |
277
+
278
+ The notifier emits `notify.requested` (see Notifications). The rules engine (ATK-315) will emit the other two.
279
+
280
+ Its checks: `pnpm -C apps/conductor test` runs `mod/tests/model.spec.ts` (the logic, in CI). `claude plugin validate apps/conductor/mod` and `claude plugin test apps/conductor/mod` (the hooks against the engine, UI on the terminal, desktop, VS Code and mobile surfaces) need Claude Code, so they run locally. To try a change before installing it: `claude --plugin-dir apps/conductor/mod`.
281
+
118
282
  ## Releases
119
283
 
120
284
  `atk-conductor-release` (`pipelines/release-conductor.yml`) runs semantic-release on each push to `develop` or `main` that touches `apps/conductor/`. It counts only commits that touch this directory, writes `CHANGELOG.md`, tags `conductor-v<version>` (`X.Y.Z-dev.N` on `develop`), and stages the version on npm under `dev` or `latest`. A maintainer publishes it with the `npm stage approve <id> --otp=<code>` line from the run summary.
@@ -127,4 +291,6 @@ pnpm -C apps/conductor typecheck
127
291
  pnpm -C apps/conductor test
128
292
  pnpm -C apps/conductor build
129
293
  cd apps/conductor && node scripts/check-changelog.mjs
294
+ claude plugin validate apps/conductor/mod
295
+ claude plugin test apps/conductor/mod
130
296
  ```