@detergent-software/conductor 3.0.0-dev.3 → 3.0.0-dev.30

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 (171) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +201 -63
  3. package/build/brief-cli.js +1 -1
  4. package/build/brief-cli.js.map +1 -1
  5. package/build/broker-cli.js +24 -25
  6. package/build/broker-cli.js.map +1 -1
  7. package/build/broker.js +66 -92
  8. package/build/broker.js.map +1 -1
  9. package/build/claude.js +50 -7
  10. package/build/claude.js.map +1 -1
  11. package/build/cli.js +143 -63
  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 +116 -32
  16. package/build/code-host-cli.js.map +1 -1
  17. package/build/code-host-service.js +8 -0
  18. package/build/code-host-service.js.map +1 -1
  19. package/build/code-host.js +22 -11
  20. package/build/code-host.js.map +1 -1
  21. package/build/config.js +58 -14
  22. package/build/config.js.map +1 -1
  23. package/build/console.js +40 -24
  24. package/build/console.js.map +1 -1
  25. package/build/credentials.js +42 -29
  26. package/build/credentials.js.map +1 -1
  27. package/build/daemon.js +38 -22
  28. package/build/daemon.js.map +1 -1
  29. package/build/help.js +179 -42
  30. package/build/help.js.map +1 -1
  31. package/build/history.js +4 -2
  32. package/build/history.js.map +1 -1
  33. package/build/host/credential-manager.js +109 -0
  34. package/build/host/credential-manager.js.map +1 -0
  35. package/build/host/keychain.js +44 -0
  36. package/build/host/keychain.js.map +1 -0
  37. package/build/host/launchd.js +118 -0
  38. package/build/host/launchd.js.map +1 -0
  39. package/build/host/libsecret.js +40 -0
  40. package/build/host/libsecret.js.map +1 -0
  41. package/build/host/macos-terminal.js +63 -0
  42. package/build/host/macos-terminal.js.map +1 -0
  43. package/build/host/platform.js +4 -0
  44. package/build/host/platform.js.map +1 -0
  45. package/build/host/secrets.js +46 -0
  46. package/build/host/secrets.js.map +1 -0
  47. package/build/host/shell.js +26 -0
  48. package/build/host/shell.js.map +1 -0
  49. package/build/host/startup.js +26 -0
  50. package/build/host/startup.js.map +1 -0
  51. package/build/host/systemd.js +91 -0
  52. package/build/host/systemd.js.map +1 -0
  53. package/build/host/task-scheduler.js +78 -0
  54. package/build/host/task-scheduler.js.map +1 -0
  55. package/build/host/terminal.js +46 -0
  56. package/build/host/terminal.js.map +1 -0
  57. package/build/host/tmux.js +39 -0
  58. package/build/host/tmux.js.map +1 -0
  59. package/build/host/windows-terminal.js +46 -0
  60. package/build/host/windows-terminal.js.map +1 -0
  61. package/build/in-process.js +6 -5
  62. package/build/in-process.js.map +1 -1
  63. package/build/inbox.js +63 -17
  64. package/build/inbox.js.map +1 -1
  65. package/build/init.js +508 -0
  66. package/build/init.js.map +1 -0
  67. package/build/landing.js +86 -7
  68. package/build/landing.js.map +1 -1
  69. package/build/lib/providers/ado/code-host.js +5 -1
  70. package/build/lib/providers/ado/code-host.js.map +1 -1
  71. package/build/lib/providers/ado/pipelines.js +130 -22
  72. package/build/lib/providers/ado/pipelines.js.map +1 -1
  73. package/build/lib/providers/ado/trigger.js +5 -0
  74. package/build/lib/providers/ado/trigger.js.map +1 -1
  75. package/build/lib/providers/ado/writes.js +10 -1
  76. package/build/lib/providers/ado/writes.js.map +1 -1
  77. package/build/lib/providers/github/actions.js +90 -3
  78. package/build/lib/providers/github/actions.js.map +1 -1
  79. package/build/lib/providers/github/code-host.js +2 -0
  80. package/build/lib/providers/github/code-host.js.map +1 -1
  81. package/build/lib/providers/github/writes.js +24 -2
  82. package/build/lib/providers/github/writes.js.map +1 -1
  83. package/build/lib/providers/index.js +4 -3
  84. package/build/lib/providers/index.js.map +1 -1
  85. package/build/lib/providers/npm/stage.js +28 -5
  86. package/build/lib/providers/npm/stage.js.map +1 -1
  87. package/build/lib/providers/timing.js +10 -0
  88. package/build/lib/providers/timing.js.map +1 -0
  89. package/build/lib/providers/verdicts/pr.js +47 -6
  90. package/build/lib/providers/verdicts/pr.js.map +1 -1
  91. package/build/lib/providers/verdicts/runs.js +12 -2
  92. package/build/lib/providers/verdicts/runs.js.map +1 -1
  93. package/build/local-files.js +95 -0
  94. package/build/local-files.js.map +1 -0
  95. package/build/manager.js +171 -47
  96. package/build/manager.js.map +1 -1
  97. package/build/note.js +1 -1
  98. package/build/note.js.map +1 -1
  99. package/build/notify.js +101 -39
  100. package/build/notify.js.map +1 -1
  101. package/build/npm-reads.js +323 -0
  102. package/build/npm-reads.js.map +1 -0
  103. package/build/paths.js +44 -1
  104. package/build/paths.js.map +1 -1
  105. package/build/plugin.js +10 -0
  106. package/build/plugin.js.map +1 -1
  107. package/build/profile-fields.js +149 -0
  108. package/build/profile-fields.js.map +1 -0
  109. package/build/profile-layers.js +264 -0
  110. package/build/profile-layers.js.map +1 -0
  111. package/build/profile.js +70 -44
  112. package/build/profile.js.map +1 -1
  113. package/build/project-check-cli.js +18 -10
  114. package/build/project-check-cli.js.map +1 -1
  115. package/build/project-check.js +187 -98
  116. package/build/project-check.js.map +1 -1
  117. package/build/pushover.js +5 -5
  118. package/build/pushover.js.map +1 -1
  119. package/build/registry.js +195 -36
  120. package/build/registry.js.map +1 -1
  121. package/build/release-kinds.js +34 -0
  122. package/build/release-kinds.js.map +1 -0
  123. package/build/rule-actions.js +4 -2
  124. package/build/rule-actions.js.map +1 -1
  125. package/build/rules-service.js +101 -44
  126. package/build/rules-service.js.map +1 -1
  127. package/build/rules.js +15 -15
  128. package/build/rules.js.map +1 -1
  129. package/build/run.js +20 -7
  130. package/build/run.js.map +1 -1
  131. package/build/runs.js +200 -45
  132. package/build/runs.js.map +1 -1
  133. package/build/scheduler.js +11 -1
  134. package/build/scheduler.js.map +1 -1
  135. package/build/server.js +6 -25
  136. package/build/server.js.map +1 -1
  137. package/build/spend.js +4 -0
  138. package/build/spend.js.map +1 -1
  139. package/build/store.js +82 -25
  140. package/build/store.js.map +1 -1
  141. package/build/timeline.js +104 -0
  142. package/build/timeline.js.map +1 -0
  143. package/build/tracker-reads-cli.js +4 -5
  144. package/build/tracker-reads-cli.js.map +1 -1
  145. package/build/train-cli.js +6 -5
  146. package/build/train-cli.js.map +1 -1
  147. package/build/upgrade.js +8 -9
  148. package/build/upgrade.js.map +1 -1
  149. package/build/wait.js +6 -6
  150. package/build/wait.js.map +1 -1
  151. package/build/watchers/ado.js +11 -3
  152. package/build/watchers/ado.js.map +1 -1
  153. package/build/watchers/github.js +15 -4
  154. package/build/watchers/github.js.map +1 -1
  155. package/build/watchers/npm.js +6 -4
  156. package/build/watchers/npm.js.map +1 -1
  157. package/build/webhook.js +80 -0
  158. package/build/webhook.js.map +1 -0
  159. package/build/worktrees.js +199 -92
  160. package/build/worktrees.js.map +1 -1
  161. package/package.json +61 -61
  162. package/plugin/skills/conductor-console/SKILL.md +4 -4
  163. package/plugin/skills/work-item/SKILL.md +13 -11
  164. package/build/checkout-sync.js +0 -249
  165. package/build/checkout-sync.js.map +0 -1
  166. package/build/credential.js +0 -66
  167. package/build/credential.js.map +0 -1
  168. package/build/startup.js +0 -92
  169. package/build/startup.js.map +0 -1
  170. package/build/tab.js +0 -32
  171. 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, prCreateCommand, 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.
