@detergent-software/conductor 3.0.0-dev.2 → 3.0.0-dev.21

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 (137) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +158 -45
  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 +119 -54
  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 +101 -26
  16. package/build/code-host-cli.js.map +1 -1
  17. package/build/code-host.js +13 -8
  18. package/build/code-host.js.map +1 -1
  19. package/build/config.js +31 -12
  20. package/build/config.js.map +1 -1
  21. package/build/console.js +40 -24
  22. package/build/console.js.map +1 -1
  23. package/build/credentials.js +26 -24
  24. package/build/credentials.js.map +1 -1
  25. package/build/daemon.js +35 -21
  26. package/build/daemon.js.map +1 -1
  27. package/build/help.js +141 -30
  28. package/build/help.js.map +1 -1
  29. package/build/host/credential-manager.js +109 -0
  30. package/build/host/credential-manager.js.map +1 -0
  31. package/build/host/keychain.js +44 -0
  32. package/build/host/keychain.js.map +1 -0
  33. package/build/host/libsecret.js +40 -0
  34. package/build/host/libsecret.js.map +1 -0
  35. package/build/host/macos-terminal.js +63 -0
  36. package/build/host/macos-terminal.js.map +1 -0
  37. package/build/host/platform.js +4 -0
  38. package/build/host/platform.js.map +1 -0
  39. package/build/host/secrets.js +46 -0
  40. package/build/host/secrets.js.map +1 -0
  41. package/build/host/shell.js +26 -0
  42. package/build/host/shell.js.map +1 -0
  43. package/build/host/terminal.js +46 -0
  44. package/build/host/terminal.js.map +1 -0
  45. package/build/host/tmux.js +39 -0
  46. package/build/host/tmux.js.map +1 -0
  47. package/build/host/windows-terminal.js +46 -0
  48. package/build/host/windows-terminal.js.map +1 -0
  49. package/build/in-process.js +6 -5
  50. package/build/in-process.js.map +1 -1
  51. package/build/inbox.js +50 -5
  52. package/build/inbox.js.map +1 -1
  53. package/build/init.js +508 -0
  54. package/build/init.js.map +1 -0
  55. package/build/lib/providers/ado/pipelines.js +102 -17
  56. package/build/lib/providers/ado/pipelines.js.map +1 -1
  57. package/build/lib/providers/ado/trigger.js +5 -0
  58. package/build/lib/providers/ado/trigger.js.map +1 -1
  59. package/build/lib/providers/github/actions.js +85 -2
  60. package/build/lib/providers/github/actions.js.map +1 -1
  61. package/build/lib/providers/index.js +3 -2
  62. package/build/lib/providers/index.js.map +1 -1
  63. package/build/lib/providers/npm/stage.js +14 -4
  64. package/build/lib/providers/npm/stage.js.map +1 -1
  65. package/build/lib/providers/timing.js +10 -0
  66. package/build/lib/providers/timing.js.map +1 -0
  67. package/build/lib/providers/verdicts/runs.js +12 -2
  68. package/build/lib/providers/verdicts/runs.js.map +1 -1
  69. package/build/local-files.js +95 -0
  70. package/build/local-files.js.map +1 -0
  71. package/build/manager.js +171 -47
  72. package/build/manager.js.map +1 -1
  73. package/build/note.js +1 -1
  74. package/build/note.js.map +1 -1
  75. package/build/npm-reads.js +323 -0
  76. package/build/npm-reads.js.map +1 -0
  77. package/build/paths.js +44 -1
  78. package/build/paths.js.map +1 -1
  79. package/build/plugin.js +10 -0
  80. package/build/plugin.js.map +1 -1
  81. package/build/profile-fields.js +149 -0
  82. package/build/profile-fields.js.map +1 -0
  83. package/build/profile-layers.js +264 -0
  84. package/build/profile-layers.js.map +1 -0
  85. package/build/profile.js +60 -38
  86. package/build/profile.js.map +1 -1
  87. package/build/project-check-cli.js +18 -10
  88. package/build/project-check-cli.js.map +1 -1
  89. package/build/project-check.js +179 -97
  90. package/build/project-check.js.map +1 -1
  91. package/build/pushover.js +5 -5
  92. package/build/pushover.js.map +1 -1
  93. package/build/registry.js +195 -36
  94. package/build/registry.js.map +1 -1
  95. package/build/rule-actions.js +3 -1
  96. package/build/rule-actions.js.map +1 -1
  97. package/build/rules-service.js +101 -44
  98. package/build/rules-service.js.map +1 -1
  99. package/build/rules.js +11 -2
  100. package/build/rules.js.map +1 -1
  101. package/build/runs.js +88 -14
  102. package/build/runs.js.map +1 -1
  103. package/build/scheduler.js +11 -1
  104. package/build/scheduler.js.map +1 -1
  105. package/build/server.js +4 -25
  106. package/build/server.js.map +1 -1
  107. package/build/spend.js +4 -0
  108. package/build/spend.js.map +1 -1
  109. package/build/store.js +82 -25
  110. package/build/store.js.map +1 -1
  111. package/build/timeline.js +104 -0
  112. package/build/timeline.js.map +1 -0
  113. package/build/tracker-reads-cli.js +4 -5
  114. package/build/tracker-reads-cli.js.map +1 -1
  115. package/build/train-cli.js +6 -5
  116. package/build/train-cli.js.map +1 -1
  117. package/build/upgrade.js +16 -1
  118. package/build/upgrade.js.map +1 -1
  119. package/build/wait.js +6 -6
  120. package/build/wait.js.map +1 -1
  121. package/build/watchers/ado.js +3 -2
  122. package/build/watchers/ado.js.map +1 -1
  123. package/build/watchers/github.js +3 -2
  124. package/build/watchers/github.js.map +1 -1
  125. package/build/watchers/npm.js +6 -4
  126. package/build/watchers/npm.js.map +1 -1
  127. package/build/worktrees.js +199 -92
  128. package/build/worktrees.js.map +1 -1
  129. package/package.json +61 -61
  130. package/plugin/skills/conductor-console/SKILL.md +3 -3
  131. package/plugin/skills/work-item/SKILL.md +8 -6
  132. package/build/checkout-sync.js +0 -249
  133. package/build/checkout-sync.js.map +0 -1
  134. package/build/credential.js +0 -66
  135. package/build/credential.js.map +0 -1
  136. package/build/tab.js +0 -32
  137. package/build/tab.js.map +0 -1
