ai-foreman 1.6.1 → 1.7.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/README.md +1 -1
- package/dist/adapters/claude.d.ts +5 -1
- package/dist/adapters/claude.js +41 -0
- package/dist/adapters/codex.d.ts +26 -28
- package/dist/adapters/codex.js +209 -148
- package/dist/adapters/recovering.d.ts +3 -1
- package/dist/adapters/recovering.js +2 -0
- package/dist/adapters/types.d.ts +29 -0
- package/dist/branch/planner.js +1 -1
- package/dist/branch/runner.d.ts +6 -0
- package/dist/branch/runner.js +149 -16
- package/dist/buildRuns.d.ts +9 -2
- package/dist/buildRuns.js +110 -7
- package/dist/cli/start.d.ts +6 -0
- package/dist/cli/start.js +319 -30
- package/dist/cli/status.js +25 -2
- package/dist/cli/tickets.d.ts +1 -0
- package/dist/cli/tickets.js +67 -27
- package/dist/foreman.d.ts +18 -3
- package/dist/foreman.js +117 -6
- package/dist/log.d.ts +1 -1
- package/dist/operationRecovery.d.ts +25 -0
- package/dist/operationRecovery.js +46 -0
- package/dist/outcomes.d.ts +16 -0
- package/dist/outcomes.js +27 -0
- package/dist/qaReview.d.ts +52 -0
- package/dist/qaReview.js +103 -0
- package/dist/qaSnapshot.d.ts +19 -0
- package/dist/qaSnapshot.js +88 -0
- package/dist/sessionLifecycle.d.ts +37 -0
- package/dist/sessionLifecycle.js +104 -0
- package/dist/statusReporter.d.ts +40 -0
- package/dist/statusReporter.js +31 -0
- package/dist/ticketPopulation.d.ts +47 -0
- package/dist/ticketPopulation.js +205 -0
- package/dist/tickets/blockers.d.ts +1 -1
- package/dist/tickets/blockers.js +2 -0
- package/dist/tickets/commands.d.ts +1 -0
- package/dist/tickets/commands.js +30 -1
- package/dist/tickets/delivery.d.ts +36 -0
- package/dist/tickets/delivery.js +204 -4
- package/dist/tickets/queue.d.ts +3 -0
- package/dist/tickets/queue.js +50 -1
- package/dist/tickets/stateDb.d.ts +2 -1
- package/dist/tickets/stateDb.js +24 -1
- package/dist/tickets/ticketSchema.d.ts +44 -0
- package/dist/tickets/ticketSchema.js +10 -0
- package/dist/tickets/validate.js +1 -1
- package/dist/workflowDb.d.ts +113 -0
- package/dist/workflowDb.js +261 -0
- package/package.json +16 -3
package/dist/cli/start.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { resolve, join, relative } from "node:path";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { select, text, isCancel } from "@clack/prompts";
|
|
5
6
|
import { loadConfig } from "../config.js";
|
|
6
7
|
import { Log } from "../log.js";
|
|
@@ -21,11 +22,14 @@ import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } fr
|
|
|
21
22
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
22
23
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
23
24
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
24
|
-
import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, releaseBuildLease } from "../buildRuns.js";
|
|
25
|
+
import { checkpointBuildRun, completeBuildRun, createBuildRun, heartbeatBuildRun, persistBuildSession, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
25
26
|
import { parse as parseYaml } from "yaml";
|
|
26
27
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
27
28
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
28
|
-
import { loadDeliveryConfig, selectDeliveryUnitForRun } from "../tickets/delivery.js";
|
|
29
|
+
import { loadDeliveryConfig, selectDeliveryUnitForRun, selectStacksForRun, updateStackDeliveryState } from "../tickets/delivery.js";
|
|
30
|
+
import { AgentStatusReporter } from "../statusReporter.js";
|
|
31
|
+
import { WorkflowDb } from "../workflowDb.js";
|
|
32
|
+
import { makeLogPath as makeRoleLogPath, readOnlyPermissionConfig, runRoleInstruction } from "../agentRun.js";
|
|
29
33
|
function fail(message) {
|
|
30
34
|
console.error(`foreman: ${message}`);
|
|
31
35
|
process.exit(1);
|
|
@@ -53,6 +57,31 @@ function collectTicket(value, previous) {
|
|
|
53
57
|
function unique(values) {
|
|
54
58
|
return [...new Set(values)];
|
|
55
59
|
}
|
|
60
|
+
export function formatStartResumeCommand(executable, projectDir, steps, sessionId) {
|
|
61
|
+
return [
|
|
62
|
+
executable,
|
|
63
|
+
"start",
|
|
64
|
+
shellQuote(projectDir),
|
|
65
|
+
"--steps",
|
|
66
|
+
String(steps),
|
|
67
|
+
"--resume",
|
|
68
|
+
shellQuote(sessionId),
|
|
69
|
+
].join(" ");
|
|
70
|
+
}
|
|
71
|
+
export function formatResumeGuidance(executable, projectDir, steps, sessionId) {
|
|
72
|
+
if (executable === "rafi") {
|
|
73
|
+
return [
|
|
74
|
+
"foreman: resume this run with:",
|
|
75
|
+
` rafi build:resume ${shellQuote(projectDir)}`,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (!sessionId)
|
|
79
|
+
return [];
|
|
80
|
+
return [
|
|
81
|
+
"foreman: resume this builder with:",
|
|
82
|
+
` ${formatStartResumeCommand("ai-foreman", projectDir, steps, sessionId)}`,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
56
85
|
function optionWasProvided(command, name) {
|
|
57
86
|
const source = command.getOptionValueSource(name);
|
|
58
87
|
return source !== undefined && source !== "default";
|
|
@@ -222,10 +251,12 @@ export function buildStartCommand() {
|
|
|
222
251
|
return new Command("start")
|
|
223
252
|
.description("Enlist a builder and drive it through a batch of N steps.")
|
|
224
253
|
.argument("<project>", "path to the project directory the builder works in")
|
|
225
|
-
.
|
|
254
|
+
.option("-s, --steps <n>", "number of tickets to drive; may stop within a stack")
|
|
255
|
+
.option("--stacks <n>", "number of complete eligible delivery stacks to build")
|
|
226
256
|
.option("-a, --agent <agent>", "builder agent (claude | codex)")
|
|
227
257
|
.option("-m, --model <model>", "override the builder's model")
|
|
228
258
|
.option("-r, --resume <sessionId>", "resume a prior builder session")
|
|
259
|
+
.option("--recover-run <id>", "continue an existing master recovery run ID")
|
|
229
260
|
.option("--continue", "resume the most recent logged session for this project")
|
|
230
261
|
.option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
|
|
231
262
|
.option("-y, --yes", "skip pre-flight confirmation prompt")
|
|
@@ -244,15 +275,36 @@ export function buildStartCommand() {
|
|
|
244
275
|
.option("--auto-merge-timeout-minutes <n>", "auto-merge dependency wait timeout in minutes (blank means no timeout)")
|
|
245
276
|
.option("--base <ref>", "base ref for root ticket branches (default: current branch or HEAD)")
|
|
246
277
|
.option("--branch-prefix <prefix>", "branch name prefix for ticket branches", "rafi")
|
|
247
|
-
.option("--max-branch-depth <n>", "maximum selected branch stack depth", "
|
|
278
|
+
.option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
|
|
248
279
|
.option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
|
|
249
280
|
.option("--keep-worktrees", "keep successful ticket worktrees for inspection")
|
|
250
281
|
.option("--ticket <id>", "ticket id to continue in branch mode; repeat for multiple tickets", collectTicket, [])
|
|
251
282
|
.option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
|
|
252
283
|
.action(async (project, opts, command) => {
|
|
253
|
-
|
|
284
|
+
if (opts.steps !== undefined && opts.stacks !== undefined)
|
|
285
|
+
fail("--steps and --stacks are mutually exclusive");
|
|
286
|
+
if (opts.steps === undefined && opts.stacks === undefined) {
|
|
287
|
+
if (opts.yes || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
288
|
+
fail("noninteractive/--yes runs require exactly one of --steps or --stacks");
|
|
289
|
+
const mode = await select({ message: "Build tickets or complete delivery stacks?", options: [
|
|
290
|
+
{ value: "steps", label: "Tickets (Recommended)" }, { value: "stacks", label: "Complete stacks" },
|
|
291
|
+
] });
|
|
292
|
+
if (isCancel(mode))
|
|
293
|
+
process.exit(0);
|
|
294
|
+
const count = await text({ message: mode === "steps" ? "How many tickets?" : "How many complete stacks?", validate: (value) => Number.isInteger(Number(value)) && Number(value) > 0 ? undefined : "Enter a positive integer" });
|
|
295
|
+
if (isCancel(count))
|
|
296
|
+
process.exit(0);
|
|
297
|
+
opts[mode] = String(count);
|
|
298
|
+
}
|
|
299
|
+
let steps = Number.parseInt(opts.steps ?? "0", 10);
|
|
300
|
+
const stackCount = opts.stacks === undefined ? undefined : Number.parseInt(opts.stacks, 10);
|
|
301
|
+
if (stackCount !== undefined && (!Number.isInteger(stackCount) || stackCount < 1))
|
|
302
|
+
fail("--stacks must be a positive integer");
|
|
303
|
+
if (stackCount !== undefined)
|
|
304
|
+
steps = 0;
|
|
254
305
|
if (!Number.isInteger(steps) || steps < 1) {
|
|
255
|
-
|
|
306
|
+
if (stackCount === undefined)
|
|
307
|
+
fail("--steps must be a positive integer");
|
|
256
308
|
}
|
|
257
309
|
const VALID_EFFORT = ["low", "medium", "high", "xhigh"];
|
|
258
310
|
if (opts.effort && !VALID_EFFORT.includes(opts.effort)) {
|
|
@@ -261,17 +313,40 @@ export function buildStartCommand() {
|
|
|
261
313
|
const cwd = resolve(project);
|
|
262
314
|
if (!existsSync(cwd))
|
|
263
315
|
fail(`project directory not found: ${cwd}`);
|
|
316
|
+
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
317
|
+
if (opts.recoverRun && !recoveryRecord)
|
|
318
|
+
fail(`recoverable build run not found: ${opts.recoverRun}`);
|
|
264
319
|
const roleDefaults = readAgentDefaults(cwd);
|
|
265
320
|
let deliveryRun;
|
|
321
|
+
let deliveryConfig;
|
|
322
|
+
let selectedStacks = [];
|
|
323
|
+
let selectedStackTickets;
|
|
266
324
|
if (isTicketsInitialized(cwd)) {
|
|
267
325
|
const delivery = loadDeliveryConfig(cwd);
|
|
326
|
+
deliveryConfig = delivery;
|
|
268
327
|
if (delivery) {
|
|
269
328
|
const earlyConfig = loadTicketsConfig(cwd);
|
|
270
329
|
const earlyPaths = resolveTicketPaths(earlyConfig, cwd);
|
|
271
330
|
const earlyDb = new StateDb(earlyPaths.stateDb);
|
|
272
331
|
const earlyStates = earlyDb.getAllStates();
|
|
273
332
|
earlyDb.close();
|
|
274
|
-
|
|
333
|
+
if (stackCount !== undefined) {
|
|
334
|
+
const earlyTickets = loadTickets(earlyPaths.tickets);
|
|
335
|
+
const selection = selectStacksForRun(delivery, earlyTickets, earlyStates, stackCount);
|
|
336
|
+
if (selection.error)
|
|
337
|
+
fail(selection.error);
|
|
338
|
+
selectedStacks = selection.stacks;
|
|
339
|
+
selectedStackTickets = selection.tickets;
|
|
340
|
+
steps = selection.tickets.length;
|
|
341
|
+
opts.branchPerTicket = true;
|
|
342
|
+
opts.completion = "pr";
|
|
343
|
+
const providers = new Set(selection.stacks.flatMap((stack) => stack.units.map((id) => delivery.units.find((unit) => unit.id === id)?.provider)).filter((provider) => provider === "github" || provider === "gitlab"));
|
|
344
|
+
if (providers.size !== 1)
|
|
345
|
+
fail("selected stacks must use one explicit GitHub or GitLab provider per run");
|
|
346
|
+
opts.provider = [...providers][0];
|
|
347
|
+
}
|
|
348
|
+
else
|
|
349
|
+
deliveryRun = selectDeliveryUnitForRun(delivery, earlyStates, opts.skipDeliveryUnit ?? []);
|
|
275
350
|
if (deliveryRun?.state === "resume" && !opts.yes && process.stdin.isTTY && process.stdout.isTTY) {
|
|
276
351
|
const answer = await select({ message: `Resume unfinished delivery unit ${deliveryRun.unit.id} (${deliveryRun.remaining.length} remaining)?`, options: [
|
|
277
352
|
{ value: "resume", label: "Resume (Recommended)" }, { value: "skip", label: "Skip for this run" },
|
|
@@ -283,6 +358,8 @@ export function buildStartCommand() {
|
|
|
283
358
|
}
|
|
284
359
|
}
|
|
285
360
|
}
|
|
361
|
+
if (stackCount !== undefined && selectedStacks.length !== stackCount)
|
|
362
|
+
fail("--stacks requires explicit eligible stacks in .tickets/delivery.yaml");
|
|
286
363
|
const branchDefaults = resolveStartBranchDefaults(cwd, opts, command, deliveryRun);
|
|
287
364
|
let agent;
|
|
288
365
|
try {
|
|
@@ -300,6 +377,7 @@ export function buildStartCommand() {
|
|
|
300
377
|
let qaExecutable;
|
|
301
378
|
const qaEffort = explicitEffort(roleDefaults.qa?.reasoning);
|
|
302
379
|
const qaFast = roleDefaults.qa?.fast ?? false;
|
|
380
|
+
const settingsRevision = roleDefaults.revision ?? 0;
|
|
303
381
|
const configuredTrackerPath = isTicketsInitialized(cwd)
|
|
304
382
|
? loadTicketsConfig(cwd).paths.progressDoc
|
|
305
383
|
: undefined;
|
|
@@ -343,7 +421,7 @@ export function buildStartCommand() {
|
|
|
343
421
|
if (opts.resume && continueTickets.length > 1) {
|
|
344
422
|
fail("--resume <sessionId> in branch mode supports exactly one --ticket; use --continue for multiple tickets");
|
|
345
423
|
}
|
|
346
|
-
if ((opts.resume || opts.continue) && continueTickets.length === 0) {
|
|
424
|
+
if ((opts.resume || opts.continue) && continueTickets.length === 0 && !recoveryRecord) {
|
|
347
425
|
fail(branchContinueTicketHelp(cwd, opts.resume ? "--resume" : "--continue"));
|
|
348
426
|
}
|
|
349
427
|
if (continueTickets.length > 0 && !(opts.resume || opts.continue)) {
|
|
@@ -447,6 +525,7 @@ export function buildStartCommand() {
|
|
|
447
525
|
},
|
|
448
526
|
});
|
|
449
527
|
};
|
|
528
|
+
const qaNonconvergence = createQaNonconvergenceHandler(cwd, Boolean(opts.yes), roleDefaults.planner);
|
|
450
529
|
if (branchMode) {
|
|
451
530
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
452
531
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
@@ -523,7 +602,7 @@ export function buildStartCommand() {
|
|
|
523
602
|
branchPrefix: opts.branchPrefix,
|
|
524
603
|
maxBranchDepth,
|
|
525
604
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
526
|
-
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
605
|
+
ticketIds: selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
|
|
527
606
|
});
|
|
528
607
|
let auditBuilder;
|
|
529
608
|
let auditViewer;
|
|
@@ -541,7 +620,7 @@ export function buildStartCommand() {
|
|
|
541
620
|
maxBranchDepth,
|
|
542
621
|
auditDependencies,
|
|
543
622
|
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
544
|
-
ticketIds: deliveryRun?.remaining.slice(0, steps),
|
|
623
|
+
ticketIds: selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
|
|
545
624
|
});
|
|
546
625
|
log.write("branch-plan", {
|
|
547
626
|
baseRef: plan.baseRef,
|
|
@@ -563,6 +642,14 @@ export function buildStartCommand() {
|
|
|
563
642
|
resumeSessionByTicket.set(first.ticket.id, { worktreePath: saved.worktreePath, sessionId: saved.sessionId });
|
|
564
643
|
}
|
|
565
644
|
}
|
|
645
|
+
if (selectedStacks.length && deliveryConfig) {
|
|
646
|
+
plan = applyExplicitStackTopology(plan, selectedStacks, deliveryConfig, opts.branchPrefix);
|
|
647
|
+
}
|
|
648
|
+
if (recoveryRecord && opts.resume && plan.nodes[0]) {
|
|
649
|
+
const prior = findResumableBranchSessions(join(cwd, ".foreman")).find((session) => session.ticket === (recoveryRecord.currentTicket ?? plan.nodes[0].ticket.id));
|
|
650
|
+
if (prior)
|
|
651
|
+
resumeSessionByTicket.set(plan.nodes[0].ticket.id, { worktreePath: prior.worktreePath, sessionId: opts.resume });
|
|
652
|
+
}
|
|
566
653
|
}
|
|
567
654
|
console.log(`foreman: ${continueTickets.length > 0 ? "resuming branch-per-ticket mode" : "branch-per-ticket mode"} for ${plan.nodes.length} ticket(s)`);
|
|
568
655
|
console.log(`foreman: project ${cwd}`);
|
|
@@ -595,9 +682,24 @@ export function buildStartCommand() {
|
|
|
595
682
|
process.exit(0);
|
|
596
683
|
}
|
|
597
684
|
}
|
|
685
|
+
const capturedBranchBuilder = {
|
|
686
|
+
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
687
|
+
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
688
|
+
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
689
|
+
};
|
|
690
|
+
const capturedBranchQa = {
|
|
691
|
+
role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
692
|
+
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
693
|
+
};
|
|
694
|
+
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: opts.resume ? String(opts.resume) : null }) : createBuildRun({
|
|
695
|
+
tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
696
|
+
repositoryRoot: cwd, branchMode: "per-ticket", builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
697
|
+
});
|
|
698
|
+
const branchHeartbeat = setInterval(() => { masterRun = heartbeatBuildRun(cwd, masterRun); }, 10_000);
|
|
699
|
+
branchHeartbeat.unref();
|
|
598
700
|
const summaries = await runBranchPlan({
|
|
599
701
|
projectDir: cwd,
|
|
600
|
-
runId:
|
|
702
|
+
runId: masterRun.runId,
|
|
601
703
|
plan,
|
|
602
704
|
log,
|
|
603
705
|
agent,
|
|
@@ -610,7 +712,7 @@ export function buildStartCommand() {
|
|
|
610
712
|
completionMode: branchDefaults.completionMode,
|
|
611
713
|
reviewProvider: branchDefaults.reviewProvider,
|
|
612
714
|
prReady: branchDefaults.prReady,
|
|
613
|
-
keepWorktrees: Boolean(opts.keepWorktrees),
|
|
715
|
+
keepWorktrees: Boolean(opts.keepWorktrees) || selectedStacks.length > 0,
|
|
614
716
|
cleanupBranches: branchDefaults.cleanupBranches,
|
|
615
717
|
autoMergeWait: branchDefaults.autoMergeWait,
|
|
616
718
|
autoMergeTimeoutMinutes: branchDefaults.autoMergeTimeoutMinutes,
|
|
@@ -622,8 +724,34 @@ export function buildStartCommand() {
|
|
|
622
724
|
},
|
|
623
725
|
resumeSessions: resumeSessionByTicket,
|
|
624
726
|
createBuilder: (builderCwd, sessionId) => createBuilder(builderCwd, sessionId),
|
|
727
|
+
createQa: (qaCwd, sessionId) => createQa(qaCwd, sessionId),
|
|
728
|
+
builderSessionStrategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
729
|
+
qaSessionStrategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
625
730
|
observeBuilder: (builder) => printEvents(builder.events()),
|
|
731
|
+
qaNonconvergence,
|
|
626
732
|
});
|
|
733
|
+
clearInterval(branchHeartbeat);
|
|
734
|
+
if (selectedStacks.length) {
|
|
735
|
+
let allComplete = true;
|
|
736
|
+
for (const stack of selectedStacks) {
|
|
737
|
+
const stackTickets = stack.units.flatMap((id) => deliveryConfig?.units.find((unit) => unit.id === id)?.tickets ?? []);
|
|
738
|
+
const rows = summaries.filter((row) => stackTickets.includes(row.ticket));
|
|
739
|
+
const complete = rows.length > 0 && rows.every((row) => row.buildStatus === "done" && (!row.pr || ["created", "existing"].includes(row.pr.status)));
|
|
740
|
+
allComplete &&= complete;
|
|
741
|
+
const successful = rows.filter((row) => row.buildStatus === "done");
|
|
742
|
+
updateStackDeliveryState(cwd, [stack.id], complete ? "awaiting_review" : "partial", {
|
|
743
|
+
review_links: [...(stack.review_links ?? []), ...successful.flatMap((row) => row.pr?.url ? [row.pr.url] : [])],
|
|
744
|
+
published_branches: { ...(stack.published_branches ?? {}), ...Object.fromEntries(successful.map((row) => [row.ticket, row.branch])) },
|
|
745
|
+
completed_prefix: [...(stack.completed_prefix ?? []), ...successful.map((row) => row.ticket)],
|
|
746
|
+
next_ticket: complete ? undefined : stackTickets.find((ticket) => !successful.some((row) => row.ticket === ticket)),
|
|
747
|
+
remote_checked_at: stack.remote_checked_at, remote_stale: true,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
console.log(allComplete ? "foreman: stack publication complete; delivery is awaiting_review (no merges were attempted)" : "foreman: partial stack prefix published; resume point is preserved");
|
|
751
|
+
}
|
|
752
|
+
masterRun = summaries.every((row) => row.buildStatus === "done")
|
|
753
|
+
? completeBuildRun(cwd, masterRun)
|
|
754
|
+
: releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, "branch-run-interrupted", { status: "recoverable" }), "recoverable");
|
|
627
755
|
console.log("foreman: branch run summary");
|
|
628
756
|
console.log("ticket\tbranch\tbase\tstatus\tcommit\tpush\tcompletion");
|
|
629
757
|
for (const row of summaries) {
|
|
@@ -666,9 +794,10 @@ export function buildStartCommand() {
|
|
|
666
794
|
const capturedBuilder = {
|
|
667
795
|
role: "builder", source: opts.agent || opts.model || opts.effort || opts.fast ? "cli" : roleDefaults.builder ? "project" : "provider",
|
|
668
796
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
797
|
+
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
669
798
|
};
|
|
670
|
-
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast };
|
|
671
|
-
let buildRun = createBuildRun({
|
|
799
|
+
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision };
|
|
800
|
+
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: opts.resume ? String(opts.resume) : null }) : createBuildRun({
|
|
672
801
|
tickets: [], repositoryRoot: cwd, branchMode: "current", builder: capturedBuilder, qa: capturedQa,
|
|
673
802
|
});
|
|
674
803
|
const heartbeat = setInterval(() => { buildRun = heartbeatBuildRun(cwd, buildRun); }, 10_000);
|
|
@@ -682,8 +811,23 @@ export function buildStartCommand() {
|
|
|
682
811
|
process.once("SIGINT", checkpointSignal);
|
|
683
812
|
process.once("SIGTERM", checkpointSignal);
|
|
684
813
|
const builder = await createBuilder(cwd, resumeSessionId);
|
|
685
|
-
const
|
|
686
|
-
const
|
|
814
|
+
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence);
|
|
815
|
+
const statusReporter = new AgentStatusReporter({
|
|
816
|
+
role: "builder", provider: capturedBuilder.make, model: capturedBuilder.model,
|
|
817
|
+
reasoning: capturedBuilder.reasoning, fast: capturedBuilder.fast, step: 1, total: steps,
|
|
818
|
+
phase: "builder work session", sessionTransition: resumeSessionId ? "resumed exact session" : "initial session", adapter: () => foreman.builderAdapter(),
|
|
819
|
+
}, (line, snapshot) => {
|
|
820
|
+
console.log(line);
|
|
821
|
+
log.write("agent-status", { ...snapshot });
|
|
822
|
+
const db = new WorkflowDb(cwd);
|
|
823
|
+
try {
|
|
824
|
+
db.recordTelemetry(buildRun.runId, snapshot);
|
|
825
|
+
}
|
|
826
|
+
finally {
|
|
827
|
+
db.close();
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
statusReporter.start();
|
|
687
831
|
const modifiers = [
|
|
688
832
|
model ? `model=${model}` : null,
|
|
689
833
|
builderEffort ? `effort=${builderEffort}` : null,
|
|
@@ -700,10 +844,10 @@ export function buildStartCommand() {
|
|
|
700
844
|
console.log("ai-foreman: asking builder to plan the next tickets or steps...\n");
|
|
701
845
|
buildRun = checkpointBuildRun(cwd, buildRun, "before-preflight");
|
|
702
846
|
await foreman.runPreflight(steps, ticketsContent);
|
|
703
|
-
if (
|
|
704
|
-
buildRun = persistBuildSession(cwd, buildRun, "builder",
|
|
705
|
-
if (
|
|
706
|
-
buildRun = persistBuildSession(cwd, buildRun, "qa",
|
|
847
|
+
if (foreman.builderSessionId())
|
|
848
|
+
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderSessionId());
|
|
849
|
+
if (foreman.qaSessionId())
|
|
850
|
+
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionId());
|
|
707
851
|
buildRun = checkpointBuildRun(cwd, buildRun, "preflight-complete");
|
|
708
852
|
if (!opts.yes) {
|
|
709
853
|
while (true) {
|
|
@@ -718,7 +862,8 @@ export function buildStartCommand() {
|
|
|
718
862
|
});
|
|
719
863
|
if (isCancel(action) || action === "cancel") {
|
|
720
864
|
console.log("ai-foreman: cancelled");
|
|
721
|
-
|
|
865
|
+
statusReporter.stop();
|
|
866
|
+
await foreman.close();
|
|
722
867
|
await viewer;
|
|
723
868
|
process.exit(0);
|
|
724
869
|
}
|
|
@@ -732,7 +877,8 @@ export function buildStartCommand() {
|
|
|
732
877
|
});
|
|
733
878
|
if (isCancel(fb)) {
|
|
734
879
|
console.log("ai-foreman: cancelled");
|
|
735
|
-
|
|
880
|
+
statusReporter.stop();
|
|
881
|
+
await foreman.close();
|
|
736
882
|
await viewer;
|
|
737
883
|
process.exit(0);
|
|
738
884
|
}
|
|
@@ -741,32 +887,123 @@ export function buildStartCommand() {
|
|
|
741
887
|
}
|
|
742
888
|
}
|
|
743
889
|
const result = await foreman.runBatch(steps, trackerRelPath);
|
|
890
|
+
if (foreman.qaSessionId())
|
|
891
|
+
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionId());
|
|
744
892
|
buildRun = result.outcome === "all-done" || result.outcome === "plan-complete"
|
|
745
893
|
? completeBuildRun(cwd, buildRun)
|
|
746
894
|
: releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "builder-or-qa-interrupted", { status: "recoverable" }), "recoverable");
|
|
747
895
|
clearInterval(heartbeat);
|
|
748
|
-
|
|
749
|
-
await
|
|
896
|
+
statusReporter.stop();
|
|
897
|
+
await foreman.close();
|
|
750
898
|
await viewer;
|
|
751
899
|
console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
|
|
752
900
|
console.log(`foreman: outcome — ${result.outcome}`);
|
|
753
901
|
if (result.detail)
|
|
754
902
|
console.log(`foreman: ${result.detail}`);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
903
|
+
if (result.outcome !== "all-done" && result.outcome !== "plan-complete") {
|
|
904
|
+
const executable = command.parent?.name() === "rafi" ? "rafi" : "ai-foreman";
|
|
905
|
+
const remainingSteps = Math.max(1, result.requested - result.completed);
|
|
906
|
+
for (const line of formatResumeGuidance(executable, cwd, remainingSteps, foreman.builderSessionId())) {
|
|
907
|
+
console.log(line);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
758
910
|
process.exit(result.outcome === "needs-human" ? 2 : 0);
|
|
759
911
|
}
|
|
760
912
|
catch (err) {
|
|
761
913
|
clearInterval(heartbeat);
|
|
914
|
+
statusReporter.stop();
|
|
762
915
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "failed", { status: "failed", failure: { category: "unknown", summary: err instanceof Error ? err.message.slice(0, 1000) : String(err).slice(0, 1000), at: new Date().toISOString() } }), "failed");
|
|
763
|
-
await
|
|
764
|
-
await builder.close().catch(() => { });
|
|
916
|
+
await foreman.close().catch(() => { });
|
|
765
917
|
log.write("error", { message: String(err) });
|
|
766
918
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
767
919
|
}
|
|
768
920
|
});
|
|
769
921
|
}
|
|
922
|
+
function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
923
|
+
return async (context) => {
|
|
924
|
+
if (noninteractive || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
925
|
+
return { action: "pause" };
|
|
926
|
+
while (true) {
|
|
927
|
+
const choice = await select({ message: `QA did not converge for ${context.ticket.id}. What next?`, options: [
|
|
928
|
+
{ value: "retry", label: "Retry another Builder fix" },
|
|
929
|
+
{ value: "pause", label: "Pause at this checkpoint" },
|
|
930
|
+
{ value: "waive", label: "Explicitly waive QA" },
|
|
931
|
+
{ value: "planner", label: "Discuss with read-only Planner" },
|
|
932
|
+
] });
|
|
933
|
+
if (isCancel(choice) || choice === "pause")
|
|
934
|
+
return { action: "pause" };
|
|
935
|
+
if (choice === "retry")
|
|
936
|
+
return { action: "retry" };
|
|
937
|
+
if (choice === "waive") {
|
|
938
|
+
const approved = await select({ message: "Waive QA and record validation_result=failed with all unresolved issues?", options: [
|
|
939
|
+
{ value: "no", label: "No (Recommended)" }, { value: "yes", label: "Yes, waive and continue" },
|
|
940
|
+
] });
|
|
941
|
+
if (approved === "yes") {
|
|
942
|
+
const db = new WorkflowDb(projectDir);
|
|
943
|
+
try {
|
|
944
|
+
const lease = db.currentLease();
|
|
945
|
+
if (lease) {
|
|
946
|
+
const detail = JSON.stringify({ decision: "waive", ticket: context.ticket.id, unresolved: context.history, at: new Date().toISOString() });
|
|
947
|
+
db.putEvidence("test", detail);
|
|
948
|
+
db.recordIssue(lease.runId, { code: "qa_nonconvergence", role: "qa", phase: "qa-waiver", ticket: context.ticket.id, detail, human_required: false, recoverable: false, suggested_action: "Review the explicit QA waiver before merging.", occurred_at: new Date().toISOString() });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
finally {
|
|
952
|
+
db.close();
|
|
953
|
+
}
|
|
954
|
+
console.log(`foreman: QA WAIVER recorded for ${context.ticket.id}; validation_result will be failed`);
|
|
955
|
+
return { action: "waive" };
|
|
956
|
+
}
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const discussion = await text({ message: "What should the Planner focus on?", defaultValue: "Explain the QA failures and propose the safest concrete remediation." });
|
|
960
|
+
if (isCancel(discussion))
|
|
961
|
+
continue;
|
|
962
|
+
const diff = execFileSync("git", ["-C", context.builderWorktree, "diff", "--binary", "HEAD"], { encoding: "utf8", maxBuffer: 128 * 1024 * 1024 });
|
|
963
|
+
let instruction = [
|
|
964
|
+
"You are the read-only Planner mediating QA nonconvergence. Do not edit files or run mutating tools.",
|
|
965
|
+
`Ticket: ${context.ticket.id} — ${context.ticket.title}`,
|
|
966
|
+
`Requirements: ${context.ticket.acceptance.join("; ")}`,
|
|
967
|
+
`Required tests: ${context.ticket.required_tests.join("; ")}`,
|
|
968
|
+
`User discussion: ${String(discussion)}`,
|
|
969
|
+
`Complete QA history:\n${JSON.stringify(context.history, null, 2)}`,
|
|
970
|
+
`Full Builder diff:\n${diff || "(no tracked diff)"}`,
|
|
971
|
+
"Return RAFI_QA_REMEDIATION_START, then one JSON object {\"summary\":\"...\",\"fix_instructions\":[\"...\"]}, then RAFI_QA_REMEDIATION_END.",
|
|
972
|
+
"End with STEP_STATUS: plan_complete | summary=\"QA remediation proposed\"",
|
|
973
|
+
].join("\n\n");
|
|
974
|
+
let resumeSessionId;
|
|
975
|
+
while (true) {
|
|
976
|
+
const run = await runRoleInstruction({
|
|
977
|
+
projectDir, role: "planner", instruction, label: "QA Planner remediation", agent: planner?.make,
|
|
978
|
+
model: explicitDefaultValue(planner?.model), effort: explicitEffort(planner?.reasoning), fast: planner?.fast,
|
|
979
|
+
resumeSessionId, permissionConfig: readOnlyPermissionConfig(), sandboxMode: "read-only",
|
|
980
|
+
logPath: makeRoleLogPath(projectDir, "qa-remediation"), logEvent: "rafi-plan",
|
|
981
|
+
});
|
|
982
|
+
if (run.turn.result.isError || run.turn.status.kind !== "plan_complete")
|
|
983
|
+
throw new Error(`Planner remediation failed: ${run.turn.status.error ?? run.turn.result.text.slice(0, 500)}`);
|
|
984
|
+
const match = /RAFI_QA_REMEDIATION_START\s*([\s\S]*?)\s*RAFI_QA_REMEDIATION_END/.exec(run.turn.result.text);
|
|
985
|
+
if (!match)
|
|
986
|
+
throw new Error("Planner remediation did not return the required structured proposal");
|
|
987
|
+
const proposal = JSON.parse(match[1].replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
|
|
988
|
+
if (!proposal.summary || !Array.isArray(proposal.fix_instructions) || !proposal.fix_instructions.every((item) => typeof item === "string"))
|
|
989
|
+
throw new Error("Planner remediation proposal is malformed");
|
|
990
|
+
console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
|
|
991
|
+
const decision = await select({ message: "Use this remediation for a new Builder fix session?", options: [
|
|
992
|
+
{ value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
|
|
993
|
+
] });
|
|
994
|
+
if (decision === "approve")
|
|
995
|
+
return { action: "remediate", remediation: [proposal.summary, ...proposal.fix_instructions].join("\n") };
|
|
996
|
+
if (isCancel(decision) || decision === "cancel")
|
|
997
|
+
break;
|
|
998
|
+
const feedback = await text({ message: "Planner revision feedback:" });
|
|
999
|
+
if (isCancel(feedback))
|
|
1000
|
+
break;
|
|
1001
|
+
resumeSessionId = run.sessionId;
|
|
1002
|
+
instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Keep the exact structured envelope and final plan_complete marker.`;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
770
1007
|
function readAgentDefaults(projectDir) {
|
|
771
1008
|
const path = join(projectDir, "rafi-config.yaml");
|
|
772
1009
|
if (!existsSync(path))
|
|
@@ -775,7 +1012,7 @@ function readAgentDefaults(projectDir) {
|
|
|
775
1012
|
const parsed = parseYaml(readFileSync(path, "utf8"));
|
|
776
1013
|
if (parsed?.agent_defaults?.version !== 1)
|
|
777
1014
|
return {};
|
|
778
|
-
return { builder: parsed.agent_defaults.roles.builder, qa: parsed.agent_defaults.roles.qa };
|
|
1015
|
+
return { revision: parsed.agent_defaults.revision, builder: parsed.agent_defaults.roles.builder, qa: parsed.agent_defaults.roles.qa, planner: parsed.agent_defaults.roles.planner };
|
|
779
1016
|
}
|
|
780
1017
|
catch {
|
|
781
1018
|
return {};
|
|
@@ -784,6 +1021,58 @@ function readAgentDefaults(projectDir) {
|
|
|
784
1021
|
function explicitDefaultValue(value) {
|
|
785
1022
|
return value && value !== "default" ? value : undefined;
|
|
786
1023
|
}
|
|
1024
|
+
/** Make the executable branch plan exactly match each explicit root-to-tip stack. */
|
|
1025
|
+
export function applyExplicitStackTopology(plan, stacks, delivery, branchPrefix) {
|
|
1026
|
+
const nodeByTicket = new Map(plan.nodes.map((node) => [node.ticket.id, node]));
|
|
1027
|
+
const unitById = new Map(delivery.units.map((unit) => [unit.id, unit]));
|
|
1028
|
+
const ordered = [];
|
|
1029
|
+
for (const stack of stacks) {
|
|
1030
|
+
let predecessorBranch = plan.baseRef;
|
|
1031
|
+
let predecessorActiveTicket;
|
|
1032
|
+
let missingPredecessorIdentity;
|
|
1033
|
+
let depth = 0;
|
|
1034
|
+
for (const unitId of stack.units) {
|
|
1035
|
+
const unit = unitById.get(unitId);
|
|
1036
|
+
if (!unit)
|
|
1037
|
+
throw new Error(`stack ${stack.id} references missing delivery unit ${unitId}`);
|
|
1038
|
+
const sharedBranch = unit.branch_mode === "shared" ? `${branchPrefix.replace(/^\/+|\/+$/g, "") || "rafi"}/${slug(unitId)}` : undefined;
|
|
1039
|
+
const unitBaseBranch = predecessorBranch;
|
|
1040
|
+
for (let ticketIndex = 0; ticketIndex < unit.tickets.length; ticketIndex++) {
|
|
1041
|
+
const ticketId = unit.tickets[ticketIndex];
|
|
1042
|
+
depth += unit.branch_mode === "shared" && ticketIndex > 0 ? 0 : 1;
|
|
1043
|
+
const node = nodeByTicket.get(ticketId);
|
|
1044
|
+
const recorded = stack.published_branches?.[ticketId];
|
|
1045
|
+
if (!node) {
|
|
1046
|
+
if (recorded || sharedBranch)
|
|
1047
|
+
predecessorBranch = recorded ?? sharedBranch;
|
|
1048
|
+
else
|
|
1049
|
+
missingPredecessorIdentity = ticketId;
|
|
1050
|
+
predecessorActiveTicket = undefined;
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
if (missingPredecessorIdentity)
|
|
1054
|
+
throw new Error(`partial stack ${stack.id} is missing the saved branch identity for completed predecessor ${missingPredecessorIdentity}`);
|
|
1055
|
+
const branch = sharedBranch ?? node.branch;
|
|
1056
|
+
ordered.push({
|
|
1057
|
+
...node, branch, baseBranch: sharedBranch ? unitBaseBranch : predecessorBranch,
|
|
1058
|
+
dependencies: predecessorActiveTicket ? [predecessorActiveTicket] : [], depth,
|
|
1059
|
+
...(unit.branch_mode === "shared" ? { deliveryUnitId: unit.id, deliveryUnitFinal: ticketIndex === unit.tickets.length - 1 } : {}),
|
|
1060
|
+
});
|
|
1061
|
+
predecessorBranch = branch;
|
|
1062
|
+
predecessorActiveTicket = ticketId;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (ordered.length !== plan.nodes.length)
|
|
1067
|
+
throw new Error("explicit stack topology did not assign every selected ticket exactly once");
|
|
1068
|
+
return { ...plan, nodes: ordered };
|
|
1069
|
+
}
|
|
1070
|
+
function slug(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group"; }
|
|
787
1071
|
function explicitEffort(value) {
|
|
788
1072
|
return value && ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
789
1073
|
}
|
|
1074
|
+
function shellQuote(value) {
|
|
1075
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(value)
|
|
1076
|
+
? value
|
|
1077
|
+
: `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1078
|
+
}
|
package/dist/cli/status.js
CHANGED
|
@@ -3,6 +3,8 @@ import { resolve, join } from "node:path";
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { findResumableBranchSessions, formatBranchContinueCommand } from "../branch/resume.js";
|
|
5
5
|
import { recoverableBuildRuns } from "../buildRuns.js";
|
|
6
|
+
import { WorkflowDb, WORKFLOW_DB_FILE } from "../workflowDb.js";
|
|
7
|
+
import { formatWorkflowIssue } from "../outcomes.js";
|
|
6
8
|
function fail(message) {
|
|
7
9
|
console.error(`foreman: ${message}`);
|
|
8
10
|
process.exit(1);
|
|
@@ -18,15 +20,24 @@ export function runStatus(project) {
|
|
|
18
20
|
const projectDir = resolve(project);
|
|
19
21
|
const dir = join(projectDir, ".foreman");
|
|
20
22
|
const recoverable = recoverableBuildRuns(projectDir);
|
|
21
|
-
|
|
23
|
+
const workflowPath = join(projectDir, WORKFLOW_DB_FILE);
|
|
24
|
+
const workflow = existsSync(workflowPath) ? new WorkflowDb(projectDir, workflowPath) : undefined;
|
|
25
|
+
const activeWorkflows = workflow?.activeRuns() ?? [];
|
|
26
|
+
if (!existsSync(dir) && activeWorkflows.length === 0) {
|
|
27
|
+
workflow?.close();
|
|
22
28
|
fail(`no foreman runs found under ${dir}`);
|
|
29
|
+
}
|
|
23
30
|
const logs = readdirSync(dir)
|
|
24
31
|
.filter((f) => f.endsWith(".jsonl"))
|
|
25
32
|
.sort();
|
|
26
33
|
if (logs.length === 0) {
|
|
27
|
-
if (recoverable.length === 0)
|
|
34
|
+
if (recoverable.length === 0 && activeWorkflows.length === 0) {
|
|
35
|
+
workflow?.close();
|
|
28
36
|
fail(`no foreman runs found under ${dir}`);
|
|
37
|
+
}
|
|
29
38
|
printRecoverable(projectDir, recoverable);
|
|
39
|
+
printWorkflowIssues(workflow, activeWorkflows);
|
|
40
|
+
workflow?.close();
|
|
30
41
|
return;
|
|
31
42
|
}
|
|
32
43
|
const latest = join(dir, logs[logs.length - 1]);
|
|
@@ -79,6 +90,18 @@ export function runStatus(project) {
|
|
|
79
90
|
}
|
|
80
91
|
if (recoverable.length > 0)
|
|
81
92
|
printRecoverable(projectDir, recoverable);
|
|
93
|
+
printWorkflowIssues(workflow, activeWorkflows);
|
|
94
|
+
workflow?.close();
|
|
95
|
+
}
|
|
96
|
+
function printWorkflowIssues(workflow, runs) {
|
|
97
|
+
if (!workflow || runs.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
console.log(`foreman: ${runs.length} active workflow record(s) in ${WORKFLOW_DB_FILE}`);
|
|
100
|
+
for (const run of runs) {
|
|
101
|
+
console.log(` ${run.runId} ${run.kind} ${run.status} checkpoint=${run.checkpoint}`);
|
|
102
|
+
for (const value of workflow.issues(run.runId))
|
|
103
|
+
console.log(` ${formatWorkflowIssue(value).replace(/\n/g, " | ")}`);
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
106
|
function printRecoverable(projectDir, runs) {
|
|
84
107
|
console.log(`foreman: ${runs.length} unfinished/recoverable build run(s):`);
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface PopulateCommandOptions {
|
|
|
22
22
|
sources?: string[];
|
|
23
23
|
fast?: boolean;
|
|
24
24
|
yes?: boolean;
|
|
25
|
+
authorizeRetire?: string[];
|
|
25
26
|
}
|
|
26
27
|
export declare function cmdPopulateCli(opts: PopulateCommandOptions): Promise<void>;
|
|
27
28
|
export declare function buildTicketsCommand(): Command;
|