@detergent-software/conductor 1.3.0-dev.5 → 1.3.0-dev.7

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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +28 -2
  3. package/build/checkout.js +139 -0
  4. package/build/checkout.js.map +1 -0
  5. package/build/cli.js +103 -27
  6. package/build/cli.js.map +1 -1
  7. package/build/code-host-cli.js +417 -0
  8. package/build/code-host-cli.js.map +1 -0
  9. package/build/code-host-service.js +74 -0
  10. package/build/code-host-service.js.map +1 -0
  11. package/build/code-host.js +143 -0
  12. package/build/code-host.js.map +1 -0
  13. package/build/conditions.js +9 -6
  14. package/build/conditions.js.map +1 -1
  15. package/build/credentials.js +29 -12
  16. package/build/credentials.js.map +1 -1
  17. package/build/daemon.js +3 -0
  18. package/build/daemon.js.map +1 -1
  19. package/build/help.js +76 -2
  20. package/build/help.js.map +1 -1
  21. package/build/landing.js +142 -0
  22. package/build/landing.js.map +1 -0
  23. package/build/lib/providers/ado/writes.js +8 -10
  24. package/build/lib/providers/ado/writes.js.map +1 -1
  25. package/build/ownership.js +57 -0
  26. package/build/ownership.js.map +1 -0
  27. package/build/server.js +74 -6
  28. package/build/server.js.map +1 -1
  29. package/build/store.js +8 -0
  30. package/build/store.js.map +1 -1
  31. package/build/watchers/ado.js +43 -6
  32. package/build/watchers/ado.js.map +1 -1
  33. package/build/watchers/github.js +9 -3
  34. package/build/watchers/github.js.map +1 -1
  35. package/build/watchers/index.js +26 -3
  36. package/build/watchers/index.js.map +1 -1
  37. package/build/watchers/source.js +17 -0
  38. package/build/watchers/source.js.map +1 -1
  39. package/build/worktrees.js +26 -36
  40. package/build/worktrees.js.map +1 -1
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
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.3.0-dev.7](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.3.0-dev.6&targetVersion=GTconductor-v1.3.0-dev.7) (2026-10-14)
6
+
7
+ ### Features
8
+
9
+ * **conductor:** conduct land, wait pr verdict, pr threads and reply (ATK-454) ([8c2275b](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/8c2275bef44fd7ce07b887ff470ffd20829f1af4))
10
+
11
+ ### Bug Fixes
12
+
13
+ * **conductor:** review fixes for land and keep-armed (ATK-454) ([7200260](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/7200260ee78c334f85abd3cdf6bb509ae12ecd51))
14
+
15
+ ## [1.3.0-dev.6](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.3.0-dev.5&targetVersion=GTconductor-v1.3.0-dev.6) (2026-10-14)
16
+
17
+ ### Features
18
+
19
+ * **conductor:** conduct state, branch, checkout and pr create (ATK-454) ([626f9eb](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/626f9eb0fb74f454190888c9e4fb420a642a8766))
20
+
21
+ ### Bug Fixes
22
+
23
+ * **conductor:** pr help names every check before the push (ATK-454) ([b866334](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/b8663346be93057a499634c35a429b809a2de523))
24
+ * **conductor:** review fixes for the worker verbs (ATK-454) ([6806665](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/68066658c029fab55bdbef56721beb1cb66af810))
25
+
5
26
  ## [1.3.0-dev.5](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v1.3.0-dev.4&targetVersion=GTconductor-v1.3.0-dev.5) (2026-10-14)
6
27
 
7
28
  ### Features
package/README.md CHANGED
@@ -51,6 +51,19 @@ conduct note <item> <file> post a phase note on the item and record its p
51
51
  conduct brief <item> [--json] [--fresh]
52
52
  the item's status, links, phase notes and approval, from the tracker (see Briefs)
53
53
  conduct wait <condition> wait for an outside event (see Waits)
