@detergent-software/conductor 3.0.0-dev.4 → 3.0.0-dev.41

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 (214) hide show
  1. package/CHANGELOG.md +352 -0
  2. package/README.md +274 -124
  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 +15 -10
  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/claude.js +48 -2
  12. package/build/claude.js.map +1 -1
  13. package/build/cli.js +225 -71
  14. package/build/cli.js.map +1 -1
  15. package/build/clones.js +92 -0
  16. package/build/clones.js.map +1 -0
  17. package/build/code-host-cli.js +172 -40
  18. package/build/code-host-cli.js.map +1 -1
  19. package/build/code-host-service.js +35 -2
  20. package/build/code-host-service.js.map +1 -1
  21. package/build/code-host.js +140 -16
  22. package/build/code-host.js.map +1 -1
  23. package/build/conditions.js +33 -16
  24. package/build/conditions.js.map +1 -1
  25. package/build/config.js +58 -28
  26. package/build/config.js.map +1 -1
  27. package/build/console.js +43 -27
  28. package/build/console.js.map +1 -1
  29. package/build/credentials.js +86 -54
  30. package/build/credentials.js.map +1 -1
  31. package/build/daemon.js +57 -66
  32. package/build/daemon.js.map +1 -1
  33. package/build/file-lock.js +155 -40
  34. package/build/file-lock.js.map +1 -1
  35. package/build/follow-ups.js +2 -2
  36. package/build/follow-ups.js.map +1 -1
  37. package/build/help.js +237 -82
  38. package/build/help.js.map +1 -1
  39. package/build/history.js +10 -4
  40. package/build/history.js.map +1 -1
  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 +46 -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 +6 -5
  70. package/build/in-process.js.map +1 -1
  71. package/build/inbox.js +57 -39
  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 +4 -3
  92. package/build/lib/providers/index.js.map +1 -1
  93. package/build/lib/providers/npm/stage.js +28 -5
  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 +131 -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 +323 -0
  112. package/build/npm-reads.js.map +1 -0
  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 +107 -51
  123. package/build/profile.js.map +1 -1
  124. package/build/project-check-cli.js +19 -19
  125. package/build/project-check-cli.js.map +1 -1
  126. package/build/project-check.js +194 -108
  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 +104 -47
  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 +89 -13
  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 -5
  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 +44 -20
  175. package/build/watchers/ado.js.map +1 -1
  176. package/build/watchers/github.js +21 -6
  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/npm.js +6 -4
  187. package/build/watchers/npm.js.map +1 -1
  188. package/build/watchers/source.js +5 -1
  189. package/build/watchers/source.js.map +1 -1
  190. package/build/webhook.js +80 -0
  191. package/build/webhook.js.map +1 -0
  192. package/build/why.js +1 -1
  193. package/build/why.js.map +1 -1
  194. package/build/worktrees.js +56 -93
  195. package/build/worktrees.js.map +1 -1
  196. package/package.json +61 -61
  197. package/plugin/.claude-plugin/plugin.json +1 -1
  198. package/plugin/hooks/model.ts +46 -6
  199. package/plugin/hooks/register.tsx +41 -2
  200. package/plugin/skills/conductor-console/SKILL.md +26 -27
  201. package/plugin/skills/work-item/SKILL.md +58 -54
  202. package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
  203. package/plugin/skills/work-item/trackers/github-issues.md +1 -1
  204. package/plugin/skills/work-item/trackers/linear.md +2 -2
  205. package/plugin/types/index.d.ts +1 -1
  206. package/protocol/PROTOCOL.md +3 -9
  207. package/build/checkout-sync.js +0 -249
  208. package/build/checkout-sync.js.map +0 -1
  209. package/build/credential.js +0 -66
  210. package/build/credential.js.map +0 -1
  211. package/build/startup.js +0 -92
  212. package/build/startup.js.map +0 -1
  213. package/build/tab.js +0 -32
  214. 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, 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";
@@ -24,32 +28,32 @@ import { rotatingLog } from "./log.js";
24
28
  import { fleetWorkerSchema, storedFleet, workerByRef, workerLine } from "./manager.js";
25
29
  import { noteCommand } from "./note.js";
26
30
  import { isQuiet, notifyLines, notifyStatusSchema, parseUntil, testResultSchema } from "./notify.js";
31
+ import { sharedNpmReads } from "./npm-reads.js";
27
32
  import { overrideViewSchema, PAUSE } from "./overrides.js";
28
33
  import { packageInfo } from "./package-info.js";
29
34
  import { conductorPaths } from "./paths.js";
30
35
  import { refreshConductorPlugin } from "./plugin.js";
31
- import { projectCheckCommand } from "./project-check-cli.js";
36
+ import { profileLines, projectProfile } from "./profile-layers.js";
37
+ import { projectCheckCommand, projectCheckReport } from "./project-check-cli.js";
32
38
  import { noProject } from "./project-check.js";
33
39
  import { pushoverCredential, pushoverSender } from "./pushover.js";
