@detergent-software/conductor 0.0.0-stage → 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 (100) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +295 -2
  3. package/build/claude.js +102 -0
  4. package/build/claude.js.map +1 -0
  5. package/build/cli.js +730 -0
  6. package/build/cli.js.map +1 -0
  7. package/build/client.js +64 -0
  8. package/build/client.js.map +1 -0
  9. package/build/clock.js +17 -0
  10. package/build/clock.js.map +1 -0
  11. package/build/conditions.js +197 -0
  12. package/build/conditions.js.map +1 -0
  13. package/build/config.js +165 -0
  14. package/build/config.js.map +1 -0
  15. package/build/credential.js +52 -0
  16. package/build/credential.js.map +1 -0
  17. package/build/daemon.js +182 -0
  18. package/build/daemon.js.map +1 -0
  19. package/build/errors.js +17 -0
  20. package/build/errors.js.map +1 -0
  21. package/build/events.js +106 -0
  22. package/build/events.js.map +1 -0
  23. package/build/inbox.js +385 -0
  24. package/build/inbox.js.map +1 -0
  25. package/build/jsonc.js +24 -0
  26. package/build/jsonc.js.map +1 -0
  27. package/build/lock.js +64 -0
  28. package/build/lock.js.map +1 -0
  29. package/build/log.js +43 -0
  30. package/build/log.js.map +1 -0
  31. package/build/manager.js +637 -0
  32. package/build/manager.js.map +1 -0
  33. package/build/mod.js +51 -0
  34. package/build/mod.js.map +1 -0
  35. package/build/note.js +71 -0
  36. package/build/note.js.map +1 -0
  37. package/build/notes.js +188 -0
  38. package/build/notes.js.map +1 -0
  39. package/build/notify.js +393 -0
  40. package/build/notify.js.map +1 -0
  41. package/build/paths.js +17 -0
  42. package/build/paths.js.map +1 -0
  43. package/build/profile.js +81 -0
  44. package/build/profile.js.map +1 -0
  45. package/build/pushover.js +67 -0
  46. package/build/pushover.js.map +1 -0
  47. package/build/registry.js +35 -0
  48. package/build/registry.js.map +1 -0
  49. package/build/resolve.js +28 -0
  50. package/build/resolve.js.map +1 -0
  51. package/build/rule-input.js +17 -0
  52. package/build/rule-input.js.map +1 -0
  53. package/build/rules-service.js +391 -0
  54. package/build/rules-service.js.map +1 -0
  55. package/build/rules.js +842 -0
  56. package/build/rules.js.map +1 -0
  57. package/build/run.js +52 -0
  58. package/build/run.js.map +1 -0
  59. package/build/server.js +367 -0
  60. package/build/server.js.map +1 -0
  61. package/build/spend.js +142 -0
  62. package/build/spend.js.map +1 -0
  63. package/build/startup.js +78 -0
  64. package/build/startup.js.map +1 -0
  65. package/build/store.js +724 -0
  66. package/build/store.js.map +1 -0
  67. package/build/tab.js +26 -0
  68. package/build/tab.js.map +1 -0
  69. package/build/token.js +58 -0
  70. package/build/token.js.map +1 -0
  71. package/build/upgrade.js +105 -0
  72. package/build/upgrade.js.map +1 -0
  73. package/build/wait.js +149 -0
  74. package/build/wait.js.map +1 -0
  75. package/build/watchers/ado.js +167 -0
  76. package/build/watchers/ado.js.map +1 -0
  77. package/build/watchers/github.js +82 -0
  78. package/build/watchers/github.js.map +1 -0
  79. package/build/watchers/health.js +42 -0
  80. package/build/watchers/health.js.map +1 -0
  81. package/build/watchers/index.js +226 -0
  82. package/build/watchers/index.js.map +1 -0
  83. package/build/watchers/linear-key.js +17 -0
  84. package/build/watchers/linear-key.js.map +1 -0
  85. package/build/watchers/linear.js +227 -0
  86. package/build/watchers/linear.js.map +1 -0
  87. package/build/watchers/npm.js +35 -0
  88. package/build/watchers/npm.js.map +1 -0
  89. package/build/watchers/source.js +94 -0
  90. package/build/watchers/source.js.map +1 -0
  91. package/build/worktrees.js +143 -0
  92. package/build/worktrees.js.map +1 -0
  93. package/mod/.claude-plugin/marketplace.json +6 -0
  94. package/mod/.claude-plugin/plugin.json +7 -0
  95. package/mod/README.md +17 -0
  96. package/mod/hooks/hooks.json +1 -0
  97. package/mod/hooks/model.ts +395 -0
  98. package/mod/hooks/register.tsx +505 -0
  99. package/mod/types/index.d.ts +57 -0
  100. package/package.json +61 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,107 @@