@@ -85,7 +89,13 @@ const retireBody = z.object({
85
89
  work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
86
90
  worker: workerSchema,
87
91
  });
88
- const attachBody = z.object({ resumed: z.boolean(), tab: z.string().nullable(), worker: workerSchema });
92
+ // hint: from ATK-529 on; an older daemon sends none.
93
+ const attachBody = z.object({
94
+ hint: z.string().nullable().default(null),
95
+ resumed: z.boolean(),
96
+ tab: z.string().nullable(),
97
+ worker: workerSchema,
98
+ });
89
99
  const inboxBody = z.object({ items: z.array(inboxItemSchema) });
90
100
  const answerBody = z.object({
91
101
  comment: z.object({ id: z.string(), url: z.string().nullable() }).nullable(),
@@ -97,11 +107,14 @@ const answerRefusal = z.object({
97
107
  error: z.string(),
98
108
  posted: z.enum(['no', 'unknown']).optional(),
99
109
  });
100
- async function addProject(paths, path) {
101
- const absolute = resolve(path);
110
+ // `conduct project add <url | path> [--name <name>]`: the clone is made here, where its progress
111
+ // shows and no API timeout applies, then the daemon registers it (or the store, with none running).
112
+ // `next`: false for `conduct init`, which says what comes next itself.
113
+ async function addProject(paths, source, name, next = true) {
114
+ const made = await cloneProject(paths, source, { ...(name === undefined ? {} : { name }), say: out });
102
115
  let result;
103
116
  try {
104
- result = addBody.parse(await expectOk(callApi(paths, { body: { path: absolute }, method: 'POST', path: '/projects' })));
117
+ result = addBody.parse(await expectOk(callApi(paths, { body: { name: made.name }, method: 'POST', path: '/projects' })));
105
118
  }
106
119
  catch (error) {
107
120
  if (!(error instanceof NotRunningError))
@@ -109,9 +122,15 @@ async function addProject(paths, path) {
109
122
  // No daemon: write the store directly. The daemon reads it when it starts.
110
123
  // Its rules are checked against config.json here too, as the daemon would.
111
124
  const check = rulesService({ config: loadConfig(paths.config), listProjects: () => [], paths }).checkProfile;
112
- result = withStore(paths, (store) => registerProject(store, absolute, check));
125
+ result = withStore(paths, (store) => registerProject(store, paths, made.name, check));
113
126
  }
114
127
  out(`${result.created ? 'registered' : 'updated'} ${projectLine(result.project).trim()}`);
128
+ out(made.cloned ? `cloned ${made.url} into ${made.clone}` : `fetched ${made.clone}, the clone it has`);
129
+ out(`its machine profile, which wins over the repo's: ${result.project.profilePath}`);
130
+ // Claude Code trusts a worktree by its repository, and nobody has run Claude in a new clone yet.
131
+ if (made.cloned && next)
132
+ out(`next: ${trustStep(made.clone)}, then \`conduct project check ${made.name}\``);
133
+ return made;
115
134
  }
116
135
  // `conduct approve|revise|done`: Jason's answer to an inbox item, by id, or for approve and revise
117
136
  // by the item whose plan waits.
@@ -150,6 +169,8 @@ async function attach(paths, ref) {
150
169
  out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
151
170
  if (result.tab)
152
171
  out(`no tab (${result.tab}); open it yourself: claude attach ${result.worker.handle ?? worker.name}`);
172
+ else if (result.hint)
173
+ out(`to see its tab: ${result.hint}`);
153
174
  }
154
175
  // The pid of the daemon once it answers /health, for upgrade's restart. failure: the detached
155
176
  // serve's spawn error, which ends the wait early.
@@ -192,7 +213,7 @@ async function consoleCommand(paths, options) {
192
213
  await claimConsole(paths, options.force);
193
214
  const child = spawn('claude', consoleArgs(paths.plugin), {
194
215
  cwd: paths.console,
195
- env: { ...process.env, CONDUCTOR_CONSOLE: '1' },
216
+ env: consoleEnv(process.env),
196
217
  stdio: 'inherit',
197
218
  });
198
219
  // Ctrl+C belongs to the session; this process only waits for it to end.
@@ -207,40 +228,33 @@ async function consoleCommand(paths, options) {
207
228
  });
208
229
  return;
209
230
  }
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)));
231
+ const where = terminal({ choice: loadConfig(paths.config).workers.terminal });
232
+ const tab = consoleTab(paths.console, process.execPath, realpathSync(fileURLToPath(import.meta.url)), process.env);
222
233
  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),