34
- import { registerProject } from "./registry.js";
40
+ import { cloneProject, registerProject } from "./registry.js";
35
41
  import { rulesLines, rulesService, rulesTestResultSchema, rulesViewSchema, testLines, } from "./rules-service.js";
36
42
  import { resolveUntil } from "./rules.js";
37
43
  import { outputOf, run } from "./run.js";
38
44
  import { queueLines, queueViewSchema } from "./scheduler.js";
39
45
  import { awaitStart, serveDetached, ServeStartError } from "./serve.js";
40
46
  import { localDay } from "./spend.js";
41
- import { installStartup, removeStartup, runStartup, startupRunsCli, startupSupervises, TASK_NAME } from "./startup.js";
42
47
  import { answerStopRequest, dropStaleStopRequest, pendingStopRequest, STOP_POLL_MS, stopSupervisor, } from "./stop-request.js";
43
48
  import { inboxItemSchema, openStore, projectSchema, waitSchema, workerSchema, } from "./store.js";
44
49
  import { RESTART_ENV, restartInfoSchema, SERVE_LOCKED_EXIT, serveChild, supervise, SUPERVISE_POLICY, } from "./supervise.js";
45
- import { openWindowsTerminal } from "./tab.js";
46
50
  import { commentsCommand, notesCommand, searchCommand } from "./tracker-reads-cli.js";
47
51
  import { trainCommand } from "./train-cli.js";
48
52
  import { parseTag, restartDaemon, tagVersion, upgrade } from "./upgrade.js";
49
53
  import { waitCommand } from "./wait.js";
50
54
  import { whyLines, whySchema } from "./why.js";
51
55
  const STOP_WAIT_MS = 10_000;
52
- // 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.
53
57
  const START_WAIT_MS = 30_000;
54
58
  const NPM_INSTALL_TIMEOUT_MS = 5 * 60_000;
55
59
  // A launch makes a worktree (pnpm install) before the session starts.
@@ -61,6 +65,9 @@ const RETIRE_TIMEOUT_MS = 12 * 60_000;
61
65
  const ATTACH_TIMEOUT_MS = 9 * 60_000;
62
66
  // Approve and revise ask Linear twice (the issue, then the comment), each within 20 s.
63
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;
64
71
  const projectsBody = z.object({ projects: z.array(projectSchema) });
65
72
  const addBody = z.object({ created: z.boolean(), project: projectSchema });
66
73
  const removeBody = z.object({ project: projectSchema, resolved: z.array(inboxItemSchema) });
@@ -85,23 +92,43 @@ const retireBody = z.object({
85
92
  work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
86
93
  worker: workerSchema,
87
94
  });
88
- const attachBody = z.object({ resumed: z.boolean(), tab: z.string().nullable(), worker: workerSchema });
95
+ // hint: from ATK-529 on; an older daemon sends none.
96
+ const attachBody = z.object({
97
+ hint: z.string().nullable().default(null),
98
+ resumed: z.boolean(),
99
+ tab: z.string().nullable(),
100
+ worker: workerSchema,
101
+ });
89
102
  const inboxBody = z.object({ items: z.array(inboxItemSchema) });
90
103
  const answerBody = z.object({
91
104
  comment: z.object({ id: z.string(), url: z.string().nullable() }).nullable(),
92
105
  item: inboxItemSchema,
93
106
  waits: z.array(waitSchema),
94
107
  });
108
+ const reviseAnswer = z.object({
109
+ revise: z.object({
110
+ comment: z.object({ id: z.string(), url: z.string().nullable() }),
111
+ item: z.string(),
112
+ landed: z.boolean(),
113
+ pending: z.string().nullable(),
114
+ revised: z.boolean(),
115
+ woken: z.number(),
116
+ worker: z.boolean(),
117
+ }),
118
+ });
95
119
  const answerRefusal = z.object({
96
120
  details: z.array(z.string()).optional(),
97
121
  error: z.string(),
98
122
  posted: z.enum(['no', 'unknown']).optional(),
99
123
  });
