ai-foreman 1.7.6 → 1.7.7
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/README.md +16 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +29 -4
- package/dist/adapters/claude.d.ts +33 -2
- package/dist/adapters/claude.js +208 -17
- package/dist/adapters/codex.d.ts +11 -1
- package/dist/adapters/codex.js +195 -20
- package/dist/adapters/recovering.d.ts +8 -2
- package/dist/adapters/recovering.js +23 -3
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +52 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +34 -0
- package/dist/branch/currentGuard.js +65 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +940 -42
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +86 -0
- package/dist/continuity.js +365 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +326 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +20 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +102 -3
- package/dist/sessionLifecycle.js +578 -20
- package/dist/statusReporter.d.ts +58 -8
- package/dist/statusReporter.js +210 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +99 -2
- package/dist/workflowDb.js +434 -8
- package/package.json +31 -3
package/dist/agentRun.js
CHANGED
|
@@ -6,12 +6,15 @@ import { PermissionPolicy } from "./permissions/policy.js";
|
|
|
6
6
|
import { ClaudeAdapter } from "./adapters/claude.js";
|
|
7
7
|
import { CodexAdapter } from "./adapters/codex.js";
|
|
8
8
|
import { RecoveringAdapter } from "./adapters/recovering.js";
|
|
9
|
+
import { SessionUnavailableError } from "./adapters/sessionFailure.js";
|
|
9
10
|
import { Foreman, createPermissionHandler } from "./foreman.js";
|
|
10
11
|
import { printEvents } from "./cli/events.js";
|
|
11
12
|
import { loadRoleBundle } from "./roles.js";
|
|
12
13
|
import { ensureRuntimeReadyForCommand } from "./cli/runtimeAuthPrompt.js";
|
|
13
14
|
import { resolveAgentForProject } from "./cli/runtimeSelection.js";
|
|
14
15
|
import { parse as parseYaml } from "yaml";
|
|
16
|
+
import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "./sessionIdentity.js";
|
|
17
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
15
18
|
export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
16
19
|
export function assertEffortLevel(effort) {
|
|
17
20
|
if (effort && !VALID_EFFORT.includes(effort)) {
|
|
@@ -87,10 +90,29 @@ export async function createRoleBuilder(opts) {
|
|
|
87
90
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
88
91
|
}
|
|
89
92
|
const saved = readRoleDefaultsForExecution(opts.projectDir, opts.role);
|
|
93
|
+
if (opts.resumeSessionRef && opts.resumeSessionId && opts.resumeSessionRef.sessionId !== opts.resumeSessionId) {
|
|
94
|
+
throw new Error("resume session ID does not match the supplied location-scoped reference");
|
|
95
|
+
}
|
|
96
|
+
let requestedResumeRef = opts.resumeSessionRef;
|
|
97
|
+
if (!requestedResumeRef && opts.resumeSessionId) {
|
|
98
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
99
|
+
try {
|
|
100
|
+
requestedResumeRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(opts.resumeSessionId), opts.resumeSessionId);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
bindingDb.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
91
107
|
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
92
108
|
assertEffortLevel(effectiveEffort);
|
|
93
|
-
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? saved?.make);
|
|
109
|
+
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? requestedResumeRef?.provider ?? saved?.make);
|
|
110
|
+
if (requestedResumeRef && requestedResumeRef.provider !== runtime) {
|
|
111
|
+
throw new Error(`session ${requestedResumeRef.sessionId} belongs to ${requestedResumeRef.provider}, but ${runtime} was selected`);
|
|
112
|
+
}
|
|
113
|
+
if (requestedResumeRef && requestedResumeRef.role !== configurableRole(opts.role)) {
|
|
114
|
+
throw new Error(`session ${requestedResumeRef.sessionId} belongs to role ${requestedResumeRef.role}, not ${configurableRole(opts.role)}`);
|
|
115
|
+
}
|
|
94
116
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
95
117
|
const log = opts.log ?? new Log(makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-")));
|
|
96
118
|
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
|
|
@@ -99,7 +121,7 @@ export async function createRoleBuilder(opts) {
|
|
|
99
121
|
const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
|
|
100
122
|
label: opts.label,
|
|
101
123
|
yes: Boolean(opts.yes),
|
|
102
|
-
allowSwitch: opts.allowSwitch,
|
|
124
|
+
allowSwitch: requestedResumeRef || opts.resumeSessionId ? false : opts.allowSwitch,
|
|
103
125
|
model: initialModel,
|
|
104
126
|
});
|
|
105
127
|
runtime = ready.runtime;
|
|
@@ -108,13 +130,42 @@ export async function createRoleBuilder(opts) {
|
|
|
108
130
|
const policy = new PermissionPolicy(opts.permissionConfig ?? config.permissions, opts.projectDir);
|
|
109
131
|
const skills = mergeSkills(roleBundle.skills, opts.extraSkills);
|
|
110
132
|
const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
111
|
-
const makeAdapter = async (nextRuntime, resumeSessionId) => {
|
|
133
|
+
const makeAdapter = async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
134
|
+
let scopedRef = resumeSessionRef;
|
|
135
|
+
if (!scopedRef && resumeSessionId) {
|
|
136
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
137
|
+
try {
|
|
138
|
+
scopedRef = resolveUniqueSessionBinding(bindingDb.providerSessionBindings(resumeSessionId), resumeSessionId);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
bindingDb.close();
|
|
142
|
+
}
|
|
143
|
+
scopedRef ??= createProviderSessionRef({
|
|
144
|
+
provider: nextRuntime,
|
|
145
|
+
sessionId: resumeSessionId,
|
|
146
|
+
role: configurableRole(opts.role),
|
|
147
|
+
stream: opts.role,
|
|
148
|
+
generation: 0,
|
|
149
|
+
cwd: opts.projectDir,
|
|
150
|
+
configRoot: opts.projectDir,
|
|
151
|
+
workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
|
|
152
|
+
source: "legacy-inferred",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (scopedRef && scopedRef.provider !== nextRuntime) {
|
|
156
|
+
throw new Error(`session ${scopedRef.sessionId} belongs to ${scopedRef.provider}, but ${nextRuntime} was selected`);
|
|
157
|
+
}
|
|
112
158
|
const adapterOpts = {
|
|
113
159
|
cwd: opts.projectDir,
|
|
160
|
+
configRoot: opts.projectDir,
|
|
114
161
|
runtimeExecutable,
|
|
115
162
|
runtimePhase: phaseForRole(opts.role),
|
|
116
163
|
model,
|
|
117
|
-
|
|
164
|
+
...(scopedRef ? { resumeSessionRef: scopedRef } : {}),
|
|
165
|
+
sessionRole: configurableRole(opts.role),
|
|
166
|
+
sessionStream: opts.role,
|
|
167
|
+
sessionGeneration: scopedRef?.generation ?? 0,
|
|
168
|
+
workspaceIdentity: captureWorkspaceIdentity(opts.projectDir),
|
|
118
169
|
permission: createPermissionHandler(policy, log, {
|
|
119
170
|
interactive,
|
|
120
171
|
onAnsweredQuestion: opts.onAnsweredQuestion,
|
|
@@ -126,18 +177,46 @@ export async function createRoleBuilder(opts) {
|
|
|
126
177
|
systemPromptAppend: roleBundle.system || undefined,
|
|
127
178
|
skills: skills.length > 0 ? skills : undefined,
|
|
128
179
|
};
|
|
129
|
-
|
|
180
|
+
const adapter = nextRuntime === "codex"
|
|
130
181
|
? new CodexAdapter(adapterOpts)
|
|
131
182
|
: await ClaudeAdapter.create(adapterOpts);
|
|
183
|
+
if (scopedRef && adapter.agent === "codex") {
|
|
184
|
+
const availability = await adapter.validateSession();
|
|
185
|
+
if (availability.status !== "available") {
|
|
186
|
+
await adapter.close().catch(() => { });
|
|
187
|
+
throw new SessionUnavailableError({
|
|
188
|
+
runtime: "codex",
|
|
189
|
+
phase: "preflight",
|
|
190
|
+
dispatchState: "not-sent",
|
|
191
|
+
executable: runtimeExecutable,
|
|
192
|
+
cwd: opts.projectDir,
|
|
193
|
+
diagnostics: availability.detail ?? `Codex session ${scopedRef.sessionId} is ${availability.status}`,
|
|
194
|
+
availability,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return adapter;
|
|
132
199
|
};
|
|
133
|
-
const initial = await makeAdapter(runtime, opts.resumeSessionId);
|
|
200
|
+
const initial = await makeAdapter(runtime, opts.resumeSessionId, requestedResumeRef);
|
|
201
|
+
const persistSessionRef = (ref) => {
|
|
202
|
+
const bindingDb = new WorkflowDb(opts.projectDir);
|
|
203
|
+
try {
|
|
204
|
+
bindingDb.recordProviderSessionBinding(ref);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
bindingDb.close();
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const initialRef = initial.sessionRef?.();
|
|
211
|
+
if (initialRef)
|
|
212
|
+
persistSessionRef(initialRef);
|
|
134
213
|
const builder = new RecoveringAdapter({
|
|
135
214
|
initial,
|
|
136
215
|
runtime,
|
|
137
216
|
label: opts.label,
|
|
138
217
|
enabled: interactive,
|
|
139
|
-
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId,
|
|
140
|
-
recreate: async (nextRuntime, resumeSessionId) => {
|
|
218
|
+
allowSwitch: opts.allowSwitch !== false && !opts.resumeSessionId && !requestedResumeRef,
|
|
219
|
+
recreate: async (nextRuntime, resumeSessionId, resumeSessionRef) => {
|
|
141
220
|
const nextReady = await ensureRuntimeReadyForCommand(opts.projectDir, nextRuntime, {
|
|
142
221
|
label: opts.label,
|
|
143
222
|
allowSwitch: false,
|
|
@@ -146,8 +225,9 @@ export async function createRoleBuilder(opts) {
|
|
|
146
225
|
runtime = nextReady.runtime;
|
|
147
226
|
model = nextReady.model;
|
|
148
227
|
runtimeExecutable = nextReady.executable;
|
|
149
|
-
return makeAdapter(runtime, resumeSessionId);
|
|
228
|
+
return makeAdapter(runtime, resumeSessionId, resumeSessionRef);
|
|
150
229
|
},
|
|
230
|
+
onSessionRef: persistSessionRef,
|
|
151
231
|
});
|
|
152
232
|
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
153
233
|
}
|
|
@@ -225,6 +305,7 @@ export async function runRoleInstruction(opts) {
|
|
|
225
305
|
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
226
306
|
effort,
|
|
227
307
|
sessionId: builder.sessionId(),
|
|
308
|
+
sessionRef: builder.sessionRef?.(),
|
|
228
309
|
statusKind: turn.status.kind,
|
|
229
310
|
summary: turn.status.summary,
|
|
230
311
|
reason: turn.status.reason,
|
|
@@ -238,6 +319,7 @@ export async function runRoleInstruction(opts) {
|
|
|
238
319
|
model: builder.agent === roleBuilder.runtime ? roleBuilder.model : undefined,
|
|
239
320
|
effort,
|
|
240
321
|
sessionId: builder.sessionId(),
|
|
322
|
+
sessionRef: builder.sessionRef?.(),
|
|
241
323
|
logPath,
|
|
242
324
|
roleBundle,
|
|
243
325
|
skills,
|
|
@@ -250,3 +332,8 @@ export async function runRoleInstruction(opts) {
|
|
|
250
332
|
throw err;
|
|
251
333
|
}
|
|
252
334
|
}
|
|
335
|
+
function configurableRole(role) {
|
|
336
|
+
return ["builder", "qa", "planner", "ticket-maker", "uninstaller"].includes(role)
|
|
337
|
+
? role
|
|
338
|
+
: "builder";
|
|
339
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "../adapters/types.js";
|
|
2
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
|
+
export interface CurrentWorkflowIdentity {
|
|
4
|
+
worktree: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class CurrentWorkflowChangedError extends Error {
|
|
8
|
+
readonly expected: CurrentWorkflowIdentity;
|
|
9
|
+
readonly actual: CurrentWorkflowIdentity;
|
|
10
|
+
constructor(expected: CurrentWorkflowIdentity, actual: CurrentWorkflowIdentity);
|
|
11
|
+
}
|
|
12
|
+
export declare function captureCurrentWorkflowIdentity(cwd: string): CurrentWorkflowIdentity;
|
|
13
|
+
export declare function assertCurrentWorkflowIdentity(cwd: string, expected: CurrentWorkflowIdentity): void;
|
|
14
|
+
export declare function currentWorkflowIdentityKey(identity: CurrentWorkflowIdentity): string;
|
|
15
|
+
export declare function captureCurrentWorkflowSessionIdentity(cwd: string): string;
|
|
16
|
+
/** Fence every provider boundary against an unexpected user/provider ref switch. */
|
|
17
|
+
export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
18
|
+
private readonly adapter;
|
|
19
|
+
private readonly cwd;
|
|
20
|
+
private readonly expected;
|
|
21
|
+
readonly agent: BuilderAdapter["agent"];
|
|
22
|
+
constructor(adapter: BuilderAdapter, cwd: string, expected?: CurrentWorkflowIdentity);
|
|
23
|
+
sendTurn(text: string): Promise<TurnResult>;
|
|
24
|
+
sessionId(): string | undefined;
|
|
25
|
+
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
26
|
+
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
27
|
+
validateSession(): Promise<SessionAvailabilityV1>;
|
|
28
|
+
compact(): Promise<CompactResult>;
|
|
29
|
+
contextUsage(): Promise<ContextUsage | undefined>;
|
|
30
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
31
|
+
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
32
|
+
events(): AsyncIterable<BuilderEvent>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
export class CurrentWorkflowChangedError extends Error {
|
|
5
|
+
expected;
|
|
6
|
+
actual;
|
|
7
|
+
constructor(expected, actual) {
|
|
8
|
+
super(`current-branch workflow paused: active worktree/ref changed (expected ${expected.worktree} @ ${expected.ref}; found ${actual.worktree} @ ${actual.ref})`);
|
|
9
|
+
this.expected = expected;
|
|
10
|
+
this.actual = actual;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function captureCurrentWorkflowIdentity(cwd) {
|
|
14
|
+
return { worktree: realpathSync(git(cwd, ["rev-parse", "--show-toplevel"])), ref: currentRef(cwd) };
|
|
15
|
+
}
|
|
16
|
+
export function assertCurrentWorkflowIdentity(cwd, expected) {
|
|
17
|
+
const actual = captureCurrentWorkflowIdentity(cwd);
|
|
18
|
+
if (actual.worktree !== expected.worktree || actual.ref !== expected.ref) {
|
|
19
|
+
throw new CurrentWorkflowChangedError(expected, actual);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function currentWorkflowIdentityKey(identity) {
|
|
23
|
+
return `current_${createHash("sha256").update(`${identity.worktree}\0${identity.ref}`).digest("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
export function captureCurrentWorkflowSessionIdentity(cwd) {
|
|
26
|
+
return currentWorkflowIdentityKey(captureCurrentWorkflowIdentity(cwd));
|
|
27
|
+
}
|
|
28
|
+
/** Fence every provider boundary against an unexpected user/provider ref switch. */
|
|
29
|
+
export class CurrentWorkflowGuardAdapter {
|
|
30
|
+
adapter;
|
|
31
|
+
cwd;
|
|
32
|
+
expected;
|
|
33
|
+
agent;
|
|
34
|
+
constructor(adapter, cwd, expected = captureCurrentWorkflowIdentity(cwd)) {
|
|
35
|
+
this.adapter = adapter;
|
|
36
|
+
this.cwd = cwd;
|
|
37
|
+
this.expected = expected;
|
|
38
|
+
this.agent = adapter.agent;
|
|
39
|
+
}
|
|
40
|
+
async sendTurn(text) {
|
|
41
|
+
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
42
|
+
const result = await this.adapter.sendTurn(text);
|
|
43
|
+
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
sessionId() { return this.adapter.sessionId(); }
|
|
47
|
+
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
48
|
+
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
49
|
+
validateSession() { assertCurrentWorkflowIdentity(this.cwd, this.expected); return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
50
|
+
async compact() { assertCurrentWorkflowIdentity(this.cwd, this.expected); const result = await (this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" })); assertCurrentWorkflowIdentity(this.cwd, this.expected); return result; }
|
|
51
|
+
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
52
|
+
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
53
|
+
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
54
|
+
events() { return this.adapter.events(); }
|
|
55
|
+
close() { return this.adapter.close(); }
|
|
56
|
+
}
|
|
57
|
+
function currentRef(cwd) {
|
|
58
|
+
try {
|
|
59
|
+
return `branch:${git(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"])}`;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return `detached:${git(cwd, ["rev-parse", "HEAD"])}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function git(cwd, args) { return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
package/dist/branch/planner.d.ts
CHANGED
|
@@ -24,4 +24,4 @@ export declare function applySharedDeliveryBranch(plan: BranchPlan, unitId: stri
|
|
|
24
24
|
export declare function applySizeBranchPolicy(plan: BranchPlan, policy: TicketBuildSetupConfig["branch_policy"], delivery?: DeliveryConfig, branchPrefix?: string): BranchPlan;
|
|
25
25
|
export declare function parseAuditDependencies(text: string): AuditDependency[];
|
|
26
26
|
export declare function buildBranchAuditInstruction(tickets: TicketDef[]): string;
|
|
27
|
-
export
|
|
27
|
+
export { normalizeBranchPrefix } from "./prefix.js";
|
package/dist/branch/planner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeBranchPrefix } from "./prefix.js";
|
|
1
2
|
export function buildBranchPlan(tickets, states, opts) {
|
|
2
3
|
const selected = selectTicketsForBranchRun(tickets, states, opts.steps, opts.ticketIds);
|
|
3
4
|
const selectedIds = new Set(selected.map((ticket) => ticket.id));
|
|
@@ -192,13 +193,7 @@ function uniqueBranchName(prefix, ticket, used) {
|
|
|
192
193
|
used.add(branch);
|
|
193
194
|
return branch;
|
|
194
195
|
}
|
|
195
|
-
export
|
|
196
|
-
const value = prefix === undefined || prefix === "" ? "feature" : prefix;
|
|
197
|
-
if (value.startsWith("/") || value.endsWith("/") || value.includes("..") || value.includes("//") || /[~^:?*\[\\\s]/.test(value)) {
|
|
198
|
-
throw new Error(`invalid Git branch prefix: ${value}`);
|
|
199
|
-
}
|
|
200
|
-
return value;
|
|
201
|
-
}
|
|
196
|
+
export { normalizeBranchPrefix } from "./prefix.js";
|
|
202
197
|
function detectSelectedCycles(tickets, deps) {
|
|
203
198
|
const selected = new Set(tickets.map((ticket) => ticket.id));
|
|
204
199
|
const visited = new Set();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Built-in prefix used only when neither a run nor project value exists. */
|
|
2
|
+
export declare const BUILTIN_BRANCH_PREFIX = "feature";
|
|
3
|
+
/**
|
|
4
|
+
* Validate a prefix as the leading components of a Git branch ref. Internal
|
|
5
|
+
* slashes are intentional (for example `team/feature`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateBranchPrefix(value: string): string;
|
|
8
|
+
export declare function normalizeBranchPrefix(prefix: string | undefined): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Built-in prefix used only when neither a run nor project value exists. */
|
|
2
|
+
export const BUILTIN_BRANCH_PREFIX = "feature";
|
|
3
|
+
/**
|
|
4
|
+
* Validate a prefix as the leading components of a Git branch ref. Internal
|
|
5
|
+
* slashes are intentional (for example `team/feature`).
|
|
6
|
+
*/
|
|
7
|
+
export function validateBranchPrefix(value) {
|
|
8
|
+
const prefix = value.trim();
|
|
9
|
+
if (!prefix)
|
|
10
|
+
throw new Error("branch prefix must not be empty");
|
|
11
|
+
if (prefix === "@" || prefix.startsWith("-") || prefix.startsWith("/") || prefix.endsWith("/")) {
|
|
12
|
+
throw new Error(`invalid Git branch prefix: ${value}`);
|
|
13
|
+
}
|
|
14
|
+
if (prefix.includes("//") || prefix.includes("..") || prefix.includes("@{") || /[\x00-\x20\x7f~^:?*\[\\]/.test(prefix)) {
|
|
15
|
+
throw new Error(`invalid Git branch prefix: ${value}`);
|
|
16
|
+
}
|
|
17
|
+
for (const segment of prefix.split("/")) {
|
|
18
|
+
if (!segment || segment === "." || segment === ".." || segment.startsWith(".") || segment.endsWith(".") || segment.endsWith(".lock")) {
|
|
19
|
+
throw new Error(`invalid Git branch prefix: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return prefix;
|
|
23
|
+
}
|
|
24
|
+
export function normalizeBranchPrefix(prefix) {
|
|
25
|
+
return validateBranchPrefix(prefix === undefined ? BUILTIN_BRANCH_PREFIX : prefix);
|
|
26
|
+
}
|
package/dist/branch/resume.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { BranchRunSummary } from "./types.js";
|
|
2
|
+
import type { ProviderSessionRefV1 } from "rafi-spec";
|
|
2
3
|
export interface BranchResumeSession {
|
|
3
4
|
ticket: string;
|
|
4
5
|
branch: string;
|
|
5
6
|
base: string;
|
|
6
7
|
worktreePath: string;
|
|
7
8
|
sessionId: string;
|
|
9
|
+
sessionRef?: ProviderSessionRefV1;
|
|
8
10
|
logPath: string;
|
|
9
11
|
agent?: string;
|
|
10
12
|
model?: string;
|
package/dist/branch/resume.js
CHANGED
|
@@ -32,6 +32,7 @@ export function findResumableBranchSessions(foremanDir) {
|
|
|
32
32
|
base,
|
|
33
33
|
worktreePath,
|
|
34
34
|
sessionId,
|
|
35
|
+
...(isProviderSessionRef(record.sessionRef) ? { sessionRef: record.sessionRef } : {}),
|
|
35
36
|
logPath,
|
|
36
37
|
agent: stringField(record.agent),
|
|
37
38
|
model: stringField(record.model),
|
|
@@ -116,6 +117,13 @@ function stringField(value) {
|
|
|
116
117
|
function booleanField(value) {
|
|
117
118
|
return typeof value === "boolean" ? value : undefined;
|
|
118
119
|
}
|
|
120
|
+
function isProviderSessionRef(value) {
|
|
121
|
+
if (!value || typeof value !== "object")
|
|
122
|
+
return false;
|
|
123
|
+
const ref = value;
|
|
124
|
+
return ref.version === 1 && (ref.provider === "claude" || ref.provider === "codex") && typeof ref.sessionId === "string"
|
|
125
|
+
&& typeof ref.cwd === "string" && typeof ref.configRoot === "string" && typeof ref.workspaceIdentity === "string";
|
|
126
|
+
}
|
|
119
127
|
function shellQuote(value) {
|
|
120
128
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
|
121
129
|
return value;
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
|
|
2
|
-
import type { SessionStrategy } from "rafi-spec";
|
|
2
|
+
import type { ProviderSessionRefV1, SessionStrategy } from "rafi-spec";
|
|
3
3
|
import { type QaNonconvergenceContext, type QaNonconvergenceDecision } from "../qaReview.js";
|
|
4
4
|
import type { Log } from "../log.js";
|
|
5
5
|
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, MergeMethod, ReviewProvider } from "./types.js";
|
|
6
|
+
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
7
|
+
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
6
8
|
export interface DeliveryUnitSession {
|
|
7
9
|
unitId: string;
|
|
8
10
|
branch: string;
|
|
9
11
|
worktreePath: string;
|
|
10
12
|
sessionId: string;
|
|
13
|
+
sessionRef?: ProviderSessionRefV1;
|
|
11
14
|
ticket: string;
|
|
12
15
|
}
|
|
13
16
|
export type BaseWorktreePolicy = "enforce" | "warn" | "skip";
|
|
@@ -41,13 +44,22 @@ export interface BranchRunnerOptions {
|
|
|
41
44
|
resumeSessions?: Map<string, {
|
|
42
45
|
worktreePath: string;
|
|
43
46
|
sessionId: string;
|
|
47
|
+
sessionRef?: ProviderSessionRefV1;
|
|
44
48
|
}>;
|
|
45
|
-
createBuilder: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
49
|
+
createBuilder: (cwd: string, sessionId?: string, sessionRef?: ProviderSessionRefV1) => Promise<BuilderAdapter>;
|
|
50
|
+
/** Persist every observed Builder binding before later work can supersede it. */
|
|
51
|
+
recordBuilderSession?: (session: string | ProviderSessionRefV1, ticketId: string, worktreePath: string) => void;
|
|
52
|
+
recordQaSession?: (session: string | ProviderSessionRefV1, ticketId: string, worktreePath: string) => void;
|
|
53
|
+
/** Notify the host and stop the branch plan without discarding its worktree. */
|
|
54
|
+
onSessionUnavailable?: (error: SessionUnavailableError | SessionUnavailableContinuityError) => void;
|
|
46
55
|
createQa?: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
47
56
|
builderSessionStrategy?: SessionStrategy;
|
|
48
57
|
qaSessionStrategy?: SessionStrategy;
|
|
49
58
|
observeBuilder?: (builder: BuilderAdapter) => Promise<void>;
|
|
50
59
|
qaNonconvergence?: (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
60
|
+
beforeBuilderTurn?: (adapter: BuilderAdapter, frozenAction: string, cwd: string) => Promise<BuilderAdapter>;
|
|
61
|
+
builderSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
62
|
+
qaSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
51
63
|
}
|
|
52
64
|
export declare function runBranchPlan(opts: BranchRunnerOptions): Promise<BranchRunSummary[]>;
|
|
53
65
|
export declare function buildBranchTicketInstruction(node: BranchPlanNode, trackerPaths?: {
|
package/dist/branch/runner.js
CHANGED
|
@@ -6,10 +6,12 @@ import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
|
6
6
|
import { commitAll, createTicketWorktree, findWorktreeForBranch, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, mergeBranchToLocalBase, } from "./git.js";
|
|
7
7
|
import { checkGitHubPrMerged, createOrReusePr, enableGitHubAutoMerge, pushBranchForPr } from "./github.js";
|
|
8
8
|
import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranchForMr } from "./gitlab.js";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { join } from "node:path";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
11
|
import { WorkflowDb } from "../workflowDb.js";
|
|
12
12
|
import { currentActivity, withActivityPhase } from "../activity.js";
|
|
13
|
+
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
14
|
+
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
13
15
|
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
14
16
|
const path = deliverySessionPath(projectDir, unitId);
|
|
15
17
|
if (!existsSync(path))
|
|
@@ -46,7 +48,7 @@ export async function runBranchPlan(opts) {
|
|
|
46
48
|
const summaries = [];
|
|
47
49
|
const successfulBranches = new Set();
|
|
48
50
|
const pushedBranches = new Set();
|
|
49
|
-
let
|
|
51
|
+
let builderStream;
|
|
50
52
|
let builderWorkSessions = 0;
|
|
51
53
|
const qaStream = { reviews: 0, modificationViolations: 0 };
|
|
52
54
|
for (const issue of opts.plan.issues) {
|
|
@@ -138,28 +140,51 @@ export async function runBranchPlan(opts) {
|
|
|
138
140
|
base: node.baseBranch,
|
|
139
141
|
worktreePath,
|
|
140
142
|
sessionId: resumeSession.sessionId,
|
|
143
|
+
sessionRef: resumeSession.sessionRef,
|
|
141
144
|
});
|
|
142
145
|
}
|
|
143
146
|
journalTracker(opts.projectDir, opts.runId, `${opts.runId}:tracker-update:${node.ticket.id}:in-progress`, { ticket: node.ticket.id, status: "in_progress" }, () => cmdUpdate(opts.projectDir, node.ticket.id, {
|
|
144
147
|
status: "in_progress", actor: "foreman",
|
|
145
148
|
summary: resumeSession ? `Resuming branch ${node.branch}` : `Starting branch ${node.branch}`,
|
|
146
149
|
}));
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
const ticketInstruction = resumeSession ? buildBranchTicketResumeInstruction(node, opts.trackerPaths) : buildBranchTicketInstruction(node, opts.trackerPaths);
|
|
151
|
+
// Reattach the predecessor even for a `fresh` strategy so the host can
|
|
152
|
+
// publish and validate a cumulative handoff before creating its successor.
|
|
153
|
+
const sameWorktreeStream = builderStream && canonical(builderStream.worktreePath) === canonical(worktreePath) ? builderStream : undefined;
|
|
154
|
+
const continuedBuilderSession = resumeSession?.sessionId ?? sameWorktreeStream?.sessionId;
|
|
155
|
+
const continuedBuilderRef = resumeSession?.sessionRef ?? sameWorktreeStream?.sessionRef;
|
|
156
|
+
builder = await opts.createBuilder(worktreePath, continuedBuilderSession, continuedBuilderRef);
|
|
157
|
+
if (builderWorkSessions > 0 && continuedBuilderSession) {
|
|
158
|
+
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
159
|
+
if (opts.builderSessionBoundary) {
|
|
160
|
+
builder = await opts.builderSessionBoundary(builder, ticketInstruction, strategy, worktreePath);
|
|
161
|
+
}
|
|
162
|
+
else if (strategy === "compact") {
|
|
163
|
+
const compacted = await compactWithRetry(builder);
|
|
164
|
+
if (!compacted.ok) {
|
|
165
|
+
await builder.close();
|
|
166
|
+
builder = await opts.createBuilder(worktreePath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
152
170
|
await builder.close();
|
|
153
171
|
builder = await opts.createBuilder(worktreePath);
|
|
154
172
|
}
|
|
155
173
|
}
|
|
156
174
|
builderWorkSessions += 1;
|
|
157
175
|
viewer = opts.observeBuilder?.(builder);
|
|
158
|
-
const foreman = new Foreman(builder, opts.log, opts.notificationsEnabled, opts.qaEnabled, 3, undefined);
|
|
159
|
-
const { result, status } = await foreman.runInstruction(
|
|
176
|
+
const foreman = new Foreman(builder, opts.log, opts.notificationsEnabled, opts.qaEnabled, 3, undefined, undefined, undefined, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined);
|
|
177
|
+
const { result, status } = await foreman.runInstruction(ticketInstruction);
|
|
178
|
+
builder = foreman.builderAdapter();
|
|
160
179
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-after", node.ticket.id, { status: status.kind, sessionId: builder.sessionId(), worktree: worktreePath });
|
|
161
180
|
const sessionId = builder.sessionId();
|
|
162
|
-
|
|
181
|
+
const sessionRef = builder.sessionRef?.();
|
|
182
|
+
if (sessionId)
|
|
183
|
+
builderStream = { sessionId, ...(sessionRef ? { sessionRef } : {}), worktreePath };
|
|
184
|
+
if (sessionId) {
|
|
185
|
+
opts.recordBuilderSession?.(sessionRef ?? sessionId, node.ticket.id, worktreePath);
|
|
186
|
+
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId, sessionRef, worktree: worktreePath, branch: node.branch });
|
|
187
|
+
}
|
|
163
188
|
if (sessionId) {
|
|
164
189
|
opts.log.write("branch-session", {
|
|
165
190
|
ticket: node.ticket.id,
|
|
@@ -167,6 +192,7 @@ export async function runBranchPlan(opts) {
|
|
|
167
192
|
base: node.baseBranch,
|
|
168
193
|
worktreePath,
|
|
169
194
|
sessionId,
|
|
195
|
+
sessionRef: builder.sessionRef?.(),
|
|
170
196
|
agent: opts.agent,
|
|
171
197
|
model: opts.model,
|
|
172
198
|
effort: opts.effort,
|
|
@@ -183,7 +209,7 @@ export async function runBranchPlan(opts) {
|
|
|
183
209
|
if (node.deliveryUnitId) {
|
|
184
210
|
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
185
211
|
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
186
|
-
writeFileSync(path, `${JSON.stringify({ unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, ticket: node.ticket.id }, null, 2)}\n`, "utf8");
|
|
212
|
+
writeFileSync(path, `${JSON.stringify({ unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, sessionRef: builder.sessionRef?.(), ticket: node.ticket.id }, null, 2)}\n`, "utf8");
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
215
|
opts.log.write("step", {
|
|
@@ -215,30 +241,47 @@ export async function runBranchPlan(opts) {
|
|
|
215
241
|
const qa = await runIsolatedQa({
|
|
216
242
|
ticket: node.ticket, builderWorktree: worktreePath, builderSummary: status.summary ?? result.text,
|
|
217
243
|
qaStrategy: opts.qaSessionStrategy ?? "compact", state: qaStream, createQa: opts.createQa, maxCycles: 3,
|
|
244
|
+
sessionBoundary: opts.qaSessionBoundary,
|
|
218
245
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
219
246
|
fix: async (issues) => {
|
|
220
247
|
if (!builder)
|
|
221
248
|
return { ok: false, detail: "Builder session unavailable" };
|
|
222
249
|
builderWorkSessions += 1;
|
|
223
|
-
|
|
250
|
+
const digest = (await withActivityPhase("recording QA fix changes", () => changeManifestAsync(worktreePath, (state, detail) => currentActivity()?.update(state, detail), "recording QA fix changes"))).diffDigest;
|
|
251
|
+
const fixInstruction = buildDurableQaFixHandoff(node.ticket, issues, worktreePath, result.text, digest);
|
|
252
|
+
const strategy = opts.builderSessionStrategy ?? "compact";
|
|
253
|
+
if (opts.builderSessionBoundary) {
|
|
254
|
+
builder = await opts.builderSessionBoundary(builder, fixInstruction, strategy, worktreePath);
|
|
255
|
+
}
|
|
256
|
+
else if (strategy === "compact" && builder.sessionId()) {
|
|
224
257
|
const compacted = await compactWithRetry(builder);
|
|
225
258
|
if (!compacted.ok) {
|
|
226
259
|
await builder.close();
|
|
227
260
|
builder = await opts.createBuilder(worktreePath);
|
|
228
261
|
}
|
|
229
262
|
}
|
|
230
|
-
else if (
|
|
263
|
+
else if (strategy === "fresh") {
|
|
231
264
|
await builder.close();
|
|
232
265
|
builder = await opts.createBuilder(worktreePath);
|
|
233
266
|
}
|
|
234
|
-
|
|
235
|
-
|
|
267
|
+
if (opts.beforeBuilderTurn)
|
|
268
|
+
builder = await opts.beforeBuilderTurn(builder, fixInstruction, worktreePath);
|
|
269
|
+
const fixed = await builder.sendTurn(fixInstruction);
|
|
236
270
|
const fixedStatus = parseStepStatus(fixed.text);
|
|
237
|
-
|
|
271
|
+
const fixedSessionId = builder.sessionId();
|
|
272
|
+
const fixedSessionRef = builder.sessionRef?.();
|
|
273
|
+
if (fixedSessionId)
|
|
274
|
+
builderStream = { sessionId: fixedSessionId, ...(fixedSessionRef ? { sessionRef: fixedSessionRef } : {}), worktreePath };
|
|
275
|
+
if (fixedSessionId) {
|
|
276
|
+
opts.recordBuilderSession?.(fixedSessionRef ?? fixedSessionId, node.ticket.id, worktreePath);
|
|
277
|
+
workflowCheckpoint(opts.projectDir, opts.runId, "builder-session-scoped", node.ticket.id, { sessionId: fixedSessionId, sessionRef: fixedSessionRef, worktree: worktreePath, branch: node.branch });
|
|
278
|
+
}
|
|
238
279
|
return { ok: !fixed.isError && fixedStatus.kind === "done", detail: fixedStatus.error ?? fixed.text.slice(0, 500) };
|
|
239
280
|
},
|
|
240
281
|
onNonconvergence: opts.qaNonconvergence,
|
|
241
282
|
});
|
|
283
|
+
if (qaStream.sessionId)
|
|
284
|
+
opts.recordQaSession?.(qaStream.sessionRef ?? qaStream.sessionId, node.ticket.id, worktreePath);
|
|
242
285
|
workflowCheckpoint(opts.projectDir, opts.runId, "qa-after", node.ticket.id, { outcome: qa.outcome, detail: qa.detail });
|
|
243
286
|
if (qa.outcome !== "passed" && qa.outcome !== "waived") {
|
|
244
287
|
const detail = qa.detail ?? "QA did not pass";
|
|
@@ -444,6 +487,14 @@ export async function runBranchPlan(opts) {
|
|
|
444
487
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
445
488
|
}
|
|
446
489
|
catch (err) {
|
|
490
|
+
if (err instanceof SessionUnavailableError || err instanceof SessionUnavailableContinuityError) {
|
|
491
|
+
const message = err.message;
|
|
492
|
+
opts.log.write("branch-issue", { ticket: node.ticket.id, code: "session_unavailable", message, blocking: true });
|
|
493
|
+
notifyIssue(opts.notificationsEnabled, `${node.ticket.id}: ${message}`);
|
|
494
|
+
summaries.push(summaryFor(node, "blocked", message));
|
|
495
|
+
opts.onSessionUnavailable?.(err);
|
|
496
|
+
return summaries;
|
|
497
|
+
}
|
|
447
498
|
const message = err instanceof Error ? err.message : String(err);
|
|
448
499
|
const code = message.includes("switched branch")
|
|
449
500
|
? "branch_switch"
|
|
@@ -510,6 +561,7 @@ function workflowCheckpoint(projectDir, runId, checkpoint, ticket, payload) {
|
|
|
510
561
|
if (!run)
|
|
511
562
|
return;
|
|
512
563
|
db.transition(runId, { checkpoint, state: { ...run.state, currentTicket: ticket, ...payload }, event: checkpoint, payload: { ticket, ...payload } });
|
|
564
|
+
db.appendContinuityEvent({ runId, role: "host", kind: checkpoint, payload: { ticket, ...payload }, authoritativeStateRevision: db.continuityHead(runId, "run")?.authoritativeStateRevision ?? 0 });
|
|
513
565
|
}
|
|
514
566
|
finally {
|
|
515
567
|
db.close();
|
|
@@ -583,6 +635,14 @@ function autoMergePollMs() {
|
|
|
583
635
|
function sleep(ms) {
|
|
584
636
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
585
637
|
}
|
|
638
|
+
function canonical(path) {
|
|
639
|
+
try {
|
|
640
|
+
return realpathSync.native(path);
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return resolve(path);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
586
646
|
function notifyIssue(enabled, message) {
|
|
587
647
|
if (enabled)
|
|
588
648
|
fireNotification("Foreman branch issue", message);
|