ai-foreman 1.7.1 → 1.7.3
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/agentRun.d.ts +2 -0
- package/dist/agentRun.js +31 -4
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +5 -2
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +14 -1
- package/dist/cli/start.js +15 -5
- package/dist/cli/tickets.js +46 -2
- package/dist/tickets/queue.d.ts +1 -1
- package/dist/tickets/queue.js +17 -6
- package/dist/tickets/setupConfig.d.ts +1 -1
- package/dist/tickets/setupConfig.js +1 -1
- package/package.json +3 -3
package/dist/agentRun.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type StepStatus } from "./foreman.js";
|
|
|
4
4
|
import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } from "./adapters/types.js";
|
|
5
5
|
import { type RoleBundle } from "./roles.js";
|
|
6
6
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
7
|
+
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
7
8
|
export type { EffortLevel } from "./adapters/types.js";
|
|
8
9
|
export declare const VALID_EFFORT: readonly EffortLevel[];
|
|
9
10
|
export interface RoleBuilderOptions {
|
|
@@ -54,4 +55,5 @@ export declare function makeLogPath(projectDir: string, label: string): string;
|
|
|
54
55
|
export declare function mergeSkills(roleSkills: readonly string[], extraSkills?: readonly string[]): string[];
|
|
55
56
|
export declare function readOnlyPermissionConfig(): PermissionConfig;
|
|
56
57
|
export declare function createRoleBuilder(opts: RoleBuilderOptions): Promise<RoleBuilder>;
|
|
58
|
+
export declare function readRoleDefaultsForExecution(projectDir: string, role: string): AgentRoleDefaultsV1 | undefined;
|
|
57
59
|
export declare function runRoleInstruction(opts: RoleInstructionRunOptions): Promise<RoleInstructionRunResult>;
|
package/dist/agentRun.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } 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";
|
|
@@ -86,7 +86,7 @@ export async function createRoleBuilder(opts) {
|
|
|
86
86
|
if (!existsSync(opts.projectDir)) {
|
|
87
87
|
throw new Error(`project directory not found: ${opts.projectDir}`);
|
|
88
88
|
}
|
|
89
|
-
const saved =
|
|
89
|
+
const saved = readRoleDefaultsForExecution(opts.projectDir, opts.role);
|
|
90
90
|
const configuredEffort = saved?.reasoning && saved.reasoning !== "default" ? saved.reasoning : undefined;
|
|
91
91
|
const effectiveEffort = opts.effort ?? (VALID_EFFORT.includes(configuredEffort) ? configuredEffort : undefined);
|
|
92
92
|
assertEffortLevel(effectiveEffort);
|
|
@@ -157,20 +157,47 @@ function phaseForRole(role) {
|
|
|
157
157
|
return "uninstaller";
|
|
158
158
|
return "builder";
|
|
159
159
|
}
|
|
160
|
-
function
|
|
160
|
+
export function readRoleDefaultsForExecution(projectDir, role) {
|
|
161
161
|
const path = join(projectDir, "rafi-config.yaml");
|
|
162
162
|
if (!existsSync(path))
|
|
163
163
|
return undefined;
|
|
164
164
|
try {
|
|
165
165
|
const config = parseYaml(readFileSync(path, "utf8"));
|
|
166
|
-
|
|
166
|
+
const saved = config?.agent_defaults?.version === 1
|
|
167
167
|
? config.agent_defaults.roles[role]
|
|
168
168
|
: undefined;
|
|
169
|
+
if (!saved)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (isFullyInitializedTracker(projectDir))
|
|
172
|
+
return saved;
|
|
173
|
+
const { make: _pendingMake, ...active } = saved;
|
|
174
|
+
return active;
|
|
169
175
|
}
|
|
170
176
|
catch {
|
|
171
177
|
return undefined;
|
|
172
178
|
}
|
|
173
179
|
}
|
|
180
|
+
function isFullyInitializedTracker(projectDir) {
|
|
181
|
+
try {
|
|
182
|
+
const trackerPath = join(projectDir, ".tickets", "config.yaml");
|
|
183
|
+
const ticketsPath = join(projectDir, ".tickets", "tickets.yaml");
|
|
184
|
+
const statePath = join(projectDir, ".tickets", "ticket-state.sqlite");
|
|
185
|
+
if (!existsSync(trackerPath) || !existsSync(ticketsPath) || !existsSync(statePath))
|
|
186
|
+
return false;
|
|
187
|
+
const tracker = parseYaml(readFileSync(trackerPath, "utf8"));
|
|
188
|
+
if (!tracker || typeof tracker !== "object" || Array.isArray(tracker))
|
|
189
|
+
return false;
|
|
190
|
+
const tickets = parseYaml(readFileSync(ticketsPath, "utf8"));
|
|
191
|
+
if (!tickets || !Array.isArray(tickets.tickets))
|
|
192
|
+
return false;
|
|
193
|
+
if (!statSync(statePath).isFile())
|
|
194
|
+
return false;
|
|
195
|
+
return readFileSync(statePath).subarray(0, 16).toString("utf8") === "SQLite format 3\u0000";
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
174
201
|
export async function runRoleInstruction(opts) {
|
|
175
202
|
const logPath = opts.logPath ?? makeLogPath(opts.projectDir, opts.label.replace(/\s+/g, "-"));
|
|
176
203
|
const log = new Log(logPath);
|
package/dist/branch/git.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function generatedTrackerDirtyPaths(paths: {
|
|
|
11
11
|
progressDoc: string;
|
|
12
12
|
archiveDoc?: string;
|
|
13
13
|
}): string[];
|
|
14
|
+
export declare function collectBaseWorktreeDirtyPaths(cwd: string, opts?: BaseWorktreeCleanOptions): string[];
|
|
14
15
|
export declare function ensureCleanBaseWorktree(cwd: string, opts?: BaseWorktreeCleanOptions): void;
|
|
15
16
|
export declare function ensureForemanExcluded(cwd: string): void;
|
|
16
17
|
export declare function createTicketWorktree(projectDir: string, runId: string, branch: string, base: string): string;
|
package/dist/branch/git.js
CHANGED
|
@@ -25,13 +25,16 @@ export function generatedTrackerDirtyPaths(paths) {
|
|
|
25
25
|
}
|
|
26
26
|
return out;
|
|
27
27
|
}
|
|
28
|
-
export function
|
|
28
|
+
export function collectBaseWorktreeDirtyPaths(cwd, opts = {}) {
|
|
29
29
|
const allowedDirtyPaths = new Set([".tickets/.gitignore", ...(opts.allowedDirtyPaths ?? []).map(normalizeRepoPath)]);
|
|
30
30
|
const status = runGit(cwd, ["status", "--porcelain", "--untracked-files=all"]).stdout;
|
|
31
|
-
|
|
31
|
+
return status.split("\n").filter(Boolean).filter((line) => {
|
|
32
32
|
const path = normalizeRepoPath(line.slice(3));
|
|
33
33
|
return !path.startsWith(".foreman/") && !allowedDirtyPaths.has(path);
|
|
34
34
|
});
|
|
35
|
+
}
|
|
36
|
+
export function ensureCleanBaseWorktree(cwd, opts = {}) {
|
|
37
|
+
const dirty = collectBaseWorktreeDirtyPaths(cwd, opts);
|
|
35
38
|
if (dirty.length > 0) {
|
|
36
39
|
throw new Error(`base worktree has uncommitted changes:\n${dirty.join("\n")}`);
|
|
37
40
|
}
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface DeliveryUnitSession {
|
|
|
10
10
|
sessionId: string;
|
|
11
11
|
ticket: string;
|
|
12
12
|
}
|
|
13
|
+
export type BaseWorktreePolicy = "enforce" | "warn" | "skip";
|
|
13
14
|
export declare function readDeliveryUnitSession(projectDir: string, unitId: string): DeliveryUnitSession | undefined;
|
|
14
15
|
export interface BranchRunnerOptions {
|
|
15
16
|
projectDir: string;
|
|
@@ -32,6 +33,7 @@ export interface BranchRunnerOptions {
|
|
|
32
33
|
autoMergeTimeoutMinutes?: number | null;
|
|
33
34
|
mergeMethod?: MergeMethod;
|
|
34
35
|
allowedBaseDirtyPaths?: string[];
|
|
36
|
+
baseWorktreePolicy?: BaseWorktreePolicy;
|
|
35
37
|
trackerPaths?: {
|
|
36
38
|
progressDoc: string;
|
|
37
39
|
archiveDoc: string;
|
package/dist/branch/runner.js
CHANGED
|
@@ -24,7 +24,20 @@ function deliverySessionPath(projectDir, unitId) {
|
|
|
24
24
|
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
25
25
|
}
|
|
26
26
|
export async function runBranchPlan(opts) {
|
|
27
|
-
|
|
27
|
+
const baseWorktreePolicy = opts.baseWorktreePolicy ?? "enforce";
|
|
28
|
+
if (baseWorktreePolicy !== "skip") {
|
|
29
|
+
try {
|
|
30
|
+
ensureCleanBaseWorktree(opts.projectDir, { allowedDirtyPaths: opts.allowedBaseDirtyPaths });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (baseWorktreePolicy === "warn") {
|
|
34
|
+
console.warn(`foreman: warning: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
28
41
|
ensureForemanExcluded(opts.projectDir);
|
|
29
42
|
const completionMode = opts.completionMode ?? (opts.createPr ? "pr" : "none");
|
|
30
43
|
const reviewProvider = opts.reviewProvider ?? "github";
|
package/dist/cli/start.js
CHANGED
|
@@ -18,7 +18,7 @@ import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../
|
|
|
18
18
|
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
19
19
|
import { StateDb } from "../tickets/stateDb.js";
|
|
20
20
|
import { applySharedDeliveryBranch, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
21
|
-
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
21
|
+
import { collectBaseWorktreeDirtyPaths, currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
22
22
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
23
23
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
24
24
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
@@ -529,11 +529,20 @@ export function buildStartCommand() {
|
|
|
529
529
|
if (branchMode) {
|
|
530
530
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
531
531
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
const baseWorktreePolicy = recoveryRecord ? "skip" : "enforce";
|
|
533
|
+
if (baseWorktreePolicy === "skip") {
|
|
534
|
+
const dirty = collectBaseWorktreeDirtyPaths(cwd, { allowedDirtyPaths: allowedBaseDirtyPaths });
|
|
535
|
+
if (dirty.length > 0) {
|
|
536
|
+
console.warn(`foreman: warning: base worktree has uncommitted changes; build recovery will continue.\n${dirty.join("\n")}`);
|
|
537
|
+
}
|
|
534
538
|
}
|
|
535
|
-
|
|
536
|
-
|
|
539
|
+
else {
|
|
540
|
+
try {
|
|
541
|
+
ensureCleanBaseWorktree(cwd, { allowedDirtyPaths: allowedBaseDirtyPaths });
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
545
|
+
}
|
|
537
546
|
}
|
|
538
547
|
if (branchDefaults.createReview && branchDefaults.reviewProvider) {
|
|
539
548
|
await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
|
|
@@ -718,6 +727,7 @@ export function buildStartCommand() {
|
|
|
718
727
|
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
719
728
|
mergeMethod: branchDefaults.mergeMethod,
|
|
720
729
|
allowedBaseDirtyPaths,
|
|
730
|
+
baseWorktreePolicy,
|
|
721
731
|
trackerPaths: {
|
|
722
732
|
progressDoc: ticketsConfig.paths.progressDoc,
|
|
723
733
|
archiveDoc: ticketsConfig.paths.archiveDoc,
|
package/dist/cli/tickets.js
CHANGED
|
@@ -266,6 +266,8 @@ async function collectTicketSetupPatch(dir, opts, current) {
|
|
|
266
266
|
const buildPatch = {};
|
|
267
267
|
if (opts.agentPreference)
|
|
268
268
|
populatePatch.agent_preference = parseAgentPreference(opts.agentPreference);
|
|
269
|
+
if (opts.branchStrategy)
|
|
270
|
+
buildPatch.branch_strategy = parseBranchStrategy(opts.branchStrategy);
|
|
269
271
|
if (opts.completion)
|
|
270
272
|
buildPatch.completion = parseCompletionMode(opts.completion);
|
|
271
273
|
if (opts.provider)
|
|
@@ -275,6 +277,15 @@ async function collectTicketSetupPatch(dir, opts, current) {
|
|
|
275
277
|
if (opts.autoMergeTimeoutMinutes !== undefined) {
|
|
276
278
|
buildPatch.auto_merge_timeout_minutes = parseOptionalPositiveInteger(opts.autoMergeTimeoutMinutes, "--auto-merge-timeout-minutes");
|
|
277
279
|
}
|
|
280
|
+
if (buildPatch.branch_strategy === "current") {
|
|
281
|
+
if (buildPatch.completion && buildPatch.completion !== "none")
|
|
282
|
+
fail("--branch-strategy current requires --completion none");
|
|
283
|
+
buildPatch.completion = "none";
|
|
284
|
+
buildPatch.provider = buildPatch.provider ?? "local";
|
|
285
|
+
buildPatch.pr_ready = false;
|
|
286
|
+
buildPatch.auto_merge_wait = false;
|
|
287
|
+
buildPatch.auto_merge_timeout_minutes = null;
|
|
288
|
+
}
|
|
278
289
|
if (!shouldPrompt(opts)) {
|
|
279
290
|
const prefill = current.sources.length === 0
|
|
280
291
|
? configuredPlanningSources(dir).map((path) => ({ type: "local", paths: [path] }))
|
|
@@ -321,6 +332,32 @@ async function collectTicketSetupPatch(dir, opts, current) {
|
|
|
321
332
|
}
|
|
322
333
|
if (section === "build" || section === "all") {
|
|
323
334
|
const recommended = recommendedBuildDefaults(dir);
|
|
335
|
+
const strategy = await select({
|
|
336
|
+
message: "Default ticket work mode:",
|
|
337
|
+
initialValue: current.build.branch_strategy,
|
|
338
|
+
options: [
|
|
339
|
+
{ value: "current", label: "One branch - work the queue on the current branch" },
|
|
340
|
+
{ value: "batch", label: "Batch branch - use shared branches for explicit delivery batches" },
|
|
341
|
+
{ value: "branch-per-ticket", label: "Branch per ticket - isolate each ticket on its own branch" },
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
if (isCancel(strategy))
|
|
345
|
+
process.exit(0);
|
|
346
|
+
buildPatch.branch_strategy = strategy;
|
|
347
|
+
if (strategy === "current") {
|
|
348
|
+
buildPatch.completion = "none";
|
|
349
|
+
buildPatch.provider = "local";
|
|
350
|
+
buildPatch.pr_ready = false;
|
|
351
|
+
buildPatch.merge_method = "squash";
|
|
352
|
+
buildPatch.cleanup = true;
|
|
353
|
+
buildPatch.auto_merge_wait = false;
|
|
354
|
+
buildPatch.auto_merge_timeout_minutes = null;
|
|
355
|
+
return {
|
|
356
|
+
sources,
|
|
357
|
+
populate: populatePatch,
|
|
358
|
+
build: buildPatch,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
324
361
|
const completion = await select({
|
|
325
362
|
message: "Default completion behavior for branch ticket runs:",
|
|
326
363
|
initialValue: current.build.completion === "none" ? recommended.completion : current.build.completion,
|
|
@@ -333,7 +370,6 @@ async function collectTicketSetupPatch(dir, opts, current) {
|
|
|
333
370
|
});
|
|
334
371
|
if (isCancel(completion))
|
|
335
372
|
process.exit(0);
|
|
336
|
-
buildPatch.branch_strategy = "branch-per-ticket";
|
|
337
373
|
buildPatch.completion = completion;
|
|
338
374
|
buildPatch.provider = recommended.provider;
|
|
339
375
|
buildPatch.pr_ready = completion === "auto-merge";
|
|
@@ -517,6 +553,11 @@ function parseCompletionMode(value) {
|
|
|
517
553
|
return value;
|
|
518
554
|
fail("--completion must be one of: pr, auto-merge, direct-merge, none");
|
|
519
555
|
}
|
|
556
|
+
function parseBranchStrategy(value) {
|
|
557
|
+
if (["current", "batch", "branch-per-ticket"].includes(value))
|
|
558
|
+
return value;
|
|
559
|
+
fail("--branch-strategy must be one of: current, batch, branch-per-ticket");
|
|
560
|
+
}
|
|
520
561
|
function parseProvider(value) {
|
|
521
562
|
if (["auto", "github", "gitlab", "local"].includes(value))
|
|
522
563
|
return value;
|
|
@@ -853,6 +894,7 @@ export function buildTicketsCommand() {
|
|
|
853
894
|
.option("--jira-jql <jql>", "Jira JQL query")
|
|
854
895
|
.option("--url-source <urls...>", "add public HTTP(S) source URLs")
|
|
855
896
|
.option("--agent-preference <agent>", "populate runtime preference (configured | claude | codex)")
|
|
897
|
+
.option("--branch-strategy <strategy>", "build branch strategy default (current | batch | branch-per-ticket)")
|
|
856
898
|
.option("--completion <mode>", "build completion default (pr | auto-merge | direct-merge | none)")
|
|
857
899
|
.option("--provider <provider>", "PR/MR provider default (auto | github | gitlab | local)")
|
|
858
900
|
.option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
|
|
@@ -883,6 +925,7 @@ export function buildTicketsCommand() {
|
|
|
883
925
|
.option("--jira-jql <jql>", "Jira JQL query")
|
|
884
926
|
.option("--url-source <urls...>", "replace saved sources with public HTTP(S) URLs")
|
|
885
927
|
.option("--agent-preference <agent>", "populate runtime preference (configured | claude | codex)")
|
|
928
|
+
.option("--branch-strategy <strategy>", "build branch strategy default (current | batch | branch-per-ticket)")
|
|
886
929
|
.option("--completion <mode>", "build completion default (pr | auto-merge | direct-merge | none)")
|
|
887
930
|
.option("--provider <provider>", "PR/MR provider default (auto | github | gitlab | local)")
|
|
888
931
|
.option("--auto-merge-wait", "wait for dependency PR/MRs to merge before starting dependent tickets")
|
|
@@ -1261,7 +1304,7 @@ export function buildTicketsCommand() {
|
|
|
1261
1304
|
.description("Print the ticket queue to stdout.")
|
|
1262
1305
|
.option("-p, --project <dir>", "project directory (default: cwd)")
|
|
1263
1306
|
.option("--limit <n>", "override view limit")
|
|
1264
|
-
.option("--refresh", "query GitHub/GitLab and refresh cached
|
|
1307
|
+
.option("--refresh", "query GitHub/GitLab and refresh cached batch review state")
|
|
1265
1308
|
.action((opts) => {
|
|
1266
1309
|
try {
|
|
1267
1310
|
const dir = cwd(opts);
|
|
@@ -1275,6 +1318,7 @@ export function buildTicketsCommand() {
|
|
|
1275
1318
|
console.log(line);
|
|
1276
1319
|
return;
|
|
1277
1320
|
}
|
|
1321
|
+
console.log("Batches: none configured. Queue is flat.");
|
|
1278
1322
|
const rows = cmdQueue(dir, opts.limit !== undefined ? Number(opts.limit) : undefined);
|
|
1279
1323
|
if (rows.length === 0) {
|
|
1280
1324
|
console.log("No remaining tickets.");
|
package/dist/tickets/queue.d.ts
CHANGED
|
@@ -35,5 +35,5 @@ export interface ActiveStatusRow {
|
|
|
35
35
|
}
|
|
36
36
|
export declare function buildNextQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, queueLimit: number): QueueRow[];
|
|
37
37
|
export declare function buildActiveStatusRows(queueRows: QueueRow[], ticketDefs: TicketDef[], states: Map<string, TicketState>): ActiveStatusRow[];
|
|
38
|
-
/** Human-readable queue blocks keep a whole unfinished
|
|
38
|
+
/** Human-readable queue blocks keep a whole unfinished batch and its resume point visible. */
|
|
39
39
|
export declare function formatStackAwareQueue(ticketDefs: TicketDef[], states: Map<string, TicketState>, delivery?: DeliveryConfig): string[];
|
package/dist/tickets/queue.js
CHANGED
|
@@ -61,15 +61,16 @@ export function buildActiveStatusRows(queueRows, ticketDefs, states) {
|
|
|
61
61
|
};
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
/** Human-readable queue blocks keep a whole unfinished
|
|
64
|
+
/** Human-readable queue blocks keep a whole unfinished batch and its resume point visible. */
|
|
65
65
|
export function formatStackAwareQueue(ticketDefs, states, delivery) {
|
|
66
66
|
if (!delivery?.stacks?.length)
|
|
67
|
-
return buildNextQueue(ticketDefs, states, Number.MAX_SAFE_INTEGER).map((row) => `${row.ticket} ${row.status} ${row.title}`);
|
|
67
|
+
return ["Batches: none configured. Queue is flat.", ...buildNextQueue(ticketDefs, states, Number.MAX_SAFE_INTEGER).map((row) => `${row.ticket} ${row.status} ${row.title}`)];
|
|
68
68
|
const defs = new Map(ticketDefs.map((ticket) => [ticket.id, ticket]));
|
|
69
69
|
const unitById = new Map(delivery.units.map((unit) => [unit.id, unit]));
|
|
70
70
|
const stackedTickets = new Set();
|
|
71
71
|
const lines = [];
|
|
72
|
-
|
|
72
|
+
lines.push(`Batches: ${delivery.stacks.length} configured.`);
|
|
73
|
+
for (const [index, stack] of delivery.stacks.entries()) {
|
|
73
74
|
const nodes = normalizeStackNodes(stack, delivery.units, ticketDefs);
|
|
74
75
|
const ticketIds = stack.units.flatMap((id) => unitById.get(id)?.tickets ?? []);
|
|
75
76
|
ticketIds.forEach((id) => stackedTickets.add(id));
|
|
@@ -79,16 +80,26 @@ export function formatStackAwareQueue(ticketDefs, states, delivery) {
|
|
|
79
80
|
const next = ticketIds.find((id) => !terminal(states.get(id)?.status));
|
|
80
81
|
const blockers = next && defs.get(next) ? resolveBlockers(defs.get(next), states) : [];
|
|
81
82
|
const state = deriveStackState(stack, ticketIds, states, blockers);
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const units = stack.units.map((id) => unitById.get(id)).filter((unit) => Boolean(unit));
|
|
84
|
+
const branchModes = [...new Set(units.map((unit) => unit.branch_mode))].join(",") || "unknown";
|
|
85
|
+
const prChain = units.some((unit) => unit.dependency_mode === "stack") || nodes.length > 1;
|
|
86
|
+
lines.push(`=== Batch ${index + 1}: ${stack.name} (${stack.id}) ===`);
|
|
87
|
+
lines.push(`status=${state}; tickets=${ticketIds.length}; branch_mode=${branchModes}; blockers=${blockers.join(",") || "none"}; completed=${completed.join(",") || "none"}; next=${next ?? "none"}`);
|
|
88
|
+
if (prChain)
|
|
89
|
+
lines.push(`delivery=PR chain (${nodes.length} dependent branch${nodes.length === 1 ? "" : "es"})`);
|
|
84
90
|
lines.push(`reviews=${stack.review_links?.join(",") || "none"}; remote_checked=${stack.remote_checked_at ?? "never"}${stack.remote_stale ? " (stale; run --refresh for a live check)" : ""}`);
|
|
85
91
|
for (const id of ticketIds)
|
|
86
92
|
lines.push(`${id} ${states.get(id)?.status ?? "planned"} ${defs.get(id)?.title ?? "unknown ticket"}`);
|
|
87
|
-
lines.push(`===
|
|
93
|
+
lines.push(`=== End Batch ${index + 1}: ${stack.name} (${stack.id}) ===`);
|
|
88
94
|
}
|
|
95
|
+
let unbatched = false;
|
|
89
96
|
for (const ticket of ticketDefs.sort((a, b) => a.order - b.order)) {
|
|
90
97
|
if (stackedTickets.has(ticket.id) || terminal(states.get(ticket.id)?.status))
|
|
91
98
|
continue;
|
|
99
|
+
if (!unbatched) {
|
|
100
|
+
lines.push("Unbatched tickets:");
|
|
101
|
+
unbatched = true;
|
|
102
|
+
}
|
|
92
103
|
const blockers = resolveBlockers(ticket, states);
|
|
93
104
|
lines.push(`${ticket.id} ${computeDisplayStatus(states.get(ticket.id)?.status ?? "planned", blockers)} ${ticket.title}`);
|
|
94
105
|
}
|
|
@@ -3,7 +3,7 @@ export type HarnessTarget = "claude" | "codex";
|
|
|
3
3
|
export type TicketBuildCompletionMode = "pr" | "auto-merge" | "direct-merge" | "none";
|
|
4
4
|
export type TicketBuildProvider = "auto" | "github" | "gitlab" | "local";
|
|
5
5
|
export type TicketBuildMergeMethod = "squash" | "merge" | "rebase";
|
|
6
|
-
export type TicketBuildBranchStrategy = "
|
|
6
|
+
export type TicketBuildBranchStrategy = "current" | "batch" | "branch-per-ticket";
|
|
7
7
|
export type TicketPopulateAgentPreference = "configured" | "claude" | "codex";
|
|
8
8
|
export type TicketPopulateEnrichmentPolicy = "none" | "recommendations" | "agent";
|
|
9
9
|
export type TicketSourceConfig = {
|
|
@@ -346,7 +346,7 @@ function normalizePopulate(value, label) {
|
|
|
346
346
|
function normalizeBuild(value, label) {
|
|
347
347
|
const raw = objectOrEmpty(value, label);
|
|
348
348
|
return {
|
|
349
|
-
branch_strategy: enumField(raw.branch_strategy, ["branch-per-ticket"
|
|
349
|
+
branch_strategy: enumField(raw.branch_strategy, ["current", "batch", "branch-per-ticket"], DEFAULT_TICKET_SETUP.build.branch_strategy, `${label}.branch_strategy`),
|
|
350
350
|
completion: enumField(raw.completion, ["pr", "auto-merge", "direct-merge", "none"], DEFAULT_TICKET_SETUP.build.completion, `${label}.completion`),
|
|
351
351
|
provider: enumField(raw.provider, ["auto", "github", "gitlab", "local"], DEFAULT_TICKET_SETUP.build.provider, `${label}.provider`),
|
|
352
352
|
pr_ready: booleanField(raw.pr_ready, DEFAULT_TICKET_SETUP.build.pr_ready, `${label}.pr_ready`),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
"@anthropic-ai/claude-agent-sdk": "^0.3.148"
|
|
119
119
|
},
|
|
120
120
|
"dependencies": {
|
|
121
|
-
"special-agents": "0.8.
|
|
122
|
-
"rafi-spec": "0.8.
|
|
121
|
+
"special-agents": "0.8.2",
|
|
122
|
+
"rafi-spec": "0.8.2",
|
|
123
123
|
"@clack/prompts": "^1.4.0",
|
|
124
124
|
"ajv": "^8.20.0",
|
|
125
125
|
"ajv-formats": "^3.0.1",
|