@detergent-software/conductor 3.0.0-dev.9 → 3.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/CHANGELOG.md +486 -0
  2. package/README.md +272 -126
  3. package/build/brief-cli.js +5 -12
  4. package/build/brief-cli.js.map +1 -1
  5. package/build/brief.js +4 -3
  6. package/build/brief.js.map +1 -1
  7. package/build/broker-cli.js +41 -14
  8. package/build/broker-cli.js.map +1 -1
  9. package/build/broker.js +1 -1
  10. package/build/broker.js.map +1 -1
  11. package/build/cli.js +226 -64
  12. package/build/cli.js.map +1 -1
  13. package/build/clones.js +92 -0
  14. package/build/clones.js.map +1 -0
  15. package/build/code-host-cli.js +172 -40
  16. package/build/code-host-cli.js.map +1 -1
  17. package/build/code-host-service.js +35 -2
  18. package/build/code-host-service.js.map +1 -1
  19. package/build/code-host.js +138 -16
  20. package/build/code-host.js.map +1 -1
  21. package/build/conditions.js +33 -16
  22. package/build/conditions.js.map +1 -1
  23. package/build/config.js +58 -28
  24. package/build/config.js.map +1 -1
  25. package/build/console.js +17 -27
  26. package/build/console.js.map +1 -1
  27. package/build/credentials.js +86 -54
  28. package/build/credentials.js.map +1 -1
  29. package/build/daemon.js +54 -65
  30. package/build/daemon.js.map +1 -1
  31. package/build/file-lock.js +155 -40
  32. package/build/file-lock.js.map +1 -1
  33. package/build/follow-ups.js +2 -2
  34. package/build/follow-ups.js.map +1 -1
  35. package/build/help.js +242 -83
  36. package/build/help.js.map +1 -1
  37. package/build/history.js +10 -4
  38. package/build/history.js.map +1 -1
  39. package/build/host/attach-clients.js +56 -0
  40. package/build/host/attach-clients.js.map +1 -0
  41. package/build/host/credential-manager.js +109 -0
  42. package/build/host/credential-manager.js.map +1 -0
  43. package/build/host/keychain.js +44 -0
  44. package/build/host/keychain.js.map +1 -0
  45. package/build/host/launchd.js +118 -0
  46. package/build/host/launchd.js.map +1 -0
  47. package/build/host/libsecret.js +40 -0
  48. package/build/host/libsecret.js.map +1 -0
  49. package/build/host/macos-terminal.js +63 -0
  50. package/build/host/macos-terminal.js.map +1 -0
  51. package/build/host/platform.js +4 -0
  52. package/build/host/platform.js.map +1 -0
  53. package/build/host/secrets.js +46 -0
  54. package/build/host/secrets.js.map +1 -0
  55. package/build/host/shell.js +26 -0
  56. package/build/host/shell.js.map +1 -0
  57. package/build/host/startup.js +26 -0
  58. package/build/host/startup.js.map +1 -0
  59. package/build/host/systemd.js +91 -0
  60. package/build/host/systemd.js.map +1 -0
  61. package/build/host/task-scheduler.js +78 -0
  62. package/build/host/task-scheduler.js.map +1 -0
  63. package/build/host/terminal.js +47 -0
  64. package/build/host/terminal.js.map +1 -0
  65. package/build/host/tmux.js +39 -0
  66. package/build/host/tmux.js.map +1 -0
  67. package/build/host/windows-terminal.js +46 -0
  68. package/build/host/windows-terminal.js.map +1 -0
  69. package/build/in-process.js +5 -4
  70. package/build/in-process.js.map +1 -1
  71. package/build/inbox.js +47 -35
  72. package/build/inbox.js.map +1 -1
  73. package/build/init.js +509 -0
  74. package/build/init.js.map +1 -0
  75. package/build/landing.js +107 -10
  76. package/build/landing.js.map +1 -1
  77. package/build/lib/providers/ado/code-host.js +5 -1
  78. package/build/lib/providers/ado/code-host.js.map +1 -1
  79. package/build/lib/providers/ado/pipelines.js +138 -26
  80. package/build/lib/providers/ado/pipelines.js.map +1 -1
  81. package/build/lib/providers/ado/trigger.js +5 -0
  82. package/build/lib/providers/ado/trigger.js.map +1 -1
  83. package/build/lib/providers/ado/writes.js +17 -2
  84. package/build/lib/providers/ado/writes.js.map +1 -1
  85. package/build/lib/providers/github/actions.js +90 -3
  86. package/build/lib/providers/github/actions.js.map +1 -1
  87. package/build/lib/providers/github/code-host.js +2 -0
  88. package/build/lib/providers/github/code-host.js.map +1 -1
  89. package/build/lib/providers/github/writes.js +25 -2
  90. package/build/lib/providers/github/writes.js.map +1 -1
  91. package/build/lib/providers/index.js +3 -2
  92. package/build/lib/providers/index.js.map +1 -1
  93. package/build/lib/providers/npm/stage.js +14 -1
  94. package/build/lib/providers/npm/stage.js.map +1 -1
  95. package/build/lib/providers/timing.js +10 -0
  96. package/build/lib/providers/timing.js.map +1 -0
  97. package/build/lib/providers/verdicts/pr.js +47 -6
  98. package/build/lib/providers/verdicts/pr.js.map +1 -1
  99. package/build/lib/providers/verdicts/runs.js +12 -2
  100. package/build/lib/providers/verdicts/runs.js.map +1 -1
  101. package/build/logins.js +13 -6
  102. package/build/logins.js.map +1 -1
  103. package/build/manager.js +200 -55
  104. package/build/manager.js.map +1 -1
  105. package/build/note.js +5 -15
  106. package/build/note.js.map +1 -1
  107. package/build/notes.js +130 -23
  108. package/build/notes.js.map +1 -1
  109. package/build/notify.js +104 -42
  110. package/build/notify.js.map +1 -1
  111. package/build/npm-reads.js +151 -22
  112. package/build/npm-reads.js.map +1 -1
  113. package/build/ownership.js +2 -2
  114. package/build/paths.js +44 -1
  115. package/build/paths.js.map +1 -1
  116. package/build/plugin.js +4 -2
  117. package/build/plugin.js.map +1 -1
  118. package/build/profile-fields.js +153 -0
  119. package/build/profile-fields.js.map +1 -0
  120. package/build/profile-layers.js +264 -0
  121. package/build/profile-layers.js.map +1 -0
  122. package/build/profile.js +69 -49
  123. package/build/profile.js.map +1 -1
  124. package/build/project-check-cli.js +17 -19
  125. package/build/project-check-cli.js.map +1 -1
  126. package/build/project-check.js +166 -104
  127. package/build/project-check.js.map +1 -1
  128. package/build/protocol.js +5 -21
  129. package/build/protocol.js.map +1 -1
  130. package/build/pushover.js +5 -5
  131. package/build/pushover.js.map +1 -1
  132. package/build/queue.js +4 -4
  133. package/build/queue.js.map +1 -1
  134. package/build/registry.js +195 -36
  135. package/build/registry.js.map +1 -1
  136. package/build/release-kinds.js +34 -0
  137. package/build/release-kinds.js.map +1 -0
  138. package/build/rule-actions.js +8 -6
  139. package/build/rule-actions.js.map +1 -1
  140. package/build/rules-service.js +83 -44
  141. package/build/rules-service.js.map +1 -1
  142. package/build/rules.js +23 -23
  143. package/build/rules.js.map +1 -1
  144. package/build/run.js +20 -7
  145. package/build/run.js.map +1 -1
  146. package/build/runs.js +218 -68
  147. package/build/runs.js.map +1 -1
  148. package/build/scheduler.js +78 -12
  149. package/build/scheduler.js.map +1 -1
  150. package/build/search.js +2 -1
  151. package/build/search.js.map +1 -1
  152. package/build/server.js +51 -14
  153. package/build/server.js.map +1 -1
  154. package/build/spend.js +1 -1
  155. package/build/spend.js.map +1 -1
  156. package/build/store.js +79 -20
  157. package/build/store.js.map +1 -1
  158. package/build/supervise.js +1 -1
  159. package/build/supervise.js.map +1 -1
  160. package/build/timeline.js +104 -0
  161. package/build/timeline.js.map +1 -0
  162. package/build/tracker-reads-cli.js +4 -3
  163. package/build/tracker-reads-cli.js.map +1 -1
  164. package/build/trackers.js +56 -10
  165. package/build/trackers.js.map +1 -1
  166. package/build/train-cli.js +6 -5
  167. package/build/train-cli.js.map +1 -1
  168. package/build/train.js +2 -2
  169. package/build/train.js.map +1 -1
  170. package/build/upgrade.js +8 -9
  171. package/build/upgrade.js.map +1 -1
  172. package/build/wait.js +10 -10
  173. package/build/wait.js.map +1 -1
  174. package/build/watchers/ado.js +42 -19
  175. package/build/watchers/ado.js.map +1 -1
  176. package/build/watchers/github.js +18 -4
  177. package/build/watchers/github.js.map +1 -1
  178. package/build/watchers/index.js +2 -2
  179. package/build/watchers/index.js.map +1 -1
  180. package/build/watchers/linear-queue.js +47 -14
  181. package/build/watchers/linear-queue.js.map +1 -1
  182. package/build/watchers/linear-writes.js +26 -12
  183. package/build/watchers/linear-writes.js.map +1 -1
  184. package/build/watchers/linear.js +2 -2
  185. package/build/watchers/linear.js.map +1 -1
  186. package/build/watchers/source.js +5 -1
  187. package/build/watchers/source.js.map +1 -1
  188. package/build/webhook.js +80 -0
  189. package/build/webhook.js.map +1 -0
  190. package/build/why.js +1 -1
  191. package/build/why.js.map +1 -1
  192. package/build/worktrees.js +56 -93
  193. package/build/worktrees.js.map +1 -1
  194. package/package.json +61 -61
  195. package/plugin/.claude-plugin/plugin.json +1 -1
  196. package/plugin/hooks/model.ts +55 -6
  197. package/plugin/hooks/register.tsx +43 -4
  198. package/plugin/skills/conductor-console/SKILL.md +26 -27
  199. package/plugin/skills/work-item/SKILL.md +58 -54
  200. package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
  201. package/plugin/skills/work-item/trackers/github-issues.md +1 -1
  202. package/plugin/skills/work-item/trackers/linear.md +2 -2
  203. package/plugin/types/index.d.ts +1 -1
  204. package/protocol/PROTOCOL.md +3 -9
  205. package/build/checkout-sync.js +0 -249
  206. package/build/checkout-sync.js.map +0 -1
  207. package/build/credential.js +0 -66
  208. package/build/credential.js.map +0 -1
  209. package/build/startup.js +0 -92
  210. package/build/startup.js.map +0 -1
  211. package/build/tab.js +0 -32
  212. package/build/tab.js.map +0 -1