234
+ const failed = await where.open(tab);
235
+ if (failed !== null)
236
+ throw new ConductorError(`could not open the console tab in ${where.name}`, [
237
+ failed,
227
238
  'run `conduct console --here --force` in a terminal instead',
228
239
  ]);
229
- out(`opened the console in ${paths.console} (Windows Terminal window "${CONSOLE_WINDOW}")`);
240
+ const hint = where.attachHint(CONSOLE_WINDOW);
241
+ out(`opened the console in ${paths.console} (${hint === null ? `${where.name} window "${CONSOLE_WINDOW}"` : `${where.name}: ${hint}`})`);
230
242
  }
231
243
  // How `conduct start` and upgrade's restart start the daemon detached from this process.
232
244
  function daemonStarter(paths, cliPath) {
233
245
  let failure = () => undefined;
246
+ const task = startup();
234
247
  return {
235
248
  awaitDaemon: () => awaitDaemon(paths, failure),
236
- runTask: runStartup,
249
+ runTask: () => task.run(),
237
250
  serveDetached: () => {
238
251
  // --fail-fast: a first serve that can't start ends the supervisor, so the start says why at once.
239
252
  failure = serveDetached(paths.home, process.execPath, [cliPath, 'supervise', '--fail-fast']);
240
253
  },
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,
254
+ taskName: task.name,
255
+ // The startup's daemon uses ~/.conductor, so it can't start one kept in CONDUCTOR_HOME.
256
+ taskRunsCli: async () => !process.env.CONDUCTOR_HOME && (await task.runsCli(cliPath)),
257
+ taskSupervises: () => task.supervises(),
244
258
  };
245
259
  }
