@detergent-software/conductor 3.0.0-dev.4 → 3.0.0-dev.40
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.
- package/CHANGELOG.md +339 -0
- package/README.md +270 -123
- package/build/brief-cli.js +5 -12
- package/build/brief-cli.js.map +1 -1
- package/build/brief.js +4 -3
- package/build/brief.js.map +1 -1
- package/build/broker-cli.js +15 -10
- package/build/broker-cli.js.map +1 -1
- package/build/broker.js +1 -1
- package/build/broker.js.map +1 -1
- package/build/claude.js +48 -2
- package/build/claude.js.map +1 -1
- package/build/cli.js +221 -71
- package/build/cli.js.map +1 -1
- package/build/clones.js +92 -0
- package/build/clones.js.map +1 -0
- package/build/code-host-cli.js +158 -39
- package/build/code-host-cli.js.map +1 -1
- package/build/code-host-service.js +35 -2
- package/build/code-host-service.js.map +1 -1
- package/build/code-host.js +140 -16
- package/build/code-host.js.map +1 -1
- package/build/conditions.js +33 -16
- package/build/conditions.js.map +1 -1
- package/build/config.js +58 -28
- package/build/config.js.map +1 -1
- package/build/console.js +43 -27
- package/build/console.js.map +1 -1
- package/build/credentials.js +49 -36
- package/build/credentials.js.map +1 -1
- package/build/daemon.js +57 -66
- package/build/daemon.js.map +1 -1
- package/build/file-lock.js +35 -3
- package/build/file-lock.js.map +1 -1
- package/build/follow-ups.js +2 -2
- package/build/follow-ups.js.map +1 -1
- package/build/help.js +228 -82
- package/build/help.js.map +1 -1
- package/build/history.js +10 -4
- package/build/history.js.map +1 -1
- package/build/host/credential-manager.js +109 -0
- package/build/host/credential-manager.js.map +1 -0
- package/build/host/keychain.js +44 -0
- package/build/host/keychain.js.map +1 -0
- package/build/host/launchd.js +118 -0
- package/build/host/launchd.js.map +1 -0
- package/build/host/libsecret.js +40 -0
- package/build/host/libsecret.js.map +1 -0
- package/build/host/macos-terminal.js +63 -0
- package/build/host/macos-terminal.js.map +1 -0
- package/build/host/platform.js +4 -0
- package/build/host/platform.js.map +1 -0
- package/build/host/secrets.js +46 -0
- package/build/host/secrets.js.map +1 -0
- package/build/host/shell.js +26 -0
- package/build/host/shell.js.map +1 -0
- package/build/host/startup.js +26 -0
- package/build/host/startup.js.map +1 -0
- package/build/host/systemd.js +91 -0
- package/build/host/systemd.js.map +1 -0
- package/build/host/task-scheduler.js +78 -0
- package/build/host/task-scheduler.js.map +1 -0
- package/build/host/terminal.js +46 -0
- package/build/host/terminal.js.map +1 -0
- package/build/host/tmux.js +39 -0
- package/build/host/tmux.js.map +1 -0
- package/build/host/windows-terminal.js +46 -0
- package/build/host/windows-terminal.js.map +1 -0
- package/build/in-process.js +6 -5
- package/build/in-process.js.map +1 -1
- package/build/inbox.js +50 -34
- package/build/inbox.js.map +1 -1
- package/build/init.js +509 -0
- package/build/init.js.map +1 -0
- package/build/landing.js +86 -7
- package/build/landing.js.map +1 -1
- package/build/lib/providers/ado/code-host.js +5 -1
- package/build/lib/providers/ado/code-host.js.map +1 -1
- package/build/lib/providers/ado/pipelines.js +138 -26
- package/build/lib/providers/ado/pipelines.js.map +1 -1
- package/build/lib/providers/ado/trigger.js +5 -0
- package/build/lib/providers/ado/trigger.js.map +1 -1
- package/build/lib/providers/ado/writes.js +17 -2
- package/build/lib/providers/ado/writes.js.map +1 -1
- package/build/lib/providers/github/actions.js +90 -3
- package/build/lib/providers/github/actions.js.map +1 -1
- package/build/lib/providers/github/code-host.js +2 -0
- package/build/lib/providers/github/code-host.js.map +1 -1
- package/build/lib/providers/github/writes.js +25 -2
- package/build/lib/providers/github/writes.js.map +1 -1
- package/build/lib/providers/index.js +4 -3
- package/build/lib/providers/index.js.map +1 -1
- package/build/lib/providers/npm/stage.js +28 -5
- package/build/lib/providers/npm/stage.js.map +1 -1
- package/build/lib/providers/timing.js +10 -0
- package/build/lib/providers/timing.js.map +1 -0
- package/build/lib/providers/verdicts/pr.js +47 -6
- package/build/lib/providers/verdicts/pr.js.map +1 -1
- package/build/lib/providers/verdicts/runs.js +12 -2
- package/build/lib/providers/verdicts/runs.js.map +1 -1
- package/build/logins.js +1 -1
- package/build/logins.js.map +1 -1
- package/build/manager.js +131 -55
- package/build/manager.js.map +1 -1
- package/build/note.js +5 -15
- package/build/note.js.map +1 -1
- package/build/notes.js +130 -23
- package/build/notes.js.map +1 -1
- package/build/notify.js +104 -42
- package/build/notify.js.map +1 -1
- package/build/npm-reads.js +323 -0
- package/build/npm-reads.js.map +1 -0
- package/build/ownership.js +2 -2
- package/build/paths.js +44 -1
- package/build/paths.js.map +1 -1
- package/build/plugin.js +4 -2
- package/build/plugin.js.map +1 -1
- package/build/profile-fields.js +153 -0
- package/build/profile-fields.js.map +1 -0
- package/build/profile-layers.js +264 -0
- package/build/profile-layers.js.map +1 -0
- package/build/profile.js +107 -51
- package/build/profile.js.map +1 -1
- package/build/project-check-cli.js +19 -19
- package/build/project-check-cli.js.map +1 -1
- package/build/project-check.js +194 -108
- package/build/project-check.js.map +1 -1
- package/build/protocol.js +5 -21
- package/build/protocol.js.map +1 -1
- package/build/pushover.js +5 -5
- package/build/pushover.js.map +1 -1
- package/build/queue.js +4 -4
- package/build/queue.js.map +1 -1
- package/build/registry.js +195 -36
- package/build/registry.js.map +1 -1
- package/build/release-kinds.js +34 -0
- package/build/release-kinds.js.map +1 -0
- package/build/rule-actions.js +8 -6
- package/build/rule-actions.js.map +1 -1
- package/build/rules-service.js +104 -47
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +23 -23
- package/build/rules.js.map +1 -1
- package/build/run.js +20 -7
- package/build/run.js.map +1 -1
- package/build/runs.js +218 -68
- package/build/runs.js.map +1 -1
- package/build/scheduler.js +89 -13
- package/build/scheduler.js.map +1 -1
- package/build/search.js +2 -1
- package/build/search.js.map +1 -1
- package/build/server.js +51 -14
- package/build/server.js.map +1 -1
- package/build/spend.js +1 -1
- package/build/spend.js.map +1 -1
- package/build/store.js +79 -20
- package/build/store.js.map +1 -1
- package/build/supervise.js +1 -1
- package/build/supervise.js.map +1 -1
- package/build/timeline.js +104 -0
- package/build/timeline.js.map +1 -0
- package/build/tracker-reads-cli.js +4 -5
- package/build/tracker-reads-cli.js.map +1 -1
- package/build/trackers.js +56 -10
- package/build/trackers.js.map +1 -1
- package/build/train-cli.js +6 -5
- package/build/train-cli.js.map +1 -1
- package/build/train.js +2 -2
- package/build/train.js.map +1 -1
- package/build/upgrade.js +8 -9
- package/build/upgrade.js.map +1 -1
- package/build/wait.js +10 -10
- package/build/wait.js.map +1 -1
- package/build/watchers/ado.js +42 -18
- package/build/watchers/ado.js.map +1 -1
- package/build/watchers/github.js +19 -4
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/index.js +2 -2
- package/build/watchers/index.js.map +1 -1
- package/build/watchers/linear-queue.js +47 -14
- package/build/watchers/linear-queue.js.map +1 -1
- package/build/watchers/linear-writes.js +26 -12
- package/build/watchers/linear-writes.js.map +1 -1
- package/build/watchers/linear.js +2 -2
- package/build/watchers/linear.js.map +1 -1
- package/build/watchers/npm.js +6 -4
- package/build/watchers/npm.js.map +1 -1
- package/build/webhook.js +80 -0
- package/build/webhook.js.map +1 -0
- package/build/why.js +1 -1
- package/build/why.js.map +1 -1
- package/build/worktrees.js +56 -93
- package/build/worktrees.js.map +1 -1
- package/package.json +61 -61
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/hooks/model.ts +46 -6
- package/plugin/hooks/register.tsx +41 -2
- package/plugin/skills/conductor-console/SKILL.md +26 -27
- package/plugin/skills/work-item/SKILL.md +57 -53
- package/plugin/skills/work-item/trackers/ado-boards.md +2 -2
- package/plugin/skills/work-item/trackers/github-issues.md +1 -1
- package/plugin/skills/work-item/trackers/linear.md +2 -2
- package/plugin/types/index.d.ts +1 -1
- package/protocol/PROTOCOL.md +3 -9
- package/build/checkout-sync.js +0 -249
- package/build/checkout-sync.js.map +0 -1
- package/build/credential.js +0 -66
- package/build/credential.js.map +0 -1
- package/build/startup.js +0 -92
- package/build/startup.js.map +0 -1
- package/build/tab.js +0 -32
- 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 {
|
|
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, prThreadsCommand, replyCommand, runsCommand, stateCommand, timelineCommand, } from "./code-host-cli.js";
|
|
12
13
|
import { configSchema, loadConfig } from "./config.js";
|
|
13
|
-
import { claimConsole, CONSOLE_WINDOW, consoleArgs,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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: {
|
|
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,
|
|
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`:
|
|
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:
|
|
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
|
-
|
|
211
|
-
|
|
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
|
|
224
|
-
if (
|
|
225
|
-
throw new ConductorError(
|
|
226
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
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
|
|
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']
|
|
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,6 +745,12 @@ 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;
|
|
@@ -687,9 +768,21 @@ async function main(argv) {
|
|
|
687
768
|
case 'ports':
|
|
688
769
|
process.exitCode = await portsCommand(paths, sub, { json: values.json === true }, brokerIo());
|
|
689
770
|
return;
|
|
771
|
+
case 'profile': {
|
|
772
|
+
if (sub === undefined || arg !== undefined)
|
|
773
|
+
throw usageError('profile');
|
|
774
|
+
// Resolved here from the machine file and the clone, as the daemon resolves it.
|
|
775
|
+
const layered = projectProfile(paths, sub);
|
|
776
|
+
if (values.json === true)
|
|
777
|
+
return out(JSON.stringify({ profile: layered.profile, sources: layered.sources }, null, 2));
|
|
778
|
+
profileLines(sub, layered).forEach((line) => out(line));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
690
781
|
case 'project':
|
|
691
|
-
if (sub === 'add' && arg !== undefined)
|
|
692
|
-
|
|
782
|
+
if (sub === 'add' && arg !== undefined) {
|
|
783
|
+
await addProject(paths, arg, values.name);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
693
786
|
if (sub === 'remove' && arg !== undefined)
|
|
694
787
|
return removeProject(paths, arg);
|
|
695
788
|
if (sub === 'list')
|
|
@@ -730,6 +823,7 @@ async function main(argv) {
|
|
|
730
823
|
...item,
|
|
731
824
|
...repo,
|
|
732
825
|
...(values.for === undefined ? {} : { for: values.for }),
|
|
826
|
+
...(values.grep === undefined ? {} : { grep: values.grep }),
|
|
733
827
|
...(values.requeue === undefined ? {} : { requeue: values.requeue }),
|
|
734
828
|
}, verbIo());
|
|
735
829
|
return;
|
|
@@ -755,6 +849,19 @@ async function main(argv) {
|
|
|
755
849
|
case 'supervise':
|
|
756
850
|
process.exitCode = await superviseCommand(paths, values['fail-fast'] === true);
|
|
757
851
|
return;
|
|
852
|
+
case 'timeline':
|
|
853
|
+
if (arg !== undefined)
|
|
854
|
+
throw usageError('timeline');
|
|
855
|
+
process.exitCode = await timelineCommand(paths, {
|
|
856
|
+
...repo,
|
|
857
|
+
...(sub === undefined ? {} : { run: sub }),
|
|
858
|
+
...(values.branch === undefined ? {} : { branch: values.branch }),
|
|
859
|
+
...(values.count === undefined ? {} : { count: values.count }),
|
|
860
|
+
...(values.for === undefined ? {} : { for: values.for }),
|
|
861
|
+
...(values.grep === undefined ? {} : { grep: values.grep }),
|
|
862
|
+
...(values.task === undefined ? {} : { task: values.task }),
|
|
863
|
+
}, verbIo());
|
|
864
|
+
return;
|
|
758
865
|
case 'train':
|
|
759
866
|
return trainCommand(paths, sub, { dryRun: values['dry-run'] === true }, { out });
|
|
760
867
|
case 'unpause':
|
|
@@ -810,7 +917,7 @@ async function notifyTest(paths, options) {
|
|
|
810
917
|
...(options.kind ? { kind: options.kind } : {}),
|
|
811
918
|
raise: options.raise,
|
|
812
919
|
};
|
|
813
|
-
// Pushover can take its 15 s, after a
|
|
920
|
+
// Pushover can take its 15 s, after a secret store read.
|
|
814
921
|
const answer = await notifyAnswer(paths, { body, method: 'POST', path: '/notify/test', timeoutMs: 60_000 });
|
|
815
922
|
if (options.raise) {
|
|
816
923
|
const { item } = z.object({ item: inboxItemSchema }).parse(answer);
|
|
@@ -888,33 +995,35 @@ function parsing(command, parse) {
|
|
|
888
995
|
: new ConductorError(message, ['run `conduct help` for the commands']);
|
|
889
996
|
}
|
|
890
997
|
}
|
|
891
|
-
// The open plan item of `ref`: the item (
|
|
892
|
-
// bare number is refused: it could be an issue's number, and the
|
|
998
|
+
// The open plan item of `ref`: the item (ENG-312) whose plan waits, or an inbox id written #12; null
|
|
999
|
+
// when no plan of the item waits. A bare number is refused: it could be an issue's number, and the
|
|
1000
|
+
// answer is posted as the operator's.
|
|
893
1001
|
async function planItem(paths, ref) {
|
|
894
1002
|
if (ref.startsWith('#'))
|
|
895
1003
|
return inboxId(ref);
|
|
1004
|
+
const items = await openItems(paths);
|
|
896
1005
|
if (/^\d+$/.test(ref)) {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1006
|
+
// The waiting plans whose key ends in the number, else a placeholder: the project's key isn't known here.
|
|
1007
|
+
const keys = (items ?? []).flatMap((i) => (i.kind === 'plan' && i.item?.endsWith(`-${ref}`) ? [i.item] : []));
|
|
1008
|
+
const item = keys.length > 0 ? keys.join(' or ') : `<KEY>-${ref}`;
|
|
1009
|
+
throw new ConductorError(`"${ref}" is ambiguous`, [`name the item, such as ${item}, or the inbox id as #${ref}`]);
|
|
900
1010
|
}
|
|
901
|
-
const items = await openItems(paths);
|
|
902
1011
|
if (items === null)
|
|
903
1012
|
throw noInbox();
|
|
904
1013
|
const plans = items.filter((i) => i.kind === 'plan' && i.item?.toUpperCase() === ref.toUpperCase());
|
|
905
1014
|
if (plans.length === 1 && plans[0])
|
|
906
1015
|
return plans[0].id;
|
|
907
1016
|
if (plans.length === 0)
|
|
908
|
-
|
|
1017
|
+
return null;
|
|
909
1018
|
throw new ConductorError(`several plans of ${ref} wait`, plans.map((p) => `#${p.id} in ${p.project ?? '?'}: pass #${p.id}`));
|
|
910
1019
|
}
|
|
911
1020
|
function projectLine(p) {
|
|
912
1021
|
const state = p.problem
|
|
913
|
-
? `${p.problem}; the daemon reads
|
|
1022
|
+
? `${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
1023
|
: p.enabled
|
|
915
1024
|
? 'enabled'
|
|
916
1025
|
: 'disabled';
|
|
917
|
-
return ` ${p.name} ${p.
|
|
1026
|
+
return ` ${p.name} ${p.clone} (protocol ${p.protocol}, ${state})`;
|
|
918
1027
|
}
|
|
919
1028
|
// The registered projects: from the daemon, or from the store when it isn't running.
|
|
920
1029
|
async function projectsNow(paths) {
|
|
@@ -959,7 +1068,7 @@ async function removeProject(paths, name) {
|
|
|
959
1068
|
});
|
|
960
1069
|
}
|
|
961
1070
|
const { project, resolved } = result;
|
|
962
|
-
out(`removed ${project.name} (${project.
|
|
1071
|
+
out(`removed ${project.name} (${project.clone})`);
|
|
963
1072
|
for (const item of resolved)
|
|
964
1073
|
out(` resolved #${item.id} (${item.kind}${item.item ? ` ${item.item}` : ''})`);
|
|
965
1074
|
}
|
|
@@ -1009,6 +1118,45 @@ async function retire(paths, ref, force) {
|
|
|
1009
1118
|
out(`retired ${worker.name}`);
|
|
1010
1119
|
steps.forEach((s) => out(` ${s}`));
|
|
1011
1120
|
}
|
|
1121
|
+
// `conduct revise <item>` with no plan of the item in the inbox: the daemon posts the revise on an
|
|
1122
|
+
// approved plan and wakes its worker at once, as its revise sweep would (ATK-664).
|
|
1123
|
+
async function reviseApproved(paths, item, text) {
|
|
1124
|
+
const { body, status } = await callApi(paths, {
|
|
1125
|
+
body: { text },
|
|
1126
|
+
method: 'POST',
|
|
1127
|
+
path: `/items/${encodeURIComponent(item)}/revise`,
|
|
1128
|
+
timeoutMs: REVISE_TIMEOUT_MS,
|
|
1129
|
+
});
|
|
1130
|
+
if (status !== 200) {
|
|
1131
|
+
const refusal = answerRefusal.safeParse(body).data;
|
|
1132
|
+
const details = [...(refusal?.details ?? [])];
|
|
1133
|
+
if (status === 502 && refusal?.posted === 'unknown')
|
|
1134
|
+
details.push('the comment may be on the item already: look before answering again');
|
|
1135
|
+
// A conductor from before ATK-664 has no such route.
|
|
1136
|
+
if (status === 404)
|
|
1137
|
+
throw new ConductorError(`no plan of ${item} waits in the inbox`, [
|
|
1138
|
+
'`conduct inbox` lists what waits',
|
|
1139
|
+
'a conductor started before ATK-664 revises no approved plan: restart it for that',
|
|
1140
|
+
]);
|
|
1141
|
+
throw new ConductorError(refusal?.error ?? `the API answered ${status}`, details);
|
|
1142
|
+
}
|
|
1143
|
+
const r = reviseAnswer.parse(body).revise;
|
|
1144
|
+
out(`asked for a revision of ${r.item}'s approved plan: ${r.comment.url ?? r.comment.id}`);
|
|
1145
|
+
out(reviseOutcome(r));
|
|
1146
|
+
}
|
|
1147
|
+
// What became of a revise of an approved plan after its comment, in one line.
|
|
1148
|
+
function reviseOutcome(r) {
|
|
1149
|
+
const none = 'no worker is on it: one launched on it reads the revise in its brief';
|
|
1150
|
+
if (r.pending !== null)
|
|
1151
|
+
return `nothing recorded yet (${r.pending}): ${r.worker ? 'the revise sweep records it and wakes its worker within 5 minutes' : none}`;
|
|
1152
|
+
if (!r.revised)
|
|
1153
|
+
return 'a revise of its plan was recorded already: its worker re-plans on the first one';
|
|
1154
|
+
if (r.woken > 0)
|
|
1155
|
+
return 'woke its worker to re-plan';
|
|
1156
|
+
if (r.landed)
|
|
1157
|
+
return 'its PR has merged: its worker learns of it at its next note, for a follow-up PR';
|
|
1158
|
+
return r.worker ? 'its worker is running: its next note is held back, and it re-plans' : none;
|
|
1159
|
+
}
|
|
1012
1160
|
// `conduct rules`: from the daemon, or straight from the files when it isn't running.
|
|
1013
1161
|
async function rules(paths, json) {
|
|
1014
1162
|
const view = await rulesAnswer(paths, { method: 'GET', path: '/rules' }, (body) => rulesViewSchema.parse(body), (service) => service.view());
|
|
@@ -1089,7 +1237,7 @@ async function shutdownDaemon(paths) {
|
|
|
1089
1237
|
if (readEndpoint(paths) !== null) {
|
|
1090
1238
|
throw new ConductorError(`the conductor (pid ${pid ?? '?'}) did not stop within ${STOP_WAIT_MS / 1000} s`);
|
|
1091
1239
|
}
|
|
1092
|
-
// The port file goes before the process ends; a start (the logon
|
|
1240
|
+
// The port file goes before the process ends; a start (the logon startup's especially) waits for it.
|
|
1093
1241
|
const exitDeadline = Date.now() + STOP_WAIT_MS;
|
|
1094
1242
|
while (pid !== null && processAlive(pid) && Date.now() < exitDeadline)
|
|
1095
1243
|
await sleep(100);
|
|
@@ -1105,7 +1253,7 @@ async function start(paths) {
|
|
|
1105
1253
|
const pid = readPid(paths.pid);
|
|
1106
1254
|
if (pid !== null && (await ping(paths, pid)))
|
|
1107
1255
|
return out(`the conductor is running (pid ${pid})`);
|
|
1108
|
-
// A daemon that is still starting (the logon
|
|
1256
|
+
// A daemon that is still starting (the logon startup's, say) holds the lock: wait for it rather than
|
|
1109
1257
|
// start a second one, which would exit on that lock. A pid a reboot reused gets a start after.
|
|
1110
1258
|
if (pid !== null && processAlive(pid)) {
|
|
1111
1259
|
try {
|
|
@@ -1153,7 +1301,7 @@ async function status(paths, json) {
|
|
|
1153
1301
|
if (json)
|
|
1154
1302
|
return out(JSON.stringify({ home: paths.home, inbox: items, locks, pid, port, projects, running: true, workers, workersFrom: 'daemon' }, null, 2));
|
|
1155
1303
|
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:');
|
|
1304
|
+
out(projects.length === 0 ? 'no projects; add one with `conduct project add <url | path>`' : 'projects:');
|
|
1157
1305
|
projects.forEach((p) => out(projectLine(p)));
|
|
1158
1306
|
out(workers.length === 0 ? 'no workers' : 'workers:');
|
|
1159
1307
|
workers.forEach((w) => out(workerLine(w)));
|
|
@@ -1191,7 +1339,7 @@ async function stopDaemon(paths) {
|
|
|
1191
1339
|
out('the conductor is not running');
|
|
1192
1340
|
return false;
|
|
1193
1341
|
case 'supervisor': {
|
|
1194
|
-
// It answers just before it ends; a start (the logon
|
|
1342
|
+
// It answers just before it ends; a start (the logon startup's especially) needs it gone.
|
|
1195
1343
|
const deadline = Date.now() + STOP_WAIT_MS;
|
|
1196
1344
|
while (processAlive(stop.pid) && Date.now() < deadline)
|
|
1197
1345
|
await sleep(100);
|
|
@@ -1200,7 +1348,7 @@ async function stopDaemon(paths) {
|
|
|
1200
1348
|
}
|
|
1201
1349
|
}
|
|
1202
1350
|
}
|
|
1203
|
-
// `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon
|
|
1351
|
+
// `conduct supervise`: keeps `conduct serve` running (supervise.ts, ATK-417). The logon startup runs
|
|
1204
1352
|
// it; `conduct start` runs it with --fail-fast. The config is read again for each push, so one fixed
|
|
1205
1353
|
// while the daemon is down counts, and one that doesn't load (a reason it may be down) leaves the
|
|
1206
1354
|
// defaults: no quiet hours, the default credential.
|
|
@@ -1222,7 +1370,7 @@ async function superviseCommand(paths, failFast) {
|
|
|
1222
1370
|
rotatingLog(paths.log, config().log).log(...line);
|
|
1223
1371
|
}
|
|
1224
1372
|
catch {
|
|
1225
|
-
// Nowhere else to say it: under the logon
|
|
1373
|
+
// Nowhere else to say it: under the logon startup nobody reads stderr.
|
|
1226
1374
|
}
|
|
1227
1375
|
},
|
|
1228
1376
|
};
|
|
@@ -1298,8 +1446,9 @@ async function superviseCommand(paths, failFast) {
|
|
|
1298
1446
|
}
|
|
1299
1447
|
return code;
|
|
1300
1448
|
}
|
|
1301
|
-
function uninstall() {
|
|
1302
|
-
|
|
1449
|
+
async function uninstall() {
|
|
1450
|
+
const task = startup();
|
|
1451
|
+
out((await task.remove()) ? `removed ${task.name}` : `no ${task.name} to remove`);
|
|
1303
1452
|
}
|
|
1304
1453
|
// --until: 17:00 (the next one), tomorrow, a day such as monday, or a date and time.
|
|
1305
1454
|
function untilOf(text) {
|
|
@@ -1307,7 +1456,8 @@ function untilOf(text) {
|
|
|
1307
1456
|
}
|
|
1308
1457
|
async function upgradeCommand(paths, tag, allowDowngrade) {
|
|
1309
1458
|
const cliPath = realpathSync(fileURLToPath(import.meta.url));
|
|
1310
|
-
|
|
1459
|
+
// npm's own settings, so the tag is read where `npm install --global` installs from (ATK-561).
|
|
1460
|
+
const release = npmStage(sharedNpmReads());
|
|
1311
1461
|
await upgrade({ allowDowngrade, tag: parseTag(tag) }, {
|
|
1312
1462
|
...daemonStarter(paths, cliPath),
|
|
1313
1463
|
cliPath,
|