package/build/cli.js CHANGED
@@ -1,36 +1,42 @@
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 { join, sep } 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 { terminal } from "./host/terminal.js";
19
21
  import { inboxLines } from "./inbox.js";
22
+ import { initProject, terminalAsk } from "./init.js";
20
23
  import { parseJsonc } from "./jsonc.js";
24
+ import { npmStage } from "./lib/providers/index.js";
21
25
  import { processAlive, readPid } from "./lock.js";
22
26
  import { rotatingLog } from "./log.js";
23
27
  import { fleetWorkerSchema, storedFleet, workerByRef, workerLine } from "./manager.js";
24
28
  import { noteCommand } from "./note.js";
25
29
  import { isQuiet, notifyLines, notifyStatusSchema, parseUntil, testResultSchema } from "./notify.js";
30
+ import { sharedNpmReads } from "./npm-reads.js";
26
31
  import { overrideViewSchema, PAUSE } from "./overrides.js";
27
32
  import { packageInfo } from "./package-info.js";
28
33
  import { conductorPaths } from "./paths.js";
29
34
  import { refreshConductorPlugin } from "./plugin.js";
30
- import { projectCheckCommand } from "./project-check-cli.js";
35
+ import { profileLines, projectProfile } from "./profile-layers.js";
36
+ import { projectCheckCommand, projectCheckReport } from "./project-check-cli.js";
31
37
  import { noProject } from "./project-check.js";
32
38
  import { pushoverCredential, pushoverSender } from "./pushover.js";
33
- import { registerProject } from "./registry.js";
39
+ import { cloneProject, registerProject } from "./registry.js";
34
40
  import { rulesLines, rulesService, rulesTestResultSchema, rulesViewSchema, testLines, } from "./rules-service.js";
35
41
  import { resolveUntil } from "./rules.js";
36
42
  import { outputOf, run } from "./run.js";