246
260
  // `conduct escalations done`: the console reports back on an escalation.
@@ -365,11 +379,20 @@ function inboxId(ref) {
365
379
  throw new ConductorError(`"${ref}" is not an inbox item id`, ['`conduct inbox` lists them as #<id>']);
366
380
  return Number(id);
367
381
  }
368
- function install() {
382
+ // `conduct install-startup`: the machine's startup (host/startup.ts) runs this build's supervise.
383
+ async function install() {
369
384
  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}`)) {
385
+ const task = startup();
386
+ // Less what `pnpm exec` or `npm run` put first, which a worktree's removal takes away.
387
+ const path = (process.env.PATH ?? '')
388
+ .split(delimiter)
389
+ .filter((dir) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/.test(dir))
390
+ .join(delimiter);
391
+ const notes = await task.install({ cliPath, nodePath: process.execPath, path });
392
+ out(`registered ${task.name}: ${process.execPath} ${cliPath} supervise`);
393
+ for (const note of notes)
394
+ out(note);
395
+ if (/[\\/](?:\.claude|\.conductor[\\/]projects[\\/][^\\/]+)[\\/]worktrees[\\/]/.test(cliPath)) {
373
396
  out('warning: that build is in a worktree, which goes away; run install-startup from the main checkout');
374
397
  }
375
398
  }
@@ -404,7 +427,7 @@ async function listProjects(paths, json) {
404
427
  if (json)
405
428
  return out(JSON.stringify(projects, null, 2));
406
429
  if (projects.length === 0)
407
- return out('no projects; add one with `conduct project add <path>`');
430
+ return out('no projects; add one with `conduct project add <url | path>`');
408
431
  projects.forEach((p) => out(projectLine(p)));
409
432
  }
410
433
  async function main(argv) {
@@ -444,12 +467,15 @@ async function main(argv) {
444
467
  options: {
445
468
  'allow-downgrade': { type: 'boolean' },
446
469
  area: { type: 'string' },
470
+ base: { type: 'string' },
447
471
  'blocked-by': { multiple: true, type: 'string' },
448
472
  'body-file': { type: 'string' },
473
+ branch: { type: 'string' },
449
474
  channel: { type: 'string' },
450
475
  comments: { type: 'boolean' },
451
476
  conductor: { type: 'string' },
452
477
  console: { type: 'boolean' },
478
+ count: { type: 'string' },
453
479
  'dry-run': { type: 'boolean' },
454
480
  'fail-fast': { type: 'boolean' },
455
481
  files: { type: 'string' },
@@ -457,21 +483,24 @@ async function main(argv) {
457
483
  for: { type: 'string' },
458
484
  force: { type: 'boolean' },
459
485
  fresh: { type: 'boolean' },
486
+ grep: { type: 'string' },
460
487
  help: { short: 'h', type: 'boolean' },
461
488
  here: { type: 'boolean' },
462
489
  human: { type: 'boolean' },
463
490
  item: { type: 'string' },
464
491
  json: { type: 'boolean' },
492
+ key: { type: 'string' },
465
493
  kind: { type: 'string' },
466
494
  last: { type: 'boolean' },
467
495
  mode: { type: 'string' },
496
+ name: { type: 'string' },
468
497
  'no-worktree': { type: 'boolean' },
469
498
  owner: { type: 'string' },
470
499
  phase: { type: 'string' },
471
- prefer: { type: 'string' },
472
500
  probe: { type: 'boolean' },
473
501
  project: { type: 'string' },
474
502
  prompt: { type: 'string' },
503
+ query: { type: 'string' },
475
504
  raise: { type: 'boolean' },
476
505
  rearm: { type: 'boolean' },
477
506
  reason: { type: 'string' },
@@ -479,18 +508,23 @@ async function main(argv) {
479
508
  remove: { type: 'boolean' },
480
509
  repo: { type: 'string' },
481
510
  requeue: { type: 'string' },
511
+ shared: { type: 'boolean' },
482
512
  since: { type: 'string' },
483
513
  slots: { type: 'string' },
484
514
  tag: { type: 'string' },
515
+ task: { type: 'string' },
516
+ team: { type: 'string' },
485
517
  text: { type: 'string' },
486
518
  tier: { type: 'string' },
487
519
  title: { type: 'string' },
520
+ tracker: { type: 'string' },
488
521
  until: { type: 'string' },
489
522
  version: { short: 'v', type: 'boolean' },
490
523
  wait: { type: 'string' },
491
524
  'waits-release': { type: 'boolean' },
492
525
  weight: { type: 'string' },
493
526
  worker: { type: 'string' },
527
+ yes: { type: 'boolean' },
494
528
  },
495
529
  }));
496
530
  const paths = conductorPaths();
@@ -517,7 +551,7 @@ async function main(argv) {
517
551
  case 'branch':
518
552
  if (sub === undefined || arg === undefined)
519
553
  throw usageError('branch');
520
- process.exitCode = await branchCommand(sub, arg, item, verbIo());
554
+ process.exitCode = await branchCommand(paths, sub, arg, item, verbIo());
521
555
  return;
522
556
  case 'brief':
523
557
  if (sub === undefined)
@@ -531,7 +565,7 @@ async function main(argv) {
531
565
  case 'checkout':
532
566
  if (sub === undefined || arg === undefined || fourth === undefined)
533
567
  throw usageError('checkout');
534
- process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
568
+ process.exitCode = await checkoutCommand(paths, sub, arg, fourth, item, verbIo());
535
569
  return;
536
570
  case 'comments': {
537
571
  if (sub === undefined || arg !== undefined)
@@ -593,6 +627,26 @@ async function main(argv) {
593
627
  });
594
628
  case 'inbox':
595
629
  return inbox(paths, values.json === true);
630
+ case 'init':
631
+ if (arg !== undefined)
632
+ throw usageError('init');
633
+ process.exitCode = await initProject({
634
+ add: (checkout, name) => addProject(paths, checkout, name, false),
635
+ ask: terminalAsk(),
636
+ check: (name) => projectCheckReport(paths, name, { worktree: true }, { err: (line) => process.stderr.write(`${line}\n`) }),
637
+ out,
638
+ paths,
639
+ }, sub ?? '.', {
640
+ shared: values.shared === true,
641
+ yes: values.yes === true,
642
+ ...(values.base === undefined ? {} : { base: values.base }),
643
+ ...(values.key === undefined ? {} : { key: values.key }),
644
+ ...(values.name === undefined ? {} : { name: values.name }),
645
+ ...(values.query === undefined ? {} : { query: values.query }),
646
+ ...(values.team === undefined ? {} : { team: values.team }),
647
+ ...(values.tracker === undefined ? {} : { tracker: values.tracker }),
648
+ });
649
+ return;
596
650
  case 'install-startup':
597
651
  return values.remove ? uninstall() : install();
598
652
  case 'land': {
@@ -612,6 +666,7 @@ async function main(argv) {
612
666
  ...repo,
613
667
  ...(mode === undefined ? {} : { mode }),
614
668
  ...(tier === undefined ? {} : { tier }),
669
+ ...(values.requeue === undefined ? {} : { requeue: wholeNumber('--requeue', values.requeue) }),
615
670
  }, verbIo());
616
671
  return;
617
672
  }
@@ -639,7 +694,7 @@ async function main(argv) {
639
694
  throw usageError('mode');
640
695
  if (tier !== 'S' && tier !== 'M' && tier !== 'L')
641
696
  throw new ConductorError('--tier takes S, M or L');
642
- process.exitCode = await modeCommand({ files: values.files, tier }, verbIo());
697
+ process.exitCode = await modeCommand(paths, { files: values.files, tier }, verbIo());
643
698
  return;
644
699
  }
645
700
  case 'note':
@@ -686,14 +741,23 @@ async function main(argv) {
686
741
  ...(values.until === undefined ? {} : { until: untilOf(values.until) }),
687
742
  });
688
743
  case 'ports':
689
- process.exitCode = await portsCommand(paths, sub, {
690
- json: values.json === true,
691
- ...(values.prefer === undefined ? {} : { prefer: wholeNumber('--prefer', values.prefer) }),
692
- }, brokerIo());
744
+ process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
693
745
  return;
746
+ case 'profile': {
747
+ if (sub === undefined || arg !== undefined)
748
+ throw usageError('profile');
749
+ // Resolved here from the machine file and the clone, as the daemon resolves it.
750
+ const layered = projectProfile(paths, sub);
751
+ if (values.json === true)
752
+ return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
753
+ profileLines(sub, layered).forEach((line) => out(line));
754
+ return;
755
+ }
694
756
  case 'project':
695
- if (sub === 'add' && arg !== undefined)
696
- return addProject(paths, arg);
757
+ if (sub === 'add' && arg !== undefined) {
758
+ await addProject(paths, arg, values.name);
759
+ return;
760
+ }
697
761
  if (sub === 'remove' && arg !== undefined)
698
762
  return removeProject(paths, arg);
699
763
  if (sub === 'list')
@@ -734,6 +798,7 @@ async function main(argv) {
734
798
  ...item,
735
799
  ...repo,
736
800
  ...(values.for === undefined ? {} : { for: values.for }),
801
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
737
802
  ...(values.requeue === undefined ? {} : { requeue: values.requeue }),
738
803
  }, verbIo());
739
804
  return;
@@ -759,6 +824,19 @@ async function main(argv) {
759
824
  case 'supervise':
760
825
  process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
761
826
  return;
827
+ case 'timeline':
828
+ if (arg !== undefined)
829
+ throw usageError('timeline');
830
+ process.exitCode = await timelineCommand(paths, {
831
+ ...repo,
832
+ ...(sub === undefined ? {} : { run: sub }),
833
+ ...(values.branch === undefined ? {} : { branch: values.branch }),
834
+ ...(values.count === undefined ? {} : { count: values.count }),
835
+ ...(values.for === undefined ? {} : { for: values.for }),
836
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
837
+ ...(values.task === undefined ? {} : { task: values.task }),
838
+ }, verbIo());
839
+ return;
762
840
  case 'train':
763
841
  return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
764
842
  case 'unpause':
@@ -814,7 +892,7 @@ async function notifyTest(paths, options) {
814
892
  ...(options.kind ? { kind: options.kind } : {}),
815
893
  raise: options.raise,
816
894
  };
817
- // Pushover can take its 15 s, after a Credential Manager read.
895
+ // Pushover can take its 15 s, after a secret store read.
818
896
  const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
819
897
  if (options.raise) {
820
898
  const { item } = z.object({ item: inboxItemSchema }).parse(answer);
@@ -914,11 +992,11 @@ async function planItem(paths, ref) {
914
992
  }
915
993
  function projectLine(p) {
916
994
  const state = p.problem
917
- ? `${p.problem}; the daemon reads ${p.profilePath} again when it starts or the file is saved`
995
+ ? `${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`
918
996
  : p.enabled
919
997
  ? 'enabled'
920
998
  : 'disabled';
921
- return ` ${p.name} ${p.root} (protocol ${p.protocol}, ${state})`;
999
+ return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
922
1000
  }
923
1001
  // The registered projects: from the daemon, or from the store when it isn't running.
924
1002
  async function projectsNow(paths) {
@@ -963,7 +1041,7 @@ async function removeProject(paths, name) {
963
1041
  });
964
1042
  }
965
1043
  const { project, resolved } = result;
966
- out(`removed ${project.name} (${project.root})`);
1044
+ out(`removed ${project.name} (${project.clone})`);
967
1045
  for (const item of resolved)
968
1046
  out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
969
1047
  }
@@ -1093,7 +1171,7 @@ async function shutdownDaemon(paths) {
1093
1171
  if (readEndpoint(paths) !== null) {
1094
1172
  throw new ConductorError(`the conductor (pid ${pid ?? '?'}) did not stop within ${STOP_WAIT_MS / 1000} s`);
1095
1173
  }
1096
- // The port file goes before the process ends; a start (the logon task's especially) waits for it.
1174
+ // The port file goes before the process ends; a start (the logon startup's especially) waits for it.
1097
1175
  const exitDeadline = Date.now() + STOP_WAIT_MS;
1098
1176
  while (pid !== null && processAlive(pid) && Date.now() < exitDeadline)
1099
1177
  await sleep(100);
@@ -1109,7 +1187,7 @@ async function start(paths) {
1109
1187
  const pid = readPid(paths.pid);
1110
1188
  if (pid !== null && (await ping(paths, pid)))
1111
1189
  return out(`the conductor is running (pid ${pid})`);
1112
- // A daemon that is still starting (the logon task's, say) holds the lock: wait for it rather than
1190
+ // A daemon that is still starting (the logon startup's, say) holds the lock: wait for it rather than
1113
1191
  // start a second one, which would exit on that lock. A pid a reboot reused gets a start after.
1114
1192
  if (pid !== null && processAlive(pid)) {
1115
1193
  try {
@@ -1157,7 +1235,7 @@ async function status(paths, json) {
1157
1235
  if (json)
1158
1236
  return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
1159
1237
  out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
1160
- out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
1238
+ out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
1161
1239
  projects.forEach((p) => out(projectLine(p)));
1162
1240
  out(workers.length === 0 ? 'no workers' : 'workers:');
1163
1241
  workers.forEach((w) => out(workerLine(w)));
@@ -1195,7 +1273,7 @@ async function stopDaemon(paths) {
1195
1273
  out('the conductor is not running');
1196
1274
  return false;
1197
1275
  case 'supervisor': {
1198
- // It answers just before it ends; a start (the logon task's especially) needs it gone.
1276
+ // It answers just before it ends; a start (the logon startup's especially) needs it gone.
1199
1277
  const deadline = Date.now() + STOP_WAIT_MS;
1200
1278
  while (processAlive(stop.pid) && Date.now() < deadline)
1201
1279
  await sleep(100);
@@ -1204,7 +1282,7 @@ async function stopDaemon(paths) {
1204
1282
  }
1205
1283
  }
1206
1284
  }
1207
- // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon task runs
1285
+ // `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
1208
1286
  // it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
1209
1287
  // while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the
1210
1288
  // defaults: no quiet hours, the default credential.
@@ -1226,7 +1304,7 @@ async function superviseCommand(paths, failFast) {
1226
1304
  rotatingLog(paths.log, config().log).log(...line);
1227
1305
  }
1228
1306
  catch {
1229
- // Nowhere else to say it: under the logon task nobody reads stderr.
1307
+ // Nowhere else to say it: under the logon startup nobody reads stderr.
1230
1308
  }
1231
1309
  },
1232
1310
  };