1
+ # Changelog
2
+
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
+
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
+
92
+ ## 1.0.0-dev.1 (2026-10-10)
93
+
94
+ ### Features
95
+
96
+ * **conductor:** add the conductor daemon skeleton (ATK-308) ([a33233b](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/a33233bd47c29416bc3f1db85d10a7904954f7de))
97
+ * **conductor:** add the session manager (ATK-309) ([aee8575](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/aee85752ece203463e8ee027459ea26d80a0c689))
98
+ * **conductor:** add the watchers and conduct wait (ATK-310) ([f803cba](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/f803cba1c8de1b31a4282d259b9439a491c28c57))
99
+
100
+ ### Bug Fixes
101
+
102
+ * **ci:** keep two npm releases from skipping or doubling (ATK-338) ([76703d3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/76703d3454ba4abc14b30d5b3b5867eb26b399ce))
103
+ * **conductor:** address code review of the session manager (ATK-309) ([724ffba](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/724ffba72e4278b6819636ccad95310921582001))
104
+ * **conductor:** address review of the watchers (ATK-310) ([05cbb88](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/05cbb88c9efc14aad83dd40a668109de77e2a74a))
105
+ * **conductor:** harden the session manager after review (ATK-309) ([9675c0d](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/9675c0d40d6373429a9f99eeaad5ba78ba54eae5))
106
+ * **conductor:** harden the skeleton after review (ATK-308) ([0930b74](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/0930b74edd9fae6dc94dfa7b62aaf4f13526b0d3))
107
+ * **conductor:** read a turn's end from status alone (ATK-309) ([a42b6d3](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/a42b6d34f45c309c6da3bae7cf2c3c9d7b2d0032))
package/README.md CHANGED
@@ -1,3 +1,296 @@
1
- # Temporary Holding Version
1
+ # Conductor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
5
+ ## Commands
6
+
7
+ ```text
8
+ conduct serve run the daemon in this process (one per machine)
9
+ conduct stop stop the running daemon
10
+ conduct status [--json] the daemon's pid, port, projects and workers
11
+ conduct project add <path> register the project whose checkout holds .claude/conductor.json
12
+ conduct project list [--json] the registered projects
13
+ conduct launch <item> start a worker: worktree, background session, tab
14
+ [--project <name>] [--probe] [--prompt <text>] [--conductor <name>]
15
+ conduct retire <worker> [--force] stop a worker, remove its worktree and session
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>]
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)
35
+ conduct install-startup start the daemon at logon (Windows Task Scheduler)
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
40
+ ```
41
+
42
+ A `<worker>` is its item (`ATK-309`) or its name (`probe-ATK-309`).
43
+
44
+ ## Workers
45
+
46
+ The session manager owns the `claude --bg` sessions it launches, and only those:
47
+
48
+ - **launch:** the worktree comes first, through the project's `.claude/hooks/worktree-hooks.mjs`. In a repo with `.atk-lock.json`, the profile's work and verify skills must match the lockfile (`atk install` fixes them when it can). Then `claude --bg --worktree <name> -n <name> --disallowedTools EnterPlanMode ExitPlanMode --permission-mode auto -- <prompt>` runs from the main checkout. A profile with `"workers": { "tab": true }` also gets a Windows Terminal tab attached to the session.
49
+ - **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.
50
+ - **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.
51
+ - **retire:** refuses uncommitted or unpushed work, and a worker whose turn still runs, unless `--force`. It never retires a session that is still starting. Then it stops the session, removes the worktree and runs `claude rm`. If the stop fails, nothing is removed.
52
+ - **restarts:** a launch the daemon was stopped in adopts its session if one was started, and otherwise fails and removes its clean worktree.
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.
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.
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.
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.
58
+
59
+ Worker statuses: `launching`, `running`, `parked`, `stopped-unexpectedly`, `failed`, `exited`. Every step is an event (`worker.launched`, `worker.parked`, `worker.resumed`, …) on `GET /events`.
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
+
170
+ ## Waits
171
+
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:
173
+
174
+ | Verb | Full form | Holds when |
175
+ |---|---|---|
176
+ | `pr 1850 merged` | `pr:ado:Emergent.AgenticToolkit#1850:merged` | the PR is completed (or abandoned: that resolves it too, as a fact) |
177
+ | `pr 1850 checks` | `…#1850:checks` | no blocking build of the PR's current commit is queued or running (passed or failed; a path-filtered build it doesn't touch counts as passed) |
178
+ | `pr 1850 comment` | `…#1850:comment` | a thread is open whose last word isn't a work-skill note |
179
+ | `pr 458 merged --repo <owner>/<repo>` | `pr:github:<owner>/<repo>#458:merged` | the same on GitHub; `comment` there is a person's comment or review after the wait was registered |
180
+ | `run atk-api-deploy 12345 done` | `run:ado:atk-api-deploy#12345:done` | the run completed, with its result |
181
+ | `npm 8.5.0-dev.2 --tag dev` | `npm:@detergent-software/atk@8.5.0-dev.2:dev` | the dist-tag points at the version |
182
+ | `health dev --version 1.2.3` / `--commit <sha>` | `health:dev:version=1.2.3` | the profile's `health.dev` URL answers with that version or commit |
183
+ | `approval` | `approval:linear:ATK-310` | an `approve` / `revise:` comment by the Linear key's owner after the item's newest `<work skill> · Plan` note |
184
+ | `item ATK-271 done` / `label human` / `note Landed` | `item:linear:ATK-271:state=done` | the state (name or type), a label, or a phase note |
185
+ | `time 2026-10-10T09:00` | `time:2026-10-10T14:00:00.000Z` | the time has passed (no zone means local time) |
186
+
187
+ - **With the daemon**, in a worker's worktree (or with `--worker <worker>`), `conduct wait` registers the wait and returns at once. The daemon parks the session when its turn ends and resumes it with `[conductor] event: <resolution> HH:MM` once the condition holds. The text is a fact, never an approval: the worker reads the source again. The daemon checks waits against the project's profile as registered (`conduct project add`), and refuses one it can't check, such as a `health` environment the profile has no URL for.
188
+ - **Without a daemon**, with `--local`, or when the daemon doesn't answer, it checks the condition itself and blocks. It prints `[conductor] event: …` and exits 0 when the condition holds, or exits 2 after `--timeout` minutes (29: Monitor ends a command at 30) so the caller can run it again. A wait that can't be checked (a PR that doesn't exist, no Linear key, `az` not signed in) exits 1. `--timeout` applies only here.
189
+
190
+ **Watchers.** One per API: Linear (GraphQL with `LINEAR_API_KEY` or the `atk-conductor-linear` credential, ported from conduct-atk), Azure DevOps (REST with the `az login` token), GitHub (`gh api`), npm (the registry's dist-tags, no cache) and `/health`. Time waits are the session manager's sweep. An API is asked nothing while no wait needs it. Otherwise it's asked once per `watchers.<api>.intervalSeconds`, within `perMinute` requests in any rolling minute shared by every project. Each pass covers all of that API's waits in as few requests as it can; Linear's is one request, filtered on `updatedAt` (with a 2-minute overlap) after the first. A pass that runs out of budget stops, and the next one starts a wait further on. A failing API waits twice as long each time (up to 10 minutes) and logs `watcher.failed` once, then `watcher.recovered`. A wait that can't be checked logs `wait.check-failed` once and stays open. Every `watchers.statsMinutes` (10) in which anything waited, a `watcher.stats` event records each API's passes, requests, budget skips, failures and waits.
191
+
192
+ ## Files
193
+
194
+ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
195
+
196
+ | File | Holds |
197
+ |---|---|
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) |
200
+ | `token` | the API token, readable by this user only; every request sends `Authorization: Bearer <token>` |
201
+ | `conductor.pid`, `port` | the single-instance lock and where the API listens (`127.0.0.1`) |
202
+ | `conductor.db` | SQLite state, including the append-only `events` log |
203
+ | `logs/conductor.log` | the rotating log |
204
+
205
+ ## API
206
+
207
+ - `GET /projects`: the registered projects. `POST /projects` with `{ "path": "<absolute path>" }` registers one.
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":…}`.
209
+ - `POST /shutdown`: a clean stop (`conduct stop`).
210
+ - `GET /fleet`: the workers in flight, with session activity, waits and spend (today, total).
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.
212
+ - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work when it refuses.
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.
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.
228
+ - A refusal (a duplicate launch, a bad condition, a busy worker) is a 409 with `error` and `details`.
229
+
230
+ ## On PATH
231
+
232
+ Install the package globally, next to `atk`. `dev` follows `develop`; `latest` follows `main`:
233
+
234
+ ```bash
235
+ npm install --global @detergent-software/conductor@dev
236
+ conduct install-startup
237
+ conduct project add <path to the project's main checkout>
238
+ ```
239
+
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.
241
+
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:
243
+
244
+ ```bash
245
+ conduct upgrade
246
+ ```
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
+
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`:
251
+
252
+ ```bash
253
+ pnpm install
254
+ pnpm -C apps/conductor build
255
+ (cd apps/conductor && npm link)
256
+ conduct install-startup
257
+ ```
258
+
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`.
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
+
282
+ ## Releases
283
+
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.
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ pnpm -C apps/conductor lint
290
+ pnpm -C apps/conductor typecheck
291
+ pnpm -C apps/conductor test
292
+ pnpm -C apps/conductor build
293
+ cd apps/conductor && node scripts/check-changelog.mjs
294
+ claude plugin validate apps/conductor/mod
295
+ claude plugin test apps/conductor/mod
296
+ ```
@@ -0,0 +1,102 @@
1
+ import { z } from 'zod';
2
+ import { ConductorError } from "./errors.js";
3
+ import { outputOf, run } from "./run.js";
4
+ // One row of `claude agents --json --all`. Only the fields the session manager reads; the rest pass.
5
+ export const agentSessionSchema = z.looseObject({
6
+ cwd: z.string().optional(),
7
+ id: z.string(),
8
+ kind: z.string().optional(),
9
+ name: z.string().optional(),
10
+ pid: z.number().optional(),
11
+ sessionId: z.string(),
12
+ // working | done | blocked: the agent view's label for the work, from the model's last reply.
13
+ // Not whether a turn runs (that is `status`), except before a new session has its pid.
14
+ state: z.string().optional(),
15
+ // idle | busy | waiting; `waiting` comes with `waitingFor` (a permission prompt, input).
16
+ status: z.string().optional(),
17
+ waitingFor: z.string().optional(),
18
+ });
19
+ export function claudeCli() {
20
+ // Resolves to stdout, and stderr after it: the CLI prints its notes (such as a copy started) there.
21
+ const claude = async (args, cwd, timeoutMs = 120_000) => {
22
+ const result = await run('claude', args, { cwd, timeoutMs });
23
+ if (result.code !== 0)
24
+ throw new ConductorError(`claude ${args[0]} failed`, [outputOf(result, 500)]);
25
+ return { all: result.stdout + result.stderr, stdout: result.stdout };
26
+ };
27
+ return {
28
+ // Only stdout is JSON; a notice on stderr must not break the listing.
29
+ agents: async () => parseAgents((await claude(['agents', '--json', '--all'], undefined, 60_000)).stdout),
30
+ launch: async (request) => {
31
+ // The create hook (branch, pnpm install, atk install) runs first, so allow it minutes.
32
+ const output = (await claude(launchArgs(request), request.cwd, 10 * 60_000)).all;
33
+ const handle = parseHandle(output);
34
+ if (!handle)
35
+ throw new ConductorError('claude --bg printed no session id', [output.trim().slice(-500)]);
36
+ return handle;
37
+ },
38
+ resume: async ({ cwd, sessionId, text }) => {
39
+ const output = await claude(['--bg', '--resume', sessionId, ...(text === null ? [] : ['--', text])], cwd);
40
+ return { copy: parseCopy(output.all) };
41
+ },
42
+ rm: async (handle) => void (await claude(['rm', handle])),
43
+ stop: async (handle) => void (await claude(['stop', handle])),
44
+ };
45
+ }
46
+ // The session's process exists. `state: "done"` only means the turn ended.
47
+ export function isLive(session) {
48
+ return session.pid !== undefined || isStarting(session);
49
+ }
50
+ // Listed as working before it has a pid: `claude --bg` is still creating its worktree.
51
+ export function isStarting(session) {
52
+ return session.pid === undefined && session.state === 'working';
53
+ }
54
+ // The flags a worker starts with. A resume keeps them: `claude --bg --resume` wakes a session
55
+ // "with its saved options (-n, --permission-mode, --disallowedTools, --model)" (ATK-309's check).
56
+ // Workers get no plan-mode tools: an ExitPlanMode dialog passes on one stray key (ATK-269).
57
+ export function launchArgs(request) {
58
+ return [
59
+ '--bg',
60
+ '--worktree',
61
+ request.worktree,
62
+ '-n',
63
+ request.name,
64
+ '--disallowedTools',
65
+ ...request.disallowedTools,
66
+ '--permission-mode',
67
+ request.permissionMode,
68
+ '--',
69
+ request.prompt,
70
+ ];
71
+ }
72
+ // The sessions in `claude agents --json --all` that have a short id. Interactive sessions are
73
+ // listed without one; the daemon never manages those, and one odd row mustn't hide the rest.
74
+ export function parseAgents(text) {
75
+ return z
76
+ .array(z.unknown())
77
+ .parse(JSON.parse(text))
78
+ .flatMap((row) => {
79
+ const session = agentSessionSchema.safeParse(row);
80
+ return session.success ? [session.data] : [];
81
+ });
82
+ }
83
+ // A resume of a session that still runs starts a copy and says so. The copy has no name and
84
+ // doesn't show in `claude agents --json --all`, so this line is the only trace of it.
85
+ export function parseCopy(output) {
86
+ return /started a copy as ([0-9a-f]{8})\b/.exec(output)?.[1] ?? null;
87
+ }
88
+ // `claude --bg` prints `backgrounded · <id> · <name>`.
89
+ export function parseHandle(output) {
90
+ return /backgrounded · ([0-9a-f]{8})\b/.exec(output)?.[1] ?? null;
91
+ }
92
+ export function permissionPrompt(session) {
93
+ return session.status === 'waiting' && /permission/i.test(session.waitingFor ?? '') ? (session.waitingFor ?? null) : null;
94
+ }
95
+ // The turn ended and nothing holds the session: no tool running, no prompt open, no Monitor armed
96
+ // (an armed Monitor reads busy). Only `status` says so. `state` is the agent view's label for how
97
+ // the work is going, taken from the last reply: a probe that answered an event with "I'm still
98
+ // waiting" stayed `state: working` at `status: idle` for minutes (ATK-309).
99
+ export function turnEnded(session) {
100
+ return session.pid !== undefined && session.status === 'idle';
101
+ }
102
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../src/claude.ts"],"names":[],"mappings":"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"}