@@ -41,10 +47,9 @@ import { installStartup, removeStartup, runStartup, startupRunsCli, startupSuper
41
47
  import { answerStopRequest, dropStaleStopRequest, pendingStopRequest, STOP_POLL_MS, stopSupervisor, } from "./stop-request.js";
42
48
  import { inboxItemSchema, openStore, projectSchema, waitSchema, workerSchema, } from "./store.js";
43
49
  import { RESTART_ENV, restartInfoSchema, SERVE_LOCKED_EXIT, serveChild, supervise, SUPERVISE_POLICY, } from "./supervise.js";
44
- import { openWindowsTerminal } from "./tab.js";
45
50
  import { commentsCommand, notesCommand, searchCommand } from "./tracker-reads-cli.js";
46
51
  import { trainCommand } from "./train-cli.js";
47
- import { parseTag, restartDaemon, upgrade } from "./upgrade.js";
52
+ import { parseTag, restartDaemon, tagVersion, upgrade } from "./upgrade.js";
48
53
  import { waitCommand } from "./wait.js";
49
54
  import { whyLines, whySchema } from "./why.js";
50
55
  const STOP_WAIT_MS = 10_000;
@@ -84,7 +89,13 @@ const retireBody = z.object({
84
89
  work: workSchema.extend({ checkouts: z.record(z.string(), workSchema).optional() }).optional(),
85
90
  worker: workerSchema,
86
91
  });
87
- 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
+ });
88
99
  const inboxBody = z.object({ items: z.array(inboxItemSchema) });
