ai-foreman 1.4.0 → 1.6.0
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/dist/adapters/codex.js +1 -0
- package/dist/agentRun.d.ts +4 -2
- package/dist/agentRun.js +32 -9
- package/dist/branch/git.d.ts +3 -0
- package/dist/branch/git.js +31 -5
- package/dist/branch/github.d.ts +2 -2
- package/dist/branch/github.js +2 -2
- package/dist/branch/gitlab.d.ts +3 -2
- package/dist/branch/gitlab.js +10 -4
- package/dist/branch/planner.d.ts +3 -1
- package/dist/branch/planner.js +24 -2
- package/dist/branch/runner.d.ts +10 -1
- package/dist/branch/runner.js +41 -13
- package/dist/branch/types.d.ts +4 -0
- package/dist/buildRuns.d.ts +35 -0
- package/dist/buildRuns.js +149 -0
- package/dist/cli/runtimeAuthPrompt.d.ts +1 -1
- package/dist/cli/runtimeAuthPrompt.js +2 -2
- package/dist/cli/start.js +153 -16
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +154 -57
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +46 -4
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +90 -0
- package/dist/interviews.js +251 -0
- package/dist/log.d.ts +1 -1
- package/dist/roles.d.ts +11 -0
- package/dist/roles.js +8 -0
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +6 -25
- package/dist/runtimeReadiness.d.ts +14 -0
- package/dist/runtimeReadiness.js +122 -0
- package/dist/ticketPlanning.d.ts +83 -0
- package/dist/ticketPlanning.js +287 -0
- package/dist/tickets/commands.js +16 -0
- package/dist/tickets/delivery.d.ts +38 -0
- package/dist/tickets/delivery.js +152 -0
- package/dist/tickets/importer.js +4 -0
- package/dist/tickets/renderMarkdown.js +9 -0
- package/dist/tickets/setupConfig.d.ts +14 -0
- package/dist/tickets/setupConfig.js +59 -2
- package/dist/tickets/sourceFetch.d.ts +24 -0
- package/dist/tickets/sourceFetch.js +246 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +24 -0
- package/dist/tickets/ticketLoader.js +18 -0
- package/dist/tickets/ticketSchema.d.ts +93 -0
- package/dist/tickets/ticketSchema.js +17 -0
- package/dist/tickets/validate.js +12 -0
- package/package.json +33 -2
package/dist/adapters/codex.js
CHANGED
package/dist/agentRun.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface RoleBuilderOptions {
|
|
|
16
16
|
yes?: boolean;
|
|
17
17
|
allowSwitch?: boolean;
|
|
18
18
|
label: string;
|
|
19
|
-
log
|
|
19
|
+
log?: Log;
|
|
20
20
|
permissionConfig?: PermissionConfig;
|
|
21
21
|
extraSkills?: string[];
|
|
22
22
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
@@ -29,11 +29,12 @@ export interface RoleBuilder {
|
|
|
29
29
|
effort?: EffortLevel;
|
|
30
30
|
roleBundle: RoleBundle;
|
|
31
31
|
skills: string[];
|
|
32
|
+
log: Log;
|
|
32
33
|
}
|
|
33
34
|
export interface RoleInstructionRunOptions extends Omit<RoleBuilderOptions, "log"> {
|
|
34
35
|
instruction: string;
|
|
35
36
|
logPath?: string;
|
|
36
|
-
logEvent?: "rafi-plan" | "ticket-populate" | "preflight";
|
|
37
|
+
logEvent?: "rafi-plan" | "ticket-populate" | "preflight" | "uninstall-proposal";
|
|
37
38
|
}
|
|
38
39
|
export interface RoleInstructionRunResult {
|
|
39
40
|
turn: {
|
|
@@ -43,6 +44,7 @@ export interface RoleInstructionRunResult {
|
|
|
43
44
|
runtime: AgentRuntime;
|
|
44
45
|
model?: string;
|
|
45
46
|
effort?: EffortLevel;
|
|
47
|
+
sessionId?: string;
|
|
46
48
|
logPath: string;
|
|
47
49
|
roleBundle: RoleBundle;
|
|
48
50
|
skills: string[];
|
package/dist/agentRun.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loadConfig, DEFAULT_CONFIG } from "./config.js";
|
|
4
4
|
import { Log } from "./log.js";
|
|
@@ -10,6 +10,7 @@ import { printEvents } from "./cli/events.js";
|
|
|
10
10
|
import { loadRoleBundle } from "./roles.js";
|
|
11
11
|
import { ensureRuntimeReadyForCommand } from "./cli/runtimeAuthPrompt.js";
|
|
12
12
|
import { resolveAgentForProject } from "./cli/runtimeSelection.js";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
13
14
|
export const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
14
15
|
export function assertEffortLevel(effort) {
|
|
15
16
|
if (effort && !VALID_EFFORT.includes(effort)) {
|
|
@@ -84,12 +85,16 @@ export async function createRoleBuilder(opts) {
|
|
|
84
85
|
if (!existsSync(opts.projectDir)) {
|
|
85
86
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const saved = readRoleDefaults(opts.projectDir, opts.role);
|
|
89
|
+
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
90
|
+
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
91
|
+
assertEffortLevel(effectiveEffort);
|
|
92
|
+
let runtime = resolveAgentForProject(opts.projectDir, opts.agent ?? saved?.make);
|
|
89
93
|
const config = loadConfig(join(opts.projectDir, "foreman.yaml"));
|
|
94
|
+
const log = opts.log ?? new Log(makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-")));
|
|
90
95
|
const roleBundle = loadRoleBundle(opts.role, { projectDir: opts.projectDir });
|
|
91
|
-
const initialModel = opts.model ?? roleBundle.model ?? undefined;
|
|
92
|
-
const effort =
|
|
96
|
+
const initialModel = opts.model ?? (saved?.model !== "default" ? saved?.model : undefined) ?? roleBundle.model ?? undefined;
|
|
97
|
+
const effort = effectiveEffort ?? roleBundle.effort ?? undefined;
|
|
93
98
|
const ready = await ensureRuntimeReadyForCommand(opts.projectDir, runtime, {
|
|
94
99
|
label: opts.label,
|
|
95
100
|
yes: Boolean(opts.yes),
|
|
@@ -104,9 +109,9 @@ export async function createRoleBuilder(opts) {
|
|
|
104
109
|
cwd: opts.projectDir,
|
|
105
110
|
model,
|
|
106
111
|
resumeSessionId: opts.resumeSessionId,
|
|
107
|
-
permission: createPermissionHandler(policy,
|
|
112
|
+
permission: createPermissionHandler(policy, log),
|
|
108
113
|
effort,
|
|
109
|
-
fast: opts.fast,
|
|
114
|
+
fast: opts.fast ?? saved?.fast,
|
|
110
115
|
sandboxMode: opts.sandboxMode,
|
|
111
116
|
systemPromptAppend: roleBundle.system || undefined,
|
|
112
117
|
skills: skills.length > 0 ? skills : undefined,
|
|
@@ -114,7 +119,21 @@ export async function createRoleBuilder(opts) {
|
|
|
114
119
|
const builder = runtime === "codex"
|
|
115
120
|
? new CodexAdapter(adapterOpts)
|
|
116
121
|
: await ClaudeAdapter.create(adapterOpts);
|
|
117
|
-
return { builder, runtime, model, effort, roleBundle, skills };
|
|
122
|
+
return { builder, runtime, model, effort, roleBundle, skills, log };
|
|
123
|
+
}
|
|
124
|
+
function readRoleDefaults(projectDir, role) {
|
|
125
|
+
const path = join(projectDir, "rafi-config.yaml");
|
|
126
|
+
if (!existsSync(path))
|
|
127
|
+
return undefined;
|
|
128
|
+
try {
|
|
129
|
+
const config = parseYaml(readFileSync(path, "utf8"));
|
|
130
|
+
return config?.agent_defaults?.version === 1
|
|
131
|
+
? config.agent_defaults.roles[role]
|
|
132
|
+
: undefined;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
export async function runRoleInstruction(opts) {
|
|
120
139
|
const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
|
|
@@ -133,6 +152,10 @@ export async function runRoleInstruction(opts) {
|
|
|
133
152
|
if (opts.logEvent) {
|
|
134
153
|
log.write(opts.logEvent, {
|
|
135
154
|
role: opts.role,
|
|
155
|
+
runtime,
|
|
156
|
+
model,
|
|
157
|
+
effort,
|
|
158
|
+
sessionId: builder.sessionId(),
|
|
136
159
|
statusKind: turn.status.kind,
|
|
137
160
|
summary: turn.status.summary,
|
|
138
161
|
reason: turn.status.reason,
|
|
@@ -140,7 +163,7 @@ export async function runRoleInstruction(opts) {
|
|
|
140
163
|
isError: turn.result.isError,
|
|
141
164
|
});
|
|
142
165
|
}
|
|
143
|
-
return { turn, runtime, model, effort, logPath, roleBundle, skills };
|
|
166
|
+
return { turn, runtime, model, effort, sessionId: builder.sessionId(), logPath, roleBundle, skills };
|
|
144
167
|
}
|
|
145
168
|
catch (err) {
|
|
146
169
|
await builder.close().catch(() => { });
|
package/dist/branch/git.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare function generatedTrackerDirtyPaths(paths: {
|
|
|
14
14
|
export declare function ensureCleanBaseWorktree(cwd: string, opts?: BaseWorktreeCleanOptions): void;
|
|
15
15
|
export declare function ensureForemanExcluded(cwd: string): void;
|
|
16
16
|
export declare function createTicketWorktree(projectDir: string, runId: string, branch: string, base: string): string;
|
|
17
|
+
export declare function findWorktreeForBranch(projectDir: string, branch: string): string | undefined;
|
|
17
18
|
export declare function removeTicketWorktree(projectDir: string, worktreePath: string): void;
|
|
18
19
|
export declare function currentWorktreeBranch(worktreePath: string): string;
|
|
19
20
|
export declare function hasTrackerChanges(worktreePath: string, paths?: {
|
|
@@ -24,5 +25,7 @@ export declare function hasWorktreeChanges(worktreePath: string): boolean;
|
|
|
24
25
|
export declare function commitAll(worktreePath: string, message: string): string | undefined;
|
|
25
26
|
export declare function headCommitIfAhead(worktreePath: string, base: string): string | undefined;
|
|
26
27
|
export declare function pushBranch(worktreePath: string, branch: string): void;
|
|
28
|
+
export declare function mergeBranchToLocalBase(projectDir: string, branch: string, baseBranch: string, message: string, method?: "squash" | "merge" | "rebase"): string;
|
|
29
|
+
/** Compatibility export for callers compiled against the older helper. */
|
|
27
30
|
export declare function squashMergeBranchToLocalBase(projectDir: string, branch: string, baseBranch: string, message: string): string;
|
|
28
31
|
export declare function deleteLocalBranch(projectDir: string, branch: string): void;
|
package/dist/branch/git.js
CHANGED
|
@@ -51,6 +51,19 @@ export function createTicketWorktree(projectDir, runId, branch, base) {
|
|
|
51
51
|
runGit(projectDir, ["worktree", "add", "-b", branch, worktreePath, base]);
|
|
52
52
|
return worktreePath;
|
|
53
53
|
}
|
|
54
|
+
export function findWorktreeForBranch(projectDir, branch) {
|
|
55
|
+
const output = runGit(projectDir, ["worktree", "list", "--porcelain"]).stdout;
|
|
56
|
+
let worktree;
|
|
57
|
+
for (const line of output.split(/\r?\n/)) {
|
|
58
|
+
if (line.startsWith("worktree "))
|
|
59
|
+
worktree = line.slice("worktree ".length);
|
|
60
|
+
if (line === `branch refs/heads/${branch}` && worktree)
|
|
61
|
+
return worktree;
|
|
62
|
+
if (!line.trim())
|
|
63
|
+
worktree = undefined;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
54
67
|
export function removeTicketWorktree(projectDir, worktreePath) {
|
|
55
68
|
try {
|
|
56
69
|
runGit(projectDir, ["worktree", "remove", "--force", worktreePath]);
|
|
@@ -101,15 +114,28 @@ export function headCommitIfAhead(worktreePath, base) {
|
|
|
101
114
|
export function pushBranch(worktreePath, branch) {
|
|
102
115
|
runGit(worktreePath, ["push", "-u", "origin", branch]);
|
|
103
116
|
}
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message, method = "squash") {
|
|
118
|
+
if (method === "rebase")
|
|
119
|
+
runGit(projectDir, ["rebase", baseBranch, branch]);
|
|
120
|
+
if (currentGitRef(projectDir) !== baseBranch) {
|
|
107
121
|
runGit(projectDir, ["checkout", baseBranch]);
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
if (method === "squash") {
|
|
124
|
+
runGit(projectDir, ["merge", "--squash", branch]);
|
|
125
|
+
runGit(projectDir, ["commit", "-m", message]);
|
|
126
|
+
}
|
|
127
|
+
else if (method === "merge") {
|
|
128
|
+
runGit(projectDir, ["merge", "--no-ff", branch, "-m", message]);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
runGit(projectDir, ["merge", "--ff-only", branch]);
|
|
132
|
+
}
|
|
111
133
|
return runGit(projectDir, ["rev-parse", "--short", "HEAD"]).stdout;
|
|
112
134
|
}
|
|
135
|
+
/** Compatibility export for callers compiled against the older helper. */
|
|
136
|
+
export function squashMergeBranchToLocalBase(projectDir, branch, baseBranch, message) {
|
|
137
|
+
return mergeBranchToLocalBase(projectDir, branch, baseBranch, message, "squash");
|
|
138
|
+
}
|
|
113
139
|
export function deleteLocalBranch(projectDir, branch) {
|
|
114
140
|
try {
|
|
115
141
|
runGit(projectDir, ["branch", "-D", branch]);
|
package/dist/branch/github.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult, ReviewMergeStatus } from "./types.js";
|
|
1
|
+
import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult, ReviewMergeStatus, MergeMethod } from "./types.js";
|
|
2
2
|
export declare function inspectGitHubRemote(cwd: string): GitHubRemoteResult;
|
|
3
3
|
export declare function originLooksLikeGitHub(cwd: string): boolean;
|
|
4
4
|
export declare function checkGitHubReadiness(cwd: string): GitHubReadinessResult;
|
|
@@ -17,7 +17,7 @@ export interface CreatePrOptions {
|
|
|
17
17
|
commit?: string;
|
|
18
18
|
}
|
|
19
19
|
export declare function createOrReusePr(cwd: string, opts: CreatePrOptions): PrResult;
|
|
20
|
-
export declare function enableGitHubAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
|
|
20
|
+
export declare function enableGitHubAutoMerge(cwd: string, branch: string, cleanup: boolean, method?: MergeMethod): PrResult;
|
|
21
21
|
export declare function checkGitHubPrMerged(cwd: string, branch: string): ReviewMergeStatus;
|
|
22
22
|
export declare function formatGitHubFailure(failure: GitHubFailure): string;
|
|
23
23
|
export {};
|
package/dist/branch/github.js
CHANGED
|
@@ -157,8 +157,8 @@ export function createOrReusePr(cwd, opts) {
|
|
|
157
157
|
return { status: "created", url: created.stdout };
|
|
158
158
|
return prFailure(classifyPrFailure(created, opts.node.branch, "Failed to create GitHub PR.", remoteForRepair));
|
|
159
159
|
}
|
|
160
|
-
export function enableGitHubAutoMerge(cwd, branch, cleanup) {
|
|
161
|
-
const args = ["pr", "merge", branch, "--auto",
|
|
160
|
+
export function enableGitHubAutoMerge(cwd, branch, cleanup, method = "squash") {
|
|
161
|
+
const args = ["pr", "merge", branch, "--auto", `--${method}`];
|
|
162
162
|
if (cleanup)
|
|
163
163
|
args.push("--delete-branch");
|
|
164
164
|
const result = runCommand(cwd, "gh", args, 30_000);
|
package/dist/branch/gitlab.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BranchPlanNode, GitHubOperationResult, PrResult, ReviewMergeStatus } from "./types.js";
|
|
1
|
+
import type { BranchPlanNode, GitHubOperationResult, PrResult, ReviewMergeStatus, MergeMethod } from "./types.js";
|
|
2
2
|
export declare function preflightGlab(cwd: string): GitHubOperationResult;
|
|
3
3
|
export declare function pushBranchForMr(cwd: string, branch: string): GitHubOperationResult;
|
|
4
4
|
export interface CreateMrOptions {
|
|
@@ -9,7 +9,8 @@ export interface CreateMrOptions {
|
|
|
9
9
|
commit?: string;
|
|
10
10
|
autoMerge?: boolean;
|
|
11
11
|
cleanup?: boolean;
|
|
12
|
+
mergeMethod?: MergeMethod;
|
|
12
13
|
}
|
|
13
14
|
export declare function createOrReuseMr(cwd: string, opts: CreateMrOptions): PrResult;
|
|
14
|
-
export declare function enableGitLabAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
|
|
15
|
+
export declare function enableGitLabAutoMerge(cwd: string, branch: string, cleanup: boolean, method?: MergeMethod): PrResult;
|
|
15
16
|
export declare function checkGitLabMrMerged(cwd: string, branch: string): ReviewMergeStatus;
|
package/dist/branch/gitlab.js
CHANGED
|
@@ -77,7 +77,9 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
77
77
|
if (!opts.ready)
|
|
78
78
|
args.push("--draft");
|
|
79
79
|
if (opts.autoMerge)
|
|
80
|
-
args.push("--auto-merge"
|
|
80
|
+
args.push("--auto-merge");
|
|
81
|
+
if (opts.mergeMethod === "squash")
|
|
82
|
+
args.push("--squash-before-merge");
|
|
81
83
|
if (opts.cleanup)
|
|
82
84
|
args.push("--remove-source-branch");
|
|
83
85
|
const created = runCommand(cwd, "glab", args);
|
|
@@ -88,15 +90,19 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
88
90
|
"glab auth status",
|
|
89
91
|
]));
|
|
90
92
|
}
|
|
91
|
-
export function enableGitLabAutoMerge(cwd, branch, cleanup) {
|
|
92
|
-
const args = ["mr", "merge", branch, "--auto-merge", "--
|
|
93
|
+
export function enableGitLabAutoMerge(cwd, branch, cleanup, method = "squash") {
|
|
94
|
+
const args = ["mr", "merge", branch, "--auto-merge", "--yes"];
|
|
95
|
+
if (method === "squash")
|
|
96
|
+
args.push("--squash");
|
|
97
|
+
if (method === "rebase")
|
|
98
|
+
args.push("--rebase");
|
|
93
99
|
if (cleanup)
|
|
94
100
|
args.push("--remove-source-branch");
|
|
95
101
|
const result = runCommand(cwd, "glab", args, 30_000);
|
|
96
102
|
if (result.ok)
|
|
97
103
|
return { status: "auto_merge_enabled", url: result.stdout };
|
|
98
104
|
return prFailure(classifyCommandFailure(result, "mr_create_failed", `Failed to enable GitLab MR auto-merge. Branch: ${branch}`, [
|
|
99
|
-
`glab mr merge ${branch} --auto-merge --squash --yes`,
|
|
105
|
+
`glab mr merge ${branch} --auto-merge${method === "squash" ? " --squash" : method === "rebase" ? " --rebase" : ""} --yes`,
|
|
100
106
|
"glab auth status",
|
|
101
107
|
]));
|
|
102
108
|
}
|
package/dist/branch/planner.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export interface BuildBranchPlanOptions {
|
|
|
13
13
|
maxBranchDepth: number;
|
|
14
14
|
auditDependencies?: AuditDependency[];
|
|
15
15
|
rootBaseBranches?: boolean;
|
|
16
|
+
ticketIds?: string[];
|
|
16
17
|
}
|
|
17
18
|
export declare function buildBranchPlan(tickets: TicketDef[], states: Map<string, TicketState>, opts: BuildBranchPlanOptions): BranchPlan;
|
|
18
|
-
export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number): TicketDef[];
|
|
19
|
+
export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number, ticketIds?: string[]): TicketDef[];
|
|
20
|
+
export declare function applySharedDeliveryBranch(plan: BranchPlan, unitId: string, allRemainingTicketIds: string[], branchPrefix?: string): BranchPlan;
|
|
19
21
|
export declare function parseAuditDependencies(text: string): AuditDependency[];
|
|
20
22
|
export declare function buildBranchAuditInstruction(tickets: TicketDef[]): string;
|
package/dist/branch/planner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function buildBranchPlan(tickets, states, opts) {
|
|
2
|
-
const selected = selectTicketsForBranchRun(tickets, states, opts.steps);
|
|
2
|
+
const selected = selectTicketsForBranchRun(tickets, states, opts.steps, opts.ticketIds);
|
|
3
3
|
const selectedIds = new Set(selected.map((ticket) => ticket.id));
|
|
4
4
|
const issues = [];
|
|
5
5
|
const dependencyMap = new Map();
|
|
@@ -75,9 +75,12 @@ export function buildBranchPlan(tickets, states, opts) {
|
|
|
75
75
|
}
|
|
76
76
|
return { baseRef: opts.baseRef, nodes: selected.map((ticket) => nodeById.get(ticket.id)), issues };
|
|
77
77
|
}
|
|
78
|
-
export function selectTicketsForBranchRun(tickets, states, steps) {
|
|
78
|
+
export function selectTicketsForBranchRun(tickets, states, steps, ticketIds) {
|
|
79
|
+
const allowed = ticketIds ? new Set(ticketIds) : undefined;
|
|
79
80
|
const remaining = tickets
|
|
80
81
|
.filter((ticket) => {
|
|
82
|
+
if (allowed && !allowed.has(ticket.id))
|
|
83
|
+
return false;
|
|
81
84
|
const status = states.get(ticket.id)?.status ?? "planned";
|
|
82
85
|
return status !== "done" && status !== "canceled";
|
|
83
86
|
})
|
|
@@ -96,6 +99,25 @@ export function selectTicketsForBranchRun(tickets, states, steps) {
|
|
|
96
99
|
}
|
|
97
100
|
return selected;
|
|
98
101
|
}
|
|
102
|
+
export function applySharedDeliveryBranch(plan, unitId, allRemainingTicketIds, branchPrefix = "rafi") {
|
|
103
|
+
if (plan.nodes.length === 0)
|
|
104
|
+
return plan;
|
|
105
|
+
const slug = unitId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
106
|
+
const branch = `${branchPrefix.replace(/^\/+|\/+$/g, "") || "rafi"}/${slug}`;
|
|
107
|
+
const finalSelected = plan.nodes.at(-1)?.ticket.id;
|
|
108
|
+
const completesUnit = plan.nodes.length === allRemainingTicketIds.length
|
|
109
|
+
&& allRemainingTicketIds.every((id) => plan.nodes.some((node) => node.ticket.id === id));
|
|
110
|
+
return {
|
|
111
|
+
...plan,
|
|
112
|
+
nodes: plan.nodes.map((node) => ({
|
|
113
|
+
...node,
|
|
114
|
+
branch,
|
|
115
|
+
baseBranch: plan.baseRef,
|
|
116
|
+
deliveryUnitId: unitId,
|
|
117
|
+
deliveryUnitFinal: completesUnit && node.ticket.id === finalSelected,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
99
121
|
export function parseAuditDependencies(text) {
|
|
100
122
|
const deps = [];
|
|
101
123
|
for (const line of text.split(/\r?\n/)) {
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
|
|
2
2
|
import type { Log } from "../log.js";
|
|
3
|
-
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, ReviewProvider } from "./types.js";
|
|
3
|
+
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, MergeMethod, ReviewProvider } from "./types.js";
|
|
4
|
+
export interface DeliveryUnitSession {
|
|
5
|
+
unitId: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
worktreePath: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
ticket: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function readDeliveryUnitSession(projectDir: string, unitId: string): DeliveryUnitSession | undefined;
|
|
4
12
|
export interface BranchRunnerOptions {
|
|
5
13
|
projectDir: string;
|
|
6
14
|
runId: string;
|
|
@@ -20,6 +28,7 @@ export interface BranchRunnerOptions {
|
|
|
20
28
|
cleanupBranches?: boolean;
|
|
21
29
|
autoMergeWait?: boolean;
|
|
22
30
|
autoMergeTimeoutMinutes?: number | null;
|
|
31
|
+
mergeMethod?: MergeMethod;
|
|
23
32
|
allowedBaseDirtyPaths?: string[];
|
|
24
33
|
trackerPaths?: {
|
|
25
34
|
progressDoc: string;
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { Foreman, MARKER_SPEC } from "../foreman.js";
|
|
2
2
|
import { fireNotification } from "../notify.js";
|
|
3
3
|
import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
4
|
-
import { commitAll, createTicketWorktree, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree,
|
|
4
|
+
import { commitAll, createTicketWorktree, findWorktreeForBranch, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, mergeBranchToLocalBase, } from "./git.js";
|
|
5
5
|
import { checkGitHubPrMerged, createOrReusePr, enableGitHubAutoMerge, pushBranchForPr } from "./github.js";
|
|
6
6
|
import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranchForMr } from "./gitlab.js";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
10
|
+
const path = deliverySessionPath(projectDir, unitId);
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function deliverySessionPath(projectDir, unitId) {
|
|
21
|
+
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
22
|
+
}
|
|
7
23
|
export async function runBranchPlan(opts) {
|
|
8
24
|
ensureCleanBaseWorktree(opts.projectDir, { allowedDirtyPaths: opts.allowedBaseDirtyPaths });
|
|
9
25
|
ensureForemanExcluded(opts.projectDir);
|
|
@@ -29,6 +45,9 @@ export async function runBranchPlan(opts) {
|
|
|
29
45
|
if (opts.plan.issues.some((issue) => issue.blocking))
|
|
30
46
|
return summaries;
|
|
31
47
|
for (const node of orderNodes(opts.plan.nodes)) {
|
|
48
|
+
const sharedUnit = Boolean(node.deliveryUnitId);
|
|
49
|
+
const completesSharedUnit = !sharedUnit || Boolean(node.deliveryUnitFinal);
|
|
50
|
+
const createsReviewForNode = createsReview && completesSharedUnit;
|
|
32
51
|
const missingDependency = node.dependencies.find((dep) => !successfulBranches.has(dep));
|
|
33
52
|
if (missingDependency) {
|
|
34
53
|
const detail = `dependency ${missingDependency} did not complete`;
|
|
@@ -42,7 +61,7 @@ export async function runBranchPlan(opts) {
|
|
|
42
61
|
notifyIssue(opts.notificationsEnabled, detail);
|
|
43
62
|
continue;
|
|
44
63
|
}
|
|
45
|
-
if (createsReview) {
|
|
64
|
+
if (createsReview && !sharedUnit) {
|
|
46
65
|
const missingPush = node.dependencies.find((dep) => {
|
|
47
66
|
const depNode = opts.plan.nodes.find((candidate) => candidate.ticket.id === dep);
|
|
48
67
|
return depNode && !pushedBranches.has(depNode.branch);
|
|
@@ -60,7 +79,7 @@ export async function runBranchPlan(opts) {
|
|
|
60
79
|
continue;
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
|
-
if (completionMode === "auto-merge" && node.dependencies.length > 0) {
|
|
82
|
+
if (completionMode === "auto-merge" && node.dependencies.length > 0 && !sharedUnit) {
|
|
64
83
|
const mergeReadiness = await waitForAutoMergeDependencies(opts, node, reviewProvider);
|
|
65
84
|
if (!mergeReadiness.ok) {
|
|
66
85
|
summaries.push(summaryFor(node, "skipped", mergeReadiness.message, "skipped"));
|
|
@@ -85,6 +104,7 @@ export async function runBranchPlan(opts) {
|
|
|
85
104
|
});
|
|
86
105
|
const resumeSession = opts.resumeSessions?.get(node.ticket.id);
|
|
87
106
|
const worktreePath = resumeSession?.worktreePath
|
|
107
|
+
?? (sharedUnit ? findWorktreeForBranch(opts.projectDir, node.branch) : undefined)
|
|
88
108
|
?? createTicketWorktree(opts.projectDir, opts.runId, node.branch, node.baseBranch);
|
|
89
109
|
node.worktreePath = worktreePath;
|
|
90
110
|
let builder;
|
|
@@ -127,6 +147,11 @@ export async function runBranchPlan(opts) {
|
|
|
127
147
|
prReady: opts.prReady,
|
|
128
148
|
keepWorktrees: opts.keepWorktrees,
|
|
129
149
|
});
|
|
150
|
+
if (node.deliveryUnitId) {
|
|
151
|
+
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
152
|
+
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
153
|
+
writeFileSync(path, `${JSON.stringify({ unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, ticket: node.ticket.id }, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
130
155
|
}
|
|
131
156
|
opts.log.write("step", {
|
|
132
157
|
index: summaries.length + 1,
|
|
@@ -180,12 +205,12 @@ export async function runBranchPlan(opts) {
|
|
|
180
205
|
if (hasWorktreeChanges(worktreePath)) {
|
|
181
206
|
commit = commitAll(worktreePath, `${node.ticket.id}: ${node.ticket.title}`);
|
|
182
207
|
}
|
|
183
|
-
if (!commit && (
|
|
208
|
+
if (!commit && (createsReviewForNode || (completionMode === "direct-merge" && completesSharedUnit))) {
|
|
184
209
|
commit = headCommitIfAhead(worktreePath, node.baseBranch);
|
|
185
210
|
}
|
|
186
211
|
const summary = summaryFor(node, "done", commit ? undefined : "no_changes");
|
|
187
212
|
summary.commit = commit;
|
|
188
|
-
if (commit &&
|
|
213
|
+
if (commit && createsReviewForNode) {
|
|
189
214
|
const push = reviewProvider === "gitlab"
|
|
190
215
|
? pushBranchForMr(worktreePath, node.branch)
|
|
191
216
|
: pushBranchForPr(worktreePath, node.branch);
|
|
@@ -226,8 +251,9 @@ export async function runBranchPlan(opts) {
|
|
|
226
251
|
runId: opts.runId,
|
|
227
252
|
qaEvidence: qaSummary,
|
|
228
253
|
commit,
|
|
229
|
-
autoMerge:
|
|
254
|
+
autoMerge: false,
|
|
230
255
|
cleanup: opts.cleanupBranches ?? true,
|
|
256
|
+
mergeMethod: opts.mergeMethod ?? "squash",
|
|
231
257
|
})
|
|
232
258
|
: createOrReusePr(opts.projectDir, {
|
|
233
259
|
node,
|
|
@@ -248,7 +274,7 @@ export async function runBranchPlan(opts) {
|
|
|
248
274
|
continue;
|
|
249
275
|
}
|
|
250
276
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
251
|
-
const autoMerge = enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
|
|
277
|
+
const autoMerge = enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
252
278
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
253
279
|
if (autoMerge.status === "failed") {
|
|
254
280
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -258,8 +284,8 @@ export async function runBranchPlan(opts) {
|
|
|
258
284
|
}
|
|
259
285
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
260
286
|
}
|
|
261
|
-
else if (completionMode === "auto-merge" && reviewProvider === "gitlab"
|
|
262
|
-
const autoMerge = enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
|
|
287
|
+
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
288
|
+
const autoMerge = enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
263
289
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
264
290
|
if (autoMerge.status === "failed") {
|
|
265
291
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -270,14 +296,14 @@ export async function runBranchPlan(opts) {
|
|
|
270
296
|
opts.log.write("mr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
271
297
|
}
|
|
272
298
|
}
|
|
273
|
-
else if (
|
|
299
|
+
else if (createsReviewForNode) {
|
|
274
300
|
summary.pushStatus = "skipped";
|
|
275
301
|
summary.pr = { status: "skipped", error: commit ? undefined : "no_changes" };
|
|
276
302
|
}
|
|
277
|
-
else if (commit && completionMode === "direct-merge") {
|
|
303
|
+
else if (commit && completionMode === "direct-merge" && completesSharedUnit) {
|
|
278
304
|
if (!opts.keepWorktrees)
|
|
279
305
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
280
|
-
const mergeCommit =
|
|
306
|
+
const mergeCommit = mergeBranchToLocalBase(opts.projectDir, node.branch, node.baseBranch, `${node.ticket.id}: ${node.ticket.title}`, opts.mergeMethod ?? "squash");
|
|
281
307
|
summary.pr = { status: "merged", url: mergeCommit };
|
|
282
308
|
opts.log.write("branch-direct-merge", {
|
|
283
309
|
ticket: node.ticket.id,
|
|
@@ -304,7 +330,9 @@ export async function runBranchPlan(opts) {
|
|
|
304
330
|
status: "done",
|
|
305
331
|
detail: summary.detail,
|
|
306
332
|
});
|
|
307
|
-
if (
|
|
333
|
+
if (node.deliveryUnitId && node.deliveryUnitFinal)
|
|
334
|
+
rmSync(deliverySessionPath(opts.projectDir, node.deliveryUnitId), { force: true });
|
|
335
|
+
if (!opts.keepWorktrees && completionMode !== "direct-merge" && completesSharedUnit)
|
|
308
336
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
309
337
|
}
|
|
310
338
|
catch (err) {
|
package/dist/branch/types.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface BranchPlanNode {
|
|
|
7
7
|
dependencies: string[];
|
|
8
8
|
depth: number;
|
|
9
9
|
worktreePath?: string;
|
|
10
|
+
/** Shared delivery units intentionally reuse one isolated branch/worktree. */
|
|
11
|
+
deliveryUnitId?: string;
|
|
12
|
+
deliveryUnitFinal?: boolean;
|
|
10
13
|
}
|
|
11
14
|
export interface BranchIssue {
|
|
12
15
|
ticket?: string;
|
|
@@ -56,6 +59,7 @@ export interface BranchPlan {
|
|
|
56
59
|
}
|
|
57
60
|
export type CompletionMode = "none" | "pr" | "auto-merge" | "direct-merge";
|
|
58
61
|
export type ReviewProvider = "github" | "gitlab";
|
|
62
|
+
export type MergeMethod = "squash" | "merge" | "rebase";
|
|
59
63
|
export interface PrResult {
|
|
60
64
|
status: "created" | "existing" | "failed" | "skipped" | "auto_merge_enabled" | "merged";
|
|
61
65
|
url?: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BuildRunRecordV1, ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
+
export declare const BUILD_RUN_VERSION = 1;
|
|
3
|
+
export declare const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
4
|
+
export declare const BUILD_HEARTBEAT_MS = 10000;
|
|
5
|
+
export declare const BUILD_LEASE_STALE_MS = 45000;
|
|
6
|
+
export interface CreateBuildRunInput {
|
|
7
|
+
tickets: string[];
|
|
8
|
+
deliveryUnit?: string;
|
|
9
|
+
branchMode?: BuildRunRecordV1["branchMode"];
|
|
10
|
+
repositoryRoot: string;
|
|
11
|
+
worktree?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
baseHead?: string;
|
|
14
|
+
startHead?: string;
|
|
15
|
+
builder?: ResolvedAgentSettings;
|
|
16
|
+
qa?: ResolvedAgentSettings;
|
|
17
|
+
now?: Date;
|
|
18
|
+
}
|
|
19
|
+
export declare function createBuildRun(input: CreateBuildRunInput): BuildRunRecordV1;
|
|
20
|
+
export declare function saveBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
21
|
+
export declare function checkpointBuildRun(projectDir: string, run: BuildRunRecordV1, checkpoint: string, patch?: Partial<BuildRunRecordV1>): BuildRunRecordV1;
|
|
22
|
+
export declare function persistBuildSession(projectDir: string, run: BuildRunRecordV1, role: "builder" | "qa", sessionId: string): BuildRunRecordV1;
|
|
23
|
+
export declare function recordBuildReceipt(projectDir: string, run: BuildRunRecordV1, operationId: string, detail?: {
|
|
24
|
+
externalId?: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}): BuildRunRecordV1;
|
|
27
|
+
export declare function heartbeatBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
28
|
+
export declare function releaseBuildLease(projectDir: string, run: BuildRunRecordV1, status?: BuildRunRecordV1["status"]): BuildRunRecordV1;
|
|
29
|
+
export declare function completeBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
30
|
+
export declare function readBuildRuns(projectDir: string): BuildRunRecordV1[];
|
|
31
|
+
export declare function recoverableBuildRuns(projectDir: string, now?: Date): Array<BuildRunRecordV1 & {
|
|
32
|
+
active: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function isLeaseActive(run: BuildRunRecordV1, now?: Date): boolean;
|
|
35
|
+
export declare function buildRecoveryPreview(run: BuildRunRecordV1): string[];
|