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