89
100
  const answerBody = z.object({
90
101
  comment: z.object({ id: z.string(), url: z.string().nullable() }).nullable(),
@@ -96,11 +107,14 @@ const answerRefusal = z.object({
96
107
  error: z.string(),
97
108
  posted: z.enum(['no', 'unknown']).optional(),
98
109
  });
99
- async function addProject(paths, path) {
100
- 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 });
101
115
  let result;
102
116
  try {
103
- 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' })));
104
118
  }
105
119
  catch (error) {
106
120
  if (!(error instanceof NotRunningError))
@@ -108,9 +122,15 @@ async function addProject(paths, path) {
108
122
  // No daemon: write the store directly. The daemon reads it when it starts.
109
123
  // Its rules are checked against config.json here too, as the daemon would.
110
124
  const check = rulesService({ config: loadConfig(paths.config), listProjects: () => [], paths }).checkProfile;
111
- result = withStore(paths, (store) => registerProject(store, absolute, check));
125
+ result = withStore(paths, (store) => registerProject(store, paths, made.name, check));
112
126
  }
113
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;
114
134
  }
115
135
  // `conduct approve|revise|done`: Jason's answer to an inbox item, by id, or for approve and revise
116
136
  // by the item whose plan waits.
@@ -149,6 +169,8 @@ async function attach(paths, ref) {
149
169
  out(`${result.resumed ? 'woke' : 'attached to'} ${worker.name} (session ${result.worker.handle ?? '?'})`);
150
170
  if (result.tab)
151
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}`);
152
174
  }
153
175
  // The pid of the daemon once it answers /health, for upgrade's restart. failure: the detached
154
176
  // serve's spawn error, which ends the wait early.
@@ -191,7 +213,7 @@ async function consoleCommand(paths, options) {
191
213
  await claimConsole(paths, options.force);
192
214
  const child = spawn('claude', consoleArgs(paths.plugin), {
193
215
  cwd: paths.console,
194
- env: { ...process.env, CONDUCTOR_CONSOLE: '1' },
216
+ env: consoleEnv(process.env),
195
217
  stdio: 'inherit',
196
218
  });
197
219
  // Ctrl+C belongs to the session; this process only waits for it to end.
@@ -206,26 +228,17 @@ async function consoleCommand(paths, options) {
206
228
  });
207
229
  return;
208
230
  }
209
- if (process.platform !== 'win32') {
210
- throw new ConductorError('conduct console opens a Windows Terminal tab', [
211
- 'run `conduct console --here` in a terminal of your own',
212
- ]);
213
- }
214
- // A tab starts with Windows Terminal's environment, not this one's.
215
- if (process.env.CONDUCTOR_HOME) {
216
- throw new ConductorError('a console tab would not see CONDUCTOR_HOME', [
217
- 'run `conduct console --here` in a terminal where it is set',
218
- ]);
219
- }
220
- 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);
221
233
  await claimConsole(paths, options.force);
222
- const result = await openWindowsTerminal(args, paths.console);
223
- if (result.code !== 0)
224
- throw new ConductorError('could not open the console tab', [
225
- 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,
226
238
  'run `conduct console --here --force` in a terminal instead',
227
239
  ]);
228
- 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}`})`);
229
242
  }
230
243
  // How `conduct start` and upgrade's restart start the daemon detached from this process.
231
244
  function daemonStarter(paths, cliPath) {
@@ -403,7 +416,7 @@ async function listProjects(paths, json) {
403
416
  if (json)
404
417
  return out(JSON.stringify(projects, null, 2));
405
418
  if (projects.length === 0)
406
- return out('no projects; add one with `conduct project add <path>`');
419
+ return out('no projects; add one with `conduct project add <url | path>`');
407
420
  projects.forEach((p) => out(projectLine(p)));
408
421
  }
409
422
  async function main(argv) {
@@ -443,12 +456,15 @@ async function main(argv) {
443
456
  options: {
444
457
  'allow-downgrade': { type: 'boolean' },
445
458
  area: { type: 'string' },
459
+ base: { type: 'string' },
446
460
  'blocked-by': { multiple: true, type: 'string' },
447
461
  'body-file': { type: 'string' },
462
+ branch: { type: 'string' },
448
463
  channel: { type: 'string' },
449
464
  comments: { type: 'boolean' },
450
465
  conductor: { type: 'string' },
451
466
  console: { type: 'boolean' },
467
+ count: { type: 'string' },
452
468
  'dry-run': { type: 'boolean' },
453
469
  'fail-fast': { type: 'boolean' },
454
470
  files: { type: 'string' },
@@ -456,21 +472,24 @@ async function main(argv) {
456
472
  for: { type: 'string' },
457
473
  force: { type: 'boolean' },
458
474
  fresh: { type: 'boolean' },
475
+ grep: { type: 'string' },
459
476
  help: { short: 'h', type: 'boolean' },
460
477
  here: { type: 'boolean' },
461
478
  human: { type: 'boolean' },
462
479
  item: { type: 'string' },
463
480
  json: { type: 'boolean' },
481
+ key: { type: 'string' },
464
482
  kind: { type: 'string' },
465
483
  last: { type: 'boolean' },
466
484
  mode: { type: 'string' },
485
+ name: { type: 'string' },
467
486
  'no-worktree': { type: 'boolean' },
468
487
  owner: { type: 'string' },
469
488
  phase: { type: 'string' },
470
- prefer: { type: 'string' },
471
489
  probe: { type: 'boolean' },
472
490
  project: { type: 'string' },
473
491
  prompt: { type: 'string' },
492
+ query: { type: 'string' },
474
493
  raise: { type: 'boolean' },
475
494
  rearm: { type: 'boolean' },
476
495
  reason: { type: 'string' },
@@ -478,18 +497,23 @@ async function main(argv) {
478
497
  remove: { type: 'boolean' },
479
498
  repo: { type: 'string' },
480
499
  requeue: { type: 'string' },
500
+ shared: { type: 'boolean' },
481
501
  since: { type: 'string' },
482
502
  slots: { type: 'string' },
483
503
  tag: { type: 'string' },
504
+ task: { type: 'string' },
505
+ team: { type: 'string' },
484
506
  text: { type: 'string' },
485
507
  tier: { type: 'string' },
486
508
  title: { type: 'string' },
509
+ tracker: { type: 'string' },
487
510
  until: { type: 'string' },
488
511
  version: { short: 'v', type: 'boolean' },
489
512
  wait: { type: 'string' },
490
513
  'waits-release': { type: 'boolean' },
491
514
  weight: { type: 'string' },
492
515
  worker: { type: 'string' },
516
+ yes: { type: 'boolean' },
493
517
  },
494
518
  }));
495
519
  const paths = conductorPaths();
@@ -516,7 +540,7 @@ async function main(argv) {
516
540
  case 'branch':
517
541
  if (sub === undefined || arg === undefined)
518
542
  throw usageError('branch');
519
- process.exitCode = await branchCommand(sub, arg, item, verbIo());
543
+ process.exitCode = await branchCommand(paths, sub, arg, item, verbIo());
520
544
  return;
521
545
  case 'brief':
522
546
  if (sub === undefined)
@@ -530,7 +554,7 @@ async function main(argv) {
530
554
  case 'checkout':
531
555
  if (sub === undefined || arg === undefined || fourth === undefined)
532
556
  throw usageError('checkout');
533
- process.exitCode = await checkoutCommand(sub, arg, fourth, item, verbIo());
557
+ process.exitCode = await checkoutCommand(paths, sub, arg, fourth, item, verbIo());
534
558
  return;
535
559
  case 'comments': {
536
560
  if (sub === undefined || arg !== undefined)
@@ -592,6 +616,26 @@ async function main(argv) {
592
616
  });
593
617
  case 'inbox':
594
618
  return inbox(paths, values.json === true);
619
+ case 'init':
620
+ if (arg !== undefined)
621
+ throw usageError('init');
622
+ process.exitCode = await initProject({
623
+ add: (checkout, name) => addProject(paths, checkout, name, false),
624
+ ask: terminalAsk(),
625
+ check: (name) => projectCheckReport(paths, name, { worktree: true }, { err: (line) => process.stderr.write(`${line}\n`) }),
626
+ out,
627
+ paths,
628
+ }, sub ?? '.', {
629
+ shared: values.shared === true,
630
+ yes: values.yes === true,
631
+ ...(values.base === undefined ? {} : { base: values.base }),
632
+ ...(values.key === undefined ? {} : { key: values.key }),
633
+ ...(values.name === undefined ? {} : { name: values.name }),
634
+ ...(values.query === undefined ? {} : { query: values.query }),
635
+ ...(values.team === undefined ? {} : { team: values.team }),
636
+ ...(values.tracker === undefined ? {} : { tracker: values.tracker }),
637
+ });
638
+ return;
595
639
  case 'install-startup':
596
640
  return values.remove ? uninstall() : install();
597
641
  case 'land': {
@@ -638,7 +682,7 @@ async function main(argv) {
638
682
  throw usageError('mode');
639
683
  if (tier !== 'S' && tier !== 'M' && tier !== 'L')
640
684
  throw new ConductorError('--tier takes S, M or L');
641
- process.exitCode = await modeCommand({ files: values.files, tier }, verbIo());
685
+ process.exitCode = await modeCommand(paths, { files: values.files, tier }, verbIo());
642
686
  return;
643
687
  }
644
688
  case 'note':
@@ -685,14 +729,23 @@ async function main(argv) {
685
729
  ...(values.until === undefined ? {} : { until: untilOf(values.until) }),
686
730
  });
687
731
  case 'ports':
688
- process.exitCode = await portsCommand(paths, sub, {
689
- json: values.json === true,
690
- ...(values.prefer === undefined ? {} : { prefer: wholeNumber('--prefer', values.prefer) }),
691
- }, brokerIo());
732
+ process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
692
733
  return;
734
+ case 'profile': {
735
+ if (sub === undefined || arg !== undefined)
736
+ throw usageError('profile');
737
+ // Resolved here from the machine file and the clone, as the daemon resolves it.
738
+ const layered = projectProfile(paths, sub);
739
+ if (values.json === true)
740
+ return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
741
+ profileLines(sub, layered).forEach((line) => out(line));
742
+ return;
743
+ }
693
744
  case 'project':
694
- if (sub === 'add' && arg !== undefined)
695
- return addProject(paths, arg);
745
+ if (sub === 'add' && arg !== undefined) {
746
+ await addProject(paths, arg, values.name);
747
+ return;
748
+ }
696
749
  if (sub === 'remove' && arg !== undefined)
697
750
  return removeProject(paths, arg);
698
751
  if (sub === 'list')
@@ -733,6 +786,7 @@ async function main(argv) {
733
786
  ...item,
734
787
  ...repo,
735
788
  ...(values.for === undefined ? {} : { for: values.for }),
789
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
736
790
  ...(values.requeue === undefined ? {} : { requeue: values.requeue }),
737
791
  }, verbIo());
738
792
  return;
@@ -758,6 +812,19 @@ async function main(argv) {
758
812
  case 'supervise':
759
813
  process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
760
814
  return;
815
+ case 'timeline':
816
+ if (arg !== undefined)
817
+ throw usageError('timeline');
818
+ process.exitCode = await timelineCommand(paths, {
819
+ ...repo,
820
+ ...(sub === undefined ? {} : { run: sub }),
821
+ ...(values.branch === undefined ? {} : { branch: values.branch }),
822
+ ...(values.count === undefined ? {} : { count: values.count }),
823
+ ...(values.for === undefined ? {} : { for: values.for }),
824
+ ...(values.grep === undefined ? {} : { grep: values.grep }),
825
+ ...(values.task === undefined ? {} : { task: values.task }),
826
+ }, verbIo());
827
+ return;
761
828
  case 'train':
762
829
  return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
763
830
  case 'unpause':
@@ -813,7 +880,7 @@ async function notifyTest(paths, options) {
813
880
  ...(options.kind ? { kind: options.kind } : {}),
814
881
  raise: options.raise,
815
882
  };
816
- // Pushover can take its 15 s, after a Credential Manager read.
883
+ // Pushover can take its 15 s, after a secret store read.
817
884
  const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
818
885
  if (options.raise) {
819
886
  const { item } = z.object({ item: inboxItemSchema }).parse(answer);
@@ -913,11 +980,11 @@ async function planItem(paths, ref) {
913
980
  }
914
981
  function projectLine(p) {
915
982
  const state = p.problem
916
- ? `${p.problem}; the daemon reads ${p.profilePath} again when it starts or the file is saved`
983
+ ? `${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`
917
984
  : p.enabled
918
985
  ? 'enabled'
919
986
  : 'disabled';
920
- return ` ${p.name} ${p.root} (protocol ${p.protocol}, ${state})`;
987
+ return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
921
988
  }
922
989
  // The registered projects: from the daemon, or from the store when it isn't running.
923
990
  async function projectsNow(paths) {
@@ -962,7 +1029,7 @@ async function removeProject(paths, name) {
962
1029
  });
963
1030
  }
964
1031
  const { project, resolved } = result;
965
- out(`removed ${project.name} (${project.root})`);
1032
+ out(`removed ${project.name} (${project.clone})`);
966
1033
  for (const item of resolved)
967
1034
  out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
968
1035
  }
@@ -1156,7 +1223,7 @@ async function status(paths, json) {
1156
1223
  if (json)
1157
1224
  return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
1158
1225
  out(`conductor running (pid ${pid ?? '?'}, port ${port ?? '?'}, home ${paths.home})`);
1159
- out(projects.length === 0 ? 'no projects; add one with `conduct project add <path>`' : 'projects:');
1226
+ out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
1160
1227
  projects.forEach((p) => out(projectLine(p)));
1161
1228
  out(workers.length === 0 ? 'no workers' : 'workers:');
1162
1229
  workers.forEach((w) => out(workerLine(w)));
@@ -1310,28 +1377,26 @@ function untilOf(text) {
1310
1377
  }
1311
1378
  async function upgradeCommand(paths, tag, allowDowngrade) {
1312
1379
  const cliPath = realpathSync(fileURLToPath(import.meta.url));
1380
+ // npm's own settings, so the tag is read where `npm install --global` installs from (ATK-561).
1381
+ const release = npmStage(sharedNpmReads());
1313
1382
  await upgrade({ allowDowngrade, tag: parseTag(tag) }, {
1314
1383
  ...daemonStarter(paths, cliPath),
1315
1384
  cliPath,
1316
1385
  installedVersion: version,
1317
- npmInstall: (spec) => npm(['install', '--global', spec], NPM_INSTALL_TIMEOUT_MS),
1386
+ // --prefer-online: npm resolves the exact version against a fresh packument, not a cached one
1387
+ // that may not list it yet (ATK-558).
1388
+ npmInstall: (spec) => npm(['install', '--global', '--prefer-online', spec], NPM_INSTALL_TIMEOUT_MS),
1318
1389
  npmRoot: async () => {
1319
1390
  const result = await npm(['root', '--global']);
1320
1391
  if (result.code !== 0)
1321
1392
  throw new ConductorError('could not ask npm for its global folder', [outputOf(result)]);
1322
1393
  return realpathSync(result.stdout.trim());
1323
1394
  },
1324
- npmView: async (spec) => {
1325
- const result = await npm(['view', spec, 'version']);
1326
- const found = result.stdout.trim();
1327
- if (result.code !== 0 || found === '')
1328
- throw new ConductorError(`npm has no ${spec}`, [outputOf(result)]);
1329
- return found;
1330
- },
1331
1395
  out,
1332
1396
  // The installed package's plugin: npm put it where this process's own plugin folder was.
1333
1397
  refreshPlugin: () => void refreshConductorPlugin(paths),
1334
1398
  stopDaemon: () => stopDaemon(paths),
1399
+ tagVersion: (tag) => tagVersion(release, tag),
1335
1400
  });
1336
1401
  }
1337
1402
  function verbIo() {