package/build/cli.js CHANGED
@@ -1,22 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
4
- import { join, resolve, sep } from 'node:path';
4
+ import { delimiter, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { parseArgs } from 'node:util';
7
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
+ import { trustStep } from "./claude.js";
10
11
  import { callApi, NotRunningError, ping, readEndpoint } from "./client.js";
11
- import { branchCommand, checkoutCommand, fileCommand, landCommand, modeCommand, prCreateCommand, prThreadsCommand, replyCommand, runsCommand, stateCommand, } from "./code-host-cli.js";
12
+ import { branchCommand, checkoutCommand, fileCommand, landCommand, modeCommand, prAbandonCommand, prCreateCommand, prStatusCommand, prThreadsCommand, replyCommand, runsCommand, stateCommand, timelineCommand, } from "./code-host-cli.js";
12
13
  import { configSchema, loadConfig } from "./config.js";
13
- import { claimConsole, CONSOLE_WINDOW, consoleArgs, consoleEnv, consoleTabArgs, escalationLines, escalationSchema, handledSchema, } from "./console.js";
14
+ import { claimConsole, CONSOLE_WINDOW, consoleArgs, consoleEnv, consoleTab, escalationLines, escalationSchema, handledSchema, } from "./console.js";
14
15
  import { AlreadyRunningError, logCrashes, startDaemon } from "./daemon.js";
15
16
  import { ConductorError } from "./errors.js";
16
17
  import { eventsCommand } from "./events.js";
17
18
  import { commandHelp, isCommand, listing, terminalStyle, terminalWidth, unknownCommand, usageError } from "./help.js";
18
19
  import { flagWindow, historyLines, historyReport, historySchema, parseRange, retroMarkdown, } from "./history.js";
20
+ import { startup } from "./host/startup.js";
21
+ import { terminal } from "./host/terminal.js";
19
22
  import { inboxLines } from "./inbox.js";
23
+ import { initProject, terminalAsk } from "./init.js";
20
24
  import { parseJsonc } from "./jsonc.js";
21
25
  import { npmStage } from "./lib/providers/index.js";
22
26
  import { processAlive, readPid } from "./lock.js";
@@ -29,28 +33,27 @@ import { overrideViewSchema, PAUSE } from "./overrides.js";
29
33
  import { packageInfo } from "./package-info.js";
30
34
  import { conductorPaths } from "./paths.js";
31
35
  import { refreshConductorPlugin } from "./plugin.js";
32
- import { projectCheckCommand } from "./project-check-cli.js";
36
+ import { profileLines, projectProfile } from "./profile-layers.js";
37
+ import { projectCheckCommand, projectCheckReport } from "./project-check-cli.js";
33
38
  import { noProject } from "./project-check.js";
34
39
  import { pushoverCredential, pushoverSender } from "./pushover.js";
35
- import { registerProject } from "./registry.js";
40
+ import { cloneProject, registerProject } from "./registry.js";
36
41
  import { rulesLines, rulesService, rulesTestResultSchema, rulesViewSchema, testLines, } from "./rules-service.js";
37
42
  import { resolveUntil } from "./rules.js";
38
43
  import { outputOf, run } from "./run.js";
39
44
  import { queueLines, queueViewSchema } from "./scheduler.js";
40
45
  import { awaitStart, serveDetached, ServeStartError } from "./serve.js";
41
46
  import { localDay } from "./spend.js";
42
- import { installStartup, removeStartup, runStartup, startupRunsCli, startupSupervises, TASK_NAME } from "./startup.js";
43
47
  import { answerStopRequest, dropStaleStopRequest, pendingStopRequest, STOP_POLL_MS, stopSupervisor, } from "./stop-request.js";
44
48
  import { inboxItemSchema, openStore, projectSchema, waitSchema, workerSchema, } from "./store.js";
45
49
  import { RESTART_ENV, restartInfoSchema, SERVE_LOCKED_EXIT, serveChild, supervise, SUPERVISE_POLICY, } from "./supervise.js";
46
- import { openWindowsTerminal } from "./tab.js";
47
50
  import { commentsCommand, notesCommand, searchCommand } from "./tracker-reads-cli.js";
48
51
  import { trainCommand } from "./train-cli.js";
49
52
  import { parseTag, restartDaemon, tagVersion, upgrade } from "./upgrade.js";
50
53
  import { waitCommand } from "./wait.js";
51
54
  import { whyLines, whySchema } from "./why.js";
52
55
  const STOP_WAIT_MS = 10_000;
53
- // A restarted daemon opens its store and binds the port; under the logon task node starts cold.
56
+ // A restarted daemon opens its store and binds the port; under the logon startup node starts cold.
54
57
  const START_WAIT_MS = 30_000;
55
58
  const NPM_INSTALL_TIMEOUT_MS = 5 * 60_000;
56
59
  // A launch makes a worktree (pnpm install) before the session starts.
@@ -62,6 +65,9 @@ const RETIRE_TIMEOUT_MS = 12 * 60_000;
62
65
  const ATTACH_TIMEOUT_MS = 9 * 60_000;
63
66
  // Approve and revise ask Linear twice (the issue, then the comment), each within 20 s.
64
67
  const ANSWER_TIMEOUT_MS = 60_000;
68
+ // A revise of an approved plan adds up to three brief reads (the tracker and the PR, each within
69
+ // 20 s) 2 s apart after the comment, so a slow tracker doesn't cut off an answer whose comment landed.
70
+ const REVISE_TIMEOUT_MS = 3 * 60_000;
65
71
  const projectsBody = z.object({ projects: z.array(projectSchema) });
66
72
  const addBody = z.object({ created: z.boolean(), project: projectSchema });
67
73
  const removeBody = z.object({ project: projectSchema, resolved: z.array(inboxItemSchema) });
@@ -86,23 +92,44 @@ const retireBody = z.object({
86
92
  work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
87
93
  worker: workerSchema,
88
94
  });
89
- const attachBody = z.object({ resumed: z.boolean(), tab: z.string().nullable(), worker: workerSchema });
95
+ // hint: from ATK-529 on, already: from ATK-520 on; an older daemon sends neither.
96
+ const attachBody = z.object({
97
+ already: z.boolean().default(false),
98
+ hint: z.string().nullable().default(null),
99
+ resumed: z.boolean(),
100
+ tab: z.string().nullable(),
101
+ worker: workerSchema,
102
+ });
90
103
  const inboxBody = z.object({ items: z.array(inboxItemSchema) });
91
104
  const answerBody = z.object({
92
105
  comment: z.object({ id: z.string(), url: z.string().nullable() }).nullable(),
93
106
  item: inboxItemSchema,
94
107
  waits: z.array(waitSchema),
95
108
  });
109
+ const reviseAnswer = z.object({
110
+ revise: z.object({
111
+ comment: z.object({ id: z.string(), url: z.string().nullable() }),
112
+ item: z.string(),
113
+ landed: z.boolean(),
114
+ pending: z.string().nullable(),
115
+ revised: z.boolean(),
116
+ woken: z.number(),
117
+ worker: z.boolean(),
118
+ }),
119
+ });
96
120
  const answerRefusal = z.object({
97
121
  details: z.array(z.string()).optional(),
98
122
  error: z.string(),
99
123
  posted: z.enum(['no', 'unknown']).optional(),
100
124
  });
101
- async function addProject(paths, path) {
102
- const absolute = resolve(path);
125
+ // `conduct project add <url | path> [--name <name>]`: the clone is made here, where its progress
126
+ // shows and no API timeout applies, then the daemon registers it (or the store, with none running).
127
+ // `next`: false for `conduct init`, which says what comes next itself.
128
+ async function addProject(paths, source, name, next = true) {
129
+ const made = await cloneProject(paths, source, { ...(name === undefined ? {} : { name }), say: out });
103
130
  let result;
104
131
  try {
105
- result = addBody.parse(await expectOk(callApi(paths, { body: { path: absolute }, method: 'POST', path: '/projects' })));
132
+ result = addBody.parse(await expectOk(callApi(paths, { body: { name: made.name }, method: 'POST', path: '/projects' })));
106
133
  }
107
134
  catch (error) {
108
135
  if (!(error instanceof NotRunningError))
@@ -110,14 +137,25 @@ async function addProject(paths, path) {
110
137
  // No daemon: write the store directly. The daemon reads it when it starts.
111
138
  // Its rules are checked against config.json here too, as the daemon would.
112
139
  const check = rulesService({ config: loadConfig(paths.config), listProjects: () => [], paths }).checkProfile;
113
- result = withStore(paths, (store) => registerProject(store, absolute, check));
140
+ result = withStore(paths, (store) => registerProject(store, paths, made.name, check));
114
141
  }
115
142
  out(`${result.created ? 'registered' : 'updated'} ${projectLine(result.project).trim()}`);
143
+ out(made.cloned ? `cloned ${made.url} into ${made.clone}` : `fetched ${made.clone}, the clone it has`);
144
+ out(`its machine profile, which wins over the repo's: ${result.project.profilePath}`);
145
+ // Claude Code trusts a worktree by its repository, and nobody has run Claude in a new clone yet.
146
+ if (made.cloned && next)
147
+ out(`next: ${trustStep(made.clone)}, then \`conduct project check ${made.name}\``);
148
+ return made;
116
149
  }
117
- // `conduct approve|revise|done`: Jason's answer to an inbox item, by id, or for approve and revise
118
- // by the item whose plan waits.
150
+ // `conduct approve|revise|done`: the operator's answer to an inbox item, by id, or for approve and revise
151
+ // by the item whose plan waits. A revise of an item with no plan waiting revises its approved plan.
119
152
  async function answer(paths, ref, request) {
120
153
  const target = request.action === 'done' ? inboxId(ref) : await planItem(paths, ref);
154
+ if (target === null) {
155
+ if (request.action === 'revise')
156
+ return reviseApproved(paths, ref, request.text);
157
+ throw new ConductorError(`no plan of ${ref} waits in the inbox`, ['`conduct inbox` lists what waits']);
158
+ }
121
159
  const { body, status } = await callApi(paths, {
122
160
  body: request,
123
161
  method: 'POST',
@@ -148,9 +186,14 @@ async function answer(paths, ref, request) {
148
186
  async function attach(paths, ref) {
149
187
  const worker = await findWorker(paths, ref);
150
188
  const result = attachBody.parse(await expectOk(callApi(paths, { method: 'POST', path: `/workers/${worker.id}/attach`, timeoutMs: ATTACH_TIMEOUT_MS })));
151
- out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
189
+ if (result.already)
190
+ out(`already attached to ${worker.name} (its tab is open)`);
191
+ else
192
+ out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
152
193
  if (result.tab)
153
194
  out(`no tab (${result.tab}); open it yourself: claude attach ${result.worker.handle ?? worker.name}`);
195
+ else if (result.hint)
196
+ out(`to see its tab: ${result.hint}`);
154
197
  }
155
198
  // The pid of the daemon once it answers /health, for upgrade's restart. failure: the detached
156
199
  // serve's spawn error, which ends the wait early.
@@ -208,34 +251,33 @@ async function consoleCommand(paths, options) {
208
251
  });
209
252
  return;
210
253
  }
211
- if (process.platform !== 'win32') {
212
- throw new ConductorError('conduct console opens a Windows Terminal tab', [
213
- 'run `conduct console --here` in a terminal of your own',
214
- ]);
215
- }
216
- const args = consoleTabArgs(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)));
254
+ const where = terminal({ choice: loadConfig(paths.config).workers.terminal });
255
+ const tab = consoleTab(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)), process.env);
217
256
  await claimConsole(paths, options.force);
218
- const result = await openWindowsTerminal(args, paths.console);
219
- if (result.code !== 0)
220
- throw new ConductorError('could not open the console tab', [
221
- outputOf(result, 500),
257
+ const failed = await where.open(tab);
258
+ if (failed !== null)
259
+ throw new ConductorError(`could not open the console tab in ${where.name}`, [
260
+ failed,
222
261
  'run `conduct console --here --force` in a terminal instead',
223
262
  ]);
224
- out(`opened the console in ${paths.console} (Windows Terminal window "${CONSOLE_WINDOW}")`);
263
+ const hint = where.attachHint(CONSOLE_WINDOW);
264
+ out(`opened the console in ${paths.console} (${hint === null ? `${where.name} window "${CONSOLE_WINDOW}"` : `${where.name}: ${hint}`})`);
225
265
  }
226
266
  // How `conduct start` and upgrade's restart start the daemon detached from this process.
227
267
  function daemonStarter(paths, cliPath) {
228
268
  let failure = () => undefined;
269
+ const task = startup();
229
270
  return {
230
271
  awaitDaemon: () => awaitDaemon(paths, failure),
231
- runTask: runStartup,
272
+ runTask: () => task.run(),
232
273
  serveDetached: () => {
233
274
  // --fail-fast: a first serve that can't start ends the supervisor, so the start says why at once.
234
275
  failure = serveDetached(paths.home, process.execPath, [cliPath, 'supervise', '--fail-fast']);
235
276
  },
236
- // The logon task's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
237
- taskRunsCli: () => !process.env.CONDUCTOR_HOME && startupRunsCli(cliPath),
238
- taskSupervises: startupSupervises,
277
+ taskName: task.name,
278
+ // The startup's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
279
+ taskRunsCli: async () => !process.env.CONDUCTOR_HOME && (await task.runsCli(cliPath)),
280
+ taskSupervises: () => task.supervises(),
239
281
  };
240
282
  }
241
283
  // `conduct escalations done`: the console reports back on an escalation.
@@ -360,11 +402,20 @@ function inboxId(ref) {
360
402
  throw new ConductorError(`"${ref}" is not an inbox item id`, ['`conduct inbox` lists them as #<id>']);
361
403
  return Number(id);
362
404
  }
363
- function install() {
405
+ // `conduct install-startup`: the machine's startup (host/startup.ts) runs this build's supervise.
406
+ async function install() {
364
407
  const cliPath = realpathSync(fileURLToPath(import.meta.url));
365
- installStartup({ cliPath, nodePath: process.execPath });
366
- out(`registered the "${TASK_NAME}" logon task: ${process.execPath} ${cliPath} supervise`);
367
- if (cliPath.includes(`${sep}.claude${sep}worktrees${sep}`)) {
408
+ const task = startup();
409
+ // Less what `pnpm exec` or `npm run` put first, which a worktree's removal takes away.
410
+ const path = (process.env.PATH ?? '')
411
+ .split(delimiter)
412
+ .filter((dir) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/.test(dir))
413
+ .join(delimiter);
414
+ const notes = await task.install({ cliPath, nodePath: process.execPath, path });
415
+ out(`registered ${task.name}: ${process.execPath} ${cliPath} supervise`);
416
+ for (const note of notes)
417
+ out(note);
418
+ if (/[\\/](?:\.claude|\.conductor[\\/]projects[\\/][^\\/]+)[\\/]worktrees[\\/]/.test(cliPath)) {
368
419
  out('warning: that build is in a worktree, which goes away; run install-startup from the main checkout');
369
420
  }
370
421
  }
@@ -399,7 +450,7 @@ async function listProjects(paths, json) {
399
450
  if (json)
400
451
  return out(JSON.stringify(projects, null, 2));
401
452
  if (projects.length === 0)
402
- return out('no projects; add one with `conduct project add <path>`');
453
+ return out('no projects; add one with `conduct project add <url | path>`');
403
454
  projects.forEach((p) => out(projectLine(p)));
404
455
  }
405
456
  async function main(argv) {
@@ -407,7 +458,7 @@ async function main(argv) {
407
458
  if (argv[0] === 'wait')
408
459
  return argv.slice(1).some(isHelpFlag) ? help('wait') : wait(argv.slice(1));
409
460
  // `revise` takes free text, which option parsing would cut at a word such as --force. Every word
410
- // after the item is the text, quoted or not: a cut-off instruction would be posted as Jason's.
461
+ // after the item is the text, quoted or not: a cut-off instruction would be posted as the operator's.
411
462
  if (argv[0] === 'revise') {
412
463
  // A lone help flag as the text is a call for help: posted, it would be a revision of "--help".
413
464
  if (isHelpFlag(argv[1]) || (argv.length === 3 && isHelpFlag(argv[2])))
@@ -432,19 +483,22 @@ async function main(argv) {
432
483
  throw usageError('escalations');
433
484
  return escalationDone(conductorPaths(), ref, { ask, text });
434
485
  }
435
- // The first word that names a command: an option's value (--project atk) comes before it at times.
486
+ // The first word that names a command: an option's value (--project web) comes before it at times.
436
487
  const { positionals, values } = parsing(argv.find((word) => isCommand(word)), () => parseArgs({
437
488
  allowPositionals: true,
438
489
  args: argv,
439
490
  options: {
440
491
  'allow-downgrade': { type: 'boolean' },
441
492
  area: { type: 'string' },
493
+ base: { type: 'string' },
442
494
  'blocked-by': { multiple: true, type: 'string' },
443
495
  'body-file': { type: 'string' },
496
+ branch: { type: 'string' },
444
497
  channel: { type: 'string' },
445
498
  comments: { type: 'boolean' },
446
499
  conductor: { type: 'string' },
447
500
  console: { type: 'boolean' },
501
+ count: { type: 'string' },
448
502
  'dry-run': { type: 'boolean' },
449
503
  'fail-fast': { type: 'boolean' },
450
504
  files: { type: 'string' },
@@ -452,20 +506,24 @@ async function main(argv) {
452
506
  for: { type: 'string' },
453
507
  force: { type: 'boolean' },
454
508
  fresh: { type: 'boolean' },
509
+ grep: { type: 'string' },
455
510
  help: { short: 'h', type: 'boolean' },
456
511
  here: { type: 'boolean' },
457
512
  human: { type: 'boolean' },
458
513
  item: { type: 'string' },
459
514
  json: { type: 'boolean' },
515
+ key: { type: 'string' },
460
516
  kind: { type: 'string' },
461
517
  last: { type: 'boolean' },
462
518
  mode: { type: 'string' },
519
+ name: { type: 'string' },
463
520
  'no-worktree': { type: 'boolean' },
464
521
  owner: { type: 'string' },
465
522
  phase: { type: 'string' },
466
523
  probe: { type: 'boolean' },
467
524
  project: { type: 'string' },
468
525
  prompt: { type: 'string' },
526
+ query: { type: 'string' },
469
527
  raise: { type: 'boolean' },
470
528
  rearm: { type: 'boolean' },
471
529
  reason: { type: 'string' },
@@ -473,18 +531,23 @@ async function main(argv) {
473
531
  remove: { type: 'boolean' },
474
532
  repo: { type: 'string' },
475
533
  requeue: { type: 'string' },
534
+ shared: { type: 'boolean' },
476
535
  since: { type: 'string' },
477
536
  slots: { type: 'string' },
478
537
  tag: { type: 'string' },
538
+ task: { type: 'string' },
539
+ team: { type: 'string' },
479
540
  text: { type: 'string' },
480
541
  tier: { type: 'string' },
481
542
  title: { type: 'string' },
543
+ tracker: { type: 'string' },
482
544
  until: { type: 'string' },
483
545
  version: { short: 'v', type: 'boolean' },
484
546
  wait: { type: 'string' },
485
547
  'waits-release': { type: 'boolean' },
486
548
  weight: { type: 'string' },
487
549
  worker: { type: 'string' },
550
+ yes: { type: 'boolean' },
488
551
  },
489
552
  }));
490
553
  const paths = conductorPaths();
@@ -511,7 +574,7 @@ async function main(argv) {
511
574
  case 'branch':
512
575
  if (sub === undefined || arg === undefined)
513
576
  throw usageError('branch');
514
- process.exitCode = await branchCommand(sub, arg, item, verbIo());
577
+ process.exitCode = await branchCommand(paths, sub, arg, item, verbIo());
515
578
  return;
516
579
  case 'brief':
517
580
  if (sub === undefined)
@@ -525,7 +588,7 @@ async function main(argv) {
525
588
  case 'checkout':
526
589
  if (sub === undefined || arg === undefined || fourth === undefined)
527
590
  throw usageError('checkout');
528
- process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
591
+ process.exitCode = await checkoutCommand(paths, sub, arg, fourth, item, verbIo());
529
592
  return;
530
593
  case 'comments': {
531
594
  if (sub === undefined || arg !== undefined)
@@ -554,10 +617,10 @@ async function main(argv) {
554
617
  // /reload-plugins after an upgrade) starts this package's new build, in the console's env.
555
618
  return events(paths, values.follow === true, values.since, values.console === true || process.env.CONDUCTOR_CONSOLE === '1');
556
619
  case 'file':
557
- if (sub !== undefined || !values.title || !values['body-file'] || !values.area)
620
+ if (sub !== undefined || !values.title || !values['body-file'])
558
621
  throw usageError('file');
559
622
  process.exitCode = await fileCommand(paths, {
560
- area: values.area,
623
+ area: values.area ?? '',
561
624
  blockedBy: values['blocked-by'] ?? [],
562
625
  bodyFile: values['body-file'],
563
626
  human: values.human === true,
@@ -587,6 +650,26 @@ async function main(argv) {
587
650
  });
588
651
  case 'inbox':
589
652
  return inbox(paths, values.json === true);
653
+ case 'init':
654
+ if (arg !== undefined)
655
+ throw usageError('init');
656
+ process.exitCode = await initProject({
657
+ add: (checkout, name) => addProject(paths, checkout, name, false),
658
+ ask: terminalAsk(),
659
+ check: (name) => projectCheckReport(paths, name, { worktree: true }, { err: (line) => process.stderr.write(`${line}\n`) }),
660
+ out,
661
+ paths,
662
+ }, sub ?? '.', {
663
+ shared: values.shared === true,
664
+ yes: values.yes === true,
665
+ ...(values.base === undefined ? {} : { base: values.base }),
666
+ ...(values.key === undefined ? {} : { key: values.key }),
667
+ ...(values.name === undefined ? {} : { name: values.name }),
668
+ ...(values.query === undefined ? {} : { query: values.query }),
669
+ ...(values.team === undefined ? {} : { team: values.team }),
670
+ ...(values.tracker === undefined ? {} : { tracker: values.tracker }),
671
+ });
672
+ return;
590
673
  case 'install-startup':
591
674
  return values.remove ? uninstall() : install();
592
675
  case 'land': {
@@ -606,6 +689,7 @@ async function main(argv) {
606
689
  ...repo,
607
690
  ...(mode === undefined ? {} : { mode }),
608
691
  ...(tier === undefined ? {} : { tier }),
692
+ ...(values.requeue === undefined ? {} : { requeue: wholeNumber('--requeue', values.requeue) }),
609
693
  }, verbIo());
610
694
  return;
611
695
  }
@@ -633,7 +717,7 @@ async function main(argv) {
633
717
  throw usageError('mode');
634
718
  if (tier !== 'S' && tier !== 'M' && tier !== 'L')
635
719
  throw new ConductorError('--tier takes S, M or L');
636
- process.exitCode = await modeCommand({ files: values.files, tier }, verbIo());
720
+ process.exitCode = await modeCommand(paths, { files: values.files, tier }, verbIo());
637
721
  return;
638
722
  }
639
723
  case 'note':
@@ -665,10 +749,20 @@ async function main(argv) {
665
749
  return notifyTest(paths, { channel: values.channel, kind: values.kind, raise: values.raise === true });
666
750
  throw usageError('notify');
667
751
  case 'pr':
752
+ if (sub === 'abandon' && arg !== undefined) {
753
+ if (values.reason === undefined)
754
+ throw usageError('pr');
755
+ process.exitCode = await prAbandonCommand(paths, { force: values.force === true, pr: wholeNumber('<pr>', arg), reason: values.reason, ...item, ...repo }, verbIo());
756
+ return;
757
+ }
668
758
  if (sub === 'threads' && arg !== undefined) {
669
759
  process.exitCode = await prThreadsCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
670
760
  return;
671
761
  }
762
+ if (sub === 'status' && arg !== undefined) {
763
+ process.exitCode = await prStatusCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
764
+ return;
765
+ }
672
766
  if (sub !== 'create' || values.title === undefined || values['body-file'] === undefined)
673
767
  throw usageError('pr');
674
768
  process.exitCode = await prCreateCommand(paths, { bodyFile: values['body-file'], dryRun: values['dry-run'] === true, title: values.title, ...item, ...repo }, verbIo());
@@ -682,9 +776,21 @@ async function main(argv) {
682
776
  case 'ports':
683
777
  process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
684
778
  return;
779
+ case 'profile': {
780
+ if (sub === undefined || arg !== undefined)
781
+ throw usageError('profile');
782
+ // Resolved here from the machine file and the clone, as the daemon resolves it.
783
+ const layered = projectProfile(paths, sub);
784
+ if (values.json === true)
785
+ return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
786
+ profileLines(sub, layered).forEach((line) => out(line));
787
+ return;
788
+ }
685
789
  case 'project':
686
- if (sub === 'add' && arg !== undefined)
687
- return addProject(paths, arg);
790
+ if (sub === 'add' && arg !== undefined) {
791
+ await addProject(paths, arg, values.name);
792
+ return;
793
+ }
688
794
  if (sub === 'remove' && arg !== undefined)
689
795
  return removeProject(paths, arg);
690
796
  if (sub === 'list')
@@ -725,6 +831,7 @@ async function main(argv) {
725
831
  ...item,
726
832
  ...repo,
727
833
  ...(values.for === undefined ? {} : { for: values.for }),
834
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
728
835
  ...(values.requeue === undefined ? {} : { requeue: values.requeue }),
729
836
  }, verbIo());
730
837
  return;
@@ -750,6 +857,19 @@ async function main(argv) {
750
857
  case 'supervise':
751
858
  process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
752
859
  return;
860
+ case 'timeline':
861
+ if (arg !== undefined)
862
+ throw usageError('timeline');
863
+ process.exitCode = await timelineCommand(paths, {
864
+ ...repo,
865
+ ...(sub === undefined ? {} : { run: sub }),
866
+ ...(values.branch === undefined ? {} : { branch: values.branch }),
867
+ ...(values.count === undefined ? {} : { count: values.count }),
868
+ ...(values.for === undefined ? {} : { for: values.for }),
869
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
870
+ ...(values.task === undefined ? {} : { task: values.task }),
871
+ }, verbIo());
872
+ return;
753
873
  case 'train':
754
874
  return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
755
875
  case 'unpause':
@@ -805,7 +925,7 @@ async function notifyTest(paths, options) {
805
925
  ...(options.kind ? { kind: options.kind } : {}),
806
926
  raise: options.raise,
807
927
  };
808
- // Pushover can take its 15 s, after a Credential Manager read.
928
+ // Pushover can take its 15 s, after a secret store read.
809
929
  const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
810
930
  if (options.raise) {
811
931
  const { item } = z.object({ item: inboxItemSchema }).parse(answer);
@@ -883,33 +1003,35 @@ function parsing(command, parse) {
883
1003
  : new ConductorError(message, ['run `conduct help` for the commands']);
884
1004
  }
885
1005
  }
886
- // The open plan item of `ref`: the item (ATK-312) whose plan waits, or an inbox id written #12. A
887
- // bare number is refused: it could be an issue's number, and the answer is posted as Jason's.
1006
+ // The open plan item of `ref`: the item (ENG-312) whose plan waits, or an inbox id written #12; null
1007
+ // when no plan of the item waits. A bare number is refused: it could be an issue's number, and the
1008
+ // answer is posted as the operator's.
888
1009
  async function planItem(paths, ref) {
889
1010
  if (ref.startsWith('#'))
890
1011
  return inboxId(ref);
1012
+ const items = await openItems(paths);
891
1013
  if (/^\d+$/.test(ref)) {
892
- throw new ConductorError(`"${ref}" is ambiguous`, [
893
- `name the item, such as ATK-${ref}, or the inbox id as #${ref}`,
894
- ]);
1014
+ // The waiting plans whose key ends in the number, else a placeholder: the project's key isn't known here.
1015
+ const keys = (items ?? []).flatMap((i) => (i.kind === 'plan' && i.item?.endsWith(`-${ref}`) ? [i.item] : []));
1016
+ const item = keys.length > 0 ? keys.join(' or ') : `<KEY>-${ref}`;
1017
+ throw new ConductorError(`"${ref}" is ambiguous`, [`name the item, such as ${item}, or the inbox id as #${ref}`]);
895
1018
  }
896
- const items = await openItems(paths);
897
1019
  if (items === null)
898
1020
  throw noInbox();
899
1021
  const plans = items.filter((i) => i.kind === 'plan' && i.item?.toUpperCase() === ref.toUpperCase());
900
1022
  if (plans.length === 1 && plans[0])
901
1023
  return plans[0].id;
902
1024
  if (plans.length === 0)
903
- throw new ConductorError(`no plan of ${ref} waits in the inbox`, ['`conduct inbox` lists what waits']);
1025
+ return null;
904
1026
  throw new ConductorError(`several plans of ${ref} wait`, plans.map((p) => `#${p.id} in ${p.project ?? '?'}: pass #${p.id}`));
905
1027
  }
906
1028
  function projectLine(p) {
907
1029
  const state = p.problem
908
- ? `${p.problem}; the daemon reads ${p.profilePath} again when it starts or the file is saved`
1030
+ ? `${p.problem}; the daemon reads the profile again when it starts, when ~/.conductor/projects/${p.name}.json is saved, and after each fetch of the clone`
909
1031
  : p.enabled
910
1032
  ? 'enabled'
911
1033
  : 'disabled';
912
- return ` ${p.name} ${p.root} (protocol ${p.protocol}, ${state})`;
1034
+ return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
913
1035
  }
914
1036
  // The registered projects: from the daemon, or from the store when it isn't running.
915
1037
  async function projectsNow(paths) {
@@ -954,7 +1076,7 @@ async function removeProject(paths, name) {
954
1076
  });
955
1077
  }
956
1078
  const { project, resolved } = result;
957
- out(`removed ${project.name} (${project.root})`);
1079
+ out(`removed ${project.name} (${project.clone})`);
958
1080
  for (const item of resolved)
959
1081
  out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
960
1082
  }
@@ -1004,6 +1126,45 @@ async function retire(paths, ref, force) {
1004
1126
  out(`retired ${worker.name}`);
1005
1127
  steps.forEach((s) => out(` ${s}`));
1006
1128
  }
1129
+ // `conduct revise <item>` with no plan of the item in the inbox: the daemon posts the revise on an
1130
+ // approved plan and wakes its worker at once, as its revise sweep would (ATK-664).
1131
+ async function reviseApproved(paths, item, text) {
1132
+ const { body, status } = await callApi(paths, {
1133
+ body: { text },
1134
+ method: 'POST',
1135
+ path: `/items/${encodeURIComponent(item)}/revise`,
1136
+ timeoutMs: REVISE_TIMEOUT_MS,
1137
+ });
1138
+ if (status !== 200) {
1139
+ const refusal = answerRefusal.safeParse(body).data;
1140
+ const details = [...(refusal?.details ?? [])];
1141
+ if (status === 502 && refusal?.posted === 'unknown')
1142
+ details.push('the comment may be on the item already: look before answering again');
1143
+ // A conductor from before ATK-664 has no such route.
1144
+ if (status === 404)
1145
+ throw new ConductorError(`no plan of ${item} waits in the inbox`, [
1146
+ '`conduct inbox` lists what waits',
1147
+ 'a conductor started before ATK-664 revises no approved plan: restart it for that',
1148
+ ]);
1149
+ throw new ConductorError(refusal?.error ?? `the API answered ${status}`, details);
1150
+ }
1151
+ const r = reviseAnswer.parse(body).revise;
1152
+ out(`asked for a revision of ${r.item}'s approved plan: ${r.comment.url ?? r.comment.id}`);
1153
+ out(reviseOutcome(r));
1154
+ }
1155
+ // What became of a revise of an approved plan after its comment, in one line.
1156
+ function reviseOutcome(r) {
1157
+ const none = 'no worker is on it: one launched on it reads the revise in its brief';
1158
+ if (r.pending !== null)
1159
+ return `nothing recorded yet (${r.pending}): ${r.worker ? 'the revise sweep records it and wakes its worker within 5 minutes' : none}`;
1160
+ if (!r.revised)
1161
+ return 'a revise of its plan was recorded already: its worker re-plans on the first one';
1162
+ if (r.woken > 0)
1163
+ return 'woke its worker to re-plan';
1164
+ if (r.landed)
1165
+ return 'its PR has merged: its worker learns of it at its next note, for a follow-up PR';
1166
+ return r.worker ? 'its worker is running: its next note is held back, and it re-plans' : none;
1167
+ }
1007
1168
  // `conduct rules`: from the daemon, or straight from the files when it isn't running.
1008
1169
  async function rules(paths, json) {
1009
1170
  const view = await rulesAnswer(paths, { method: 'GET', path: '/rules' }, (body) => rulesViewSchema.parse(body), (service) => service.view());
@@ -1084,7 +1245,7 @@ async function shutdownDaemon(paths) {
1084
1245
  if (readEndpoint(paths) !== null) {
1085
1246
  throw new ConductorError(`the conductor (pid ${pid ?? '?'}) did not stop within ${STOP_WAIT_MS / 1000} s`);
1086
1247
  }
1087
- // The port file goes before the process ends; a start (the logon task's especially) waits for it.
1248
+ // The port file goes before the process ends; a start (the logon startup's especially) waits for it.
1088
1249
  const exitDeadline = Date.now() + STOP_WAIT_MS;
1089
1250
  while (pid !== null && processAlive(pid) && Date.now() < exitDeadline)
1090
1251
  await sleep(100);
@@ -1100,7 +1261,7 @@ async function start(paths) {
1100
1261
  const pid = readPid(paths.pid);
1101
1262
  if (pid !== null && (await ping(paths, pid)))
1102
1263
  return out(`the conductor is running (pid ${pid})`);
1103
- // A daemon that is still starting (the logon task's, say) holds the lock: wait for it rather than
1264
+ // A daemon that is still starting (the logon startup's, say) holds the lock: wait for it rather than
1104
1265
  // start a second one, which would exit on that lock. A pid a reboot reused gets a start after.
1105
1266
  if (pid !== null && processAlive(pid)) {
1106
1267
  try {
@@ -1148,7 +1309,7 @@ async function status(paths, json) {
1148
1309
  if (json)
1149
1310
  return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
1150
1311
  out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
1151
- out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
1312
+ out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
1152
1313
  projects.forEach((p) => out(projectLine(p)));
1153
1314
  out(workers.length === 0 ? 'no workers' : 'workers:');
1154
1315
  workers.forEach((w) => out(workerLine(w)));
@@ -1186,7 +1347,7 @@ async function stopDaemon(paths) {
1186
1347
  out('the conductor is not running');
1187
1348
  return false;
1188
1349
  case 'supervisor': {
1189
- // It answers just before it ends; a start (the logon task's especially) needs it gone.
1350
+ // It answers just before it ends; a start (the logon startup's especially) needs it gone.
1190
1351
  const deadline = Date.now() + STOP_WAIT_MS;
1191
1352
  while (processAlive(stop.pid) && Date.now() < deadline)
1192
1353
  await sleep(100);
@@ -1195,7 +1356,7 @@ async function stopDaemon(paths) {
1195
1356
  }
1196
1357
  }
1197
1358
  }
1198
- // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon task runs
1359
+ // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
1199
1360
  // it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
1200
1361
  // while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the
1201
1362
  // defaults: no quiet hours, the default credential.
@@ -1217,7 +1378,7 @@ async function superviseCommand(paths, failFast) {
1217
1378
  rotatingLog(paths.log, config().log).log(...line);
1218
1379
  }
1219
1380
  catch {
1220
- // Nowhere else to say it: under the logon task nobody reads stderr.
1381
+ // Nowhere else to say it: under the logon startup nobody reads stderr.
1221
1382
  }
1222
1383
  },
1223
1384
  };
@@ -1293,8 +1454,9 @@ async function superviseCommand(paths, failFast) {
1293
1454
  }
1294
1455
  return code;
1295
1456
  }
1296
- function uninstall() {
1297
- out(removeStartup() ? `removed the "${TASK_NAME}" logon task` : `no "${TASK_NAME}" logon task to remove`);
1457
+ async function uninstall() {
1458
+ const task = startup();
1459
+ out((await task.remove()) ? `removed ${task.name}` : `no ${task.name} to remove`);
1298
1460
  }
1299
1461
  // --until: 17:00 (the next one), tomorrow, a day such as monday, or a date and time.
1300
1462
  function untilOf(text) {