@detergent-software/conductor 3.0.0-dev.2 → 3.0.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -0
- package/README.md +158 -45
- package/build/brief-cli.js +1 -1
- package/build/brief-cli.js.map +1 -1
- package/build/broker-cli.js +24 -25
- package/build/broker-cli.js.map +1 -1
- package/build/broker.js +66 -92
- package/build/broker.js.map +1 -1
- package/build/claude.js +50 -7
- package/build/claude.js.map +1 -1
- package/build/cli.js +119 -54
- 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 +101 -26
- package/build/code-host-cli.js.map +1 -1
- package/build/code-host.js +13 -8
- package/build/code-host.js.map +1 -1
- package/build/config.js +31 -12
- package/build/config.js.map +1 -1
- package/build/console.js +40 -24
- package/build/console.js.map +1 -1
- package/build/credentials.js +26 -24
- package/build/credentials.js.map +1 -1
- package/build/daemon.js +35 -21
- package/build/daemon.js.map +1 -1
- package/build/help.js +141 -30
- package/build/help.js.map +1 -1
- package/build/host/credential-manager.js +109 -0
- package/build/host/credential-manager.js.map +1 -0
- package/build/host/keychain.js +44 -0
- package/build/host/keychain.js.map +1 -0
- package/build/host/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/terminal.js +46 -0
- package/build/host/terminal.js.map +1 -0
- package/build/host/tmux.js +39 -0
- package/build/host/tmux.js.map +1 -0
- package/build/host/windows-terminal.js +46 -0
- package/build/host/windows-terminal.js.map +1 -0
- package/build/in-process.js +6 -5
- package/build/in-process.js.map +1 -1
- package/build/inbox.js +50 -5
- package/build/inbox.js.map +1 -1
- package/build/init.js +508 -0
- package/build/init.js.map +1 -0
- package/build/lib/providers/ado/pipelines.js +102 -17
- 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/github/actions.js +85 -2
- package/build/lib/providers/github/actions.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 -4
- 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/runs.js +12 -2
- package/build/lib/providers/verdicts/runs.js.map +1 -1
- package/build/local-files.js +95 -0
- package/build/local-files.js.map +1 -0
- package/build/manager.js +171 -47
- package/build/manager.js.map +1 -1
- package/build/note.js +1 -1
- package/build/note.js.map +1 -1
- package/build/npm-reads.js +323 -0
- package/build/npm-reads.js.map +1 -0
- package/build/paths.js +44 -1
- package/build/paths.js.map +1 -1
- package/build/plugin.js +10 -0
- package/build/plugin.js.map +1 -1
- package/build/profile-fields.js +149 -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 +60 -38
- package/build/profile.js.map +1 -1
- package/build/project-check-cli.js +18 -10
- package/build/project-check-cli.js.map +1 -1
- package/build/project-check.js +179 -97
- package/build/project-check.js.map +1 -1
- package/build/pushover.js +5 -5
- package/build/pushover.js.map +1 -1
- package/build/registry.js +195 -36
- package/build/registry.js.map +1 -1
- package/build/rule-actions.js +3 -1
- package/build/rule-actions.js.map +1 -1
- package/build/rules-service.js +101 -44
- package/build/rules-service.js.map +1 -1
- package/build/rules.js +11 -2
- package/build/rules.js.map +1 -1
- package/build/runs.js +88 -14
- package/build/runs.js.map +1 -1
- package/build/scheduler.js +11 -1
- package/build/scheduler.js.map +1 -1
- package/build/server.js +4 -25
- package/build/server.js.map +1 -1
- package/build/spend.js +4 -0
- package/build/spend.js.map +1 -1
- package/build/store.js +82 -25
- package/build/store.js.map +1 -1
- package/build/timeline.js +104 -0
- package/build/timeline.js.map +1 -0
- package/build/tracker-reads-cli.js +4 -5
- package/build/tracker-reads-cli.js.map +1 -1
- package/build/train-cli.js +6 -5
- package/build/train-cli.js.map +1 -1
- package/build/upgrade.js +16 -1
- package/build/upgrade.js.map +1 -1
- package/build/wait.js +6 -6
- package/build/wait.js.map +1 -1
- package/build/watchers/ado.js +3 -2
- package/build/watchers/ado.js.map +1 -1
- package/build/watchers/github.js +3 -2
- package/build/watchers/github.js.map +1 -1
- package/build/watchers/npm.js +6 -4
- package/build/watchers/npm.js.map +1 -1
- package/build/worktrees.js +199 -92
- package/build/worktrees.js.map +1 -1
- package/package.json +61 -61
- package/plugin/skills/conductor-console/SKILL.md +3 -3
- package/plugin/skills/work-item/SKILL.md +8 -6
- 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/tab.js +0 -32
- package/build/tab.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../../../../src/lib/providers/verdicts/runs.ts"],"names":[],"mappings":"AAGA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAG,MAAM,CAAC;AAYpC,iGAAiG;AACjG,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAExG,kFAAkF;AAClF,2DAA2D;AAC3D,MAAM,UAAU,SAAS,CAAC,QAAkB,EAAE,IAAY;IACxD,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AACtH,CAAC;AAED,iGAAiG;AACjG,6BAA6B;AAC7B,MAAM,UAAU,cAAc,CAAC,KAAwB,EAAE,SAA8B;IACrF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAC1D,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACrB,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAC5C,MAAM,KAAK,GAAG,SAAS;aACpB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aACxB,IAAI,EAAE;aACN,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CACb,KAAK,CAAC,MAAM,KAAK,CAAC;YAChB,CAAC,CAAC,SAAS,IAAI,yDAAyD,KAAK,EAAE;YAC/E,CAAC,CAAC,SAAS,IAAI,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,wCAAwC,CACrG,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iGAAiG;AACjG,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAC1B,IAA4B,EAC5B,QAAsD;IAEtD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;QAC/G,OAAO,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,6EAA6E;AAC7E,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,gGAAgG;AAChG,iGAAiG;AACjG,
|
|
1
|
+
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../../../../src/lib/providers/verdicts/runs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { cp } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, matchesGlob, relative } from 'node:path';
|
|
4
|
+
import { ConductorError } from "./errors.js";
|
|
5
|
+
import { outputOf } from "./run.js";
|
|
6
|
+
export const INCLUDE_FILE = '.worktreeinclude';
|
|
7
|
+
// Copies the source checkout's included files into `target`; returns their paths, relative and with
|
|
8
|
+
// forward slashes. Git lists ignored paths with whole ignored directories collapsed, which keeps
|
|
9
|
+
// node_modules out of the walk. A pattern without a slash (".env*") matches at any depth but not
|
|
10
|
+
// inside an ignored directory; an anchored one ("apps/api/secrets/*.json") also reaches into one.
|
|
11
|
+
export async function copyLocalFiles(git, source, target) {
|
|
12
|
+
if (!existsSync(join(source, '.git')))
|
|
13
|
+
throw new ConductorError(`localFilesFrom ${source} is not a git checkout`, [
|
|
14
|
+
"set the profile's localFilesFrom to a checkout of the project you keep",
|
|
15
|
+
]);
|
|
16
|
+
const patterns = includePatterns(join(source, INCLUDE_FILE));
|
|
17
|
+
if (patterns.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const listed = await git(['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'], source);
|
|
20
|
+
if (listed.code !== 0)
|
|
21
|
+
throw new ConductorError(`git couldn't list the ignored files in ${source}`, [outputOf(listed, 500)]);
|
|
22
|
+
const copied = [];
|
|
23
|
+
// Asynchronous, so an included directory of any size doesn't hold up the daemon.
|
|
24
|
+
const copy = async (rel) => {
|
|
25
|
+
const to = join(target, rel);
|
|
26
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
27
|
+
await cp(join(source, rel), to, { recursive: true });
|
|
28
|
+
copied.push(rel);
|
|
29
|
+
};
|
|
30
|
+
// Git can list a directory and also entries inside it (apps/, apps/api/, ...), parents first, so
|
|
31
|
+
// anything under a directory already copied or walked is skipped.
|
|
32
|
+
const handled = ['.claude/worktrees'];
|
|
33
|
+
for (const entry of listed.stdout.split('\0').filter(Boolean)) {
|
|
34
|
+
const isDir = entry.endsWith('/');
|
|
35
|
+
const rel = isDir ? entry.slice(0, -1) : entry;
|
|
36
|
+
if (handled.some((dir) => rel === dir || rel.startsWith(`${dir}/`)))
|
|
37
|
+
continue;
|
|
38
|
+
if (isIncluded(patterns, rel, isDir))
|
|
39
|
+
await copy(rel);
|
|
40
|
+
else if (isDir)
|
|
41
|
+
for (const file of walkAnchored(patterns, source, rel))
|
|
42
|
+
await copy(file);
|
|
43
|
+
if (isDir)
|
|
44
|
+
handled.push(rel);
|
|
45
|
+
}
|
|
46
|
+
return copied;
|
|
47
|
+
}
|
|
48
|
+
export function includePatterns(file) {
|
|
49
|
+
if (!existsSync(file))
|
|
50
|
+
return [];
|
|
51
|
+
return readFileSync(file, 'utf8')
|
|
52
|
+
.split(/\r?\n/)
|
|
53
|
+
.map((line) => line.trim())
|
|
54
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
55
|
+
.map((line) => {
|
|
56
|
+
const negated = line.startsWith('!');
|
|
57
|
+
let pattern = negated ? line.slice(1) : line;
|
|
58
|
+
const dirOnly = pattern.endsWith('/');
|
|
59
|
+
if (dirOnly)
|
|
60
|
+
pattern = pattern.slice(0, -1);
|
|
61
|
+
const anchored = pattern.includes('/');
|
|
62
|
+
if (pattern.startsWith('/'))
|
|
63
|
+
pattern = pattern.slice(1);
|
|
64
|
+
return { anchored, dirOnly, glob: anchored ? pattern : `**/${pattern}`, negated };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function isIncluded(patterns, rel, isDir) {
|
|
68
|
+
const parts = rel.split('/');
|
|
69
|
+
for (let i = 1; i < parts.length; i++)
|
|
70
|
+
if (matchesLast(patterns, parts.slice(0, i).join('/'), true))
|
|
71
|
+
return true;
|
|
72
|
+
return matchesLast(patterns, rel, isDir);
|
|
73
|
+
}
|
|
74
|
+
function matchesLast(patterns, rel, isDir) {
|
|
75
|
+
let result = false;
|
|
76
|
+
for (const p of patterns) {
|
|
77
|
+
if (p.dirOnly && !isDir)
|
|
78
|
+
continue;
|
|
79
|
+
if (matchesGlob(rel, p.glob))
|
|
80
|
+
result = !p.negated;
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function* walkAnchored(patterns, source, dir) {
|
|
85
|
+
if (!patterns.some((p) => p.anchored && !p.negated && p.glob.startsWith(`${dir}/`)))
|
|
86
|
+
return;
|
|
87
|
+
for (const entry of readdirSync(join(source, dir), { recursive: true, withFileTypes: true })) {
|
|
88
|
+
if (!entry.isFile())
|
|
89
|
+
continue;
|
|
90
|
+
const rel = relative(source, join(entry.parentPath, entry.name)).split('\\').join('/');
|
|
91
|
+
if (isIncluded(patterns, rel, false))
|
|
92
|
+
yield rel;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=local-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-files.js","sourceRoot":"","sources":["../src/local-files.ts"],"names":[],"mappings":"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"}
|
package/build/manager.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { freePorts, releaseLocks } from "./broker.js";
|
|
5
|
-
import { isLive, isStarting, permissionPrompt, SessionGone, turnEnded, } from "./claude.js";
|
|
4
|
+
import { freePorts, portEnv, releaseLocks } from "./broker.js";
|
|
5
|
+
import { isLive, isStarting, permissionPrompt, SessionGone, trustStep, turnEnded, WorkspaceUntrusted, } from "./claude.js";
|
|
6
6
|
import { formatCondition, parseCondition, prRef, uncheckable } from "./conditions.js";
|
|
7
7
|
import { ConductorError } from "./errors.js";
|
|
8
8
|
import { UNWATCHED_HANDOFFS } from "./inbox.js";
|
|
9
9
|
import { processAlive as defaultProcessAlive } from "./lock.js";
|
|
10
|
+
import { projectPaths } from "./paths.js";
|
|
10
11
|
import { missingPluginSkills, projectSkills } from "./plugin.js";
|
|
11
12
|
import { WAKE_PREFIX } from "./protocol.js";
|
|
13
|
+
import { registeredByCheckout } from "./registry.js";
|
|
12
14
|
import { resolveOpenWait } from "./resolve.js";
|
|
13
15
|
import { findTranscript, formatTokens, lastTurnAt, localDay, projectSlug, readUsage, transcriptFiles, } from "./spend.js";
|
|
14
16
|
import { ACTIVE_STATUSES, openStore, waitSchema, workerSchema, } from "./store.js";
|
|
17
|
+
import { linkedPr } from "./tracker-writes.js";
|
|
15
18
|
import { pendingCall } from "./transcript.js";
|
|
19
|
+
import { BRANCH_PREFIX } from "./worktrees.js";
|
|
16
20
|
// A worker as GET /fleet shows it.
|
|
17
21
|
export const fleetWorkerSchema = workerSchema.extend({
|
|
18
22
|
// launching, starting, working, idle, waiting on <x>, parked, stopped, no session
|
|
@@ -94,6 +98,9 @@ const ITEM = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
|
94
98
|
const STOP_WAIT_MS = 30_000;
|
|
95
99
|
// A park that failed (the process outlived `claude stop`) is tried again this much later.
|
|
96
100
|
const PARK_RETRY_MS = 5 * 60_000;
|
|
101
|
+
// How long a new session id has to stay listed before the worker follows it. A resuming session is
|
|
102
|
+
// listed for under a second under the id its process started with, before it loads its own (ATK-574).
|
|
103
|
+
const SESSION_ID_SETTLE_MS = 5_000;
|
|
97
104
|
const TRANSCRIPT_SCAN_MS = 60_000;
|
|
98
105
|
// A compaction with no boundary in the transcript this long after it started, its turn over, failed.
|
|
99
106
|
const COMPACT_MINUTES = 10;
|
|
@@ -116,7 +123,7 @@ export function eventText(waits) {
|
|
|
116
123
|
return `${WAKE_PREFIX}${[...facts].map(([fact, at]) => `${fact} ${clock(new Date(at))}`).join('; ')}`;
|
|
117
124
|
}
|
|
118
125
|
export function sessionManager(deps) {
|
|
119
|
-
const { claude, config, log,
|
|
126
|
+
const { broker, claude, config, log, pluginDir, prs, store, tabs, worktrees } = deps;
|
|
120
127
|
const now = deps.now ?? (() => new Date());
|
|
121
128
|
const alive = deps.processAlive ?? defaultProcessAlive;
|
|
122
129
|
const sleep = deps.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
@@ -126,6 +133,8 @@ export function sessionManager(deps) {
|
|
|
126
133
|
const stalls = new Map();
|
|
127
134
|
const transcripts = new Map();
|
|
128
135
|
const transcriptScans = new Map();
|
|
136
|
+
// worker id → a session id the listing shows for it that it hasn't followed yet, since when
|
|
137
|
+
const newSessionIds = new Map();
|
|
129
138
|
let sessions = [];
|
|
130
139
|
// Until the first sweep, nothing is known about any session.
|
|
131
140
|
let listed = false;
|
|
@@ -226,10 +235,15 @@ export function sessionManager(deps) {
|
|
|
226
235
|
throw new ConductorError(`project "${found.name}" has a problem: ${found.problem ?? 'no profile'}`);
|
|
227
236
|
if (!found.enabled)
|
|
228
237
|
throw new ConductorError(`project "${found.name}" is disabled`);
|
|
238
|
+
if (registeredByCheckout(deps.paths, found)) {
|
|
239
|
+
throw new ConductorError(`project "${found.name}" is registered by the checkout ${found.clone}`, [
|
|
240
|
+
`the conductor works only in a clone of its own: run \`conduct project add "${found.clone}"\` first`,
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
229
243
|
return { ...found, profile: found.profile };
|
|
230
244
|
};
|
|
231
|
-
// Where a worker's commands run: its project's
|
|
232
|
-
const
|
|
245
|
+
// Where a worker's commands run when it has no worktree: its project's clone.
|
|
246
|
+
const cloneOf = (worker) => projectPaths(deps.paths, worker.project).clone;
|
|
233
247
|
const sessionOf = (worker) => sessions.find((s) => (worker.sessionId !== null && s.sessionId === worker.sessionId) ||
|
|
234
248
|
(worker.handle !== null && s.id === worker.handle));
|
|
235
249
|
const minutesSince = (iso) => (now().getTime() - Date.parse(iso)) / 60_000;
|
|
@@ -266,27 +280,58 @@ export function sessionManager(deps) {
|
|
|
266
280
|
}
|
|
267
281
|
};
|
|
268
282
|
// ---------- launch ----------
|
|
269
|
-
|
|
283
|
+
// The worktree first (design note v2, section 2): its ports from the broker, then `git worktree add`
|
|
284
|
+
// in the project's clone from origin/<base>, the local files and the profile's setup. The session
|
|
285
|
+
// starts in it.
|
|
286
|
+
const launchWorker = async (worker, profile, prompt) => {
|
|
270
287
|
let current = worker;
|
|
271
|
-
let stage = '
|
|
288
|
+
let stage = 'assign ports';
|
|
289
|
+
const { clone, worktrees: dir } = projectPaths(deps.paths, worker.project);
|
|
290
|
+
const path = join(dir, worker.name);
|
|
291
|
+
// A worktree there already (a kept one) isn't this launch's to remove when it fails.
|
|
292
|
+
const reused = existsSync(join(path, '.git'));
|
|
272
293
|
try {
|
|
273
|
-
|
|
294
|
+
// Recorded before it exists: the broker keeps an in-flight worker's ports while its directory
|
|
295
|
+
// isn't there yet, and a create that fails part way is cleaned up.
|
|
296
|
+
current = update(current, { worktree: path });
|
|
297
|
+
const { ports } = await broker.assignPorts(path, worker.project, profile.ports);
|
|
298
|
+
stage = 'create worktree';
|
|
299
|
+
const env = portEnv(ports);
|
|
300
|
+
const worktree = await worktrees.create({
|
|
301
|
+
base: profile.baseBranch,
|
|
302
|
+
clone,
|
|
303
|
+
env: { ...env, CONDUCTOR_ITEM: worker.item, CONDUCTOR_PROJECT: worker.project, CONDUCTOR_WORKTREE: path },
|
|
304
|
+
...(profile.localFilesFrom === undefined ? {} : { localFilesFrom: profile.localFilesFrom }),
|
|
305
|
+
name: worker.name,
|
|
306
|
+
path,
|
|
307
|
+
sessionEnv: env,
|
|
308
|
+
setup: profile.setup,
|
|
309
|
+
});
|
|
274
310
|
current = update(current, { branch: await worktrees.branchOf(worktree), worktree });
|
|
275
311
|
stage = 'check skills';
|
|
276
312
|
const skills = [profile.skills.work, profile.skills.verify];
|
|
277
|
-
const problem =
|
|
313
|
+
const problem = worktrees.missingSkills(worktree, projectSkills(skills)) ?? missingPluginSkills(pluginDir, skills);
|
|
278
314
|
if (problem)
|
|
279
315
|
throw new ConductorError(problem);
|
|
280
316
|
stage = 'start session';
|
|
281
317
|
const started = now().getTime();
|
|
282
|
-
const handle = await claude
|
|
283
|
-
|
|
318
|
+
const handle = await claude
|
|
319
|
+
.launch({
|
|
320
|
+
cwd: worktree,
|
|
284
321
|
disallowedTools: DISALLOWED_TOOLS,
|
|
285
322
|
name: worker.name,
|
|
286
323
|
permissionMode: PERMISSION_MODE,
|
|
287
324
|
pluginDir,
|
|
288
325
|
prompt,
|
|
289
|
-
|
|
326
|
+
})
|
|
327
|
+
.catch((error) => {
|
|
328
|
+
// Claude Code's own message names the worktree, which this failed launch removes.
|
|
329
|
+
if (!(error instanceof WorkspaceUntrusted))
|
|
330
|
+
throw error;
|
|
331
|
+
throw new ConductorError(`Claude Code doesn't trust ${clone}`, [
|
|
332
|
+
`${trustStep(clone)}, then \`conduct project check ${worker.project}\``,
|
|
333
|
+
...error.details,
|
|
334
|
+
]);
|
|
290
335
|
});
|
|
291
336
|
try {
|
|
292
337
|
sessions = await claude.agents();
|
|
@@ -299,9 +344,9 @@ export function sessionManager(deps) {
|
|
|
299
344
|
current = update(current, { handle, sessionId: session?.sessionId ?? null, status: 'running' });
|
|
300
345
|
event(current, 'worker.launched', { handle, pluginDir, sessionId: current.sessionId, worktree });
|
|
301
346
|
expectPlugin(current, started);
|
|
302
|
-
if (
|
|
303
|
-
const tab = await
|
|
304
|
-
cwd:
|
|
347
|
+
if (tabs.onLaunch) {
|
|
348
|
+
const tab = await tabs.open({
|
|
349
|
+
cwd: worktree,
|
|
305
350
|
handle,
|
|
306
351
|
title: worker.name,
|
|
307
352
|
window: `${worker.probe ? 'probe' : 'Conductor'} · ${worker.project}`,
|
|
@@ -312,24 +357,62 @@ export function sessionManager(deps) {
|
|
|
312
357
|
}
|
|
313
358
|
catch (error) {
|
|
314
359
|
// A worktree with no session would read as in flight; one with work in it is kept.
|
|
315
|
-
const cleanup =
|
|
360
|
+
const cleanup = reused
|
|
361
|
+
? { kept: path, keptBecause: 'it was there before the launch' }
|
|
362
|
+
: current.worktree && existsSync(current.worktree)
|
|
363
|
+
? await removeIfClean(clone, current.worktree)
|
|
364
|
+
: {};
|
|
365
|
+
freePorts(store, path, 'launch failed');
|
|
316
366
|
current = update(current, { status: 'failed' });
|
|
317
367
|
event(current, 'worker.launch-failed', { error: messageOf(error), step: stage, ...cleanup });
|
|
318
368
|
log.log('warn', 'launch failed', { error: messageOf(error), step: stage, worker: worker.id });
|
|
319
369
|
}
|
|
320
370
|
};
|
|
321
|
-
|
|
371
|
+
// A failed launch's worktree, unless it holds work. Its branch goes with it while it still has the
|
|
372
|
+
// name the launch gave it; a renamed one was the worker's.
|
|
373
|
+
const removeIfClean = async (clone, worktree) => {
|
|
322
374
|
try {
|
|
323
375
|
const work = await worktrees.unfinishedWork(worktree);
|
|
324
376
|
if (work)
|
|
325
377
|
return { kept: worktree, ...work };
|
|
326
|
-
await worktrees.
|
|
378
|
+
const branch = await worktrees.branchOf(worktree);
|
|
379
|
+
await worktrees.remove(worktree, { deleteBranch: branch?.startsWith(BRANCH_PREFIX) === true, repo: clone });
|
|
327
380
|
return { removed: worktree };
|
|
328
381
|
}
|
|
329
382
|
catch (error) {
|
|
330
383
|
return { removeError: messageOf(error) };
|
|
331
384
|
}
|
|
332
385
|
};
|
|
386
|
+
// Whether retiring the worker removes its worktree and branch: once its PR has merged, and for a
|
|
387
|
+
// probe or a forced retire. `unread`: its PR couldn't be read, so retire waits.
|
|
388
|
+
const worktreeFate = async (worker, force) => {
|
|
389
|
+
const decided = (remove, reason) => ({ reason, remove, unread: null });
|
|
390
|
+
if (worker.worktree === null || !existsSync(worker.worktree))
|
|
391
|
+
return decided(true, 'no worktree');
|
|
392
|
+
if (worker.probe)
|
|
393
|
+
return decided(true, 'a probe');
|
|
394
|
+
if (force)
|
|
395
|
+
return decided(true, 'retire --force');
|
|
396
|
+
// With no PR and nothing uncommitted or unpushed, nothing in it is lost (a canceled item, a worker
|
|
397
|
+
// retired before its first commit): it goes, with no inbox item.
|
|
398
|
+
if (worker.pr === null)
|
|
399
|
+
return (await worktrees.unfinishedWork(worker.worktree)) === null
|
|
400
|
+
? decided(true, 'no PR and nothing unpushed')
|
|
401
|
+
: decided(false, 'it has no PR');
|
|
402
|
+
const profile = store.listProjects().find((p) => p.name === worker.project)?.profile;
|
|
403
|
+
if (!profile)
|
|
404
|
+
return { reason: '', remove: false, unread: `project ${worker.project} has no readable profile` };
|
|
405
|
+
// No read would ever answer for it, so waiting would refuse the retire for good.
|
|
406
|
+
if (linkedPr(worker.pr, profile) === null)
|
|
407
|
+
return decided(false, `its PR ${worker.pr} isn't one ${worker.project}'s code host reads`);
|
|
408
|
+
try {
|
|
409
|
+
const state = await prs(profile, worker.pr);
|
|
410
|
+
return decided(state === 'merged', `its PR ${worker.pr} is ${state}`);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return { reason: '', remove: false, unread: messageOf(error) };
|
|
414
|
+
}
|
|
415
|
+
};
|
|
333
416
|
// ---------- the sweep ----------
|
|
334
417
|
// The clock: the sweep is its timer. The other kinds are the watchers'.
|
|
335
418
|
const resolveTimeWaits = () => {
|
|
@@ -383,7 +466,7 @@ export function sessionManager(deps) {
|
|
|
383
466
|
const text = eventText(resolved);
|
|
384
467
|
const started = now().getTime();
|
|
385
468
|
const { copy } = await claude.resume({
|
|
386
|
-
cwd: session?.cwd ?? worker.worktree ??
|
|
469
|
+
cwd: session?.cwd ?? worker.worktree ?? cloneOf(worker),
|
|
387
470
|
sessionId: worker.sessionId,
|
|
388
471
|
text,
|
|
389
472
|
});
|
|
@@ -432,7 +515,7 @@ export function sessionManager(deps) {
|
|
|
432
515
|
let copy;
|
|
433
516
|
try {
|
|
434
517
|
({ copy } = await claude.resume({
|
|
435
|
-
cwd: session?.cwd ?? worker.worktree ??
|
|
518
|
+
cwd: session?.cwd ?? worker.worktree ?? cloneOf(worker),
|
|
436
519
|
sessionId: worker.sessionId,
|
|
437
520
|
text: COMPACT_TEXT,
|
|
438
521
|
}));
|
|
@@ -578,14 +661,29 @@ export function sessionManager(deps) {
|
|
|
578
661
|
let current = worker;
|
|
579
662
|
const session = sessionOf(current);
|
|
580
663
|
// The listing is the truth for ids: fill them in after a launch, and follow a changed short id.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
664
|
+
// A changed session id is followed once it has stayed listed SESSION_ID_SETTLE_MS.
|
|
665
|
+
if (session) {
|
|
666
|
+
let sessionId = current.sessionId;
|
|
667
|
+
const pending = newSessionIds.get(current.id);
|
|
668
|
+
if (sessionId === null || session.sessionId === sessionId)
|
|
669
|
+
newSessionIds.delete(current.id);
|
|
670
|
+
else if (pending?.sessionId !== session.sessionId) {
|
|
671
|
+
newSessionIds.set(current.id, { sessionId: session.sessionId, since: now().getTime() });
|
|
672
|
+
}
|
|
673
|
+
else if (now().getTime() - pending.since >= SESSION_ID_SETTLE_MS) {
|
|
674
|
+
newSessionIds.delete(current.id);
|
|
675
|
+
// A new session id (a /clear) writes a new transcript.
|
|
584
676
|
transcripts.delete(current.id);
|
|
585
677
|
transcriptScans.delete(current.id);
|
|
678
|
+
sessionId = session.sessionId;
|
|
679
|
+
}
|
|
680
|
+
sessionId ??= session.sessionId;
|
|
681
|
+
if (sessionId !== current.sessionId || session.id !== current.handle) {
|
|
682
|
+
current = update(current, { handle: session.id, sessionId });
|
|
586
683
|
}
|
|
587
|
-
current = update(current, { handle: session.id, sessionId: session.sessionId });
|
|
588
684
|
}
|
|
685
|
+
else
|
|
686
|
+
newSessionIds.delete(current.id);
|
|
589
687
|
const waits = store.waitsOf(current.id);
|
|
590
688
|
const resolved = waits.filter((w) => w.resolvedAt !== null);
|
|
591
689
|
// A listed pid can outlive its process for a moment after a stop.
|
|
@@ -773,9 +871,17 @@ export function sessionManager(deps) {
|
|
|
773
871
|
});
|
|
774
872
|
continue;
|
|
775
873
|
}
|
|
776
|
-
const root =
|
|
777
|
-
const worktree = worker.worktree ?? join(
|
|
778
|
-
const
|
|
874
|
+
const root = cloneOf(worker);
|
|
875
|
+
const worktree = worker.worktree ?? join(projectPaths(deps.paths, worker.project).worktrees, worker.name);
|
|
876
|
+
const reused = store
|
|
877
|
+
.workerEvents(worker.id, 1000)
|
|
878
|
+
.some((e) => e.kind === 'worker.launching' && e.data.reused === true);
|
|
879
|
+
const cleanup = reused
|
|
880
|
+
? { kept: worktree, keptBecause: 'it was there before the launch' }
|
|
881
|
+
: listing !== null && existsSync(worktree)
|
|
882
|
+
? await removeIfClean(root, worktree)
|
|
883
|
+
: {};
|
|
884
|
+
freePorts(store, worktree, 'launch failed');
|
|
779
885
|
store.closeWaits(worker.id, 'launch failed');
|
|
780
886
|
event(update(worker, { status: 'failed' }), 'worker.launch-failed', {
|
|
781
887
|
error: 'the daemon stopped during the launch',
|
|
@@ -805,7 +911,7 @@ export function sessionManager(deps) {
|
|
|
805
911
|
const problem = uncheckable(parsed, project?.profile ?? null);
|
|
806
912
|
if (problem) {
|
|
807
913
|
throw new ConductorError(problem, [
|
|
808
|
-
`the conductor takes ${worker.project}'s profile
|
|
914
|
+
`the conductor takes ${worker.project}'s profile again when it starts, when ${project?.profilePath ?? 'its machine profile'} is saved, and after each fetch of its clone; \`conduct profile ${worker.project}\` shows it, and \`conduct rules\` lists one it refused`,
|
|
809
915
|
]);
|
|
810
916
|
}
|
|
811
917
|
// Stored in its normal full form, so the watchers and `conduct status` read one spelling. A
|
|
@@ -847,7 +953,7 @@ export function sessionManager(deps) {
|
|
|
847
953
|
// No event text: the session wakes idle and keeps running when the tab closes.
|
|
848
954
|
started = now().getTime();
|
|
849
955
|
const { copy } = await claude.resume({
|
|
850
|
-
cwd: sessionOf(current)?.cwd ?? current.worktree ??
|
|
956
|
+
cwd: sessionOf(current)?.cwd ?? current.worktree ?? cloneOf(current),
|
|
851
957
|
sessionId,
|
|
852
958
|
text: null,
|
|
853
959
|
});
|
|
@@ -859,16 +965,12 @@ export function sessionManager(deps) {
|
|
|
859
965
|
}
|
|
860
966
|
current = update(current, { attachedAt: now().toISOString(), status: 'running', stoppedAt: null });
|
|
861
967
|
forgetAttach(current.id);
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
handle,
|
|
865
|
-
title: current.name,
|
|
866
|
-
window: `${current.probe ? 'probe' : 'Conductor'} · ${current.project}`,
|
|
867
|
-
});
|
|
968
|
+
const window = `${current.probe ? 'probe' : 'Conductor'} · ${current.project}`;
|
|
969
|
+
const tab = await tabs.open({ cwd: current.worktree ?? cloneOf(current), handle, title: current.name, window });
|
|
868
970
|
event(current, 'worker.attached', { resumed, ...(tab ? { tabError: tab } : {}) });
|
|
869
971
|
if (started !== null)
|
|
870
972
|
expectPlugin(current, started);
|
|
871
|
-
return { resumed, tab, worker: current };
|
|
973
|
+
return { hint: tab === null ? tabs.hint(window) : null, resumed, tab, worker: current };
|
|
872
974
|
}
|
|
873
975
|
finally {
|
|
874
976
|
busy.delete(worker.id);
|
|
@@ -896,17 +998,23 @@ export function sessionManager(deps) {
|
|
|
896
998
|
probe: request.probe === true,
|
|
897
999
|
project: target.name,
|
|
898
1000
|
});
|
|
1001
|
+
// A worktree there already isn't the launch's to remove if it fails, after a restart too.
|
|
1002
|
+
const reused = existsSync(join(projectPaths(deps.paths, target.name).worktrees, name, '.git'));
|
|
899
1003
|
event(worker, 'worker.launching', {
|
|
900
1004
|
probe: worker.probe,
|
|
1005
|
+
...(reused ? { reused } : {}),
|
|
901
1006
|
...(request.cause === undefined ? {} : { cause: request.cause }),
|
|
902
1007
|
});
|
|
903
1008
|
busy.add(worker.id);
|
|
904
|
-
void track(launchWorker(worker, target.
|
|
1009
|
+
void track(launchWorker(worker, target.profile, prompt).finally(() => busy.delete(worker.id)));
|
|
905
1010
|
return worker;
|
|
906
1011
|
},
|
|
907
1012
|
pluginSeen: (sessionId) => {
|
|
908
1013
|
pluginReports.set(sessionId, now().getTime());
|
|
909
|
-
|
|
1014
|
+
// A /clear's session reports before the worker follows its new id.
|
|
1015
|
+
return (store
|
|
1016
|
+
.listWorkers(ACTIVE_STATUSES)
|
|
1017
|
+
.find((w) => w.sessionId === sessionId || newSessionIds.get(w.id)?.sessionId === sessionId) ?? null);
|
|
910
1018
|
},
|
|
911
1019
|
retire: async (workerId, { cause, force = false } = {}) => {
|
|
912
1020
|
const worker = activeWorker(workerId);
|
|
@@ -938,10 +1046,15 @@ export function sessionManager(deps) {
|
|
|
938
1046
|
if (session && isLive(session) && !turnEnded(session) && !force) {
|
|
939
1047
|
return refuse('its turn is still running: retire once it ends, or retire --force', true);
|
|
940
1048
|
}
|
|
941
|
-
|
|
1049
|
+
// The worktree and its branch go once the PR has merged; a probe's and a forced retire's go
|
|
1050
|
+
// anyway. Otherwise both are kept, so nothing the PR didn't land is lost (design note v2,
|
|
1051
|
+
// section 2), and the inbox says where they are.
|
|
1052
|
+
const fate = await worktreeFate(worker, force);
|
|
1053
|
+
if (fate.unread)
|
|
1054
|
+
return refuse(`its PR couldn't be read: ${fate.unread}`, true);
|
|
1055
|
+
const work = fate.remove && worker.worktree ? await worktrees.unfinishedWork(worker.worktree) : null;
|
|
942
1056
|
if (work && !force)
|
|
943
1057
|
return refuse('unfinished work: commit and push it, or retire --force to throw it away', false, work);
|
|
944
|
-
const root = store.listProjects().find((p) => p.name === worker.project)?.root;
|
|
945
1058
|
const steps = [];
|
|
946
1059
|
const handle = worker.handle;
|
|
947
1060
|
if (handle !== null && session?.pid !== undefined) {
|
|
@@ -959,14 +1072,24 @@ export function sessionManager(deps) {
|
|
|
959
1072
|
return { outcome: 'partial', steps, worker };
|
|
960
1073
|
}
|
|
961
1074
|
}
|
|
1075
|
+
let kept = null;
|
|
962
1076
|
if (worker.worktree !== null) {
|
|
963
1077
|
const worktree = worker.worktree;
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
await worktrees.remove(
|
|
968
|
-
|
|
969
|
-
|
|
1078
|
+
if (fate.remove) {
|
|
1079
|
+
// The repository is the worktree's own: one launched before clones is in a person's
|
|
1080
|
+
// checkout, and retires from there.
|
|
1081
|
+
await step(steps, 'remove worktree', () => worktrees.remove(worktree, { deleteBranch: true, repo: cloneOf(worker) }));
|
|
1082
|
+
}
|
|
1083
|
+
else if (existsSync(worktree)) {
|
|
1084
|
+
kept = {
|
|
1085
|
+
branch: await worktrees.branchOf(worktree),
|
|
1086
|
+
reason: fate.reason,
|
|
1087
|
+
repo: await worktrees.repoOf(worktree),
|
|
1088
|
+
worktree,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
// No session serves on them any more, kept worktree or not.
|
|
1092
|
+
freePorts(store, worktree, 'worker retired');
|
|
970
1093
|
}
|
|
971
1094
|
try {
|
|
972
1095
|
readSpend(worker); // the last of it, before the session goes
|
|
@@ -988,6 +1111,7 @@ export function sessionManager(deps) {
|
|
|
988
1111
|
event(current, ok ? 'worker.retired' : 'worker.retire-partial', {
|
|
989
1112
|
forced: force && work !== null,
|
|
990
1113
|
steps,
|
|
1114
|
+
...(kept === null ? {} : { kept }),
|
|
991
1115
|
...(cause === undefined ? {} : { cause }),
|
|
992
1116
|
});
|
|
993
1117
|
return { outcome: ok ? 'retired' : 'partial', steps, worker: current };
|
|
@@ -1008,7 +1132,7 @@ export function sessionManager(deps) {
|
|
|
1008
1132
|
try {
|
|
1009
1133
|
const started = now().getTime();
|
|
1010
1134
|
const { copy } = await claude.resume({
|
|
1011
|
-
cwd: worker.worktree ??
|
|
1135
|
+
cwd: worker.worktree ?? cloneOf(worker),
|
|
1012
1136
|
sessionId: worker.sessionId,
|
|
1013
1137
|
text,
|
|
1014
1138
|
});
|