54
+ conduct state [--repo <repo>] the worktree's branch, changes and PR, and its other repos' (see Worker verbs)
55
+ conduct branch <type> <slug> [--item <item>]
56
+ rename worktree-ATK-n to <type>/atk-n-<slug>
57
+ conduct checkout <owner>/<repo> <type> <slug> [--item <item>]
58
+ clone another GitHub repo into .scratch/repos/<repo> for the item
59
+ conduct pr create --title <title> --body-file <file> [--repo <repo>] [--item <item>] [--dry-run]
60
+ check, push and open the worker's PR (see Worker verbs)
61
+ conduct pr threads <pr> [--repo <repo>]
62
+ the PR's threads, with the ones that wait on the worker
63
+ conduct land <pr> --reason <why> [--mode auto|review] [--rearm] [--tier S|M|L] [--repo <repo>]
64
+ arm the worker's PR to land and give its first verdict (see Worker verbs)
65
+ conduct reply <pr> [<thread>] --body-file <file> [--repo <repo>]
66
+ answer on the worker's PR, headed as a note
54
67
  conduct train [<project>] [--dry-run]
55
68
  merge the waiting skill publishes into one registry PR (see Skill trains)
56
69
  conduct ports [<path>] [--prefer <slot>] [--json]
@@ -124,6 +137,16 @@ A brief is kept for 30 seconds. Anything the daemon sees happen to the item drop
124
137
  | 3 | no daemon, one without the brief API, or one that couldn't read the tracker (no key, the tracker down) | reads the item from the tracker itself (the Linear MCP, or the work skill's helper) |
125
138
  | 1 | refused: no project tracks the item, or the tracker has no such item | reads the message |
126
139
 
140
+ ## Worker verbs
141
+
142
+ Protocol 2's verbs on git and the code host (providers design note, section 4), run in the item's worktree. They come beside the work-item helper's `state`, `rename`, `checkout` and `pr-create`, which stay until the cutover (ATK-457). Each prints one JSON object with `via`: `daemon` when the daemon wrote for the worker that owns the checkout, `local` when it ran in the `conduct` process (no daemon, or one without the route). Exit codes: 0 done, 1 refused or failed (the message says why), 5 a lost login (run `conduct wait login <provider>` and end the turn).
143
+
144
+ - `conduct state`, `conduct branch` and `conduct checkout` are git in the worktree, so they always run here. `state` reads the branch's PR as the project's account (see Accounts). `checkout` clones another GitHub repo into `.scratch/repos/<repo>`, keeps it out of the worktree's git status, and retire checks it for unpushed or uncommitted work (ATK-429).
145
+ - `conduct pr create` checks everything before it pushes, `--dry-run` included: the title carries the item key, the branch carries it too and isn't the base, the description fits the host (ADO 4,000 characters, GitHub 65,536), no PR is open from the branch yet, nothing is uncommitted, the branch is ahead of its base, and commitlint passes (skipped in a clone without it installed, whose own CI lints the PR). Then it pushes from the checkout and has the daemon open the PR (`POST /workers/:id/prs`). The daemon checks ownership again from its own records and the worktree, never from the request: the worker is live, the branch the worktree is on now (or the `--repo` checkout's) carries the worker's item, and the title carries it. It records the PR on the worker (`pr`; another repo's only while the worker has none) and logs `code-host.pr-created`. `conduct branch` renames only the item's own branch, never the base, `main` or another item's.
146
+ - `conduct land <pr>` sets the worker's PR up to land, as the helper's `land` did, and is safe to repeat. The PR must be the worker's (the same checks, and from the branch the checkout is on now, into its base). A brake already on it (a comment, a failed build, conflicts) is reported, and nothing is armed. A Landing note whose landing armed something, with that gone now, means someone cancelled it: `auto-merge-cancelled`, until `--rearm`. `--mode` is held to the profile's landing rules (`work.landing`) for the PR's files as pushed (`origin/<base>...origin/<head>`, fetched first) and the plan's tier. The daemon reads the tier from the item's Plan note; in-process it takes `--tier`, and auto is refused without one. Without `--mode` the rules pick it. Then it arms the host's own landing behind its capability check (`landingCheck`). On ADO that is a +10 vote in auto mode, with auto-complete and noFastForward. On GitHub it is auto-merge with a merge commit in auto mode, where the repo allows it; in review mode nothing is armed. It posts the Landing note last, and a new one whenever the mode changes, then prints the verdict. A failed build comes with its log, and a merge with whether it has two parents. The daemon logs `code-host.landing-armed` with the mode.
147
+ - `conduct wait pr <pr>` (no event word) is the verdict wait, `pr:<host>:<repo>#<n>:verdict`: it holds on the first verdict that isn't `waiting`, with a line such as `pr 2030 build-failed: <pipeline>`. While it waits on a PR whose landing the daemon recorded, the ADO watcher keeps it armed. It queues an expired build again (once per wait) and, in auto mode, restores a cleared vote (never over a negative one), logged as `code-host.kept-armed`. A local wait (no daemon) gives the same verdicts but keeps nothing armed.
148
+ - `conduct pr threads <pr>` lists the PR's threads (on GitHub each comment and review) with `needsAnswer` (ATK-384). `conduct reply <pr> [<thread>]` answers on the worker's own PR: in the ADO thread named, or as a note on the PR. The text is headed `## <work skill> · Reply` unless it starts with a note heading. The daemon logs `code-host.replied`. `--repo` takes `github:<owner>/<repo>` (from its `.scratch/repos` checkout, into its default branch) or `ado:<repository>`; the profile's `codeHost` is the default.
149
+
127
150
  ## Inbox
