@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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ConductorError } from "../errors.js";
|
|
4
|
+
import { outputOf } from "../run.js";
|
|
5
|
+
export const SYSTEMD_UNIT = 'conductor.service';
|
|
6
|
+
// A systemd user unit that runs `conduct supervise` when the user's service manager starts: at
|
|
7
|
+
// login, or at boot with lingering on. supervise restarts the daemon and conduct stop must stick,
|
|
8
|
+
// so no Restart=; and KillMode=process, or the unit ending (a stop, an upgrade) would kill its whole
|
|
9
|
+
// cgroup, the workers and tmux the daemon started included. Its output goes to the journal
|
|
10
|
+
// (journalctl --user -u conductor).
|
|
11
|
+
export function systemd({ env, home, run, user, }) {
|
|
12
|
+
// systemd reads user units from $XDG_CONFIG_HOME/systemd/user, ~/.config without it.
|
|
13
|
+
const unit = join(env.XDG_CONFIG_HOME || join(home, '.config'), 'systemd', 'user', SYSTEMD_UNIT);
|
|
14
|
+
const systemctl = async (what, args) => {
|
|
15
|
+
const result = await run('systemctl', ['--user', ...args], { timeoutMs: 60_000 });
|
|
16
|
+
if (result.code !== 0)
|
|
17
|
+
throw systemctlFailed(what, result);
|
|
18
|
+
};
|
|
19
|
+
const text = () => (existsSync(unit) ? readFileSync(unit, 'utf8') : undefined);
|
|
20
|
+
return {
|
|
21
|
+
install: async (spec) => {
|
|
22
|
+
mkdirSync(dirname(unit), { recursive: true });
|
|
23
|
+
writeFileSync(unit, systemdUnit(spec));
|
|
24
|
+
await systemctl('could not load the systemd unit', ['daemon-reload']);
|
|
25
|
+
await systemctl('could not enable the systemd unit', ['enable', SYSTEMD_UNIT]);
|
|
26
|
+
// A running one keeps the command it started with until it restarts.
|
|
27
|
+
await systemctl('could not start the systemd unit', ['restart', SYSTEMD_UNIT]);
|
|
28
|
+
const linger = await run('loginctl', ['show-user', user, '--property=Linger', '--value'], { timeoutMs: 30_000 });
|
|
29
|
+
return [
|
|
30
|
+
'systemd started it now',
|
|
31
|
+
...(linger.stdout.trim() === 'yes'
|
|
32
|
+
? []
|
|
33
|
+
: [
|
|
34
|
+
`it starts at your login; to start it at boot on a machine nobody logs in to, run \`loginctl enable-linger ${user || '$USER'}\``,
|
|
35
|
+
]),
|
|
36
|
+
];
|
|
37
|
+
},
|
|
38
|
+
name: `the systemd user unit ${SYSTEMD_UNIT}`,
|
|
39
|
+
// Disabled and gone, but not stopped: a daemon it started keeps running, as on Windows.
|
|
40
|
+
remove: async () => {
|
|
41
|
+
if (!existsSync(unit))
|
|
42
|
+
return false;
|
|
43
|
+
await systemctl('could not disable the systemd unit', ['disable', SYSTEMD_UNIT]);
|
|
44
|
+
rmSync(unit);
|
|
45
|
+
await run('systemctl', ['--user', 'daemon-reload'], { timeoutMs: 60_000 });
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
run: () => systemctl('could not start the systemd unit', ['start', SYSTEMD_UNIT]),
|
|
49
|
+
runsCli: async (cliPath) => execStart(text())?.includes(` ${unitWord(cliPath)} `) ?? false,
|
|
50
|
+
supervises: async () => / supervise$/.test(execStart(text()) ?? ''),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// The unit file.
|
|
54
|
+
export function systemdUnit({ cliPath, nodePath, path }) {
|
|
55
|
+
return [
|
|
56
|
+
'[Unit]',
|
|
57
|
+
'Description=The conductor daemon (conduct supervise)',
|
|
58
|
+
'',
|
|
59
|
+
'[Service]',
|
|
60
|
+
`ExecStart=${[nodePath, cliPath].map((word) => unitWord(word)).join(' ')} supervise`,
|
|
61
|
+
`Environment=${unitWord(`PATH=${path}`, false)}`,
|
|
62
|
+
'KillMode=process',
|
|
63
|
+
'',
|
|
64
|
+
'[Install]',
|
|
65
|
+
'WantedBy=default.target',
|
|
66
|
+
'',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
function execStart(unit) {
|
|
70
|
+
return /^ExecStart=(.*)$/m.exec(unit ?? '')?.[1];
|
|
71
|
+
}
|
|
72
|
+
function systemctlFailed(what, result) {
|
|
73
|
+
if (/ENOENT/.test(result.error ?? '')) {
|
|
74
|
+
return new ConductorError(`${what}: no systemctl`, ['start `conduct supervise` from your own login items']);
|
|
75
|
+
}
|
|
76
|
+
return new ConductorError(what, [
|
|
77
|
+
outputOf(result, 1000),
|
|
78
|
+
"systemctl --user needs your login session's service manager: run it in a session you logged in to, not after su",
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
// A word in quotes, as ExecStart= and Environment= read it: `%` starts a specifier, and in
|
|
82
|
+
// ExecStart= `$` an environment variable.
|
|
83
|
+
function unitWord(text, command = true) {
|
|
84
|
+
const escaped = text
|
|
85
|
+
.replaceAll('\\', '\\\\')
|
|
86
|
+
.replaceAll('"', '\\"')
|
|
87
|
+
.replaceAll('%', '%%')
|
|
88
|
+
.replaceAll('$', () => (command ? '$$' : '$'));
|
|
89
|
+
return `"${escaped}"`;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=systemd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemd.js","sourceRoot":"","sources":["../../src/host/systemd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ConductorError } from "../errors.js";
|
|
2
|
+
import { outputOf } from "../run.js";
|
|
3
|
+
export const TASK_NAME = 'Conductor';
|
|
4
|
+
// The task's <Arguments> quote the cli.js path (install); XML escapes it, and Windows paths compare
|
|
5
|
+
// without case.
|
|
6
|
+
export function taskRunsCli(xml, cliPath) {
|
|
7
|
+
const args = taskArguments(xml);
|
|
8
|
+
return args !== undefined && args.toLowerCase().includes(`"${cliPath.toLowerCase()}"`);
|
|
9
|
+
}
|
|
10
|
+
// A per-user Task Scheduler task that runs `conduct supervise` at logon, which keeps `conduct serve`
|
|
11
|
+
// running (supervise.ts). No elevation is needed for a task that runs as the current user.
|
|
12
|
+
// conhost --headless keeps a console window (or a Windows Terminal tab) from opening, and the zero
|
|
13
|
+
// time limit stops Task Scheduler from ending the daemon after its default 72 hours. A second start
|
|
14
|
+
// while one runs exits on the pid lock, and its supervisor with it. Installing doesn't start it.
|
|
15
|
+
export function taskScheduler(run) {
|
|
16
|
+
const schtasks = (args) => run('schtasks', args, { timeoutMs: 60_000 });
|
|
17
|
+
const taskXml = async () => {
|
|
18
|
+
const result = await schtasks(['/Query', '/TN', TASK_NAME, '/XML']);
|
|
19
|
+
return result.code === 0 ? result.stdout : undefined;
|
|
20
|
+
};
|
|
21
|
+
const installed = async () => (await schtasks(['/Query', '/TN', TASK_NAME])).code === 0;
|
|
22
|
+
return {
|
|
23
|
+
install: async ({ cliPath, nodePath }) => {
|
|
24
|
+
const script = [
|
|
25
|
+
"$ErrorActionPreference = 'Stop'",
|
|
26
|
+
'$user = "$env:USERDOMAIN\\$env:USERNAME"',
|
|
27
|
+
'$action = New-ScheduledTaskAction -Execute "$env:SystemRoot\\System32\\conhost.exe" -Argument $env:CONDUCTOR_TASK_ARGS',
|
|
28
|
+
'$trigger = New-ScheduledTaskTrigger -AtLogOn -User $user',
|
|
29
|
+
'$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit ([TimeSpan]::Zero)',
|
|
30
|
+
'$principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Limited',
|
|
31
|
+
`Register-ScheduledTask -TaskName '${TASK_NAME}' -Description 'Starts the conductor daemon (conduct supervise) at logon.' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Force | Out-Null`,
|
|
32
|
+
].join('\n');
|
|
33
|
+
const result = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
34
|
+
env: { ...process.env, CONDUCTOR_TASK_ARGS: `--headless "${nodePath}" "${cliPath}" supervise` },
|
|
35
|
+
timeoutMs: 60_000,
|
|
36
|
+
});
|
|
37
|
+
if (result.code !== 0)
|
|
38
|
+
throw new ConductorError('could not register the logon task', [outputOf(result, 1000)]);
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
name: `the "${TASK_NAME}" logon task`,
|
|
42
|
+
remove: async () => {
|
|
43
|
+
if (!(await installed()))
|
|
44
|
+
return false;
|
|
45
|
+
const result = await schtasks(['/Delete', '/TN', TASK_NAME, '/F']);
|
|
46
|
+
if (result.code !== 0)
|
|
47
|
+
throw new ConductorError('could not remove the logon task', [outputOf(result, 1000)]);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
run: async () => {
|
|
51
|
+
const result = await schtasks(['/Run', '/TN', TASK_NAME]);
|
|
52
|
+
if (result.code !== 0)
|
|
53
|
+
throw new ConductorError(`could not run the "${TASK_NAME}" logon task`, [outputOf(result, 1000)]);
|
|
54
|
+
},
|
|
55
|
+
runsCli: async (cliPath) => {
|
|
56
|
+
const xml = await taskXml();
|
|
57
|
+
return xml !== undefined && taskRunsCli(xml, cliPath);
|
|
58
|
+
},
|
|
59
|
+
supervises: async () => {
|
|
60
|
+
const xml = await taskXml();
|
|
61
|
+
return xml !== undefined && taskSupervises(xml);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function taskSupervises(xml) {
|
|
66
|
+
return /\ssupervise\s*$/.test(taskArguments(xml) ?? '');
|
|
67
|
+
}
|
|
68
|
+
// The task action's <Arguments>, XML unescaped.
|
|
69
|
+
function taskArguments(xml) {
|
|
70
|
+
return /<Arguments>([^<]*)<\/Arguments>/
|
|
71
|
+
.exec(xml)?.[1]
|
|
72
|
+
?.replaceAll('"', '"')
|
|
73
|
+
.replaceAll(''', "'")
|
|
74
|
+
.replaceAll('<', '<')
|
|
75
|
+
.replaceAll('>', '>')
|
|
76
|
+
.replaceAll('&', '&');
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=task-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-scheduler.js","sourceRoot":"","sources":["../../src/host/task-scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { run as runCommand } from "../run.js";
|
|
5
|
+
import { appleTerminal, iterm } from "./macos-terminal.js";
|
|
6
|
+
import { hostPlatform } from "./platform.js";
|
|
7
|
+
import { tmux } from "./tmux.js";
|
|
8
|
+
import { windowsTerminal } from "./windows-terminal.js";
|
|
9
|
+
// config.json's workers.terminal: auto is Windows Terminal on Windows, iTerm on macOS where it is
|
|
10
|
+
// installed and Terminal where it isn't, and tmux elsewhere.
|
|
11
|
+
export const TERMINALS = ['auto', 'windows-terminal', 'terminal', 'iterm', 'tmux'];
|
|
12
|
+
const ITERM_APPS = ['/Applications/iTerm.app', join(homedir(), 'Applications', 'iTerm.app')];
|
|
13
|
+
// This platform's terminal, or the one config.json names.
|
|
14
|
+
export function terminal({ choice = 'auto', installed = existsSync, platform = hostPlatform(), run = runCommand, } = {}) {
|
|
15
|
+
const picked = choice !== 'auto'
|
|
16
|
+
? choice
|
|
17
|
+
: platform === 'win32'
|
|
18
|
+
? 'windows-terminal'
|
|
19
|
+
: platform === 'darwin'
|
|
20
|
+
? ITERM_APPS.some((app) => installed(app))
|
|
21
|
+
? 'iterm'
|
|
22
|
+
: 'terminal'
|
|
23
|
+
: 'tmux';
|
|
24
|
+
const terminals = {
|
|
25
|
+
iterm: () => iterm(run),
|
|
26
|
+
terminal: () => appleTerminal(run),
|
|
27
|
+
tmux: () => tmux(run),
|
|
28
|
+
'windows-terminal': () => windowsTerminal(run),
|
|
29
|
+
};
|
|
30
|
+
return terminals[picked]();
|
|
31
|
+
}
|
|
32
|
+
export function workerTabs(where, onLaunch, clients) {
|
|
33
|
+
return {
|
|
34
|
+
...(clients ? { clients } : {}),
|
|
35
|
+
hint: (window) => where.attachHint(window),
|
|
36
|
+
onLaunch,
|
|
37
|
+
open: (tab) => where.open({
|
|
38
|
+
command: ['claude', 'attach', tab.handle],
|
|
39
|
+
cwd: tab.cwd,
|
|
40
|
+
env: {},
|
|
41
|
+
hold: true,
|
|
42
|
+
title: tab.title,
|
|
43
|
+
window: tab.window,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.js","sourceRoot":"","sources":["../../src/host/terminal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,OAAO,EAAE,GAAG,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAqB,YAAY,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,kGAAkG;AAClG,6DAA6D;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,kBAAkB,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,CAAU,CAAC;AA+C5F,MAAM,UAAU,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;AAE7F,0DAA0D;AAC1D,MAAM,UAAU,QAAQ,CAAC,EACvB,MAAM,GAAG,MAAM,EACf,SAAS,GAAG,UAAU,EACtB,QAAQ,GAAG,YAAY,EAAE,EACzB,GAAG,GAAG,UAAU,MAOd,EAAE;IACJ,MAAM,MAAM,GACV,MAAM,KAAK,MAAM;QACf,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,QAAQ,KAAK,OAAO;YACpB,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,QAAQ,KAAK,QAAQ;gBACrB,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;oBACxC,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,UAAU;gBACd,CAAC,CAAC,MAAM,CAAC;IACjB,MAAM,SAAS,GAA0C;QACvD,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;QACvB,QAAQ,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC;QAClC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC;QACrB,kBAAkB,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC;KAC/C,CAAC;IACF,OAAO,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAe,EAAE,QAAiB,EAAE,OAAuB;IACpF,OAAO;QACL,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;QAC1C,QAAQ;QACR,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CACZ,KAAK,CAAC,IAAI,CAAC;YACT,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC;YACzC,GAAG,EAAE,GAAG,CAAC,GAAG;YACZ,GAAG,EAAE,EAAE;YACP,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { outputOf } from "../run.js";
|
|
2
|
+
import { shLine, viaSh } from "./shell.js";
|
|
3
|
+
// tmux: a detached session per window, made by its first tab, and a tmux window per tab. It runs
|
|
4
|
+
// where there is no desktop (a server over ssh), so a person reaches it with `tmux attach`. tmux
|
|
5
|
+
// starts a tab in the tmux server's environment, so `env` goes on the command line, and runs it in
|
|
6
|
+
// its default-shell, so the line goes through sh.
|
|
7
|
+
export function tmux(run) {
|
|
8
|
+
return {
|
|
9
|
+
attachHint: (window) => `tmux attach -t ${tmuxSession(window)}`,
|
|
10
|
+
name: 'tmux',
|
|
11
|
+
open: async (tab) => {
|
|
12
|
+
const session = tmuxSession(tab.window);
|
|
13
|
+
// `=` matches the name exactly, not as a prefix.
|
|
14
|
+
const has = await run('tmux', ['has-session', '-t', `=${session}`], { timeoutMs: 30_000 });
|
|
15
|
+
if (has.error !== null && /ENOENT/.test(has.error))
|
|
16
|
+
return 'no tmux to open it in';
|
|
17
|
+
// tmux expands formats in -n and -c, and `#(…)` in one runs a command: `##` is a plain `#`.
|
|
18
|
+
const literal = (text) => text.replace(/#/g, '##');
|
|
19
|
+
const title = literal(tab.title.replace(/\s+/g, ' ').trim().slice(0, 80));
|
|
20
|
+
const open = (where) => run('tmux', [...where, '-n', title, '-c', literal(tab.cwd), viaSh(shLine(tab))], { timeoutMs: 30_000 });
|
|
21
|
+
const inSession = ['new-window', '-t', `=${session}:`];
|
|
22
|
+
let result = await open(has.code === 0 ? inSession : ['new-session', '-d', '-s', session]);
|
|
23
|
+
// Another tab made the session since has-session asked.
|
|
24
|
+
if (has.code !== 0 && /duplicate session/.test(result.stderr))
|
|
25
|
+
result = await open(inSession);
|
|
26
|
+
return result.code === 0 ? null : outputOf(result, 500);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// The tmux session a window's tabs open in: its name in lower case, with a dash for anything tmux
|
|
31
|
+
// reads in a target (`.` and `:`) and anything a person would have to quote. "Conductor · web"
|
|
32
|
+
// becomes conductor-web.
|
|
33
|
+
export function tmuxSession(window) {
|
|
34
|
+
return (window
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '') || 'conductor');
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=tmux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tmux.js","sourceRoot":"","sources":["../../src/host/tmux.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE3C,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,kDAAkD;AAClD,MAAM,UAAU,IAAI,CAAC,GAAQ;IAC3B,OAAO;QACL,UAAU,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,kBAAkB,WAAW,CAAC,MAAM,CAAC,EAAE;QAC/D,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAClB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACxC,iDAAiD;YACjD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,OAAO,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3F,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,OAAO,uBAAuB,CAAC;YACnF,4FAA4F;YAC5F,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YACnE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YAC1E,MAAM,IAAI,GAAG,CAAC,KAAwB,EAAE,EAAE,CACxC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;YAC1G,MAAM,SAAS,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,OAAO,GAAG,CAAC,CAAC;YACvD,IAAI,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YAC3F,wDAAwD;YACxD,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;gBAAE,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC;YAC9F,OAAO,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC;AAED,kGAAkG;AAClG,+FAA+F;AAC/F,yBAAyB;AACzB,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,OAAO,CACL,MAAM;SACH,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,WAAW,CAC1C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { outputOf } from "../run.js";
|
|
2
|
+
import { failedText } from "./shell.js";
|
|
3
|
+
// Windows Terminal (wt.exe). Not hidden: when the named window doesn't exist yet, Windows Terminal
|
|
4
|
+
// creates it with the spawn's SW_HIDE, and the tab runs where nobody sees it (ATK-469, ATK-336).
|
|
5
|
+
export function windowsTerminal(run) {
|
|
6
|
+
return {
|
|
7
|
+
attachHint: () => null,
|
|
8
|
+
name: 'Windows Terminal',
|
|
9
|
+
open: async (tab) => {
|
|
10
|
+
const args = windowsTerminalArgs(tab);
|
|
11
|
+
if (typeof args === 'string')
|
|
12
|
+
return args;
|
|
13
|
+
const result = await run('wt.exe', args, { cwd: tab.cwd, timeoutMs: 60_000, windowsHide: false });
|
|
14
|
+
if (result.code === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return result.error !== null && /ENOENT/.test(result.error)
|
|
17
|
+
? 'no Windows Terminal (wt.exe) to open it in'
|
|
18
|
+
: outputOf(result, 500);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// wt.exe's arguments for a tab in the named window. wt.exe splits its own arguments on `;`, so the
|
|
23
|
+
// window and title lose it, a folder with one is left out (the tab starts in its profile's, as
|
|
24
|
+
// tabs did before ATK-529), and a command with one can't open (a string says why). A held command
|
|
25
|
+
// runs in pwsh and chains its prompt with `||`. The tab gets wt.exe's environment, which is the
|
|
26
|
+
// caller's even in a window already open, so `env` needs no setting.
|
|
27
|
+
export function windowsTerminalArgs(tab) {
|
|
28
|
+
const split = tab.command.find((part) => part.includes(';'));
|
|
29
|
+
if (split !== undefined)
|
|
30
|
+
return `a Windows Terminal tab can't take ";": ${split}`;
|
|
31
|
+
const clean = (text) => text.replace(/;/g, ',').replace(/\s+/g, ' ').trim();
|
|
32
|
+
const command = tab.hold
|
|
33
|
+
? ['pwsh', '-Command', `${psLine(tab.command)} || Read-Host ${psQuote(failedText(tab.command))}`]
|
|
34
|
+
: tab.command;
|
|
35
|
+
const folder = tab.cwd.includes(';') ? [] : ['-d', tab.cwd];
|
|
36
|
+
return ['-w', clean(tab.window), 'new-tab', '--title', clean(tab.title).slice(0, 80), ...folder, ...command];
|
|
37
|
+
}
|
|
38
|
+
// A command as pwsh runs it: words bare where they can be, and a quoted program called with `&`.
|
|
39
|
+
function psLine(command) {
|
|
40
|
+
const words = command.map((word) => (/^[\w.:/\\-]+$/.test(word) ? word : psQuote(word)));
|
|
41
|
+
return words[0]?.startsWith("'") ? `& ${words.join(' ')}` : words.join(' ');
|
|
42
|
+
}
|
|
43
|
+
function psQuote(text) {
|
|
44
|
+
return `'${text.replace(/'/g, "''")}'`;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=windows-terminal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"windows-terminal.js","sourceRoot":"","sources":["../../src/host/windows-terminal.ts"],"names":[],"mappings":"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"}
|
package/build/in-process.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
2
|
import { providerCredentials } from "./credentials.js";
|
|
3
|
-
import {
|
|
3
|
+
import { profileHere } from "./profile-layers.js";
|
|
4
|
+
import { adoOrganization, profileProblem, trackerKey } from "./profile.js";
|
|
4
5
|
import { gitToplevel } from "./wait.js";
|
|
5
6
|
// What a tracker verb run in-process (no daemon) needs: the project of the checkout it runs in, and
|
|
6
7
|
// the machine's accounts. `conduct note` and `conduct brief` share it.
|
|
7
8
|
// The profile of the checkout around `cwd`, when `key` (an item key, checked already) is one of its
|
|
8
9
|
// items; else why not, after `why` (why it runs here), for the verb's refusal. `purpose` ends the
|
|
9
10
|
// sentence: "post it from", "read it from".
|
|
10
|
-
export async function checkoutProject(cwd, key, why, purpose) {
|
|
11
|
+
export async function checkoutProject(paths, cwd, key, why, purpose) {
|
|
11
12
|
const checkout = await gitToplevel(cwd);
|
|
12
13
|
if (checkout === null)
|
|
13
14
|
return { refusal: `${why}, and this isn't a project checkout to ${purpose}` };
|
|
14
15
|
let profile;
|
|
15
16
|
try {
|
|
16
|
-
profile =
|
|
17
|
+
profile = profileHere(checkout, paths);
|
|
17
18
|
}
|
|
18
19
|
catch (error) {
|
|
19
|
-
return { refusal: `${why}, and ${checkout} has no readable
|
|
20
|
+
return { refusal: `${why}, and ${checkout} has no readable profile: ${profileProblem(error)}` };
|
|
20
21
|
}
|
|
21
22
|
if (key.split('-')[0] !== trackerKey(profile).toUpperCase())
|
|
22
23
|
return { refusal: `${why}, and ${key} isn't an item of ${profile.name}, this checkout's project` };
|
package/build/in-process.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-process.js","sourceRoot":"","sources":["../src/in-process.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAoB,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"in-process.js","sourceRoot":"","sources":["../src/in-process.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAoB,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,eAAe,EAAgB,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,oGAAoG;AACpG,uEAAuE;AAEvE,oGAAoG;AACpG,kGAAkG;AAClG,4CAA4C;AAC5C,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,KAAuC,EACvC,GAAW,EACX,GAAW,EACX,GAAW,EACX,OAAe;IAEf,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,GAAG,GAAG,0CAA0C,OAAO,EAAE,EAAE,CAAC;IACrG,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,GAAG,GAAG,SAAS,QAAQ,6BAA6B,cAAc,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;IAClG,CAAC;IACD,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE;QACzD,OAAO,EAAE,OAAO,EAAE,GAAG,GAAG,SAAS,GAAG,qBAAqB,OAAO,CAAC,IAAI,2BAA2B,EAAE,CAAC;IACrG,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC;AAED,8FAA8F;AAC9F,kGAAkG;AAClG,4BAA4B;AAC5B,MAAM,UAAU,gBAAgB,CAAC,KAAqB,EAAE,KAAmC;IACzF,OAAO,mBAAmB,CAAC;QACzB,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9E,QAAQ,EAAE,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,QAAQ;KAC5C,CAAC,CAAC;AACL,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,kBAAkB,CAAC,KAAkC;IACnE,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC;QACtF,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAC5D,CAAC,CAAC;AACJ,CAAC"}
|
package/build/inbox.js
CHANGED
|
@@ -3,7 +3,8 @@ import { formatCondition, parseCondition } from "./conditions.js";
|
|
|
3
3
|
import { loginText } from "./credentials.js";
|
|
4
4
|
import { ConductorError } from "./errors.js";
|
|
5
5
|
import { inboxLinkSchema, link, uniqueLinks } from "./links.js";
|
|
6
|
-
import { handoffKindSchema
|
|
6
|
+
import { handoffKindSchema } from "./protocol.js";
|
|
7
|
+
import { RELEASE_GATES, releaseAsk, releaseGate } from "./release-kinds.js";
|
|
7
8
|
import { resolveOpenWait } from "./resolve.js";
|
|
8
9
|
import { refusalProblems } from "./rules-service.js";
|
|
9
10
|
import { formatTokens } from "./spend.js";
|
|
@@ -34,10 +35,10 @@ export const UNWATCHED_HANDOFFS = new Set([
|
|
|
34
35
|
'scope',
|
|
35
36
|
]);
|
|
36
37
|
// The phases that close a gate besides its own condition. A merge waits for the PR, so a Progress
|
|
37
|
-
// note while it waits leaves it open.
|
|
38
|
+
// note while it waits leaves it open; a release's gate (npm-otp) closes once the worker goes on.
|
|
38
39
|
const GATE_PHASES = {
|
|
40
|
+
...Object.fromEntries(RELEASE_GATES.map((gate) => [gate, ['Resumed', 'Landed', 'Done']])),
|
|
39
41
|
merge: ['Landed', 'Done'],
|
|
40
|
-
'npm-otp': ['Resumed', 'Landed', 'Done'],
|
|
41
42
|
plan: ['Plan approved'],
|
|
42
43
|
};
|
|
43
44
|
// How a merge item resolves while its PR's checks are red; green checks raise it again.
|
|
@@ -76,6 +77,8 @@ const loginData = z.looseObject({
|
|
|
76
77
|
command: z.string(),
|
|
77
78
|
error: z.string().nullable().optional(),
|
|
78
79
|
label: z.string(),
|
|
80
|
+
// The ADO organization a machine token's spell is kept to (ATK-536).
|
|
81
|
+
organization: z.string().optional(),
|
|
79
82
|
provider: z.string(),
|
|
80
83
|
});
|
|
81
84
|
const ignoredData = z.looseObject({ author: z.string(), kind: z.enum(['approve', 'revise']) });
|
|
@@ -101,9 +104,15 @@ const checksData = z.looseObject({
|
|
|
101
104
|
});
|
|
102
105
|
const workerData = z.looseObject({
|
|
103
106
|
error: z.string().optional(),
|
|
104
|
-
// The worktree a retire kept, because its PR hadn't merged (worker.retired)
|
|
107
|
+
// The worktree a retire kept, because its PR hadn't merged (worker.retired), and the repository it
|
|
108
|
+
// belongs to: the project's clone, or for one made before clones the person's checkout.
|
|
105
109
|
kept: z
|
|
106
|
-
.looseObject({
|
|
110
|
+
.looseObject({
|
|
111
|
+
branch: z.string().nullable(),
|
|
112
|
+
reason: z.string(),
|
|
113
|
+
repo: z.string().nullable().optional(),
|
|
114
|
+
worktree: z.string(),
|
|
115
|
+
})
|
|
107
116
|
.optional()
|
|
108
117
|
.catch(undefined),
|
|
109
118
|
kind: z.string().optional(),
|
|
@@ -140,10 +149,10 @@ export function inboxLines(items, now) {
|
|
|
140
149
|
...i.links.map((l) => ` ${l.label}: ${l.href}`),
|
|
141
150
|
]);
|
|
142
151
|
}
|
|
143
|
-
// The inbox (design note, section 7): everything that waits on
|
|
152
|
+
// The inbox (design note, section 7): everything that waits on the operator, raised from phase notes,
|
|
144
153
|
// waits and session events, each with its exact commands and links. Items resolve themselves when
|
|
145
154
|
// their condition is seen (the approval comment, the PR merged, the version on its tag, a later
|
|
146
|
-
// note), or when
|
|
155
|
+
// note), or when the operator answers one (`conduct approve`, `revise`, `done`).
|
|
147
156
|
export function inboxService({ onWaitResolved, store, tracker }) {
|
|
148
157
|
// Items whose approve or revise comment is being posted.
|
|
149
158
|
const answering = new Set();
|
|
@@ -267,6 +276,8 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
267
276
|
// The worker waits on something now, so it isn't idle. A park that keeps failing still is.
|
|
268
277
|
resolveAll(openOf(worker, ['stall']).filter((i) => !isParkStall(i)), `waiting on ${text}`);
|
|
269
278
|
const condition = conditionOf(text);
|
|
279
|
+
// A wait on a release (an npm version on its dist-tag) holds its kind's gate (release-kinds.ts).
|
|
280
|
+
const asked = condition && event.subject !== null ? releaseAsk(condition, event.subject) : null;
|
|
270
281
|
if (condition?.kind === 'approval') {
|
|
271
282
|
gate(event.project, condition.item, 'plan', text, worker, () => ({
|
|
272
283
|
commands: planCommands(condition.item),
|
|
@@ -274,15 +285,11 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
274
285
|
text: planText(condition.item),
|
|
275
286
|
}));
|
|
276
287
|
}
|
|
277
|
-
else if (
|
|
278
|
-
gate(event.project,
|
|
279
|
-
commands: [],
|
|
280
|
-
links: [],
|
|
281
|
-
text: `approve the npm stage of ${condition.package}@${condition.version} (${condition.tag}); the npm stage approve line is on ${event.subject}`,
|
|
282
|
-
}));
|
|
288
|
+
else if (asked !== null) {
|
|
289
|
+
gate(event.project, asked.item, asked.gate, text, worker, () => ({ commands: [], links: [], text: asked.text }));
|
|
283
290
|
}
|
|
284
291
|
else if (condition?.kind === 'pr' && condition.on === 'merged') {
|
|
285
|
-
// A merge is
|
|
292
|
+
// A merge is the operator's only when a note said so; the wait just names its PR.
|
|
286
293
|
for (const open of openOn(event.project, event.subject, ['merge'])) {
|
|
287
294
|
if (open.condition === null)
|
|
288
295
|
store.setInboxCondition(open.id, text);
|
|
@@ -294,7 +301,7 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
294
301
|
const gate = (project, item, kind, condition, worker, fresh) => {
|
|
295
302
|
const open = openOn(project, item, [kind]);
|
|
296
303
|
if (open.length === 0) {
|
|
297
|
-
//
|
|
304
|
+
// The operator approved the plan before its worker waited on it: the comment is there, and the
|
|
298
305
|
// watcher resolves this wait on its next pass. A new Plan note raises a new item anyway.
|
|
299
306
|
const latest = store.latestInbox(project, item, kind);
|
|
300
307
|
if (kind === 'plan' && latest?.resolvedBy?.startsWith('approve (conduct approve'))
|
|
@@ -310,9 +317,10 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
310
317
|
raise({ ...fresh(), condition, item, kind, project, worker });
|
|
311
318
|
return;
|
|
312
319
|
}
|
|
313
|
-
//
|
|
320
|
+
// A release's newest wait is the version that waits: a stage that failed is replaced by the next.
|
|
321
|
+
const release = RELEASE_GATES.includes(kind);
|
|
314
322
|
for (const o of open)
|
|
315
|
-
if (o.condition === null ||
|
|
323
|
+
if (o.condition === null || release)
|
|
316
324
|
store.setInboxCondition(o.id, condition);
|
|
317
325
|
};
|
|
318
326
|
// The item's open merge items for a PR, and its open checks failures on it.
|
|
@@ -334,11 +342,12 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
334
342
|
return;
|
|
335
343
|
const { checks, condition: text, resolution = text, worker } = data.data;
|
|
336
344
|
const condition = conditionOf(text);
|
|
345
|
+
const release = condition ? releaseGate(condition.kind) : null;
|
|
337
346
|
if (condition?.kind === 'approval') {
|
|
338
347
|
resolveAll(openOn(event.project, condition.item, ['plan']), resolution);
|
|
339
348
|
}
|
|
340
|
-
else if (
|
|
341
|
-
resolveAll(openOn(event.project, event.subject, [
|
|
349
|
+
else if (release !== null) {
|
|
350
|
+
resolveAll(openOn(event.project, event.subject, [release]).filter((i) => i.condition === null || i.condition === text), resolution);
|
|
342
351
|
}
|
|
343
352
|
else if (condition?.kind === 'pr' && /^pr \S+ (merged|abandoned|closed unmerged)\b/.test(resolution)) {
|
|
344
353
|
// Right after the PR, so a check named "… merged …" in a checks line isn't the PR merging.
|
|
@@ -357,7 +366,7 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
357
366
|
}
|
|
358
367
|
};
|
|
359
368
|
// A PR's required checks failed (watchers/index.ts): a failure with each run's link, and the
|
|
360
|
-
// PR's merge held, so nothing asks
|
|
369
|
+
// PR's merge held, so nothing asks the operator to merge it while it is red. One failure per PR.
|
|
361
370
|
const onChecksFailed = (event) => {
|
|
362
371
|
const data = checksData.safeParse(event.data);
|
|
363
372
|
if (!data.success || event.subject === null)
|
|
@@ -418,16 +427,16 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
418
427
|
worker,
|
|
419
428
|
});
|
|
420
429
|
};
|
|
421
|
-
// A lost provider login (logins.ts): one item per provider and account,
|
|
422
|
-
// and calls find it, resolved when the login works again. Spells live in memory, so a restart
|
|
430
|
+
// A lost provider login (logins.ts): one item per provider and account (and organization, for a
|
|
431
|
+
// machine ADO token), however many projects and calls find it, resolved when the login works again. Spells live in memory, so a restart
|
|
423
432
|
// resolves the items (start) and a login still lost raises its item again.
|
|
424
433
|
const logins = new Map();
|
|
425
434
|
const onLogin = (event) => {
|
|
426
435
|
const data = loginData.safeParse(event.data);
|
|
427
436
|
if (!data.success)
|
|
428
437
|
return;
|
|
429
|
-
const { account, command, error = null, label, provider } = data.data;
|
|
430
|
-
const key = `${provider}:${account}`;
|
|
438
|
+
const { account, command, error = null, label, organization = null, provider } = data.data;
|
|
439
|
+
const key = organization === null ? `${provider}:${account}` : `${provider}:${account} ${organization}`;
|
|
431
440
|
const open = logins.get(key);
|
|
432
441
|
const item = open === undefined ? null : store.getInbox(open);
|
|
433
442
|
if (event.kind === 'provider.login-restored') {
|
|
@@ -445,22 +454,26 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
445
454
|
kind: 'login',
|
|
446
455
|
links: [],
|
|
447
456
|
project: event.project,
|
|
448
|
-
text: `${loginText({ label, provider }, event.project)}${error ? `: ${error.slice(0, 120)}` : ''}`,
|
|
457
|
+
text: `${loginText({ label, provider }, event.project, organization)}${error ? `: ${error.slice(0, 120)}` : ''}`,
|
|
449
458
|
worker: null,
|
|
450
459
|
});
|
|
451
460
|
logins.set(key, raised.id);
|
|
452
461
|
};
|
|
453
|
-
// A rules file the conductor refused waits on
|
|
462
|
+
// A rules file the conductor refused waits on the operator until it loads again; the last good rules
|
|
454
463
|
// (a project's whole last good profile, when it has one) stay in force meanwhile. One item per
|
|
455
464
|
// file, found by its link.
|
|
456
465
|
const openRules = (file) => store.openInbox().filter((i) => isRulesItem(i) && i.links[0]?.href === file);
|
|
466
|
+
// A project's profile loading clears every refusal of it, whichever of its files that one named
|
|
467
|
+
// (its machine profile, its repo layer, or the checkout it was registered by before clones).
|
|
468
|
+
const openProfile = (project) => store.openInbox().filter((i) => isRulesItem(i) && i.project === project && i.links[0]?.label === 'profile');
|
|
457
469
|
const onRules = (event) => {
|
|
458
470
|
const data = rulesData.safeParse(event.data);
|
|
459
471
|
if (!data.success)
|
|
460
472
|
return;
|
|
461
473
|
const { file, problems = [], upgrade } = data.data;
|
|
462
474
|
if (event.kind === 'rules.loaded') {
|
|
463
|
-
|
|
475
|
+
const loaded = [...openRules(file), ...(event.project === null ? [] : openProfile(event.project))];
|
|
476
|
+
resolveAll([...new Map(loaded.map((i) => [i.id, i])).values()], 'the rules loaded');
|
|
464
477
|
return;
|
|
465
478
|
}
|
|
466
479
|
resolveAll(openRules(file), 'refused again');
|
|
@@ -491,7 +504,7 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
491
504
|
const earlierFailures = () => openOn(event.project, event.subject, ['failed']).filter((i) => i.worker !== null && i.worker !== worker && store.getWorker(i.worker)?.name === name);
|
|
492
505
|
switch (event.kind) {
|
|
493
506
|
case 'worker.compact-failed':
|
|
494
|
-
// The manager tries a worker once a run (ATK-497): a session it couldn't compact is
|
|
507
|
+
// The manager tries a worker once a run (ATK-497): a session it couldn't compact is the operator's
|
|
495
508
|
// to compact or retire. A rule's own `inbox context` item is another matter.
|
|
496
509
|
if (compactFailures(worker).length > 0)
|
|
497
510
|
return;
|
|
@@ -559,7 +572,7 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
559
572
|
// behind on Windows.
|
|
560
573
|
const { kept } = data.data;
|
|
561
574
|
if (kept) {
|
|
562
|
-
const root = store.listProjects().find((p) => p.name === event.project)?.
|
|
575
|
+
const root = kept.repo ?? store.listProjects().find((p) => p.name === event.project)?.clone;
|
|
563
576
|
raise({
|
|
564
577
|
commands: root
|
|
565
578
|
? [
|
|
@@ -636,7 +649,7 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
636
649
|
answering.add(id);
|
|
637
650
|
let comment;
|
|
638
651
|
try {
|
|
639
|
-
// As
|
|
652
|
+
// As the operator: the comment carries the key owner's name, and the worker reads it on the tracker.
|
|
640
653
|
comment = await tracker.comment(key, body, profile);
|
|
641
654
|
}
|
|
642
655
|
finally {
|
|
@@ -653,8 +666,8 @@ export function inboxService({ onWaitResolved, store, tracker }) {
|
|
|
653
666
|
const profile = profileOf(project);
|
|
654
667
|
let comment;
|
|
655
668
|
try {
|
|
656
|
-
// As
|
|
657
|
-
// work skill records it as the conductor's, not
|
|
669
|
+
// As the operator, as `conduct approve` posts it: the worker's approval wait reads it, and the
|
|
670
|
+
// work skill records it as the conductor's, not theirs.
|
|
658
671
|
comment = await tracker.comment(key, `approve: auto-approved by the conductor: rule "${rule}", at once`, profile);
|
|
659
672
|
}
|
|
660
673
|
catch (error) {
|
|
@@ -755,10 +768,9 @@ function mergeIsFor(item, pr) {
|
|
|
755
768
|
return named.size === 0 || named.has(Number(/#(\d+)$/.exec(pr)?.[1]));
|
|
756
769
|
}
|
|
757
770
|
// The item a note's Handoff kind raises; undefined for `conductor` and for a kind protocol 2 doesn't
|
|
758
|
-
// have.
|
|
759
|
-
// kind is called now; `conduct note` refuses a new one.
|
|
771
|
+
// have.
|
|
760
772
|
function noteKindOf(kind) {
|
|
761
|
-
const parsed = handoffKindSchema.safeParse(
|
|
773
|
+
const parsed = handoffKindSchema.safeParse(kind);
|
|
762
774
|
return parsed.success && parsed.data !== 'conductor' ? NOTE_KINDS[parsed.data] : undefined;
|
|
763
775
|
}
|
|
764
776
|
function planCommands(item) {
|