100
- async function addProject(paths, path) {
101
- const absolute = resolve(path);
124
+ // `conduct project add <url | path> [--name <name>]`: the clone is made here, where its progress
125
+ // shows and no API timeout applies, then the daemon registers it (or the store, with none running).
126
+ // `next`: false for `conduct init`, which says what comes next itself.
127
+ async function addProject(paths, source, name, next = true) {
128
+ const made = await cloneProject(paths, source, { ...(name === undefined ? {} : { name }), say: out });
102
129
  let result;
103
130
  try {
104
- result = addBody.parse(await expectOk(callApi(paths, { body: { path: absolute }, method: 'POST', path: '/projects' })));
131
+ result = addBody.parse(await expectOk(callApi(paths, { body: { name: made.name }, method: 'POST', path: '/projects' })));
105
132
  }
106
133
  catch (error) {
107
134
  if (!(error instanceof NotRunningError))
@@ -109,14 +136,25 @@ async function addProject(paths, path) {
109
136
  // No daemon: write the store directly. The daemon reads it when it starts.
110
137
  // Its rules are checked against config.json here too, as the daemon would.
111
138
  const check = rulesService({ config: loadConfig(paths.config), listProjects: () => [], paths }).checkProfile;
112
- result = withStore(paths, (store) => registerProject(store, absolute, check));
139
+ result = withStore(paths, (store) => registerProject(store, paths, made.name, check));
113
140
  }
114
141
  out(`${result.created ? 'registered' : 'updated'} ${projectLine(result.project).trim()}`);
142
+ out(made.cloned ? `cloned ${made.url} into ${made.clone}` : `fetched ${made.clone}, the clone it has`);
143
+ out(`its machine profile, which wins over the repo's: ${result.project.profilePath}`);
144
+ // Claude Code trusts a worktree by its repository, and nobody has run Claude in a new clone yet.
145
+ if (made.cloned && next)
146
+ out(`next: ${trustStep(made.clone)}, then \`conduct project check ${made.name}\``);
147
+ return made;
115
148
  }
116
- // `conduct approve|revise|done`: Jason's answer to an inbox item, by id, or for approve and revise
117
- // by the item whose plan waits.
149
+ // `conduct approve|revise|done`: the operator's answer to an inbox item, by id, or for approve and revise
150
+ // by the item whose plan waits. A revise of an item with no plan waiting revises its approved plan.
118
151
  async function answer(paths, ref, request) {
119
152
  const target = request.action === 'done' ? inboxId(ref) : await planItem(paths, ref);
153
+ if (target === null) {
154
+ if (request.action === 'revise')
155
+ return reviseApproved(paths, ref, request.text);
156
+ throw new ConductorError(`no plan of ${ref} waits in the inbox`, ['`conduct inbox` lists what waits']);
157
+ }
120
158
  const { body, status } = await callApi(paths, {
121
159
  body: request,
122
160
  method: 'POST',
@@ -150,6 +188,8 @@ async function attach(paths, ref) {
150
188
  out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
151
189
  if (result.tab)
152
190
  out(`no tab (${result.tab}); open it yourself: claude attach ${result.worker.handle ?? worker.name}`);
191
+ else if (result.hint)
192
+ out(`to see its tab: ${result.hint}`);
153
193
  }
154
194
  // The pid of the daemon once it answers /health, for upgrade's restart. failure: the detached
155
195
  // serve's spawn error, which ends the wait early.
@@ -192,7 +232,7 @@ async function consoleCommand(paths, options) {
192
232
  await claimConsole(paths, options.force);
193
233
  const child = spawn('claude', consoleArgs(paths.plugin), {
194
234
  cwd: paths.console,
195
- env: { ...process.env, CONDUCTOR_CONSOLE: '1' },
235
+ env: consoleEnv(process.env),
196
236
  stdio: 'inherit',
197
237
  });
198
238
  // Ctrl+C belongs to the session; this process only waits for it to end.
@@ -207,40 +247,33 @@ async function consoleCommand(paths, options) {
207
247
  });
208
248
  return;
209
249
  }
210
- if (process.platform !== 'win32') {
211
- throw new ConductorError('conduct console opens a Windows Terminal tab', [
212
- 'run `conduct console --here` in a terminal of your own',
213
- ]);
214
- }
215
- // A tab starts with Windows Terminal's environment, not this one's.
216
- if (process.env.CONDUCTOR_HOME) {
217
- throw new ConductorError('a console tab would not see CONDUCTOR_HOME', [
218
- 'run `conduct console --here` in a terminal where it is set',
219
- ]);
220
- }
221
- const args = consoleTabArgs(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)));
250
+ const where = terminal({ choice: loadConfig(paths.config).workers.terminal });
251
+ const tab = consoleTab(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)), process.env);
222
252
  await claimConsole(paths, options.force);