128
151
 
129
152
  The `inbox` table holds everything that waits on Jason, across projects. Each item records:
@@ -303,6 +326,7 @@ A worker waits by posting its note, running `conduct wait …` and ending the tu
303
326
 
304
327
  | Verb | Full form | Holds when |
305
328
  |---|---|---|
329
+ | `pr 1850` | `pr:ado:Emergent.AgenticToolkit#1850:verdict` | the PR's verdict isn't `waiting` (see Worker verbs); the daemon keeps a recorded landing armed meanwhile |
306
330
  | `pr 1850 merged` | `pr:ado:Emergent.AgenticToolkit#1850:merged` | the PR is completed (or abandoned: that resolves it too, as a fact) |
307
331
  | `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) |
308
332
  | `pr 1850 comment` | `…#1850:comment` | a thread is open whose last word isn't a work-skill note |
@@ -349,7 +373,7 @@ Each token is kept per account: an az token 20 minutes, renewed when ADO answers
349
373
  2. While the spell lasts, the account's calls fail at once, asking nothing. Every `watchers.login.intervalSeconds` (60) the daemon probes each lost account again: a fresh token for ADO, the token and `gh api user` for GitHub, and the key and `{ viewer { id } }` for Linear. That is the spell's back-off.
350
374
  3. The first probe the provider answers (an answer other than a refusal; a timeout, a network error or a 5xx says nothing) logs `provider.login-restored`, which resolves the item, and calls go through again.
351
375
 
352
- `conduct wait login <provider>` waits on the same probe for the worktree's project (`login:<provider>:<project>`), within the `login` watcher's budget. A worker that can't go on without a login posts its note, waits on it and ends its turn; once Jason signs in, the daemon wakes it. **Exit code 5** ("login needed") is reserved for the worker verbs to come, which will exit with it when they hit a lost login; no verb returns it yet.
376
+ `conduct wait login <provider>` waits on the same probe for the worktree's project (`login:<provider>:<project>`), within the `login` watcher's budget. A worker that can't go on without a login posts its note, waits on it and ends its turn; once Jason signs in, the daemon wakes it. **Exit code 5** ("login needed") is what the worker verbs (`state`, `pr`, `land`, `reply`) exit with when they hit a lost login, here or in the daemon.
353
377
 
354
378
  ## ADO Boards
355
379
 
@@ -456,8 +480,10 @@ Everything lives in `~/.conductor` (`CONDUCTOR_HOME` overrides it):
456
480
  - `POST /console/cache` with `{ "action": "warm" | "cold" | "failed", "contextTokens", "usage": { "cacheRead", "cacheWrite", "input", "output" } | null, "warms" }` or `{ "action": "clear", "contextTokens", "reason" }`: the mod's report on the idle console's prompt cache (see Mod); 201 with the `console.cache` event's id.
457
481
  - `GET /fleet`: the workers in flight, with session activity, phase, PR, waits and spend (today, total).
