ai-foreman 1.7.26 → 1.7.27
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.d.ts +12 -0
- package/dist/adapters/codex.js +50 -1
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/sessionFailure.d.ts +6 -0
- package/dist/adapters/sessionFailure.js +19 -0
- package/dist/adapters/types.d.ts +6 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +6 -2
- package/dist/buildApproval.js +2 -1
- package/dist/buildRuns.d.ts +12 -0
- package/dist/buildRuns.js +50 -0
- package/dist/buildWorkContext.js +1 -1
- package/dist/buildWorkMigration.d.ts +10 -0
- package/dist/buildWorkMigration.js +18 -2
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +164 -27
- package/dist/cli/state.js +39 -1
- package/dist/cli/tickets.d.ts +37 -0
- package/dist/cli/tickets.js +367 -12
- package/dist/config.js +1 -0
- package/dist/continuity.d.ts +1 -0
- package/dist/continuity.js +5 -0
- package/dist/foreman.js +89 -8
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.js +2 -1
- package/dist/handoffs.js +7 -19
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/permissions/policy.js +21 -0
- package/dist/planChanges.js +1 -1
- package/dist/projectDiagnostics.js +30 -0
- package/dist/providerQuestions.js +4 -24
- package/dist/qaBuildGate.d.ts +6 -1
- package/dist/qaBuildGate.js +76 -16
- package/dist/qaBuilderCoverage.d.ts +2 -1
- package/dist/qaBuilderCoverage.js +47 -5
- package/dist/qaBuilderEvidence.d.ts +18 -0
- package/dist/qaBuilderEvidence.js +60 -0
- package/dist/qaContractCoverage.d.ts +12 -1
- package/dist/qaContractCoverage.js +34 -2
- package/dist/qaContractDelivery.d.ts +2 -2
- package/dist/qaContractDelivery.js +88 -10
- package/dist/qaEffectiveConfig.js +2 -1
- package/dist/qaFailureDelivery.d.ts +23 -0
- package/dist/qaFailureDelivery.js +87 -6
- package/dist/qaHostFacts.d.ts +2 -0
- package/dist/qaHostFacts.js +11 -0
- package/dist/qaPreparation.d.ts +38 -1
- package/dist/qaPreparation.js +372 -31
- package/dist/qaPreparationInputs.js +5 -1
- package/dist/qaPreparationPolicy.d.ts +2 -1
- package/dist/qaPreparationPolicy.js +39 -9
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +73 -1
- package/dist/qaReview.js +35 -8
- package/dist/qaReviewEvidence.d.ts +6 -0
- package/dist/qaReviewEvidence.js +37 -0
- package/dist/qaRollbackRehearsal.d.ts +2 -0
- package/dist/qaRollbackRehearsal.js +78 -0
- package/dist/qaSnapshot.d.ts +3 -1
- package/dist/qaSnapshot.js +3 -2
- package/dist/qaVerificationContract.d.ts +9 -0
- package/dist/qaVerificationContract.js +143 -17
- package/dist/sources/sourceRegistry.js +16 -5
- package/dist/stateTransfer.d.ts +14 -0
- package/dist/stateTransfer.js +51 -0
- package/dist/statusReporter.d.ts +8 -0
- package/dist/statusReporter.js +5 -0
- package/dist/ticketPlanning.d.ts +373 -0
- package/dist/ticketPlanning.js +34 -3
- package/dist/ticketPopulation.d.ts +28 -3
- package/dist/ticketPopulation.js +125 -15
- package/dist/tickets/details.d.ts +2 -1
- package/dist/tickets/details.js +7 -3
- package/dist/tickets/eligibility.d.ts +6 -0
- package/dist/tickets/eligibility.js +12 -1
- package/dist/tickets/riskWaivers.d.ts +3 -0
- package/dist/tickets/riskWaivers.js +12 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +23 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketLoader.js +15 -2
- package/dist/tickets/ticketSchema.d.ts +14 -1
- package/dist/tickets/ticketSchema.js +2 -1
- package/dist/tickets/validate.js +11 -1
- package/dist/workflowDb.js +11 -2
- package/package.json +9 -4
package/dist/cli/start.js
CHANGED
|
@@ -31,8 +31,9 @@ import { loadRoleBundle } from "../roles.js";
|
|
|
31
31
|
import { ensureBuildRuntimeReadyForCommand } from "./runtimeAuthPrompt.js";
|
|
32
32
|
import { resolveAgentForProject } from "./runtimeSelection.js";
|
|
33
33
|
import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../tickets/config.js";
|
|
34
|
-
import { loadTickets } from "../tickets/ticketLoader.js";
|
|
34
|
+
import { hasRollbackOrMitigationPlan, loadTickets } from "../tickets/ticketLoader.js";
|
|
35
35
|
import { StateDb } from "../tickets/stateDb.js";
|
|
36
|
+
import { ticketDefinitionDigest } from "../tickets/riskWaivers.js";
|
|
36
37
|
import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
37
38
|
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
38
39
|
import { captureDirtyBaseline } from "../branch/dirtyBaseline.js";
|
|
@@ -40,7 +41,8 @@ import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
|
40
41
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
41
42
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
42
43
|
import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
|
|
43
|
-
import { buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, persistCurrentBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
44
|
+
import { branchTicketScopeProgress, buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishBranchTicketScope, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, persistCurrentBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
45
|
+
import { parseProviderContract, qaRemediationProposalV1Schema } from "rafi-spec";
|
|
44
46
|
import { parse as parseYaml } from "yaml";
|
|
45
47
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
46
48
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
@@ -51,6 +53,8 @@ import { fireTerminalBell } from "../notify.js";
|
|
|
51
53
|
import { WorkflowDb, readCurrentWorkflowLease } from "../workflowDb.js";
|
|
52
54
|
import { makeLogPath as makeRoleLogPath, readOnlyPermissionConfig, runRoleInstruction } from "../agentRun.js";
|
|
53
55
|
import { eligibleTickets, evaluateTicketEligibility } from "../tickets/eligibility.js";
|
|
56
|
+
import { globallyEligibleTickets } from "../tickets/eligibility.js";
|
|
57
|
+
import { cmdValidate } from "../tickets/commands.js";
|
|
54
58
|
import { BUILTIN_BRANCH_PREFIX, validateBranchPrefix } from "../branch/prefix.js";
|
|
55
59
|
import { captureCurrentWorkflowSessionIdentity, CurrentWorkflowChangedError, CurrentWorkflowGuardAdapter } from "../branch/currentGuard.js";
|
|
56
60
|
import { ContinuityAdapter, ContinuityRecoveryRequiredError, SessionUnavailableContinuityError } from "../continuity.js";
|
|
@@ -61,7 +65,7 @@ import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
|
61
65
|
import { resolveProviderSessionAvailability } from "../sessionAvailability.js";
|
|
62
66
|
import { ObservabilityStore, RunObserver } from "../observability.js";
|
|
63
67
|
import { loadProjectAutonomyConfig, resolveAutonomyPolicy, resolveQaEnablement } from "../recoveryPolicy.js";
|
|
64
|
-
import { loadQaRecoveryPacket, recoverPendingQaRecoveryPublications } from "../qaRecovery.js";
|
|
68
|
+
import { canonicalizeQaRecoveryRequirements, loadQaRecoveryPacket, recoverPendingQaRecoveryPublications } from "../qaRecovery.js";
|
|
65
69
|
import { qaDigest } from "../qaProtocolV2.js";
|
|
66
70
|
import { acceptQaRuntimeHandoff, describeQaRuntimeHandle, frozenQaRuntimeSettings } from "../qaRuntime.js";
|
|
67
71
|
const FOREMAN_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
@@ -79,7 +83,7 @@ function pendingQaRecoveryPacket(projectDir, runId, ticketId) {
|
|
|
79
83
|
const selected = ticketId ? pending.find((item) => item.ticketId === ticketId) : pending[0];
|
|
80
84
|
if (!selected || selected.pendingAction === "resolved")
|
|
81
85
|
return undefined;
|
|
82
|
-
const packet = loadQaRecoveryPacket(selected.packetPath);
|
|
86
|
+
const packet = canonicalizeQaRecoveryRequirements(loadQaRecoveryPacket(selected.packetPath));
|
|
83
87
|
const state = db.qaRecoveryHead(runId, packet.manifest.ticketId);
|
|
84
88
|
if (!state || packet.manifest.packetDigest !== state.packetDigest || packet.manifest.runId !== runId
|
|
85
89
|
|| packet.manifest.ticketId !== state.ticketId || packet.manifest.packetId !== state.packetId
|
|
@@ -423,6 +427,57 @@ export function validateStartOptions(opts, requireCount) {
|
|
|
423
427
|
if (opts.launchToken && !opts.preparationRun && !opts.recoverRun)
|
|
424
428
|
throw new Error("Launch token requires a preparation or recovery run");
|
|
425
429
|
}
|
|
430
|
+
/** A current waiver is an already-recorded decision: surface it, never demand a second start-time approval. */
|
|
431
|
+
function warnRiskPlanWaivers(projectDir, ticketIds) {
|
|
432
|
+
if (!ticketIds.length || !isTicketsInitialized(projectDir))
|
|
433
|
+
return;
|
|
434
|
+
const paths = resolveTicketPaths(loadTicketsConfig(projectDir), projectDir);
|
|
435
|
+
const definitions = new Map(loadTickets(paths.tickets).map((ticket) => [ticket.id, ticket]));
|
|
436
|
+
const db = new StateDb(paths.stateDb, { readOnly: true });
|
|
437
|
+
try {
|
|
438
|
+
const waived = ticketIds.filter((id) => {
|
|
439
|
+
const ticket = definitions.get(id);
|
|
440
|
+
return ticket && ticket.risk !== "Low" && !hasRollbackOrMitigationPlan(ticket.rollback)
|
|
441
|
+
&& Boolean(db.getTicketRiskWaiver(id, ticketDefinitionDigest(ticket)));
|
|
442
|
+
});
|
|
443
|
+
if (waived.length)
|
|
444
|
+
console.warn(`foreman: warning: ${waived.join(", ")} have explicitly waived missing rollback or mitigation plans.`);
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
db.close();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Preserve the distinction between a staged handoff and an unattempted acceptance. */
|
|
451
|
+
export function recordHandoffAcceptanceNotAttempted(projectDir, runId, role, generation, stage, detail) {
|
|
452
|
+
const db = new WorkflowDb(projectDir);
|
|
453
|
+
try {
|
|
454
|
+
db.appendContinuityEvent({
|
|
455
|
+
runId,
|
|
456
|
+
role: "host",
|
|
457
|
+
kind: "handoff_acceptance_not_attempted",
|
|
458
|
+
payload: { generation, role, stage, detail },
|
|
459
|
+
authoritativeStateRevision: db.continuityHead(runId, role)?.authoritativeStateRevision ?? 0,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
db.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** Default start selection is deliberately the same eligibility model users see in the ticket queue. */
|
|
467
|
+
export function selectDefaultExecutionTickets(tickets, states, implementationLimit, steps, delivery) {
|
|
468
|
+
return globallyEligibleTickets({ tickets, states, implementationLimit, delivery }).slice(0, steps).map((ticket) => ticket.id);
|
|
469
|
+
}
|
|
470
|
+
/** Fail before runtime/provider admission when canonical ticket data is unsafe. */
|
|
471
|
+
export function assertStartTicketPreflight(projectDir) {
|
|
472
|
+
const validation = cmdValidate(projectDir);
|
|
473
|
+
const errors = validation.issues.filter((issue) => issue.severity === "error");
|
|
474
|
+
const warnings = validation.issues.filter((issue) => issue.severity === "warn");
|
|
475
|
+
for (const issue of warnings)
|
|
476
|
+
console.warn(`foreman: warning: ${issue.message}`);
|
|
477
|
+
if (errors.length) {
|
|
478
|
+
throw new Error(`ticket validation must be resolved before starting:\n${errors.map((issue) => `- ${issue.message}`).join("\n")}\nRun \`rafi tickets validate\` after repair to confirm readiness.`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
426
481
|
/** Validate generated established recovery before reserving execution authority. */
|
|
427
482
|
export function validateRecoveryArguments(args, projectDir) {
|
|
428
483
|
if (args[0] !== "start" || canonicalProject(args[1] ?? "") !== canonicalProject(projectDir))
|
|
@@ -566,6 +621,17 @@ export function buildStartCommand(options = {}) {
|
|
|
566
621
|
let dirtyWork = recoveryRecord?.runDecisions?.dirtyWork;
|
|
567
622
|
if (opts.recoverRun && !recoveryRecord)
|
|
568
623
|
fail(`recoverable build run not found: ${opts.recoverRun}`);
|
|
624
|
+
// Both current-branch and branch delivery runs share this gate. A
|
|
625
|
+
// recovery resumes a previously admitted scope and is deliberately not
|
|
626
|
+
// reclassified by later tracker edits.
|
|
627
|
+
if (!recoveryRecord && isTicketsInitialized(cwd)) {
|
|
628
|
+
try {
|
|
629
|
+
assertStartTicketPreflight(cwd);
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
569
635
|
const autonomyProfile = opts.autonomy;
|
|
570
636
|
if (autonomyProfile && !["supervised", "balanced", "unattended"].includes(autonomyProfile))
|
|
571
637
|
fail("--autonomy must be supervised, balanced, or unattended");
|
|
@@ -756,7 +822,7 @@ export function buildStartCommand(options = {}) {
|
|
|
756
822
|
? requestedTicketIds.length > 0
|
|
757
823
|
? requestedTicketIds
|
|
758
824
|
: branchDefaults.branchMode && recoveryRecord
|
|
759
|
-
? [...recoveryRecord.tickets]
|
|
825
|
+
? branchTicketScopeProgress(cwd, recoveryRecord)?.remaining ?? [...recoveryRecord.tickets]
|
|
760
826
|
: recoveryRecord ? [currentBranchRecoveryTicket(cwd, recoveryRecord)].filter((ticket) => ticket !== undefined) : []
|
|
761
827
|
: [];
|
|
762
828
|
if (recoveryRecord && continueTickets.some(ticket => !recoveryRecord.tickets.includes(ticket)))
|
|
@@ -1249,7 +1315,12 @@ export function buildStartCommand(options = {}) {
|
|
|
1249
1315
|
const recoveryMode = String(opts.recoveryMode ?? "");
|
|
1250
1316
|
const effectiveSessionId = recoveryMode && recoveryMode !== "exact-session" ? undefined : sessionId;
|
|
1251
1317
|
let adapter = await createRecoveringBuilder(builderCwd, effectiveSessionId, effectiveSessionId ? sessionRef : undefined);
|
|
1252
|
-
|
|
1318
|
+
// The persisted recovery record is the authority for the staged
|
|
1319
|
+
// handoff's run. During QA-only recovery the active continuity ID is
|
|
1320
|
+
// assigned later than adapter construction, so gating acceptance on
|
|
1321
|
+
// that transient variable can silently skip the acceptance branch and
|
|
1322
|
+
// leave the staged generation pending until the post-run guard fails.
|
|
1323
|
+
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "builder" && recoveryRecord?.runId) {
|
|
1253
1324
|
const service = new HandoffService(cwd);
|
|
1254
1325
|
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
1255
1326
|
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(resolvedContinuitySettings("builder"), runtime)), handoffRecoveryOptions("builder"));
|
|
@@ -1566,10 +1637,26 @@ export function buildStartCommand(options = {}) {
|
|
|
1566
1637
|
// compatibility caller still supplies the previous raw ID.
|
|
1567
1638
|
const effectiveSessionId = undefined;
|
|
1568
1639
|
void sessionId;
|
|
1569
|
-
let adapter
|
|
1640
|
+
let adapter;
|
|
1641
|
+
try {
|
|
1642
|
+
adapter = await createRecoveringQa(qaCwd, effectiveSessionId);
|
|
1643
|
+
}
|
|
1644
|
+
catch (error) {
|
|
1645
|
+
// A staged QA handoff is intentionally preserved when the successor
|
|
1646
|
+
// cannot reach a validated session (for example, host runtime
|
|
1647
|
+
// preparation fails). Record the exact pre-acceptance boundary so a
|
|
1648
|
+
// later recovery can distinguish "not attempted" from a rejected
|
|
1649
|
+
// acknowledgement without weakening the acceptance gate.
|
|
1650
|
+
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "qa" && recoveryRecord?.runId) {
|
|
1651
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1652
|
+
recordHandoffAcceptanceNotAttempted(cwd, recoveryRecord.runId, "qa", pendingHandoffGeneration, "qa-runtime-preparation", detail);
|
|
1653
|
+
log.write("handoff-acceptance-not-attempted", { role: "qa", generation: pendingHandoffGeneration, stage: "qa-runtime-preparation", detail });
|
|
1654
|
+
}
|
|
1655
|
+
throw error;
|
|
1656
|
+
}
|
|
1570
1657
|
let explicitRecoveryAccepted = false;
|
|
1571
1658
|
let handoffReceipt = { kind: "initial" };
|
|
1572
|
-
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "qa" &&
|
|
1659
|
+
if (pendingHandoffGeneration !== undefined && pendingHandoffRole === "qa" && recoveryRecord?.runId) {
|
|
1573
1660
|
const service = new HandoffService(cwd);
|
|
1574
1661
|
const staged = service.loadStaged(recoveryRecord.runId, pendingHandoffGeneration);
|
|
1575
1662
|
const accepted = await service.acceptStagedWithRecovery(staged, adapter, (_handoff, runtime) => createQaForSettings(qaCwd, settingsForRequestedRuntime(resolvedContinuitySettings("qa"), runtime)), handoffRecoveryOptions("qa"));
|
|
@@ -1716,7 +1803,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1716
1803
|
cumulativeTotalTokens: nativeUsage.totalTokens, authoritativeCostUsd: nativeUsage.authoritativeCostUsd,
|
|
1717
1804
|
} } : {}),
|
|
1718
1805
|
compactionCount, compactMaximum: settings.compact_maximum ?? 10,
|
|
1719
|
-
resources: [{ label: "frozen-qa-action", content: frozenAction, authoritative: true, purpose: "Frozen QA action across the session boundary" }, ...(recovery ? recovery.resources.map((resource) => ({
|
|
1806
|
+
resources: [{ label: "frozen-qa-action", content: frozenAction, authoritative: true, purpose: "Frozen QA action across the session boundary" }, ...(recovery ? recovery.resources.filter((resource) => resource.requiredForRecovery).map((resource) => ({
|
|
1720
1807
|
label: resource.label, digest: resource.digest, authoritative: true, requiredForRecovery: resource.requiredForRecovery,
|
|
1721
1808
|
mediaType: resource.mediaType, path: resource.path, purpose: resource.purpose, bytes: resource.bytes,
|
|
1722
1809
|
})) : [])],
|
|
@@ -1822,14 +1909,31 @@ export function buildStartCommand(options = {}) {
|
|
|
1822
1909
|
if (continueTickets.length > 0) {
|
|
1823
1910
|
const sessions = findResumableBranchSessions(join(cwd, ".foreman"));
|
|
1824
1911
|
const sessionByTicket = new Map(sessions.map((session) => [session.ticket, session]));
|
|
1912
|
+
const historicalReader = new WorkflowReader(cwd);
|
|
1913
|
+
let historicalSessionTickets;
|
|
1914
|
+
try {
|
|
1915
|
+
historicalSessionTickets = new Set(historicalReader.branchResumeSessions(false).map((session) => session.ticket));
|
|
1916
|
+
}
|
|
1917
|
+
finally {
|
|
1918
|
+
historicalReader.close();
|
|
1919
|
+
}
|
|
1825
1920
|
const nodes = [];
|
|
1921
|
+
const freshTicketIds = [];
|
|
1826
1922
|
for (const ticketId of continueTickets) {
|
|
1827
1923
|
const session = sessionByTicket.get(ticketId);
|
|
1828
1924
|
if (!session) {
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1925
|
+
const ticketState = states.get(ticketId)?.status ?? "planned";
|
|
1926
|
+
if (opts.resume || !recoveryRecord || recoveryRecord.branchMode !== "per-ticket" || historicalSessionTickets.has(ticketId) || !["planned", "next"].includes(ticketState)) {
|
|
1927
|
+
fail([
|
|
1928
|
+
`no resumable branch session found for ticket ${ticketId}`,
|
|
1929
|
+
branchContinueTicketHelp(cwd),
|
|
1930
|
+
].join("\n"));
|
|
1931
|
+
}
|
|
1932
|
+
// This ticket has no prior branch/session evidence and remains in a
|
|
1933
|
+
// not-started tracker state. It is new work within the already
|
|
1934
|
+
// admitted original scope, not an interrupted session to replay.
|
|
1935
|
+
freshTicketIds.push(ticketId);
|
|
1936
|
+
continue;
|
|
1833
1937
|
}
|
|
1834
1938
|
const ticket = ticketById.get(ticketId);
|
|
1835
1939
|
if (!ticket)
|
|
@@ -1857,6 +1961,20 @@ export function buildStartCommand(options = {}) {
|
|
|
1857
1961
|
deliveryUnitFinal: session.deliveryUnitFinal,
|
|
1858
1962
|
});
|
|
1859
1963
|
}
|
|
1964
|
+
if (freshTicketIds.length) {
|
|
1965
|
+
const fresh = buildBranchPlan(tickets, states, {
|
|
1966
|
+
steps: freshTicketIds.length,
|
|
1967
|
+
baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
|
|
1968
|
+
branchPrefix,
|
|
1969
|
+
maxBranchDepth,
|
|
1970
|
+
rootBaseBranches: branchDefaults.rootBaseBranches,
|
|
1971
|
+
ticketIds: freshTicketIds,
|
|
1972
|
+
});
|
|
1973
|
+
if (fresh.issues.some(issue => issue.blocking) || fresh.nodes.length !== freshTicketIds.length || fresh.nodes.some(node => !freshTicketIds.includes(node.ticket.id))) {
|
|
1974
|
+
fail(`remaining branch tickets are not runnable as fresh approved work: ${fresh.issues.filter(issue => issue.blocking).map(issue => issue.message).join("; ") || "planner did not return the exact remaining scope"}`);
|
|
1975
|
+
}
|
|
1976
|
+
nodes.push(...fresh.nodes);
|
|
1977
|
+
}
|
|
1860
1978
|
plan = {
|
|
1861
1979
|
baseRef: nodes[0]?.baseRef ?? (opts.base ?? currentGitRef(cwd)),
|
|
1862
1980
|
nodes,
|
|
@@ -1939,6 +2057,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1939
2057
|
stacked: selectedStacks.length > 0,
|
|
1940
2058
|
resumed: continueTickets.length > 0 || Boolean(recoveryRecord),
|
|
1941
2059
|
});
|
|
2060
|
+
warnRiskPlanWaivers(cwd, plan.nodes.map((node) => node.ticket.id));
|
|
1942
2061
|
log.write("branch-plan", {
|
|
1943
2062
|
...branchPlanLogMetadata(plan, branchPresentation),
|
|
1944
2063
|
...(auditDependencyCount === undefined ? {} : { auditDependencyCount }),
|
|
@@ -2234,8 +2353,9 @@ export function buildStartCommand(options = {}) {
|
|
|
2234
2353
|
: `foreman: inspect recovery with: ${formatRecoveryCommand(cwd)}`);
|
|
2235
2354
|
process.exit(2);
|
|
2236
2355
|
}
|
|
2237
|
-
if (pendingHandoffGeneration !== undefined)
|
|
2238
|
-
throw new
|
|
2356
|
+
if (pendingHandoffGeneration !== undefined) {
|
|
2357
|
+
throw new HandoffRecoveryPausedError(recoveryRecord?.runId ?? invocationRunId, pendingHandoffGeneration, `Recovery handoff generation ${pendingHandoffGeneration} was not consumed; the staged handoff remains available for a subsequent validated recovery`);
|
|
2358
|
+
}
|
|
2239
2359
|
clearInterval(branchHeartbeat);
|
|
2240
2360
|
if (selectedStacks.length) {
|
|
2241
2361
|
let allComplete = true;
|
|
@@ -2256,9 +2376,9 @@ export function buildStartCommand(options = {}) {
|
|
|
2256
2376
|
console.log(allComplete ? "foreman: stack publication complete; delivery is awaiting_review (no merges were attempted)" : "foreman: partial stack prefix published; resume point is preserved");
|
|
2257
2377
|
}
|
|
2258
2378
|
masterRun = withRunLocalAcceptedRuntimes(masterRun);
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2379
|
+
// A targeted QA recovery returns one completed summary. Reconcile the
|
|
2380
|
+
// original admitted scope before declaring the master run complete.
|
|
2381
|
+
masterRun = finishBranchTicketScope(cwd, masterRun, summaries.every((row) => row.buildStatus === "done") ? "branch-partial-scope-complete" : "branch-run-interrupted");
|
|
2262
2382
|
activeObserver?.finish(masterRun.status, observationSummaryMetadata(cwd, masterRun, qaEnabled, capturedBranchBuilder.make, summaries.find(row => row.pr?.url)));
|
|
2263
2383
|
observabilityStore?.closeLogFile(logPath, masterRun.status);
|
|
2264
2384
|
observabilityStore?.enforceLimits();
|
|
@@ -2295,14 +2415,20 @@ export function buildStartCommand(options = {}) {
|
|
|
2295
2415
|
process.exit(failed ? 2 : 0);
|
|
2296
2416
|
}
|
|
2297
2417
|
if (!recoveryRecord && isTicketsInitialized(cwd)) {
|
|
2298
|
-
const
|
|
2418
|
+
const ticketsConfig = loadTicketsConfig(cwd);
|
|
2419
|
+
const paths = resolveTicketPaths(ticketsConfig, cwd);
|
|
2299
2420
|
const state = new StateDb(paths.stateDb);
|
|
2300
2421
|
try {
|
|
2301
|
-
|
|
2422
|
+
if (!executionTickets) {
|
|
2423
|
+
executionTickets = selectDefaultExecutionTickets(loadTickets(paths.tickets), state.getAllStates(), ticketsConfig.implementationLimit, steps, deliveryConfig);
|
|
2424
|
+
if (!executionTickets.length)
|
|
2425
|
+
fail("no eligible tickets are available to start; run `rafi tickets queue` to inspect blockers");
|
|
2426
|
+
}
|
|
2302
2427
|
}
|
|
2303
2428
|
finally {
|
|
2304
2429
|
state.close();
|
|
2305
2430
|
}
|
|
2431
|
+
warnRiskPlanWaivers(cwd, executionTickets);
|
|
2306
2432
|
}
|
|
2307
2433
|
if (grantedNetwork || opts.builderApprovals)
|
|
2308
2434
|
console.log(`foreman: requested Builder shell capabilities: network ${grantedNetwork ? "approved for this build" : "off"}; approvals ${opts.builderApprovals ? "per operation" : "never"}. Effective permissions will be verified with the provider.`);
|
|
@@ -2578,8 +2704,13 @@ export function buildStartCommand(options = {}) {
|
|
|
2578
2704
|
const qa = await (activeObserver
|
|
2579
2705
|
? activeObserver.span("qa_recovery", "Exact QA-only recovery", () => foreman.completePendingQaRecovery(ticketId))
|
|
2580
2706
|
: foreman.completePendingQaRecovery(ticketId));
|
|
2581
|
-
if (pendingHandoffGeneration !== undefined)
|
|
2582
|
-
|
|
2707
|
+
if (pendingHandoffGeneration !== undefined) {
|
|
2708
|
+
// QA can stop before allocating a provider session (for example
|
|
2709
|
+
// when host prerequisites are unavailable). Keep the staged
|
|
2710
|
+
// handoff recoverable and report that boundary explicitly; do not
|
|
2711
|
+
// turn an unattempted acceptance into a terminal build failure.
|
|
2712
|
+
throw new HandoffRecoveryPausedError(buildRun.runId, pendingHandoffGeneration, `QA handoff generation ${pendingHandoffGeneration} was not consumed because QA stopped before session acceptance; the handoff remains staged for recovery`);
|
|
2713
|
+
}
|
|
2583
2714
|
if (foreman.qaSessionId())
|
|
2584
2715
|
buildRun = persistBuildSession(cwd, buildRun, "qa", foreman.qaSessionRef() ?? foreman.qaSessionId());
|
|
2585
2716
|
buildRun = qa.outcome === "passed" || qa.outcome === "waived"
|
|
@@ -2679,8 +2810,9 @@ export function buildStartCommand(options = {}) {
|
|
|
2679
2810
|
};
|
|
2680
2811
|
const batch = () => foreman.runBatch(steps, trackerRelPath, onTicketStart, preferredTicket, executionTickets);
|
|
2681
2812
|
const result = await (activeObserver ? activeObserver.span("builder_work", "Builder and QA batch", batch) : batch());
|
|
2682
|
-
if (pendingHandoffGeneration !== undefined)
|
|
2683
|
-
throw new
|
|
2813
|
+
if (pendingHandoffGeneration !== undefined) {
|
|
2814
|
+
throw new HandoffRecoveryPausedError(buildRun.runId, pendingHandoffGeneration, `Recovery handoff generation ${pendingHandoffGeneration} was not consumed; the staged handoff remains available for a subsequent validated recovery`);
|
|
2815
|
+
}
|
|
2684
2816
|
if (foreman.builderSessionId())
|
|
2685
2817
|
buildRun = persistBuildSession(cwd, buildRun, "builder", foreman.builderAdapter().sessionRef?.() ?? foreman.builderSessionId());
|
|
2686
2818
|
if (foreman.qaSessionId())
|
|
@@ -3003,9 +3135,14 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
3003
3135
|
const match = /RAFI_QA_REMEDIATION_START\s*([\s\S]*?)\s*RAFI_QA_REMEDIATION_END/.exec(run.turn.result.text);
|
|
3004
3136
|
if (!match)
|
|
3005
3137
|
throw new Error("Planner remediation did not return the required structured proposal");
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3138
|
+
let parsed;
|
|
3139
|
+
try {
|
|
3140
|
+
parsed = JSON.parse(match[1].replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
|
|
3141
|
+
}
|
|
3142
|
+
catch (error) {
|
|
3143
|
+
throw new Error(`Planner remediation proposal is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3144
|
+
}
|
|
3145
|
+
const proposal = parseProviderContract(qaRemediationProposalV1Schema, parsed, "RAFI_QA_REMEDIATION");
|
|
3009
3146
|
console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
|
|
3010
3147
|
const decision = await ask(`QA remediation approval ${revision}: ${JSON.stringify(proposal)}`, [{ id: "approve", label: "Approve" }, { id: "revise", label: "Revise" }, { id: "cancel", label: "Return to choices" }], () => select({ message: "Use this remediation for a new Builder fix session?", options: [
|
|
3011
3148
|
{ value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
|
package/dist/cli/state.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { exportStateBundle, hasTransferableState, importStateBundle, inspectStateBundle } from "../stateTransfer.js";
|
|
3
|
+
import { exportStateBundle, hasTransferableState, importStateBundle, inspectBuildProjectIdentity, inspectStateBundle, rebindBuildProjectIdentity } from "../stateTransfer.js";
|
|
4
4
|
export function buildStateCommand(opts = {}) {
|
|
5
5
|
const command = new Command("state")
|
|
6
6
|
.description("Export, inspect, and import portable Rafi local state bundles.");
|
|
@@ -65,5 +65,43 @@ export function buildStateCommand(opts = {}) {
|
|
|
65
65
|
console.log(` previous state backup: ${result.backupDir}`);
|
|
66
66
|
console.log(" provider sessions are historical only; resume with `rafi build:resume --fresh-with-handoff` if exact session recovery fails");
|
|
67
67
|
});
|
|
68
|
+
command
|
|
69
|
+
.command("rebind")
|
|
70
|
+
.description("Inspect or explicitly rebind copied local build state to this checkout.")
|
|
71
|
+
.argument("[project]", "project directory", ".")
|
|
72
|
+
.option("-y, --yes", "confirm rebinding copied local state")
|
|
73
|
+
.action(async (project, options) => {
|
|
74
|
+
const root = opts.resolveProject ? opts.resolveProject(project) : resolve(project);
|
|
75
|
+
const status = inspectBuildProjectIdentity(root);
|
|
76
|
+
if (!status.recoveryDatabasePresent) {
|
|
77
|
+
console.log(`rafi state rebind: no local recovery database at ${status.currentRoot}; nothing to rebind`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!status.storedRoot) {
|
|
81
|
+
console.log(`rafi state rebind: local recovery database at ${status.currentRoot} predates project-root binding; nothing to rebind`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log("rafi state rebind:");
|
|
85
|
+
console.log(` stored path: ${status.storedRoot}`);
|
|
86
|
+
console.log(` current path: ${status.currentRoot}`);
|
|
87
|
+
if (status.matches) {
|
|
88
|
+
console.log(" status: already bound to this checkout");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(" status: copied or moved local build state");
|
|
92
|
+
console.log(" rebinding preserves local history and records the path change; it does not merge state from the original checkout.");
|
|
93
|
+
if (!options.yes) {
|
|
94
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
95
|
+
throw new Error("rebinding changes local build-state identity; rerun with --yes after reviewing the stored and current paths");
|
|
96
|
+
const { confirm, isCancel } = await import("@clack/prompts");
|
|
97
|
+
const approved = await confirm({ message: "Rebind this copied local build state to the current checkout?", initialValue: false });
|
|
98
|
+
if (isCancel(approved) || !approved) {
|
|
99
|
+
console.log("rafi state rebind: cancelled; no state changed");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
rebindBuildProjectIdentity(root);
|
|
104
|
+
console.log(`rafi state rebind: rebound copied local state to ${status.currentRoot}`);
|
|
105
|
+
});
|
|
68
106
|
return command;
|
|
69
107
|
}
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -5,7 +5,29 @@ import type { EffortLevel } from "../adapters/types.js";
|
|
|
5
5
|
import { type RoleBuilderOptions } from "../agentRun.js";
|
|
6
6
|
import type { TicketsConfig } from "../tickets/config.js";
|
|
7
7
|
import { type TicketPopulateRuntimeMode, type TicketSourceConfig, type TicketsSetupConfig } from "../tickets/setupConfig.js";
|
|
8
|
+
/** Commander preserves comma-delimited values as one argument; accept both documented forms. */
|
|
9
|
+
export declare function normalizeRiskWaiverTicketIds(values: readonly string[] | undefined): string[];
|
|
10
|
+
/** Record an explicit, definition-bound exception for existing risk tickets. */
|
|
11
|
+
export declare function waiveTicketRiskNotes(projectDir: string, ticketIds: string[], reason: string, actor?: string): void;
|
|
12
|
+
export interface RiskNoteRepairProposal {
|
|
13
|
+
version: 1;
|
|
14
|
+
tickets: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
rollback: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare function extractRiskNoteRepairProposal(output: string): RiskNoteRepairProposal;
|
|
20
|
+
/** Validate a deliberately narrow repair: only exactly the missing rollback fields may change. */
|
|
21
|
+
export declare function applyRiskNoteRepairProposal(tickets: import("../tickets/ticketSchema.js").TicketDef[], proposal: RiskNoteRepairProposal): import("../tickets/ticketSchema.js").TicketDef[];
|
|
8
22
|
export declare function buildPopulateInstruction(approvedPlanPath: string, contextSources?: string[], progressDoc?: string): string;
|
|
23
|
+
/** A bounded correction loop ended without a publishable proposal. */
|
|
24
|
+
export declare class TicketPopulationRiskCorrectionExhaustedError extends Error {
|
|
25
|
+
readonly ticketIds: string[];
|
|
26
|
+
readonly projectDir: string;
|
|
27
|
+
readonly interviewId: string;
|
|
28
|
+
readonly proposalFile: string;
|
|
29
|
+
constructor(ticketIds: string[], projectDir: string, interviewId: string, proposalFile: string);
|
|
30
|
+
}
|
|
9
31
|
export declare function resolvePopulateSources(projectDir: string, explicitSources: string[] | undefined, ticketsConfig: TicketsConfig): string[] | undefined;
|
|
10
32
|
export declare function buildPopulateAgentRunOptions(opts: {
|
|
11
33
|
projectDir: string;
|
|
@@ -27,10 +49,25 @@ interface PopulateCommandOptions {
|
|
|
27
49
|
fast?: boolean;
|
|
28
50
|
yes?: boolean;
|
|
29
51
|
authorizeRetire?: string[];
|
|
52
|
+
waiveRiskNotes?: string[];
|
|
53
|
+
waiverReason?: string;
|
|
54
|
+
waiverActor?: string;
|
|
30
55
|
resumeSession?: string;
|
|
56
|
+
proposalFile?: string;
|
|
31
57
|
}
|
|
32
58
|
export declare function combineRequirementSelections(selected: string[], additions: string): string[];
|
|
33
59
|
export declare function cmdPopulateCli(opts: PopulateCommandOptions): Promise<void>;
|
|
60
|
+
interface RepairRiskNotesOptions {
|
|
61
|
+
project?: string;
|
|
62
|
+
agent?: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
effort?: string;
|
|
65
|
+
fast?: boolean;
|
|
66
|
+
apply?: boolean;
|
|
67
|
+
yes?: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Ask Ticket Maker for a reviewable, rollback-only repair of published tickets. */
|
|
70
|
+
export declare function cmdRepairRiskNotesCli(opts: RepairRiskNotesOptions): Promise<void>;
|
|
34
71
|
/** Resolve only the strategy. Context documents never select an import mode. */
|
|
35
72
|
export declare function resolvePopulationMode(setup: TicketsSetupConfig | undefined): TicketPopulateRuntimeMode;
|
|
36
73
|
export interface PopulationContextSelection {
|