@bakers-build/conductor 0.0.0-stage → 0.1.0-next.42
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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/src/bakers-project-context.d.ts +12 -0
- package/dist/src/bakers-project-context.js +59 -0
- package/dist/src/cli/config-plan.d.ts +35 -0
- package/dist/src/cli/config-plan.js +1264 -0
- package/dist/src/cli/config-schema.d.ts +137 -0
- package/dist/src/cli/config-schema.js +239 -0
- package/dist/src/cli/config-source-edit.d.ts +17 -0
- package/dist/src/cli/config-source-edit.js +95 -0
- package/dist/src/cli/config-templates.d.ts +43 -0
- package/dist/src/cli/config-templates.js +130 -0
- package/dist/src/cli/config-validation.d.ts +12 -0
- package/dist/src/cli/config-validation.js +353 -0
- package/dist/src/cli/migrate-addon-names.d.ts +82 -0
- package/dist/src/cli/migrate-addon-names.js +334 -0
- package/dist/src/cli/project-context.d.ts +21 -0
- package/dist/src/cli/project-context.js +105 -0
- package/dist/src/commands/conductor.d.ts +91 -0
- package/dist/src/commands/conductor.js +843 -0
- package/dist/src/config.d.ts +109 -0
- package/dist/src/config.js +712 -0
- package/dist/src/contract.d.ts +13 -0
- package/dist/src/contract.js +19 -0
- package/dist/src/core/action-queue.d.ts +31 -0
- package/dist/src/core/action-queue.js +69 -0
- package/dist/src/core/attempt.d.ts +25 -0
- package/dist/src/core/attempt.js +41 -0
- package/dist/src/core/conductor.d.ts +236 -0
- package/dist/src/core/conductor.js +1293 -0
- package/dist/src/core/host-key.d.ts +19 -0
- package/dist/src/core/host-key.js +27 -0
- package/dist/src/core/inbox-runtime.d.ts +48 -0
- package/dist/src/core/inbox-runtime.js +180 -0
- package/dist/src/core/mr-changes.d.ts +18 -0
- package/dist/src/core/mr-changes.js +47 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +27 -0
- package/dist/src/plugins/agent-args.d.ts +43 -0
- package/dist/src/plugins/agent-args.js +221 -0
- package/dist/src/plugins/agent.d.ts +121 -0
- package/dist/src/plugins/agent.js +12 -0
- package/dist/src/plugins/executor.d.ts +182 -0
- package/dist/src/plugins/executor.js +1 -0
- package/dist/src/plugins/message.d.ts +61 -0
- package/dist/src/plugins/message.js +47 -0
- package/dist/src/plugins/plugins.d.ts +96 -0
- package/dist/src/plugins/plugins.js +120 -0
- package/dist/src/plugins/preset.d.ts +61 -0
- package/dist/src/plugins/preset.js +23 -0
- package/dist/src/plugins/process-host.d.ts +7 -0
- package/dist/src/plugins/process-host.js +1 -0
- package/dist/src/plugins/remote.d.ts +420 -0
- package/dist/src/plugins/remote.js +1 -0
- package/dist/src/plugins/workspace.d.ts +69 -0
- package/dist/src/plugins/workspace.js +1 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +8 -0
- package/dist/src/types.d.ts +132 -0
- package/dist/src/types.js +6 -0
- package/package.json +38 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { AmbiguousProjectNameError, type ResolvedBbProjectContext, resolveBbProjectContext, } from './bakers-project-context.js';
|
|
2
|
+
export { type AgentFootprint, type AgentRunIdentity, type BbAgent, emptyAgentFootprint, } from './plugins/agent.js';
|
|
3
|
+
export { detectKnownOverrides, type KnownOptionSpec, type KnownOverrides, type KnownScalarKind, resolveAgentCliOptions, validateAgentArgs, validateOptionalBoolean, validateOptionalString, } from './plugins/agent-args.js';
|
|
4
|
+
export type { BbExecutor, ExecutorCapabilities, HookContext, HookListener, HookListenerContext, HookListenerFn, HookName, HookResult, SpawnedSession, SpawnRunInput, WorktreeTeardown, } from './plugins/executor.js';
|
|
5
|
+
export type { DiscoveredInbox, InboxMessageHandler, InboxMessageRequest, InboxMessages, LoadedInbox, } from './plugins/message.js';
|
|
6
|
+
export { loadInbox, validateRequestPayload, } from './plugins/message.js';
|
|
7
|
+
export type { AgentCandidate, AgentPlugin, ExecutorDeps, ExecutorPlugin, RemotePlugin, ResolvedAgent, WorkspacePlugin, } from './plugins/plugins.js';
|
|
8
|
+
export { resolveAgent, selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
9
|
+
export { type ConductorAddonEntry, type ConductorContributions, narrowConductorContributions, type Preset, } from './plugins/preset.js';
|
|
10
|
+
export type { HerdrHost } from './plugins/process-host.js';
|
|
11
|
+
export type { ActiveRun, AddonDefinitionV2, AddonRegistration, BbRemote, ClaimedRun, ClaimOptions, ClaimWithheld, ConductorRegistration, ConductorStatus, FinalizableRun, FrozenTicket, InboxMessage, InboxSnapshot, RunMetrics, RunWriteAttributes, Ticket, TicketMetric, UncleanTicket, } from './plugins/remote.js';
|
|
12
|
+
export type { BbWorkspace, EnsuredWorkspace, } from './plugins/workspace.js';
|
|
13
|
+
export type { ConductorEngineConfig, ConductorFileConfig, ConductorSettings, } from './types.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// BB-224 (Finding 6): the CONDUCTOR-SURFACE CONTRACT, exposed as the
|
|
2
|
+
// runtime-light subpath `@bakers-build/conductor/contract`.
|
|
3
|
+
//
|
|
4
|
+
// This is the module a conductor-surface addon (`addons/remote-drupal`,
|
|
5
|
+
// `addons/workspace-git`, `addons/herdr`, `addons/claude`, …) imports. It carries
|
|
6
|
+
// ONLY the surface interfaces, the config contract, the preset view, and the two
|
|
7
|
+
// tiny runtime helpers (`emptyAgentFootprint`, the `select*` slot selectors) — it
|
|
8
|
+
// imports NO engine, NO commander, NO dropsh runtime. Importing it therefore
|
|
9
|
+
// costs an addon nothing at boot, unlike the package main (`@bakers-build/conductor`),
|
|
10
|
+
// which pulls the whole run engine + the `conductor` command plugin.
|
|
11
|
+
//
|
|
12
|
+
// The package main re-exports everything here too, so `import type { BbRemote }
|
|
13
|
+
// from '@bakers-build/conductor'` keeps working for type-only consumers.
|
|
14
|
+
export { AmbiguousProjectNameError, resolveBbProjectContext, } from './bakers-project-context.js';
|
|
15
|
+
export { emptyAgentFootprint, } from './plugins/agent.js';
|
|
16
|
+
export { detectKnownOverrides, resolveAgentCliOptions, validateAgentArgs, validateOptionalBoolean, validateOptionalString, } from './plugins/agent-args.js';
|
|
17
|
+
export { loadInbox, validateRequestPayload, } from './plugins/message.js';
|
|
18
|
+
export { resolveAgent, selectAgent, selectAgents, selectExecutor, selectRemote, selectWorkspace, } from './plugins/plugins.js';
|
|
19
|
+
export { narrowConductorContributions, } from './plugins/preset.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConductorLogger } from '@bakers-build/core';
|
|
2
|
+
export interface Action {
|
|
3
|
+
/** An action whose key is queued or running is not enqueued again. */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Actions of one lane run one after another; lanes run in parallel. */
|
|
6
|
+
lane: string;
|
|
7
|
+
run: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare class ActionQueue {
|
|
10
|
+
private readonly logger;
|
|
11
|
+
private readonly now;
|
|
12
|
+
private readonly keys;
|
|
13
|
+
private readonly lanes;
|
|
14
|
+
/** Running action key → start (ms). */
|
|
15
|
+
private readonly running;
|
|
16
|
+
constructor(logger: ConductorLogger, now?: () => number);
|
|
17
|
+
/** Queued or running actions. */
|
|
18
|
+
get size(): number;
|
|
19
|
+
/** Starts the action after its lane's last one; false when its key is pending. */
|
|
20
|
+
enqueue(action: Action): boolean;
|
|
21
|
+
/** Resolves once every queued and running action has settled. */
|
|
22
|
+
drain(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Warns once per call about each action running longer than `limitMs`: only
|
|
25
|
+
* addon handlers time out, so a hung hook would otherwise block its ticket's
|
|
26
|
+
* lane without a trace while the heartbeat keeps the conductor healthy.
|
|
27
|
+
*/
|
|
28
|
+
warnOverdue(limitMs: number): void;
|
|
29
|
+
/** Never rejects: a lane's chain must survive a failing action. */
|
|
30
|
+
private execute;
|
|
31
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The conductor's per-item work: the tick only reads what is due and enqueues
|
|
2
|
+
// one action per item. Every action starts at once, fails and logs on its own;
|
|
3
|
+
// actions of one lane (a ticket) run in order, lanes run in parallel.
|
|
4
|
+
export class ActionQueue {
|
|
5
|
+
logger;
|
|
6
|
+
now;
|
|
7
|
+
keys = new Set();
|
|
8
|
+
lanes = new Map();
|
|
9
|
+
/** Running action key → start (ms). */
|
|
10
|
+
running = new Map();
|
|
11
|
+
constructor(logger, now = Date.now) {
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
this.now = now;
|
|
14
|
+
}
|
|
15
|
+
/** Queued or running actions. */
|
|
16
|
+
get size() {
|
|
17
|
+
return this.keys.size;
|
|
18
|
+
}
|
|
19
|
+
/** Starts the action after its lane's last one; false when its key is pending. */
|
|
20
|
+
enqueue(action) {
|
|
21
|
+
if (this.keys.has(action.key))
|
|
22
|
+
return false;
|
|
23
|
+
this.keys.add(action.key);
|
|
24
|
+
const tail = (this.lanes.get(action.lane) ?? Promise.resolve()).then(() => this.execute(action));
|
|
25
|
+
this.lanes.set(action.lane, tail);
|
|
26
|
+
void tail.then(() => {
|
|
27
|
+
if (this.lanes.get(action.lane) === tail)
|
|
28
|
+
this.lanes.delete(action.lane);
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Resolves once every queued and running action has settled. */
|
|
33
|
+
async drain() {
|
|
34
|
+
while (this.lanes.size > 0)
|
|
35
|
+
await Promise.all(this.lanes.values());
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Warns once per call about each action running longer than `limitMs`: only
|
|
39
|
+
* addon handlers time out, so a hung hook would otherwise block its ticket's
|
|
40
|
+
* lane without a trace while the heartbeat keeps the conductor healthy.
|
|
41
|
+
*/
|
|
42
|
+
warnOverdue(limitMs) {
|
|
43
|
+
const now = this.now();
|
|
44
|
+
for (const [key, started] of this.running) {
|
|
45
|
+
const ms = now - started;
|
|
46
|
+
if (ms > limitMs) {
|
|
47
|
+
this.logger.warn({ action: key, ms }, `action ${key} running for ${ms}ms`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Never rejects: a lane's chain must survive a failing action. */
|
|
52
|
+
async execute(action) {
|
|
53
|
+
const started = this.now();
|
|
54
|
+
this.running.set(action.key, started);
|
|
55
|
+
try {
|
|
56
|
+
await action.run();
|
|
57
|
+
const ms = this.now() - started;
|
|
58
|
+
this.logger.debug({ action: action.key, ms }, `action ${action.key} done in ${ms}ms`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const ms = this.now() - started;
|
|
62
|
+
this.logger.warn({ action: action.key, ms, err: String(err) }, `action ${action.key} failed after ${ms}ms`);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.running.delete(action.key);
|
|
66
|
+
this.keys.delete(action.key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic per-attempt session id family for a run (BB-473 review
|
|
3
|
+
* round 4, Critical 2). A re-dispatch of the same run needs its own
|
|
4
|
+
* collision-free session id — `claude`/`grok --session-id` refuse to start
|
|
5
|
+
* when the id already names a transcript from a prior attempt — but the
|
|
6
|
+
* previous fix (a `startedAt` time window) selects by *when*, not by *which
|
|
7
|
+
* attempt*, and a worktree-keyed transcript directory can hold a sibling
|
|
8
|
+
* run's session newer than that bound. An attempt family gives finalise an
|
|
9
|
+
* IDENTITY to select by instead: attempt 0 is the run's own UUID (unchanged,
|
|
10
|
+
* so every caller that only knows a single `sessionId` keeps working);
|
|
11
|
+
* attempt n>0 is a stable UUID-shaped id derived from `${runUuid}:${n}`, so
|
|
12
|
+
* both dispatch (which candidate to pin) and finalise (which candidate's
|
|
13
|
+
* file exists) derive the SAME sequence from nothing but the run's own UUID
|
|
14
|
+
* — no new persisted field, no schema change.
|
|
15
|
+
*/
|
|
16
|
+
export declare function attemptSessionId(runUuid: string, attempt: number): string;
|
|
17
|
+
/** Bound on how many re-dispatches of one run this family covers. */
|
|
18
|
+
export declare const MAX_ATTEMPTS = 25;
|
|
19
|
+
/**
|
|
20
|
+
* The full candidate sequence for a run, attempt 0 first. Both the dispatch
|
|
21
|
+
* pin-selection loop and finalise's selection loop derive this from nothing
|
|
22
|
+
* but the run's own UUID, so the two sides never need to agree through any
|
|
23
|
+
* channel but the UUID itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare function attemptFamily(runUuid: string, max?: number): string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic per-attempt session id family for a run (BB-473 review
|
|
4
|
+
* round 4, Critical 2). A re-dispatch of the same run needs its own
|
|
5
|
+
* collision-free session id — `claude`/`grok --session-id` refuse to start
|
|
6
|
+
* when the id already names a transcript from a prior attempt — but the
|
|
7
|
+
* previous fix (a `startedAt` time window) selects by *when*, not by *which
|
|
8
|
+
* attempt*, and a worktree-keyed transcript directory can hold a sibling
|
|
9
|
+
* run's session newer than that bound. An attempt family gives finalise an
|
|
10
|
+
* IDENTITY to select by instead: attempt 0 is the run's own UUID (unchanged,
|
|
11
|
+
* so every caller that only knows a single `sessionId` keeps working);
|
|
12
|
+
* attempt n>0 is a stable UUID-shaped id derived from `${runUuid}:${n}`, so
|
|
13
|
+
* both dispatch (which candidate to pin) and finalise (which candidate's
|
|
14
|
+
* file exists) derive the SAME sequence from nothing but the run's own UUID
|
|
15
|
+
* — no new persisted field, no schema change.
|
|
16
|
+
*/
|
|
17
|
+
export function attemptSessionId(runUuid, attempt) {
|
|
18
|
+
if (attempt <= 0) {
|
|
19
|
+
return runUuid;
|
|
20
|
+
}
|
|
21
|
+
const hex = createHash('sha256')
|
|
22
|
+
.update(`${runUuid}:${attempt}`)
|
|
23
|
+
.digest('hex')
|
|
24
|
+
.slice(0, 32)
|
|
25
|
+
.split('');
|
|
26
|
+
hex[12] = '5'; // version nibble — cosmetic, just keeps the shape UUID-like.
|
|
27
|
+
hex[16] = ((Number.parseInt(hex[16] ?? '0', 16) & 0x3) | 0x8).toString(16); // variant nibble.
|
|
28
|
+
const h = hex.join('');
|
|
29
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
30
|
+
}
|
|
31
|
+
/** Bound on how many re-dispatches of one run this family covers. */
|
|
32
|
+
export const MAX_ATTEMPTS = 25;
|
|
33
|
+
/**
|
|
34
|
+
* The full candidate sequence for a run, attempt 0 first. Both the dispatch
|
|
35
|
+
* pin-selection loop and finalise's selection loop derive this from nothing
|
|
36
|
+
* but the run's own UUID, so the two sides never need to agree through any
|
|
37
|
+
* channel but the UUID itself.
|
|
38
|
+
*/
|
|
39
|
+
export function attemptFamily(runUuid, max = MAX_ATTEMPTS) {
|
|
40
|
+
return Array.from({ length: max }, (_, i) => attemptSessionId(runUuid, i));
|
|
41
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { type ConductorLogger } from '@bakers-build/core';
|
|
2
|
+
import type { BbExecutor } from '../plugins/executor.js';
|
|
3
|
+
import { type ResolvedAgent } from '../plugins/plugins.js';
|
|
4
|
+
import type { BbRemote } from '../plugins/remote.js';
|
|
5
|
+
import type { BbWorkspace } from '../plugins/workspace.js';
|
|
6
|
+
import type { ConductorFileConfig } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the environment a run executes in (BB-99): parse the ticket's
|
|
9
|
+
* effective env_vars, drop any reserved key (loud warn — key NAME only, never
|
|
10
|
+
* the value), then overlay the conductor's core vars so they always win.
|
|
11
|
+
*
|
|
12
|
+
* Values are potentially secret (e.g. a DB DSN): this function logs key names
|
|
13
|
+
* only, never a value. The returned map is injected into BOTH the agent run env
|
|
14
|
+
* and every workspace hook.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveRunEnv(effectiveEnvVars: string | undefined, core: Record<string, string>, logger: ConductorLogger): Record<string, string>;
|
|
17
|
+
export declare class Conductor {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly remote;
|
|
20
|
+
private readonly executor;
|
|
21
|
+
private readonly workspace;
|
|
22
|
+
private readonly agents;
|
|
23
|
+
private readonly logger;
|
|
24
|
+
private readonly checkoutRoot;
|
|
25
|
+
private readonly frozenStateFile?;
|
|
26
|
+
private uuid;
|
|
27
|
+
/**
|
|
28
|
+
* Frozen tickets seen on the last poll, by uuid, with the worktree their
|
|
29
|
+
* hook ran in. Mirrored to `frozenStateFile` when one is given, so an
|
|
30
|
+
* unfreeze while the conductor is down still gets its `on_resume` and a
|
|
31
|
+
* restart does not repeat `on_freeze`; loaded on the first observation.
|
|
32
|
+
*/
|
|
33
|
+
private frozenSeen;
|
|
34
|
+
/** The addons with contract-2 inbox messages this conductor runs. */
|
|
35
|
+
private readonly inboxAddons;
|
|
36
|
+
/** The per-item work each tick enqueues. */
|
|
37
|
+
private readonly queue;
|
|
38
|
+
/** Serializes git worktree add/remove: parallel ones fight over git locks. */
|
|
39
|
+
private gitLock;
|
|
40
|
+
constructor(config: ConductorFileConfig, remote: BbRemote, executor: BbExecutor, workspace: BbWorkspace, agents: ResolvedAgent[], logger: ConductorLogger, checkoutRoot?: string, frozenStateFile?: string | undefined);
|
|
41
|
+
private serialGit;
|
|
42
|
+
/**
|
|
43
|
+
* `conductor_id` is required and the loader throws without it (BB-353), so
|
|
44
|
+
* there is no directory-derived fallback here (BB-450 D5) — that fallback
|
|
45
|
+
* was the mechanism by which one checkout came to be registered under two
|
|
46
|
+
* different ids.
|
|
47
|
+
*/
|
|
48
|
+
get id(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Host stem for bb_machine.host_key. Prefer config.host_key from
|
|
51
|
+
* MachineContext.machine_id; fall back to a best-effort parse of the composed
|
|
52
|
+
* conductor_id only when the loader could not supply the stem.
|
|
53
|
+
*/
|
|
54
|
+
private get hostKey();
|
|
55
|
+
/**
|
|
56
|
+
* Whether this instance is the serve-loop process itself (BB-232).
|
|
57
|
+
*
|
|
58
|
+
* Off by default so that only a caller which genuinely owns the process can
|
|
59
|
+
* report a pid — see {@link claimServeProcess}.
|
|
60
|
+
*/
|
|
61
|
+
private ownsServeProcess;
|
|
62
|
+
/**
|
|
63
|
+
* Declares that this instance runs *as* the conductor process, so its
|
|
64
|
+
* registrations may carry the process identity a cockpit needs to stop it.
|
|
65
|
+
*
|
|
66
|
+
* Only `cmdStartForeground` calls this. `cmdPoll` and `cmdReap` also build a
|
|
67
|
+
* Conductor and call start() + tick(), then exit; if they reported a pid,
|
|
68
|
+
* every `bakers oven once` would overwrite the running conductor's pid
|
|
69
|
+
* with its own short-lived one and the next stop would signal a dead — or
|
|
70
|
+
* recycled — process. Making the report opt-in means those two commands
|
|
71
|
+
* cannot do so even by accident.
|
|
72
|
+
*/
|
|
73
|
+
claimServeProcess(): void;
|
|
74
|
+
/** This conductor's registration payload, built from config. */
|
|
75
|
+
private registration;
|
|
76
|
+
/**
|
|
77
|
+
* The stem of the config file this conductor was loaded from.
|
|
78
|
+
*
|
|
79
|
+
* Derived through core's own mapping rather than by slicing the filename
|
|
80
|
+
* here, so the CLI and the conductor cannot disagree about which files count
|
|
81
|
+
* as a conductor config.
|
|
82
|
+
*/
|
|
83
|
+
private get conductorName();
|
|
84
|
+
start(): Promise<void>;
|
|
85
|
+
/** One tick that also waits for its actions: `bakers oven once`, tests. */
|
|
86
|
+
tick(): Promise<void>;
|
|
87
|
+
/** A failing list query must not stop the other steps or the claims. */
|
|
88
|
+
private step;
|
|
89
|
+
/**
|
|
90
|
+
* The tick body: heartbeat, the list queries and the claim loop. Every
|
|
91
|
+
* per-item job becomes an action on the queue, so no slow item can delay
|
|
92
|
+
* the heartbeat or the claims.
|
|
93
|
+
*/
|
|
94
|
+
produce(): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* One-shot finalisation: for each of this conductor's runs that is state in
|
|
97
|
+
* (done, cancelled) but not yet closed, read the agent transcript from its
|
|
98
|
+
* worktree and write it as the final log, then mark the run closed. Keyed off
|
|
99
|
+
* the durable terminal-state query — no in-memory state, so a restarted
|
|
100
|
+
* conductor still finalises any unclosed run on its next tick.
|
|
101
|
+
*/
|
|
102
|
+
private enqueueFinalize;
|
|
103
|
+
/** Finalises one run; a failure is logged and retried next tick. */
|
|
104
|
+
private finalizeOne;
|
|
105
|
+
/** Read the run's transcript and parse its footprint for the close write. */
|
|
106
|
+
private captureRun;
|
|
107
|
+
/**
|
|
108
|
+
* Close one run's tab. Run-scoped (BB-183): only the `#<runId>` tab, never
|
|
109
|
+
* a sibling run's. Best-effort: a close failure never mislabels the run as
|
|
110
|
+
* "finalise failed" or replaces a dispatch error.
|
|
111
|
+
*/
|
|
112
|
+
private closeRunTab;
|
|
113
|
+
/**
|
|
114
|
+
* `after_run` for an ended run (BB-599), rebuilt from durable state: the
|
|
115
|
+
* run record names its ticket and start state, and the env is resolved from
|
|
116
|
+
* the CURRENT ticket — so a restarted conductor stops the same sidecars.
|
|
117
|
+
* Like `after_done`, it needs the worktree directory to exist.
|
|
118
|
+
*/
|
|
119
|
+
private runFinalizeHook;
|
|
120
|
+
/**
|
|
121
|
+
* Rebuild a listener context from the ticket's current record, or
|
|
122
|
+
* `undefined` (logged) when it cannot be read — the caller still runs the
|
|
123
|
+
* project shell hook, it only loses the addon listeners. Built even with no
|
|
124
|
+
* listeners: the shell `after_run` takes its env from it.
|
|
125
|
+
*/
|
|
126
|
+
private listenerContext;
|
|
127
|
+
private get listeners();
|
|
128
|
+
/**
|
|
129
|
+
* Run one lifecycle slot (BB-599): every addon listener in accumulator
|
|
130
|
+
* order, awaited, then the project shell hook through the executor. Never
|
|
131
|
+
* throws. A failing listener is logged by slot, listener, ticket and run —
|
|
132
|
+
* never with its message or the env — and reported in the returned result
|
|
133
|
+
* the same way a failing shell command is, so the rest of the slot, the
|
|
134
|
+
* shell hook and dispatch all continue.
|
|
135
|
+
*/
|
|
136
|
+
private runSlot;
|
|
137
|
+
/**
|
|
138
|
+
* Undo a listener's env change the conductor does not accept: a reserved
|
|
139
|
+
* key keeps the conductor's value, and an invalid name or a non-string value
|
|
140
|
+
* never reaches a spawn. Key names only are logged.
|
|
141
|
+
*/
|
|
142
|
+
private guardEnv;
|
|
143
|
+
/**
|
|
144
|
+
* Run `on_freeze` once for each newly frozen ticket and `on_resume` once
|
|
145
|
+
* for each that left the frozen list, in the ticket's worktree on this
|
|
146
|
+
* machine. A ticket without a worktree here gets no hook. Never throws: an
|
|
147
|
+
* unreadable list keeps the previous observation for the next poll.
|
|
148
|
+
*/
|
|
149
|
+
private observeFreezes;
|
|
150
|
+
private loadFrozenSeen;
|
|
151
|
+
private saveFrozenSeen;
|
|
152
|
+
private runFreezeSlot;
|
|
153
|
+
/**
|
|
154
|
+
* Reconcile closed tickets against their herdr worktrees and tear down any
|
|
155
|
+
* whose workspace still lingers (BB-89). Driven by the durable `cleaned_up`
|
|
156
|
+
* flag via {@link BbRemote.fetchUncleanedTickets}: every ticket the server
|
|
157
|
+
* has already closed but not yet cleaned. The SAME reconciliation runs on
|
|
158
|
+
* every tick AND standalone as `bakers oven clean`, and the query is
|
|
159
|
+
* scoped to THIS conductor (BB-121) — so it catches the orphans a
|
|
160
|
+
* live-tick-only path structurally cannot (the conductor was down when the
|
|
161
|
+
* ticket closed, or crashed mid-cleanup) for its OWN tickets. A foreign
|
|
162
|
+
* (non-bakers) worktree has no bakers ticket, so it is never on the work list; a
|
|
163
|
+
* ticket assigned to another conductor is filtered out at the query — both are
|
|
164
|
+
* left untouched with no host enumeration.
|
|
165
|
+
*
|
|
166
|
+
* The conductor is structurally UNABLE to close a ticket (BB-451): closing
|
|
167
|
+
* is a server-side projection of terminality written by
|
|
168
|
+
* `BbTicket::preSave()` on any save of a terminal, unclosed ticket, so it
|
|
169
|
+
* holds for every write path and cannot be withheld by a teardown hiccup —
|
|
170
|
+
* the reap reasons about `cleaned_up` alone. `cleaned_up` is withheld unless
|
|
171
|
+
* the teardown is ACCOUNTED FOR (BB-141 RC-2), which `removeWorktree`
|
|
172
|
+
* reports as one of three outcomes (BB-293):
|
|
173
|
+
* - `removed` — gone because this host tore it down → flag cleaned;
|
|
174
|
+
* - `already_absent` — nothing was there to tear down. A FINISHED teardown,
|
|
175
|
+
* not a failure: it is recorded in this log AND on the run record, then
|
|
176
|
+
* flagged cleaned. Recording it is the point — swallowing the absence and
|
|
177
|
+
* leaving `cleaned_up=0` is what made every later reap repeat the same
|
|
178
|
+
* failing teardown;
|
|
179
|
+
* - `unverified` — it could not be resolved and may still be on disk (e.g.
|
|
180
|
+
* its `worktree_path` was never persisted). Like a THROW, it logs loudly
|
|
181
|
+
* and leaves `cleaned_up=0` so the next reconciliation retries.
|
|
182
|
+
* Flagging cleaned on an unverified teardown was the root cause of the ~28
|
|
183
|
+
* never-touched leftovers, because the ticket then dropped off the
|
|
184
|
+
* (cleaned_up=0) work list forever.
|
|
185
|
+
* Re-running on an already-cleaned ticket is a no-op — it is off the list
|
|
186
|
+
* (idempotent). Each ticket is isolated in a try/catch so one failure never
|
|
187
|
+
* aborts the rest.
|
|
188
|
+
*
|
|
189
|
+
* The `after_done` hook runs through the executor (BB-84), which owns the
|
|
190
|
+
* best-effort policy — it logs+swallows a hook failure and never throws, so
|
|
191
|
+
* the core needs no per-call-site try/catch around it.
|
|
192
|
+
*/
|
|
193
|
+
reap(): Promise<void>;
|
|
194
|
+
private enqueueReap;
|
|
195
|
+
/**
|
|
196
|
+
* Record the merge request's size on a done ticket while its worktree still
|
|
197
|
+
* exists. The server keeps `metrics` writable until `cleaned_up`; a failure
|
|
198
|
+
* is logged and never holds up the teardown. A teardown that keeps failing
|
|
199
|
+
* re-measures on every reap; the rows are replaced, so they stay correct.
|
|
200
|
+
*/
|
|
201
|
+
private recordMrChanges;
|
|
202
|
+
/** Reaps one ticket; isolated, so one failure never aborts the rest. */
|
|
203
|
+
private reapOne;
|
|
204
|
+
/**
|
|
205
|
+
* The env a run executes in: the ticket's effective env_vars under the core
|
|
206
|
+
* vars. Used at dispatch and, rebuilt from the current ticket, for the
|
|
207
|
+
* `after_run` / `after_done` listener contexts (BB-599).
|
|
208
|
+
*/
|
|
209
|
+
private runEnv;
|
|
210
|
+
/** Dispatches one claimed run; a failure terminalises it (BB-149). */
|
|
211
|
+
private dispatchOrFail;
|
|
212
|
+
private dispatch;
|
|
213
|
+
/**
|
|
214
|
+
* The part of a dispatch that can leave a sidecar or a worker behind
|
|
215
|
+
* (BB-599): its caller unwinds it through `after_run` on any failure.
|
|
216
|
+
*/
|
|
217
|
+
private launch;
|
|
218
|
+
/**
|
|
219
|
+
* Stop what a failed dispatch started (BB-599): the worker if it was
|
|
220
|
+
* launched, then the `after_run` listeners and shell hook. Best-effort — it
|
|
221
|
+
* never replaces the dispatch error the caller re-throws.
|
|
222
|
+
*/
|
|
223
|
+
private unwindDispatch;
|
|
224
|
+
serve(signal?: AbortSignal): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* True once this conductor's own checkout has been removed from disk (its
|
|
227
|
+
* worktree was reaped while the process kept running). Such a conductor is a
|
|
228
|
+
* pure liability: it still heartbeats and still claims runs, but every
|
|
229
|
+
* dispatch fails — no git command and no relative path can resolve from a
|
|
230
|
+
* deleted directory — so each claim burns one attempt and three of them park
|
|
231
|
+
* the ticket behind the circuit breaker. Observed as 30 orphans claiming and
|
|
232
|
+
* failing tickets they could never dispatch.
|
|
233
|
+
*/
|
|
234
|
+
private checkoutGone;
|
|
235
|
+
private pollLoop;
|
|
236
|
+
}
|