458
482
  - `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.
459
- - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work when it refuses.
483
+ - `DELETE /workers/:id` (`?force=true`): retire; 409 with the unfinished work (the worktree's, and each `.scratch/repos` checkout's under `checkouts`) when it refuses.
460
484
  - `POST /workers/:id/attach`: wake if parked, mark attached, open the tab.
485
+ - `POST /workers/:id/prs` with `{ "title", "body", "repo"? }`: open the worker's PR after the ownership checks (see Worker verbs); 201 with the PR, 409 refused, 503 a lost login (with `login.provider` and `login.command`), 502 the host failed.
486
+ - `POST /workers/:id/landings` with `{ "pr", "reason", "mode"?, "rearm"?, "repo"? }`: `conduct land` for the worker; 200 with the landing and its verdict. `POST /workers/:id/replies` with `{ "pr", "body", "thread"?, "repo"? }`: `conduct reply`; 201. Both answer 409, 503 and 502 as `/prs` does.
461
487
  - `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"`.
462
488
  - `GET /items/:id/brief` (`?fresh=true`): the item's brief (see Briefs), as `{ "brief": … }`. 404 when the tracker has no such item, 409 when no project tracks it, 502 when the tracker couldn't be read.
463
489
  - `GET /inbox`: the open inbox items, oldest first.