223
- const result = await openWindowsTerminal(args, paths.console);
224
- if (result.code !== 0)
225
- throw new ConductorError('could not open the console tab', [
226
- outputOf(result, 500),
253
+ const failed = await where.open(tab);
254
+ if (failed !== null)
255
+ throw new ConductorError(`could not open the console tab in ${where.name}`, [
256
+ failed,
227
257
  'run `conduct console --here --force` in a terminal instead',
228
258
  ]);
229
- out(`opened the console in ${paths.console} (Windows Terminal window "${CONSOLE_WINDOW}")`);
259
+ const hint = where.attachHint(CONSOLE_WINDOW);
260
+ out(`opened the console in ${paths.console} (${hint === null ? `${where.name} window "${CONSOLE_WINDOW}"` : `${where.name}: ${hint}`})`);
230
261
  }
231
262
  // How `conduct start` and upgrade's restart start the daemon detached from this process.
232
263
  function daemonStarter(paths, cliPath) {
233
264
  let failure = () => undefined;
265
+ const task = startup();
234
266
  return {
235
267
  awaitDaemon: () => awaitDaemon(paths, failure),
236
- runTask: runStartup,
268
+ runTask: () => task.run(),
237
269
  serveDetached: () => {
238
270
  // --fail-fast: a first serve that can't start ends the supervisor, so the start says why at once.
239
271
  failure = serveDetached(paths.home, process.execPath, [cliPath, 'supervise', '--fail-fast']);
240
272
  },
241
- // The logon task's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
242
- taskRunsCli: () => !process.env.CONDUCTOR_HOME && startupRunsCli(cliPath),
243
- taskSupervises: startupSupervises,
273
+ taskName: task.name,
274
+ // The startup's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
275
+ taskRunsCli: async () => !process.env.CONDUCTOR_HOME && (await task.runsCli(cliPath)),
276
+ taskSupervises: () => task.supervises(),
244
277
  };
245
278
  }
246
279
  // `conduct escalations done`: the console reports back on an escalation.
@@ -365,11 +398,20 @@ function inboxId(ref) {
365
398
  throw new ConductorError(`"${ref}" is not an inbox item id`, ['`conduct inbox` lists them as #<id>']);
366
399
  return Number(id);
367
400
  }
368
- function install() {
401
+ // `conduct install-startup`: the machine's startup (host/startup.ts) runs this build's supervise.
402
+ async function install() {
369
403
  const cliPath = realpathSync(fileURLToPath(import.meta.url));
370
- installStartup({ cliPath, nodePath: process.execPath });
371
- out(`registered the "${TASK_NAME}" logon task: ${process.execPath} ${cliPath} supervise`);
372
- if (cliPath.includes(`${sep}.claude${sep}worktrees${sep}`)) {
404
+ const task = startup();
405
+ // Less what `pnpm exec` or `npm run` put first, which a worktree's removal takes away.
406
+ const path = (process.env.PATH ?? '')
407
+ .split(delimiter)
408
+ .filter((dir) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/.test(dir))
409
+ .join(delimiter);
410
+ const notes = await task.install({ cliPath, nodePath: process.execPath, path });
411
+ out(`registered ${task.name}: ${process.execPath} ${cliPath} supervise`);
412
+ for (const note of notes)
413
+ out(note);
414
+ if (/[\\/](?:\.claude|\.conductor[\\/]projects[\\/][^\\/]+)[\\/]worktrees[\\/]/.test(cliPath)) {
373
415
  out('warning: that build is in a worktree, which goes away; run install-startup from the main checkout');
374
416
  }
375
417
  }
@@ -404,7 +446,7 @@ async function listProjects(paths, json) {
404
446
  if (json)
405
447
  return out(JSON.stringify(projects, null, 2));
406
448
  if (projects.length === 0)
407
- return out('no projects; add one with `conduct project add <path>`');
449
+ return out('no projects; add one with `conduct project add <url | path>`');
408
450
  projects.forEach((p) => out(projectLine(p)));
409
451
  }
410
452
  async function main(argv) {
@@ -412,7 +454,7 @@ async function main(argv) {
412
454
  if (argv[0] === 'wait')
413
455
  return argv.slice(1).some(isHelpFlag) ? help('wait') : wait(argv.slice(1));
414
456
  // `revise` takes free text, which option parsing would cut at a word such as --force. Every word
415
- // after the item is the text, quoted or not: a cut-off instruction would be posted as Jason's.
457
+ // after the item is the text, quoted or not: a cut-off instruction would be posted as the operator's.
416
458
  if (argv[0] === 'revise') {
417
459
  // A lone help flag as the text is a call for help: posted, it would be a revision of "--help".
418
460
  if (isHelpFlag(argv[1]) || (argv.length === 3 && isHelpFlag(argv[2])))
@@ -437,19 +479,22 @@ async function main(argv) {
437
479
  throw usageError('escalations');
438
480
  return escalationDone(conductorPaths(), ref, { ask, text });
439
481
  }
440
- // The first word that names a command: an option's value (--project atk) comes before it at times.
482
+ // The first word that names a command: an option's value (--project web) comes before it at times.
441
483
  const { positionals, values } = parsing(argv.find((word) => isCommand(word)), () => parseArgs({
442
484
  allowPositionals: true,
443
485
  args: argv,
444
486
  options: {
445
487
  'allow-downgrade': { type: 'boolean' },
446
488
  area: { type: 'string' },
489
+ base: { type: 'string' },
447
490
  'blocked-by': { multiple: true, type: 'string' },
448
491
  'body-file': { type: 'string' },
492
+ branch: { type: 'string' },
449
493
  channel: { type: 'string' },
450
494
  comments: { type: 'boolean' },
451
495
  conductor: { type: 'string' },
452
496
  console: { type: 'boolean' },
497
+ count: { type: 'string' },
453
498
  'dry-run': { type: 'boolean' },
454
499
  'fail-fast': { type: 'boolean' },
455
500
  files: { type: 'string' },
@@ -457,20 +502,24 @@ async function main(argv) {
457
502
  for: { type: 'string' },
458
503
  force: { type: 'boolean' },
459
504
  fresh: { type: 'boolean' },
505
+ grep: { type: 'string' },
460
506
  help: { short: 'h', type: 'boolean' },
461
507
  here: { type: 'boolean' },
462
508
  human: { type: 'boolean' },
463
509
  item: { type: 'string' },
464
510
  json: { type: 'boolean' },
511
+ key: { type: 'string' },
465
512
  kind: { type: 'string' },
466
513
  last: { type: 'boolean' },
467
514
  mode: { type: 'string' },
515
+ name: { type: 'string' },
468
516
  'no-worktree': { type: 'boolean' },
469
517
  owner: { type: 'string' },
470
518
  phase: { type: 'string' },
471
519
  probe: { type: 'boolean' },
472
520
  project: { type: 'string' },
473
521
  prompt: { type: 'string' },
522
+ query: { type: 'string' },
474
523
  raise: { type: 'boolean' },
475
524
  rearm: { type: 'boolean' },
476
525
  reason: { type: 'string' },
@@ -478,18 +527,23 @@ async function main(argv) {
478
527
  remove: { type: 'boolean' },
479
528
  repo: { type: 'string' },
480
529
  requeue: { type: 'string' },
530
+ shared: { type: 'boolean' },
481
531
  since: { type: 'string' },
482
532
  slots: { type: 'string' },
483
533
  tag: { type: 'string' },
534
+ task: { type: 'string' },
535
+ team: { type: 'string' },
484
536
  text: { type: 'string' },
485
537
  tier: { type: 'string' },
486
538
  title: { type: 'string' },
539
+ tracker: { type: 'string' },
487
540
  until: { type: 'string' },
488
541
  version: { short: 'v', type: 'boolean' },
489
542
  wait: { type: 'string' },
490
543
  'waits-release': { type: 'boolean' },
491
544
  weight: { type: 'string' },
492
545
  worker: { type: 'string' },
546
+ yes: { type: 'boolean' },
493
547
  },
494
548
  }));
495
549
  const paths = conductorPaths();
@@ -516,7 +570,7 @@ async function main(argv) {
516
570
  case 'branch':
517
571
  if (sub === undefined || arg === undefined)
518
572
  throw usageError('branch');
519
- process.exitCode = await branchCommand(sub, arg, item, verbIo());
573
+ process.exitCode = await branchCommand(paths, sub, arg, item, verbIo());
520
574
  return;
521
575
  case 'brief':
522
576
  if (sub === undefined)
@@ -530,7 +584,7 @@ async function main(argv) {
530
584
  case 'checkout':
531
585
  if (sub === undefined || arg === undefined || fourth === undefined)
532
586
  throw usageError('checkout');
533
- process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
587
+ process.exitCode = await checkoutCommand(paths, sub, arg, fourth, item, verbIo());
534
588
  return;
535
589
  case 'comments': {
536
590
  if (sub === undefined || arg !== undefined)
@@ -559,10 +613,10 @@ async function main(argv) {
559
613
  // /reload-plugins after an upgrade) starts this package's new build, in the console's env.
560
614
  return events(paths, values.follow === true, values.since, values.console === true || process.env.CONDUCTOR_CONSOLE === '1');
561
615
  case 'file':
562
- if (sub !== undefined || !values.title || !values['body-file'] || !values.area)
616
+ if (sub !== undefined || !values.title || !values['body-file'])
563
617
  throw usageError('file');
564
618
  process.exitCode = await fileCommand(paths, {
565
- area: values.area,
619
+ area: values.area ?? '',
566
620
  blockedBy: values['blocked-by'] ?? [],
567
621
  bodyFile: values['body-file'],
568
622
  human: values.human === true,
@@ -592,6 +646,26 @@ async function main(argv) {
592
646
  });
593
647
  case 'inbox':
594
648
  return inbox(paths, values.json === true);
649
+ case 'init':
650
+ if (arg !== undefined)
651
+ throw usageError('init');
652
+ process.exitCode = await initProject({
653
+ add: (checkout, name) => addProject(paths, checkout, name, false),
654
+ ask: terminalAsk(),
655
+ check: (name) => projectCheckReport(paths, name, { worktree: true }, { err: (line) => process.stderr.write(`${line}\n`) }),
656
+ out,
657
+ paths,
658
+ }, sub ?? '.', {
659
+ shared: values.shared === true,
660
+ yes: values.yes === true,
661
+ ...(values.base === undefined ? {} : { base: values.base }),
662
+ ...(values.key === undefined ? {} : { key: values.key }),
663
+ ...(values.name === undefined ? {} : { name: values.name }),
664
+ ...(values.query === undefined ? {} : { query: values.query }),
665
+ ...(values.team === undefined ? {} : { team: values.team }),
666
+ ...(values.tracker === undefined ? {} : { tracker: values.tracker }),
667
+ });
668
+ return;
595
669
  case 'install-startup':
596
670
  return values.remove ? uninstall() : install();
597
671
  case 'land': {
@@ -611,6 +685,7 @@ async function main(argv) {
611
685
  ...repo,
612
686
  ...(mode === undefined ? {} : { mode }),
613
687
  ...(tier === undefined ? {} : { tier }),
688
+ ...(values.requeue === undefined ? {} : { requeue: wholeNumber('--requeue', values.requeue) }),
614
689
  }, verbIo());
615
690
  return;
616
691
  }
@@ -638,7 +713,7 @@ async function main(argv) {
638
713
  throw usageError('mode');
639
714
  if (tier !== 'S' && tier !== 'M' && tier !== 'L')
640
715
  throw new ConductorError('--tier takes S, M or L');
641
- process.exitCode = await modeCommand({ files: values.files, tier }, verbIo());
716
+ process.exitCode = await modeCommand(paths, { files: values.files, tier }, verbIo());
642
717
  return;
643
718
  }
644
719
  case 'note':
@@ -670,10 +745,20 @@ async function main(argv) {
670
745
  return notifyTest(paths, { channel: values.channel, kind: values.kind, raise: values.raise === true });
671
746
  throw usageError('notify');
672
747
  case 'pr':
748
+ if (sub === 'abandon' && arg !== undefined) {
749
+ if (values.reason === undefined)
750
+ throw usageError('pr');
751
+ process.exitCode = await prAbandonCommand(paths, { force: values.force === true, pr: wholeNumber('<pr>', arg), reason: values.reason, ...item, ...repo }, verbIo());
752
+ return;
753
+ }
673
754
  if (sub === 'threads' && arg !== undefined) {
674
755
  process.exitCode = await prThreadsCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
675
756
  return;
676
757
  }
758
+ if (sub === 'status' && arg !== undefined) {
759
+ process.exitCode = await prStatusCommand(paths, { pr: wholeNumber('<pr>', arg), ...repo }, verbIo());
760
+ return;
761
+ }
677
762
  if (sub !== 'create' || values.title === undefined || values['body-file'] === undefined)
678
763
  throw usageError('pr');
679
764
  process.exitCode = await prCreateCommand(paths, { bodyFile: values['body-file'], dryRun: values['dry-run'] === true, title: values.title, ...item, ...repo }, verbIo());
@@ -687,9 +772,21 @@ async function main(argv) {
687
772
  case 'ports':
688
773
  process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
689
774
  return;
775
+ case 'profile': {
776
+ if (sub === undefined || arg !== undefined)
777
+ throw usageError('profile');
778
+ // Resolved here from the machine file and the clone, as the daemon resolves it.
779
+ const layered = projectProfile(paths, sub);
780
+ if (values.json === true)
781
+ return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
782
+ profileLines(sub, layered).forEach((line) => out(line));
783
+ return;
784
+ }
690
785
  case 'project':
691
- if (sub === 'add' && arg !== undefined)
692
- return addProject(paths, arg);
786
+ if (sub === 'add' && arg !== undefined) {
787
+ await addProject(paths, arg, values.name);
788
+ return;
789
+ }
693
790
  if (sub === 'remove' && arg !== undefined)
694
791
  return removeProject(paths, arg);
695
792
  if (sub === 'list')
@@ -730,6 +827,7 @@ async function main(argv) {
730
827
  ...item,
731
828
  ...repo,
732
829
  ...(values.for === undefined ? {} : { for: values.for }),
830
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
733
831
  ...(values.requeue === undefined ? {} : { requeue: values.requeue }),
734
832
  }, verbIo());
735
833
  return;
@@ -755,6 +853,19 @@ async function main(argv) {
755
853
  case 'supervise':
756
854
  process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
757
855
  return;
856
+ case 'timeline':
857
+ if (arg !== undefined)
858
+ throw usageError('timeline');
859
+ process.exitCode = await timelineCommand(paths, {
860
+ ...repo,
861
+ ...(sub === undefined ? {} : { run: sub }),
862
+ ...(values.branch === undefined ? {} : { branch: values.branch }),
863
+ ...(values.count === undefined ? {} : { count: values.count }),
864
+ ...(values.for === undefined ? {} : { for: values.for }),
865
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
866
+ ...(values.task === undefined ? {} : { task: values.task }),
867
+ }, verbIo());
868
+ return;
758
869
  case 'train':
759
870
  return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
760
871
  case 'unpause':
@@ -810,7 +921,7 @@ async function notifyTest(paths, options) {
810
921
  ...(options.kind ? { kind: options.kind } : {}),
811
922
  raise: options.raise,
812
923
  };
813
- // Pushover can take its 15 s, after a Credential Manager read.
924
+ // Pushover can take its 15 s, after a secret store read.
814
925
  const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
815
926
  if (options.raise) {
816
927
  const { item } = z.object({ item: inboxItemSchema }).parse(answer);
@@ -888,33 +999,35 @@ function parsing(command, parse) {
888
999
  : new ConductorError(message, ['run `conduct help` for the commands']);
889
1000
  }
890
1001
  }
891
- // The open plan item of `ref`: the item (ATK-312) whose plan waits, or an inbox id written #12. A
892
- // bare number is refused: it could be an issue's number, and the answer is posted as Jason's.
1002
+ // The open plan item of `ref`: the item (ENG-312) whose plan waits, or an inbox id written #12; null
1003
+ // when no plan of the item waits. A bare number is refused: it could be an issue's number, and the
1004
+ // answer is posted as the operator's.
893
1005
  async function planItem(paths, ref) {
894
1006
  if (ref.startsWith('#'))
895
1007
  return inboxId(ref);
1008
+ const items = await openItems(paths);
896
1009
  if (/^\d+$/.test(ref)) {
897
- throw new ConductorError(`"${ref}" is ambiguous`, [
898
- `name the item, such as ATK-${ref}, or the inbox id as #${ref}`,
899
- ]);
1010
+ // The waiting plans whose key ends in the number, else a placeholder: the project's key isn't known here.
1011
+ const keys = (items ?? []).flatMap((i) => (i.kind === 'plan' && i.item?.endsWith(`-${ref}`) ? [i.item] : []));
1012
+ const item = keys.length > 0 ? keys.join(' or ') : `<KEY>-${ref}`;
1013
+ throw new ConductorError(`"${ref}" is ambiguous`, [`name the item, such as ${item}, or the inbox id as #${ref}`]);
900
1014
  }
901
- const items = await openItems(paths);
902
1015
  if (items === null)
903
1016
  throw noInbox();
904
1017
  const plans = items.filter((i) => i.kind === 'plan' && i.item?.toUpperCase() === ref.toUpperCase());
905
1018
  if (plans.length === 1 && plans[0])
906
1019
  return plans[0].id;
907
1020
  if (plans.length === 0)
908
- throw new ConductorError(`no plan of ${ref} waits in the inbox`, ['`conduct inbox` lists what waits']);
1021
+ return null;
909
1022
  throw new ConductorError(`several plans of ${ref} wait`, plans.map((p) => `#${p.id} in ${p.project ?? '?'}: pass #${p.id}`));
910
1023
  }
911
1024
  function projectLine(p) {
912
1025
  const state = p.problem
913
- ? `${p.problem}; the daemon reads ${p.profilePath} again when it starts or the file is saved`
1026
+ ? `${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`
914
1027
  : p.enabled
915
1028
  ? 'enabled'
916
1029
  : 'disabled';
917
- return ` ${p.name} ${p.root} (protocol ${p.protocol}, ${state})`;
1030
+ return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
918
1031
  }
919
1032
  // The registered projects: from the daemon, or from the store when it isn't running.
920
1033
  async function projectsNow(paths) {
@@ -959,7 +1072,7 @@ async function removeProject(paths, name) {
959
1072
  });
960
1073
  }
961
1074
  const { project, resolved } = result;
962
- out(`removed ${project.name} (${project.root})`);
1075
+ out(`removed ${project.name} (${project.clone})`);
963
1076
  for (const item of resolved)
964
1077
  out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
965
1078
  }
@@ -1009,6 +1122,45 @@ async function retire(paths, ref, force) {
1009
1122
  out(`retired ${worker.name}`);
1010
1123
  steps.forEach((s) => out(` ${s}`));
1011
1124
  }
1125
+ // `conduct revise <item>` with no plan of the item in the inbox: the daemon posts the revise on an
1126
+ // approved plan and wakes its worker at once, as its revise sweep would (ATK-664).
1127
+ async function reviseApproved(paths, item, text) {
1128
+ const { body, status } = await callApi(paths, {
1129
+ body: { text },
1130
+ method: 'POST',
1131
+ path: `/items/${encodeURIComponent(item)}/revise`,
1132
+ timeoutMs: REVISE_TIMEOUT_MS,
1133
+ });
1134
+ if (status !== 200) {
1135
+ const refusal = answerRefusal.safeParse(body).data;
1136
+ const details = [...(refusal?.details ?? [])];
1137
+ if (status === 502 && refusal?.posted === 'unknown')
1138
+ details.push('the comment may be on the item already: look before answering again');
1139
+ // A conductor from before ATK-664 has no such route.
1140
+ if (status === 404)
1141
+ throw new ConductorError(`no plan of ${item} waits in the inbox`, [
1142
+ '`conduct inbox` lists what waits',
1143
+ 'a conductor started before ATK-664 revises no approved plan: restart it for that',
1144
+ ]);
1145
+ throw new ConductorError(refusal?.error ?? `the API answered ${status}`, details);
1146
+ }
1147
+ const r = reviseAnswer.parse(body).revise;
1148
+ out(`asked for a revision of ${r.item}'s approved plan: ${r.comment.url ?? r.comment.id}`);
1149
+ out(reviseOutcome(r));
1150
+ }
1151
+ // What became of a revise of an approved plan after its comment, in one line.
1152
+ function reviseOutcome(r) {
1153
+ const none = 'no worker is on it: one launched on it reads the revise in its brief';
1154
+ if (r.pending !== null)
1155
+ return `nothing recorded yet (${r.pending}): ${r.worker ? 'the revise sweep records it and wakes its worker within 5 minutes' : none}`;
1156
+ if (!r.revised)
1157
+ return 'a revise of its plan was recorded already: its worker re-plans on the first one';
1158
+ if (r.woken > 0)
1159
+ return 'woke its worker to re-plan';
1160
+ if (r.landed)
1161
+ return 'its PR has merged: its worker learns of it at its next note, for a follow-up PR';
1162
+ return r.worker ? 'its worker is running: its next note is held back, and it re-plans' : none;
1163
+ }
1012
1164
  // `conduct rules`: from the daemon, or straight from the files when it isn't running.
1013
1165
  async function rules(paths, json) {
1014
1166
  const view = await rulesAnswer(paths, { method: 'GET', path: '/rules' }, (body) => rulesViewSchema.parse(body), (service) => service.view());
@@ -1089,7 +1241,7 @@ async function shutdownDaemon(paths) {
1089
1241
  if (readEndpoint(paths) !== null) {
1090
1242
  throw new ConductorError(`the conductor (pid ${pid ?? '?'}) did not stop within ${STOP_WAIT_MS / 1000} s`);
1091
1243
  }
1092
- // The port file goes before the process ends; a start (the logon task's especially) waits for it.
1244
+ // The port file goes before the process ends; a start (the logon startup's especially) waits for it.
1093
1245
  const exitDeadline = Date.now() + STOP_WAIT_MS;
1094
1246
  while (pid !== null && processAlive(pid) && Date.now() < exitDeadline)
1095
1247
  await sleep(100);
@@ -1105,7 +1257,7 @@ async function start(paths) {
1105
1257
  const pid = readPid(paths.pid);
1106
1258
  if (pid !== null && (await ping(paths, pid)))
1107
1259
  return out(`the conductor is running (pid ${pid})`);
1108
- // A daemon that is still starting (the logon task's, say) holds the lock: wait for it rather than
1260
+ // A daemon that is still starting (the logon startup's, say) holds the lock: wait for it rather than
1109
1261
  // start a second one, which would exit on that lock. A pid a reboot reused gets a start after.
1110
1262
  if (pid !== null && processAlive(pid)) {
1111
1263
  try {
@@ -1153,7 +1305,7 @@ async function status(paths, json) {
1153
1305
  if (json)
1154
1306
  return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
1155
1307
  out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
1156
- out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
1308
+ out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
1157
1309
  projects.forEach((p) => out(projectLine(p)));
1158
1310
  out(workers.length === 0 ? 'no workers' : 'workers:');
1159
1311
  workers.forEach((w) => out(workerLine(w)));
@@ -1191,7 +1343,7 @@ async function stopDaemon(paths) {
1191
1343
  out('the conductor is not running');
1192
1344
  return false;
1193
1345
  case 'supervisor': {
1194
- // It answers just before it ends; a start (the logon task's especially) needs it gone.
1346
+ // It answers just before it ends; a start (the logon startup's especially) needs it gone.
1195
1347
  const deadline = Date.now() + STOP_WAIT_MS;
1196
1348
  while (processAlive(stop.pid) && Date.now() < deadline)
1197
1349
  await sleep(100);
@@ -1200,7 +1352,7 @@ async function stopDaemon(paths) {
1200
1352
  }
1201
1353
  }
1202
1354
  }
1203
- // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon task runs
1355
+ // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
1204
1356
  // it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
1205
1357
  // while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the
1206
1358
  // defaults: no quiet hours, the default credential.
@@ -1222,7 +1374,7 @@ async function superviseCommand(paths, failFast) {
1222
1374
  rotatingLog(paths.log, config().log).log(...line);
1223
1375
  }
1224
1376
  catch {
1225
- // Nowhere else to say it: under the logon task nobody reads stderr.
1377
+ // Nowhere else to say it: under the logon startup nobody reads stderr.
1226
1378
  }
1227
1379
  },
1228
1380
  };
@@ -1298,8 +1450,9 @@ async function superviseCommand(paths, failFast) {
1298
1450
  }
1299
1451
  return code;
1300
1452
  }
1301
- function uninstall() {
1302
- out(removeStartup() ? `removed the "${TASK_NAME}" logon task` : `no "${TASK_NAME}" logon task to remove`);
1453
+ async function uninstall() {
1454
+ const task = startup();
1455
+ out((await task.remove()) ? `removed ${task.name}` : `no ${task.name} to remove`);
1303
1456
  }
1304
1457
  // --until: 17:00 (the next one), tomorrow, a day such as monday, or a date and time.
1305
1458
  function untilOf(text) {
@@ -1307,7 +1460,8 @@ function untilOf(text) {
1307
1460
  }
1308
1461
  async function upgradeCommand(paths, tag, allowDowngrade) {
1309
1462
  const cliPath = realpathSync(fileURLToPath(import.meta.url));
1310
- const release = npmStage({});
1463
+ // npm's own settings, so the tag is read where `npm install --global` installs from (ATK-561).
1464
+ const release = npmStage(sharedNpmReads());
1311
1465
  await upgrade({ allowDowngrade, tag: parseTag(tag) }, {
1312
1466
  ...daemonStarter(paths, cliPath),
1313
1467
  cliPath,