@@ -1302,8 +1380,9 @@ async function superviseCommand(paths, failFast) {
1302
1380
  }
1303
1381
  return code;
1304
1382
  }
1305
- function uninstall() {
1306
- out(removeStartup() ? `removed the "${TASK_NAME}" logon task` : `no "${TASK_NAME}" logon task to remove`);
1383
+ async function uninstall() {
1384
+ const task = startup();
1385
+ out((await task.remove()) ? `removed ${task.name}` : `no ${task.name} to remove`);
1307
1386
  }
1308
1387
  // --until: 17:00 (the next one), tomorrow, a day such as monday, or a date and time.
1309
1388
  function untilOf(text) {
@@ -1311,7 +1390,8 @@ function untilOf(text) {
1311
1390
  }
1312
1391
  async function upgradeCommand(paths, tag, allowDowngrade) {
1313
1392
  const cliPath = realpathSync(fileURLToPath(import.meta.url));
1314
- const release = npmStage({});
1393
+ // npm's own settings, so the tag is read where `npm install --global` installs from (ATK-561).
1394
+ const release = npmStage(sharedNpmReads());
1315
1395
  await upgrade({ allowDowngrade, tag: parseTag(tag) }, {
1316
1396
  ...daemonStarter(paths, cliPath),
1317
1397
  cliPath,