@@ -0,0 +1,139 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { stripVTControlCharacters } from 'node:util';
4
+ import { ConductorError } from "./errors.js";
5
+ import { githubRepoRef } from "./lib/providers/index.js";
6
+ import { outputOf, run } from "./run.js";
7
+ // Git in a worker's checkouts, for the worker verbs and retire: the item's worktree and the other
8
+ // repos it cloned under .scratch/repos/<repo> (`conduct checkout`).
9
+ // Other repos a worker clones into its worktree (`conduct checkout`), each at
10
+ // <worktree>/.scratch/repos/<repo>.
11
+ export const REPOS_DIR = join('.scratch', 'repos');
12
+ export const runGit = (args, cwd) => run('git', args, { cwd, timeoutMs: 120_000 });
13
+ // The commits on HEAD that aren't on origin/<base>, and the ones on origin/<base> that aren't on
14
+ // HEAD, after fetching the base.
15
+ export async function aheadBehind(git, dir, base) {
16
+ await git(['fetch', '--quiet', 'origin', base], dir);
17
+ const count = async (range) => Number((await out(git, ['rev-list', '--count', range], dir)) ?? 0);
18
+ return { ahead: await count(`origin/${base}..HEAD`), behind: await count(`HEAD..origin/${base}`) };
19
+ }
20
+ // The branch a checkout is on now, or null (detached, or not a checkout).
21
+ export async function branchOf(git, dir) {
22
+ return (await out(git, ['branch', '--show-current'], dir)) || null;
23
+ }
24
+ // A checkout's base: its origin's default branch (refs/remotes/origin/HEAD, which a clone sets).
25
+ export async function checkoutBase(git, dir) {
26
+ const head = await out(git, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], dir);
27
+ if (!head)
28
+ throw new ConductorError(`${dir} has no origin/HEAD`, ['run git remote set-head origin --auto there']);
29
+ return head.replace(/^origin\//, '');
30
+ }
31
+ // commitlint over every commit the PR will carry, from the checkout's root where its config lives:
32
+ // 'passed', or 'skipped' without a commitlint config. Throws with its output when it fails.
33
+ export async function commitlint(dir, base) {
34
+ if (!hasCommitlint(dir))
35
+ return 'skipped (no commitlint config)';
36
+ // npx is a .cmd on Windows; its arguments are constants and the base branch's name.
37
+ const result = await run('npx', ['--no-install', 'commitlint', '--from', `origin/${base}`, '--to', 'HEAD'], {
38
+ cwd: dir,
39
+ shell: process.platform === 'win32',
40
+ timeoutMs: 120_000,
41
+ });
42
+ if (result.code === 0)
43
+ return 'passed';
44
+ const text = stripVTControlCharacters(`${result.stdout}\n${result.stderr}`);
45
+ // A clone that was never installed (`conduct checkout` doesn't) has the config but not the tool:
46
+ // its own CI lints the PR.
47
+ if (/could not determine executable|commitlint: (?:command )?not found|is not recognized/i.test(text)) {
48
+ return 'skipped (commitlint is not installed here)';
49
+ }
50
+ const said = text
51
+ .split(/\r?\n/)
52
+ .filter((line) => line.trim() && !line.startsWith('npm notice'))
53
+ .slice(-30);
54
+ throw new ConductorError(`commitlint failed over origin/${base}..HEAD`, said);
55
+ }
56
+ // Whether HEAD has commits its upstream hasn't (or it has no upstream yet).
57
+ export async function needsPush(git, dir) {
58
+ const upstream = await out(git, ['rev-parse', '--abbrev-ref', '@{upstream}'], dir);
59
+ if (!upstream)
60
+ return true;
61
+ return (await out(git, ['rev-list', '--count', `${upstream}..HEAD`], dir)) !== '0';
62
+ }
63
+ // The other repos checked out under the worktree's .scratch/repos/, with the repo each origin names.
64
+ export async function otherCheckouts(git, worktree) {
65
+ return Promise.all(scratchClones(worktree).map(async (dir) => ({
66
+ dir,
67
+ repo: githubRepoRef(await out(git, ['remote', 'get-url', 'origin'], dir)),
68
+ })));
69
+ }
70
+ // `git status --porcelain=v1 -z` as { code, path }. A rename or copy carries its source path as the
71
+ // next NUL-separated field.
72
+ export function parsePorcelain(text) {
73
+ const fields = text.split('\0');
74
+ const entries = [];
75
+ for (let i = 0; i < fields.length; i++) {
76
+ const field = fields[i] ?? '';
77
+ if (field.length < 4)
78
+ continue;
79
+ const code = field.slice(0, 2);
80
+ entries.push({ code, path: field.slice(3) });
81
+ if (code[0] === 'R' || code[0] === 'C')
82
+ i++;
83
+ }
84
+ return entries;
85
+ }
86
+ // Pushes the branch with an upstream, once more after a failure: a push has failed on a connection
87
+ // reset before.
88
+ export async function push(git, dir, branch, sleep = (ms) => new Promise((done) => setTimeout(done, ms))) {
89
+ const args = ['push', '--quiet', '-u', 'origin', branch];
90
+ const first = await git(args, dir);
91
+ if (first.code === 0)
92
+ return { attempts: 1 };
93
+ await sleep(5_000);
94
+ const second = await git(args, dir);
95
+ if (second.code === 0)
96
+ return { attempts: 2, firstError: outputOf(first, 300) };
97
+ throw new ConductorError(`git push of ${branch} failed twice`, [outputOf(second, 500)]);
98
+ }
99
+ // Uncommitted changes, without files whose only difference from HEAD is their line endings (a
100
+ // checkout with core.autocrlf can show those as modified).
101
+ export async function realDirty(git, dir) {
102
+ const status = await git(['status', '--porcelain=v1', '-z'], dir);
103
+ if (status.code !== 0)
104
+ throw new ConductorError(`git status failed in ${dir}`, [outputOf(status, 300)]);
105
+ const dirty = [];
106
+ const lineEndingOnly = [];
107
+ for (const entry of parsePorcelain(status.stdout)) {
108
+ const same = /^[ M][ M]$/.test(entry.code) &&
109
+ (await git(['diff', '--quiet', '--ignore-cr-at-eol', 'HEAD', '--', entry.path], dir)).code === 0;
110
+ (same ? lineEndingOnly : dirty).push(entry.path);
111
+ }
112
+ return { dirty, lineEndingOnly };
113
+ }
114
+ // The clones under the worktree's .scratch/repos/, by directory.
115
+ export function scratchClones(worktree) {
116
+ const root = join(worktree, REPOS_DIR);
117
+ if (!existsSync(root))
118
+ return [];
119
+ return readdirSync(root, { withFileTypes: true })
120
+ .filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, '.git')))
121
+ .map((entry) => join(root, entry.name));
122
+ }
123
+ function hasCommitlint(dir) {
124
+ const configured = readdirSync(dir).some((name) => /^(?:commitlint\.config\.[cm]?[jt]s|\.commitlintrc(?:\.(?:json|ya?ml|[cm]?[jt]s))?)$/.test(name));
125
+ if (configured)
126
+ return true;
127
+ try {
128
+ return 'commitlint' in JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ // A git command's output, trimmed, or null when it failed.
135
+ async function out(git, args, dir) {
136
+ const result = await git(args, dir);
137
+ return result.code === 0 ? result.stdout.trim() : null;
138
+ }
139
+ //# sourceMappingURL=checkout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"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"}
package/build/cli.js CHANGED
@@ -8,6 +8,7 @@ import { z } from 'zod';
8
8
  import { briefCommand } from "./brief-cli.js";
9
9
  import { heldLocks, lockCommand, lockLine, portsCommand } from "./broker-cli.js";
10
10
  import { callApi, NotRunningError, ping, readEndpoint } from "./client.js";
11
+ import { branchCommand, checkoutCommand, landCommand, prCreateCommand, prThreadsCommand, replyCommand, stateCommand, } from "./code-host-cli.js";
11
12
  import { configSchema, loadConfig } from "./config.js";
12
13
  import { claimConsole, CONSOLE_ARGS, CONSOLE_WINDOW, consoleProject, consoleTabArgs, escalationLines, escalationSchema, handledSchema, } from "./console.js";
13
14
  import { AlreadyRunningError, logCrashes, startDaemon } from "./daemon.js";
@@ -63,15 +64,18 @@ const workerDetailBody = z.object({
63
64
  events: z.array(z.object({ data: z.record(z.string(), z.unknown()), kind: z.string() })),
64
65
  worker: workerSchema,
65
66
  });
67
+ const workSchema = z.object({
68
+ dirty: z.array(z.string()).optional(),
69
+ unknown: z.string().optional(),
70
+ unpushed: z.number().optional(),
71
+ });
66
72
  const retireBody = z.object({
67
73
  outcome: z.enum(['partial', 'refused', 'retired']),
68
74
  reason: z.string().optional(),
69
75
  steps: z
70
76
  .array(z.object({ error: z.string().optional(), note: z.string().optional(), ok: z.boolean(), step: z.string() }))
71
77
  .optional(),
72
- work: z
73
- .object({ dirty: z.array(z.string()).optional(), unknown: z.string().optional(), unpushed: z.number().optional() })
74
- .optional(),
78
+ work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
75
79
  worker: workerSchema,
76
80
  });
77
81
  const attachBody = z.object({ resumed: z.boolean(), tab: z.string().nullable(), worker: workerSchema });
@@ -421,6 +425,7 @@ async function main(argv) {
421
425
  args: argv,
422
426
  options: {
423
427
  'allow-downgrade': { type: 'boolean' },
428
+ 'body-file': { type: 'string' },
424
429
  channel: { type: 'string' },
425
430
  conductor: { type: 'string' },
426
431
  console: { type: 'boolean' },
@@ -431,19 +436,26 @@ async function main(argv) {
431
436
  fresh: { type: 'boolean' },
432
437
  help: { short: 'h', type: 'boolean' },
433
438
  here: { type: 'boolean' },
439
+ item: { type: 'string' },
434
440
  json: { type: 'boolean' },
435
441
  kind: { type: 'string' },
442
+ mode: { type: 'string' },
436
443
  owner: { type: 'string' },
437
444
  prefer: { type: 'string' },
438
445
  probe: { type: 'boolean' },
439
446
  project: { type: 'string' },
440
447
  prompt: { type: 'string' },
441
448
  raise: { type: 'boolean' },
449
+ rearm: { type: 'boolean' },
450
+ reason: { type: 'string' },
442
451
  remove: { type: 'boolean' },
452
+ repo: { type: 'string' },
443
453
  since: { type: 'string' },
444
454
  slots: { type: 'string' },
445
455
  tag: { type: 'string' },
446
456
  text: { type: 'string' },
457
+ tier: { type: 'string' },
458
+ title: { type: 'string' },
447
459
  until: { type: 'string' },
448
460
  version: { short: 'v', type: 'boolean' },
449
461
  wait: { type: 'string' },
@@ -452,7 +464,9 @@ async function main(argv) {
452
464
  },
453
465
  }));
454
466
  const paths = conductorPaths();
455
- const [command, sub, arg] = positionals;
467
+ const [command, sub, arg, fourth] = positionals;
468
+ const item = values.item === undefined ? {} : { item: values.item };
469
+ const repo = values.repo === undefined ? {} : { repo: values.repo };
456
470
  if (values.version)
457
471
  return out(version());
458
472
  if (command === undefined || command === 'help')
@@ -470,6 +484,11 @@ async function main(argv) {
470
484
  if (sub === undefined)
471
485
  throw usageError('attach');
472
486
  return attach(paths, sub);
487
+ case 'branch':
488
+ if (sub === undefined || arg === undefined)
489
+ throw usageError('branch');
490
+ process.exitCode = await branchCommand(sub, arg, item, verbIo());
491
+ return;
473
492
  case 'brief':
474
493
  if (sub === undefined)
475
494
  throw usageError('brief');
@@ -478,6 +497,11 @@ async function main(argv) {
478
497
  out,
479
498
  });
480
499
  return;
500
+ case 'checkout':
501
+ if (sub === undefined || arg === undefined || fourth === undefined)
502
+ throw usageError('checkout');
503
+ process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
504
+ return;
481
505
  case 'console':
482
506
  return consoleCommand(paths, {
483
507
  force: values.force === true,
@@ -496,6 +520,16 @@ async function main(argv) {
496
520
  // CONDUCTOR_CONSOLE as well: a console still running a mod from before --console (until
497
521
  // /reload-plugins after an upgrade) starts this package's new build, in the console's env.
498
522
  return events(paths, values.follow === true, values.since, values.console === true || process.env.CONDUCTOR_CONSOLE === '1');
523
+ case 'focus':
524
+ if (sub === undefined || values.until === undefined)
525
+ throw usageError('focus');
526
+ return override(paths, {
527
+ action: 'focus',
528
+ project: sub,
529
+ ...(values.slots === undefined ? {} : { slots: wholeNumber('--slots', values.slots) }),
530
+ until: untilOf(values.until),
531
+ ...(values.weight === undefined ? {} : { weight: wholeNumber('--weight', values.weight) }),
532
+ });
499
533
  case 'history':
500
534
  case 'retro':
501
535
  return history(paths, command, sub, {
@@ -512,6 +546,26 @@ async function main(argv) {
512
546
  : installMod(fileURLToPath(new URL('../mod', import.meta.url)), modDeps());
513
547
  case 'install-startup':
514
548
  return values.remove ? uninstall() : install();
549
+ case 'land': {
550
+ if (sub === undefined || values.reason === undefined)
551
+ throw usageError('land');
552
+ const mode = values.mode;
553
+ if (mode !== undefined && mode !== 'auto' && mode !== 'review')
554
+ throw new ConductorError('--mode takes auto or review');
555
+ const tier = values.tier;
556
+ if (tier !== undefined && tier !== 'S' && tier !== 'M' && tier !== 'L')
557
+ throw new ConductorError('--tier takes S, M or L');
558
+ process.exitCode = await landCommand(paths, {
559
+ pr: wholeNumber('<pr>', sub),
560
+ rearm: values.rearm === true,
561
+ reason: values.reason,
562
+ ...item,
563
+ ...repo,
564
+ ...(mode === undefined ? {} : { mode }),
565
+ ...(tier === undefined ? {} : { tier }),
566
+ }, verbIo());
567
+ return;
568
+ }
515
569
  case 'launch':
516
570
  if (sub === undefined)
517
571
  throw usageError('launch');
@@ -547,22 +601,15 @@ async function main(argv) {
547
601
  if (sub === 'test')
548
602
  return notifyTest(paths, { channel: values.channel, kind: values.kind, raise: values.raise === true });
549
603
  throw usageError('notify');
550
- case 'project':
551
- if (sub === 'add' && arg !== undefined)
552
- return addProject(paths, arg);
553
- if (sub === 'list')
554
- return listProjects(paths, values.json === true);
555
- throw usageError('project');
556
- case 'focus':
557
- if (sub === undefined || values.until === undefined)
558
- throw usageError('focus');
559
- return override(paths, {
560
- action: 'focus',
561
- project: sub,
562
- ...(values.slots === undefined ? {} : { slots: wholeNumber('--slots', values.slots) }),
563
- until: untilOf(values.until),
564
- ...(values.weight === undefined ? {} : { weight: wholeNumber('--weight', values.weight) }),
565
- });
604
+ case 'pr':
605
+ if (sub === 'threads' && arg !== undefined) {
606
+ process.exitCode = await prThreadsCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
607
+ return;
608
+ }
609
+ if (sub !== 'create' || values.title === undefined || values['body-file'] === undefined)
610
+ throw usageError('pr');
611
+ process.exitCode = await prCreateCommand(paths, { bodyFile: values['body-file'], dryRun: values['dry-run'] === true, title: values.title, ...item, ...repo }, verbIo());
612
+ return;
566
613
  case 'pause':
567
614
  return override(paths, {
568
615
  action: 'pause',
@@ -575,22 +622,44 @@ async function main(argv) {
575
622
  ...(values.prefer === undefined ? {} : { prefer: wholeNumber('--prefer', values.prefer) }),
576
623
  }, brokerIo());
577
624
  return;
625
+ case 'project':
626
+ if (sub === 'add' && arg !== undefined)
627
+ return addProject(paths, arg);
628
+ if (sub === 'list')
629
+ return listProjects(paths, values.json === true);
630
+ throw usageError('project');
578
631
  case 'queue':
579
632
  return queue(paths, values.json === true, values.fresh === true);
633
+ case 'retire':
634
+ if (sub === undefined)
635
+ throw usageError('retire');
636
+ return retire(paths, sub, values.force === true);
580
637
  case 'rules':
581
638
  if (sub === undefined)
582
639
  return rules(paths, values.json === true);
583
640
  if (sub === 'test' && arg !== undefined)
584
641
  return rulesTest(paths, arg, values.json === true);
585
642
  throw usageError('rules');
586
- case 'retire':
587
- if (sub === undefined)
588
- throw usageError('retire');
589
- return retire(paths, sub, values.force === true);
643
+ case 'reply':
644
+ if (sub === undefined || values['body-file'] === undefined)
645
+ throw usageError('reply');
646
+ process.exitCode = await replyCommand(paths, {
647
+ bodyFile: values['body-file'],
648
+ pr: wholeNumber('<pr>', sub),
649
+ thread: arg === undefined ? null : wholeNumber('<thread>', arg),
650
+ ...item,
651
+ ...repo,
652
+ }, verbIo());
653
+ return;
590
654
  case 'serve':
591
655
  return serve(paths);
592
656
  case 'start':
593
657
  return start(paths);
658
+ case 'state':
659
+ if (sub !== undefined)
660
+ throw usageError('state');
661
+ process.exitCode = await stateCommand(paths, repo, verbIo());
662
+ return;
594
663
  case 'status':
595
664
  return status(paths, values.json === true);
596
665
  case 'stop':
@@ -814,10 +883,14 @@ async function retire(paths, ref, force) {
814
883
  const result = parsed.data;
815
884
  const steps = (result.steps ?? []).map((s) => `${s.step}: ${s.ok ? `ok${s.note ? ` (${s.note})` : ''}` : `failed (${s.error ?? '?'})`}`);
816
885
  if (result.outcome === 'refused') {
886
+ const lines = (w, where = '') => [
887
+ ...(w?.unpushed ? [`${where}${w.unpushed} unpushed commit(s)`] : []),
888
+ ...(w?.dirty ? [`${where}uncommitted: ${w.dirty.join(', ')}`] : []),
889
+ ...(w?.unknown ? [`${where}not checked: ${w.unknown}`] : []),
890
+ ];
817
891
  const work = [
818
- ...(result.work?.unpushed ? [`${result.work.unpushed} unpushed commit(s)`] : []),
819
- ...(result.work?.dirty ? [`uncommitted: ${result.work.dirty.join(', ')}`] : []),
820
- ...(result.work?.unknown ? [`not checked: ${result.work.unknown}`] : []),
892
+ ...lines(result.work),
893
+ ...Object.entries(result.work?.checkouts ?? {}).flatMap(([name, w]) => lines(w, `.scratch/repos/${name}: `)),
821
894
  ];
822
895
  throw new ConductorError(`refused to retire ${worker.name}: ${result.reason ?? ''}`, work);
823
896
  }
@@ -1146,6 +1219,9 @@ async function upgradeCommand(paths, tag, allowDowngrade) {
1146
1219
  stopDaemon: () => stopDaemon(paths),
1147
1220
  });
1148
1221
  }
1222
+ function verbIo() {
1223
+ return { cwd: process.cwd(), err: (line) => process.stderr.write(`${line}\n`), out };
1224
+ }
1149
1225
  // Read from disk each time: upgrade asks again after npm has replaced the package.
1150
1226
  function version() {
1151
1227
  const file = fileURLToPath(new URL('../package.json', import.meta.url));