ai-foreman 1.7.29 → 2.0.1
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 +23 -12
- package/dist/adapters/claude.js +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/git.js +3 -8
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +28 -9
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +127 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
package/dist/cli/start.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativePolicy, operationInstruction, operationPayload, structuredTurnErrorMessage } from "../structuredOperation.js";
|
|
1
2
|
import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "../qaEffectiveConfig.js";
|
|
2
3
|
import { activateQaRecoveryBuilder } from "../qaBuilderActivation.js";
|
|
3
4
|
import { graphContext, sendGraphTurn } from "../graph/turn.js";
|
|
@@ -10,8 +11,8 @@ import { durableReadOnlyProposal } from "../readOnlyProposal.js";
|
|
|
10
11
|
import { captureFrozenQaSource } from "../qaSnapshot.js";
|
|
11
12
|
import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
|
|
12
13
|
import { assertBuildAssignmentReconciled, isInterruptedBuilderRecovery, assertInterruptedSourceSnapshot } from "../buildAssignment.js";
|
|
13
|
-
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
14
|
-
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate, validatePlanApprovalBinding } from "../buildApproval.js";
|
|
14
|
+
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, reusableCapabilityDecision, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
15
|
+
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate, validatePlanApprovalBinding, validateFrozenWorkApprovalBinding } from "../buildApproval.js";
|
|
15
16
|
import { Command, Option } from "commander";
|
|
16
17
|
import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
17
18
|
import { resolve, join, relative } from "node:path";
|
|
@@ -34,7 +35,7 @@ import { isTicketsInitialized, loadTicketsConfig, resolveTicketPaths } from "../
|
|
|
34
35
|
import { hasRollbackOrMitigationPlan, loadTickets } from "../tickets/ticketLoader.js";
|
|
35
36
|
import { StateDb } from "../tickets/stateDb.js";
|
|
36
37
|
import { ticketDefinitionDigest } from "../tickets/riskWaivers.js";
|
|
37
|
-
import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan
|
|
38
|
+
import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan } from "../branch/planner.js";
|
|
38
39
|
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
39
40
|
import { captureDirtyBaseline } from "../branch/dirtyBaseline.js";
|
|
40
41
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
@@ -42,7 +43,7 @@ import { preflightGlab } from "../branch/gitlab.js";
|
|
|
42
43
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
43
44
|
import { branchPlanLogMetadata, presentBranchPlan } from "../branch/presentation.js";
|
|
44
45
|
import { branchTicketScopeProgress, buildRunSessionBinding, checkpointBuildRun, completeBuildRun, createBuildRun, currentBranchRecoveryTicket, currentBranchTicketProgress, recoveryExecutionTickets, finishBranchTicketScope, finishRecoveredTicketScope, heartbeatBuildRun, persistBuildSession, persistCurrentBuildSession, projectBuildRecovery, readBuildRuns, releaseBuildLease, resumeBuildRun } from "../buildRuns.js";
|
|
45
|
-
import {
|
|
46
|
+
import { branchAuditOutput, plannerRemediationOutput } from "rafi-spec";
|
|
46
47
|
import { parse as parseYaml } from "yaml";
|
|
47
48
|
import { findResumableBranchSessions, formatBranchContinueCommand, formatBranchSummaryFollowupCommands, } from "../branch/resume.js";
|
|
48
49
|
import { detectGitProvider, loadTicketSetupConfig } from "../tickets/setupConfig.js";
|
|
@@ -539,7 +540,7 @@ export function buildStartCommand(options = {}) {
|
|
|
539
540
|
.option("--accept-handoff-role <role>", "role owning the pre-staged handoff (builder | qa)", "builder")
|
|
540
541
|
.option("--continue", "resume the most recent logged session for this project")
|
|
541
542
|
.option("-t, --tickets <path>", "path to ticket file (.md, .txt, .yaml, …) — passed to the builder as context")
|
|
542
|
-
.option("-y, --yes", "
|
|
543
|
+
.option("-y, --yes", "never stop to ask: approve the pre-flight plan, plan updates, and capabilities requested by flag (--builder-network, --qa-network)")
|
|
543
544
|
.option("--effort <level>", "reasoning effort level (low|medium|high|xhigh)")
|
|
544
545
|
.option("--fast", "use native fast mode when available; keep the model and reasoning settings")
|
|
545
546
|
.option("--no-qa", "disable per-ticket QA review (enabled by default)")
|
|
@@ -564,13 +565,15 @@ export function buildStartCommand(options = {}) {
|
|
|
564
565
|
.option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
|
|
565
566
|
.option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
|
|
566
567
|
.option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
|
|
567
|
-
.option("--builder-network", "request build-scoped builder internet access
|
|
568
|
+
.option("--builder-network", "request build-scoped builder internet access; asks for approval unless --yes; rechecked on resume")
|
|
569
|
+
.option("--qa-network", "allow independent QA network access for this build (e.g. live local services); asks for approval unless --yes; rechecked on resume")
|
|
568
570
|
.option("--builder-approvals", "allow the builder to request explicit per-operation sandbox approvals; QA stays restricted")
|
|
569
571
|
.option("--keep-worktrees", "keep successful ticket worktrees for inspection")
|
|
570
572
|
.option("--dirty-work <mode>", "preserve | include: continue in isolated branches without merging into the dirty checkout")
|
|
571
573
|
.option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
|
|
572
574
|
.option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
|
|
573
575
|
.action(async (project, opts, command) => withBuildInvocation(async () => {
|
|
576
|
+
let waitingForDecision = false;
|
|
574
577
|
try {
|
|
575
578
|
validateStartOptions(opts, false);
|
|
576
579
|
}
|
|
@@ -666,18 +669,26 @@ export function buildStartCommand(options = {}) {
|
|
|
666
669
|
return;
|
|
667
670
|
}
|
|
668
671
|
const requestedNetwork = Boolean(opts.builderNetwork || recoveryRecord?.builderCapabilities?.requestedNetwork);
|
|
672
|
+
// --yes never stops to ask: it approves capabilities the operator
|
|
673
|
+
// requested by flag. A resumed run counts only an explicit --yes.
|
|
674
|
+
const explicitYes = recoveryRecord ? recoveryRecord.recoveryDecision?.planUpdateApprovalSource === "explicit-yes" : Boolean(opts.yes);
|
|
669
675
|
let grantedNetwork = false;
|
|
670
|
-
|
|
676
|
+
let networkDecisionId;
|
|
677
|
+
if (requestedNetwork && explicitYes) {
|
|
678
|
+
grantedNetwork = true;
|
|
679
|
+
console.error("foreman: builder internet for this build approved by --yes (requested with --builder-network)");
|
|
680
|
+
}
|
|
681
|
+
else if (requestedNetwork) {
|
|
671
682
|
const permissionDb = new WorkflowDb(cwd);
|
|
672
683
|
let networkKey;
|
|
673
684
|
try {
|
|
674
|
-
networkKey = permissionDb
|
|
685
|
+
networkKey = reusableCapabilityDecision(permissionDb, supervisedInvocationId, "builder-network:", recoveryRecord?.builderCapabilities?.decisionId) ?? `builder-network:${process.pid}:${randomUUID()}`;
|
|
675
686
|
}
|
|
676
687
|
finally {
|
|
677
688
|
permissionDb.close();
|
|
678
689
|
}
|
|
679
|
-
const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey,
|
|
680
|
-
prompt: "Allow the builder shell to access the internet for this build? This permits network use by any builder command, not only downloads. Workspace write confinement remains enforced. QA stays offline. Recovery requires approval again
|
|
690
|
+
const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey, onAnswered: decision => { networkDecisionId = decision.decisionId; },
|
|
691
|
+
prompt: "Allow the builder shell to access the internet for this build? This permits network use by any builder command, not only downloads. Workspace write confinement remains enforced. QA stays offline unless --qa-network is separately approved. Recovery requires approval again unless resumed with --yes.",
|
|
681
692
|
choices: [{ id: "allow-network", label: "Allow builder internet for this build" }, { id: "stay-offline", label: "Stay offline and use a verified local bundle" }],
|
|
682
693
|
operation: async () => {
|
|
683
694
|
const action = await select({ message: "Allow builder internet for this build?", options: [{ value: "stay-offline", label: "Stay offline" }, { value: "allow-network", label: "Allow builder internet for this build" }] });
|
|
@@ -687,7 +698,37 @@ export function buildStartCommand(options = {}) {
|
|
|
687
698
|
return;
|
|
688
699
|
grantedNetwork = networkDecision === "allow-network";
|
|
689
700
|
}
|
|
690
|
-
const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}) };
|
|
701
|
+
const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}), ...(networkDecisionId ? { decisionId: networkDecisionId } : {}) };
|
|
702
|
+
// Independent QA is offline unless the operator grants --qa-network, e.g.
|
|
703
|
+
// so QA can verify a live local service the ticket requires.
|
|
704
|
+
const requestedQaNetwork = Boolean(opts.qaNetwork || recoveryRecord?.qaCapabilities?.requestedNetwork);
|
|
705
|
+
let grantedQaNetwork = false;
|
|
706
|
+
let qaNetworkDecisionId;
|
|
707
|
+
if (requestedQaNetwork && explicitYes) {
|
|
708
|
+
grantedQaNetwork = true;
|
|
709
|
+
console.error("foreman: QA network for this build approved by --yes (requested with --qa-network)");
|
|
710
|
+
}
|
|
711
|
+
else if (requestedQaNetwork) {
|
|
712
|
+
const permissionDb = new WorkflowDb(cwd);
|
|
713
|
+
let qaNetworkKey;
|
|
714
|
+
try {
|
|
715
|
+
qaNetworkKey = reusableCapabilityDecision(permissionDb, supervisedInvocationId, "qa-network:", recoveryRecord?.qaCapabilities?.decisionId) ?? `qa-network:${process.pid}:${randomUUID()}`;
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
718
|
+
permissionDb.close();
|
|
719
|
+
}
|
|
720
|
+
const qaNetworkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: qaNetworkKey, onAnswered: decision => { qaNetworkDecisionId = decision.decisionId; },
|
|
721
|
+
prompt: "Allow independent QA shell commands to access the network for this build? This lets QA run live checks (for example against a local service). QA keeps a read-only workspace. Recovery requires approval again unless resumed with --yes.",
|
|
722
|
+
choices: [{ id: "allow-qa-network", label: "Allow QA network for this build" }, { id: "qa-offline", label: "Keep QA offline" }],
|
|
723
|
+
operation: async () => {
|
|
724
|
+
const action = await select({ message: "Allow QA network for this build?", options: [{ value: "qa-offline", label: "Keep QA offline" }, { value: "allow-qa-network", label: "Allow QA network for this build" }] });
|
|
725
|
+
return isCancel(action) ? undefined : action;
|
|
726
|
+
} });
|
|
727
|
+
if (qaNetworkDecision === undefined || isCancel(qaNetworkDecision))
|
|
728
|
+
return;
|
|
729
|
+
grantedQaNetwork = qaNetworkDecision === "allow-qa-network";
|
|
730
|
+
}
|
|
731
|
+
const qaCapabilities = { requestedNetwork: requestedQaNetwork, grantedNetwork: grantedQaNetwork, ...(grantedQaNetwork ? { approvedAt: new Date().toISOString() } : {}), ...(qaNetworkDecisionId ? { decisionId: qaNetworkDecisionId } : {}) };
|
|
691
732
|
const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
|
|
692
733
|
const autoApprovePlanUpdates = recoveryRecord
|
|
693
734
|
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
@@ -699,9 +740,11 @@ export function buildStartCommand(options = {}) {
|
|
|
699
740
|
// recovery remains fail-closed on an exact current-plan approval.
|
|
700
741
|
if (recoveryRecord && autoApprovePlanUpdates && recoveryTickets.length > 0) {
|
|
701
742
|
const decision = recoveryRecord.recoveryDecision;
|
|
702
|
-
if (!decision?.planApproval)
|
|
743
|
+
if (!decision?.planApproval && !decision?.frozenWorkApproval)
|
|
703
744
|
fail("saved --yes approval has no exact plan binding; rerun rafi build:resume --yes to approve the current plan");
|
|
704
|
-
const approvalCheck =
|
|
745
|
+
const approvalCheck = decision.frozenWorkApproval
|
|
746
|
+
? validateFrozenWorkApprovalBinding(cwd, recoveryRecord.runId, recoveryTickets, decision.frozenWorkApproval)
|
|
747
|
+
: validatePlanApprovalBinding(cwd, recoveryTickets, decision.planApproval);
|
|
705
748
|
if (!approvalCheck.valid)
|
|
706
749
|
fail(`saved --yes approval is stale (${approvalCheck.reason}); rerun rafi build:resume --yes before provider work`);
|
|
707
750
|
}
|
|
@@ -1098,6 +1141,9 @@ export function buildStartCommand(options = {}) {
|
|
|
1098
1141
|
: base;
|
|
1099
1142
|
const handoffRecoveryOptions = (role) => ({
|
|
1100
1143
|
allowProviderSwitch: true,
|
|
1144
|
+
// --yes never stops to ask: retry with a fresh successor. The handoff
|
|
1145
|
+
// still pauses after three rejected successors.
|
|
1146
|
+
...(explicitYes ? { choose: async () => "retry" } : {}),
|
|
1101
1147
|
desktopNotifications: config.notifications.enabled,
|
|
1102
1148
|
terminalBell: config.notifications.terminal_bell,
|
|
1103
1149
|
onAccepted: (accepted) => {
|
|
@@ -1182,6 +1228,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1182
1228
|
configRoot: cwd,
|
|
1183
1229
|
sessionRole: "qa",
|
|
1184
1230
|
sessionStream: "qa",
|
|
1231
|
+
networkAccess: grantedQaNetwork,
|
|
1185
1232
|
runtimeExecutable: ready.executable,
|
|
1186
1233
|
runtimePhase: "qa",
|
|
1187
1234
|
preparationTimeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
@@ -1305,7 +1352,6 @@ export function buildStartCommand(options = {}) {
|
|
|
1305
1352
|
try {
|
|
1306
1353
|
const revision = countDb.continuityHead(runId, "builder")?.authoritativeStateRevision ?? settings.settings_revision;
|
|
1307
1354
|
countDb.appendContinuityEvent({ runId, role: "builder", kind: "builder_requested_handoff_delta", payload: request.delta, authoritativeStateRevision: revision });
|
|
1308
|
-
countDb.publishContinuityCheckpoint({ runId, role: "builder", delta: request.delta, authoritativeStateRevision: revision });
|
|
1309
1355
|
compactionCount = countDb.successfulCompactionCount(runId, "builder", predecessor.sessionRef?.() ?? sessionId);
|
|
1310
1356
|
}
|
|
1311
1357
|
finally {
|
|
@@ -1318,6 +1364,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1318
1364
|
predecessorSessionId: sessionId,
|
|
1319
1365
|
predecessorSessionRef: predecessor.sessionRef?.(),
|
|
1320
1366
|
requestedByBuilder: true,
|
|
1367
|
+
predecessorDispatchId: request.predecessorDispatchId,
|
|
1321
1368
|
roleState: {
|
|
1322
1369
|
...request.roleState,
|
|
1323
1370
|
frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"),
|
|
@@ -1334,7 +1381,8 @@ export function buildStartCommand(options = {}) {
|
|
|
1334
1381
|
resources: [{ label: "frozen-action", content: frozenAction, authoritative: true }],
|
|
1335
1382
|
}, (_handoff, runtime) => createBuilderForSettings(builderCwd, settingsForRequestedRuntime(effectiveSettings, runtime)), handoffRecoveryOptions("builder"));
|
|
1336
1383
|
log.write("handoff-transfer", {
|
|
1337
|
-
requestedByBuilder: true,
|
|
1384
|
+
requestedByBuilder: true,
|
|
1385
|
+
predecessorDispatchId: request.predecessorDispatchId, reason: request.reason, generation: transfer.manifest.generation,
|
|
1338
1386
|
predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
|
|
1339
1387
|
acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest,
|
|
1340
1388
|
});
|
|
@@ -1452,6 +1500,7 @@ export function buildStartCommand(options = {}) {
|
|
|
1452
1500
|
configRoot: cwd,
|
|
1453
1501
|
sessionRole: "qa",
|
|
1454
1502
|
sessionStream: "qa",
|
|
1503
|
+
networkAccess: grantedQaNetwork,
|
|
1455
1504
|
runtimeExecutable: ready.executable,
|
|
1456
1505
|
runtimePhase: "qa",
|
|
1457
1506
|
preparationTimeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
@@ -2027,10 +2076,13 @@ export function buildStartCommand(options = {}) {
|
|
|
2027
2076
|
try {
|
|
2028
2077
|
auditBuilder = await createBuilder(cwd);
|
|
2029
2078
|
auditViewer = printEvents(auditBuilder.events());
|
|
2030
|
-
const audit = await sendGraphTurn(auditBuilder, buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket))
|
|
2079
|
+
const audit = await sendGraphTurn(auditBuilder, `${buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket))}\n${operationInstruction(branchAuditOutput)}`, graphContext(cwd, auditBuilder.sessionRef?.()?.cwd ?? cwd, "branch-dependency-audit", plan.nodes.map(node => node.ticket.id).join(" ")), nativePolicy(branchAuditOutput, { purpose: "planning", journalRunId: auditRunId }));
|
|
2031
2080
|
if (audit.isError)
|
|
2032
|
-
throw new Error(audit
|
|
2033
|
-
const auditDependencies =
|
|
2081
|
+
throw new Error(structuredTurnErrorMessage(audit, "Branch dependency audit"));
|
|
2082
|
+
const auditDependencies = operationPayload(audit, branchAuditOutput).dependencies;
|
|
2083
|
+
const selectedIds = new Set(plan.nodes.map(node => node.ticket.id));
|
|
2084
|
+
if (auditDependencies.some(dep => dep.ticket === dep.dependsOn || !selectedIds.has(dep.ticket) || !selectedIds.has(dep.dependsOn)))
|
|
2085
|
+
throw new Error("Branch audit returned a foreign or self dependency");
|
|
2034
2086
|
plan = buildBranchPlan(tickets, states, {
|
|
2035
2087
|
steps,
|
|
2036
2088
|
baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch ?? currentGitRef(cwd),
|
|
@@ -2148,7 +2200,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2148
2200
|
&& (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
|
|
2149
2201
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(branchQaOnlyRecovery ? {} : { builder: capturedBranchBuilder, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }), qa: capturedBranchQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
2150
2202
|
runId: invocationRunId,
|
|
2151
|
-
builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
2203
|
+
builderCapabilities, qaCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
2152
2204
|
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
2153
2205
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
2154
2206
|
runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
@@ -2182,7 +2234,7 @@ export function buildStartCommand(options = {}) {
|
|
|
2182
2234
|
}
|
|
2183
2235
|
for (const node of plan.nodes)
|
|
2184
2236
|
admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
|
|
2185
|
-
masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
|
|
2237
|
+
masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities, qaCapabilities });
|
|
2186
2238
|
const qaResumedRecovery = recoveryRecord && !isInterruptedBuilderRecovery(recoveryRecord) ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
2187
2239
|
let qaProtocolResumeTicket;
|
|
2188
2240
|
if (recoveryRecord && !qaResumedRecovery && !isInterruptedBuilderRecovery(recoveryRecord)) {
|
|
@@ -2506,11 +2558,11 @@ export function buildStartCommand(options = {}) {
|
|
|
2506
2558
|
&& (opts.qaRevision !== undefined || pendingQaRecoveryPacket(cwd, recoveryRecord.runId, requestedTicketIds[0])));
|
|
2507
2559
|
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { ...(qaOnlyRecovery ? {} : { builder: capturedBuilder, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }), qa: capturedQa, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
2508
2560
|
runId: invocationRunId,
|
|
2509
|
-
tickets: [...(executionTickets ?? [])], builderCapabilities, authorizedBatch: { tickets: [...(executionTickets ?? [])], requestedSteps: steps, scopeRevision: buildScopeRevision(cwd, approvalConsequences), startedTickets: [] }, repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
2561
|
+
tickets: [...(executionTickets ?? [])], builderCapabilities, qaCapabilities, authorizedBatch: { tickets: [...(executionTickets ?? [])], requestedSteps: steps, scopeRevision: buildScopeRevision(cwd, approvalConsequences), startedTickets: [] }, repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
2510
2562
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
2511
2563
|
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
2512
2564
|
});
|
|
2513
|
-
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
|
|
2565
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities, qaCapabilities });
|
|
2514
2566
|
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
2515
2567
|
activeContinuityRunId = buildRun.runId;
|
|
2516
2568
|
publishActivatedBuilderSession = ref => { buildRun = persistCurrentBuildSession(cwd, buildRun, "builder", ref); };
|
|
@@ -2981,11 +3033,16 @@ export function buildStartCommand(options = {}) {
|
|
|
2981
3033
|
if (!(error instanceof HumanDecisionRequired))
|
|
2982
3034
|
throw error;
|
|
2983
3035
|
console.error(error.message);
|
|
2984
|
-
|
|
3036
|
+
waitingForDecision = true;
|
|
2985
3037
|
}
|
|
2986
3038
|
finally {
|
|
2987
3039
|
finishStartAdmission(cwd, supervisedInvocationId);
|
|
2988
3040
|
}
|
|
3041
|
+
// Exit explicitly: supervisor IPC, heartbeats and runtime handles keep a
|
|
3042
|
+
// supervised worker alive, so it would otherwise wait forever while its
|
|
3043
|
+
// supervisor waits for it, and `rafi resume` refuses the "live" owner.
|
|
3044
|
+
if (waitingForDecision)
|
|
3045
|
+
process.exit(2);
|
|
2989
3046
|
}));
|
|
2990
3047
|
}
|
|
2991
3048
|
function ticketSizeBucket(projectDir, ticketIds) {
|
|
@@ -3146,8 +3203,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
3146
3203
|
`User discussion: ${String(discussion)}`,
|
|
3147
3204
|
`Complete QA history:\n${JSON.stringify(context.history, null, 2)}`,
|
|
3148
3205
|
`${diffTruncated ? "Builder diff (TRUNCATED at 128 KiB; inspect the actual worktree before relying on it)" : "Full Builder diff"}:\n${diff || "(no tracked diff)"}`,
|
|
3149
|
-
"
|
|
3150
|
-
"End with STEP_STATUS: plan_complete | summary=\"QA remediation proposed\"",
|
|
3206
|
+
"Submit the native planner-remediation payload with summary and fix_instructions.",
|
|
3151
3207
|
].join("\n\n");
|
|
3152
3208
|
let resumeSessionId;
|
|
3153
3209
|
let revision = 0;
|
|
@@ -3155,24 +3211,14 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
3155
3211
|
revision++;
|
|
3156
3212
|
const proposalDigest = createHash("sha256").update(JSON.stringify([scope, round, revision, instruction, planner, captureFrozenQaSource(context.builderWorktree).digest])).digest("hex");
|
|
3157
3213
|
const run = await durableReadOnlyProposal({ projectDir, runId, ticketId: context.ticket.id, digest: proposalDigest, operation: () => runRoleInstruction({
|
|
3158
|
-
projectDir, role: "planner", instruction, label: "QA Planner remediation", agent: planner?.make,
|
|
3214
|
+
projectDir, role: "planner", instruction, output: plannerRemediationOutput, label: "QA Planner remediation", agent: planner?.make,
|
|
3159
3215
|
model: explicitDefaultValue(planner?.model), effort: explicitEffort(planner?.reasoning), fast: planner?.fast,
|
|
3160
3216
|
resumeSessionId, permissionConfig: readOnlyPermissionConfig(), sandboxMode: "read-only",
|
|
3161
3217
|
logPath: makeRoleLogPath(projectDir, "qa-remediation"), logEvent: "rafi-plan",
|
|
3162
3218
|
}) });
|
|
3163
|
-
if (run.turn.result.isError || run.turn.status.kind !== "
|
|
3219
|
+
if (run.turn.result.isError || run.turn.status.kind !== "done")
|
|
3164
3220
|
throw new Error(`Planner remediation failed: ${run.turn.status.error ?? run.turn.result.text.slice(0, 500)}`);
|
|
3165
|
-
const
|
|
3166
|
-
if (!match)
|
|
3167
|
-
throw new Error("Planner remediation did not return the required structured proposal");
|
|
3168
|
-
let parsed;
|
|
3169
|
-
try {
|
|
3170
|
-
parsed = JSON.parse(match[1].replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, ""));
|
|
3171
|
-
}
|
|
3172
|
-
catch (error) {
|
|
3173
|
-
throw new Error(`Planner remediation proposal is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
3174
|
-
}
|
|
3175
|
-
const proposal = parseProviderContract(qaRemediationProposalV1Schema, parsed, "RAFI_QA_REMEDIATION");
|
|
3221
|
+
const proposal = operationPayload(run.turn.result, plannerRemediationOutput);
|
|
3176
3222
|
console.log(`\nPlanner remediation: ${proposal.summary}\n${proposal.fix_instructions.map((item) => `- ${item}`).join("\n")}`);
|
|
3177
3223
|
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: [
|
|
3178
3224
|
{ value: "approve", label: "Approve (Recommended)" }, { value: "revise", label: "Discuss/revise" }, { value: "cancel", label: "Cancel and return to choices" },
|
|
@@ -3185,7 +3231,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
3185
3231
|
if (isCancel(feedback))
|
|
3186
3232
|
break;
|
|
3187
3233
|
resumeSessionId = run.sessionId;
|
|
3188
|
-
instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}.
|
|
3234
|
+
instruction = `Revise the complete QA remediation proposal using this feedback: ${String(feedback)}. Use the native planner-remediation contract.`;
|
|
3189
3235
|
}
|
|
3190
3236
|
}
|
|
3191
3237
|
};
|
package/dist/cli/tickets.d.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { StructuredPlanV1 } from "rafi-spec";
|
|
3
3
|
import { Log } from "../log.js";
|
|
4
|
-
import { type StepStatus } from "../foreman.js";
|
|
5
4
|
import type { EffortLevel } from "../adapters/types.js";
|
|
6
5
|
import { type RoleBuilderOptions } from "../agentRun.js";
|
|
7
6
|
import type { TicketsConfig } from "../tickets/config.js";
|
|
8
7
|
import { type TicketPopulateRuntimeMode, type TicketSourceConfig, type TicketsSetupConfig } from "../tickets/setupConfig.js";
|
|
9
|
-
/**
|
|
10
|
-
* Ticket Maker may append an explanatory note after its machine envelope.
|
|
11
|
-
* Preserve a schema-valid proposal before Foreman's strict generic marker
|
|
12
|
-
* repair path replaces the response with a status-only correction.
|
|
13
|
-
*/
|
|
14
|
-
export declare function acceptTicketPopulationCompletion(text: string): StepStatus | undefined;
|
|
15
8
|
/** Commander preserves comma-delimited values as one argument; accept both documented forms. */
|
|
16
9
|
export declare function normalizeRiskWaiverTicketIds(values: readonly string[] | undefined): string[];
|
|
17
10
|
/** Record an explicit, definition-bound exception for existing risk tickets. */
|
|
@@ -23,7 +16,6 @@ export interface RiskNoteRepairProposal {
|
|
|
23
16
|
rollback: string;
|
|
24
17
|
}>;
|
|
25
18
|
}
|
|
26
|
-
export declare function extractRiskNoteRepairProposal(output: string): RiskNoteRepairProposal;
|
|
27
19
|
/** Validate a deliberately narrow repair: only exactly the missing rollback fields may change. */
|
|
28
20
|
export declare function applyRiskNoteRepairProposal(tickets: import("../tickets/ticketSchema.js").TicketDef[], proposal: RiskNoteRepairProposal): import("../tickets/ticketSchema.js").TicketDef[];
|
|
29
21
|
export declare function buildPopulateInstruction(approvedPlanPath: string, contextSources?: string[], progressDoc?: string): string;
|
package/dist/cli/tickets.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { streamTicketPopulation } from "../ticketPopulationPaging.js";
|
|
2
|
+
import { operationPayload } from "../structuredOperation.js";
|
|
1
3
|
import { Command } from "commander";
|
|
2
4
|
import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
3
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
6
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
7
|
import { parse as parseYaml } from "yaml";
|
|
6
|
-
import {
|
|
8
|
+
import { riskNoteRepairOutput } from "rafi-spec";
|
|
7
9
|
import { select, text, confirm, isCancel, multiselect } from "@clack/prompts";
|
|
8
10
|
import { loadConfig } from "../config.js";
|
|
9
11
|
import { Log } from "../log.js";
|
|
10
|
-
import { Foreman
|
|
12
|
+
import { Foreman } from "../foreman.js";
|
|
11
13
|
import { printEvents } from "./events.js";
|
|
12
14
|
import { cmdInit, cmdUpdate, cmdComplete, cmdBlock, cmdUnblock, cmdCancel, cmdDiscover, cmdAcceptFutureWork, cmdReorder, cmdRender, cmdValidate, cmdQueue, cmdStackQueue, cmdArchive, cmdReview, } from "../tickets/commands.js";
|
|
13
15
|
import { loadDeliveryConfig } from "../tickets/delivery.js";
|
|
@@ -20,7 +22,7 @@ import { DEFAULT_TICKET_SETUP, configuredPlanningSources, configuredAppName, def
|
|
|
20
22
|
import { deactivateSource, discardStagedSourceCaptures, loadSourceRegistry, refreshSourceRegistry, registerSourceRequests, removeSource, saveSourceRegistry, } from "../sources/sourceRegistry.js";
|
|
21
23
|
import { fetchAndSnapshotUrl, snapshotExternalLocalFile } from "../tickets/sourceFetch.js";
|
|
22
24
|
import { checkpointInterview, completeInterview, createInterviewRecord, failInterview, readInterviewRecords, resumeInterviewRecord, } from "../interviews.js";
|
|
23
|
-
import { applyTicketPopulation, assertRiskPlanOnlyCorrection, authorizeTicketRetirements,
|
|
25
|
+
import { applyTicketPopulation, assertRiskPlanOnlyCorrection, authorizeTicketRetirements, materializeTicketPopulation, recoverTicketPublications, TicketPopulationValidationError } from "../ticketPopulation.js";
|
|
24
26
|
import { loadTickets, saveTickets, validateTicketDefs } from "../tickets/ticketLoader.js";
|
|
25
27
|
import { resolveTicketPaths } from "../tickets/config.js";
|
|
26
28
|
import { formatAllTicketDetails, formatTicketDetails, getAllTicketDetails, getTicketDetails } from "../tickets/details.js";
|
|
@@ -31,25 +33,11 @@ import { listTicketGroups, previewTicketGroupRepair, repairTicketGroups } from "
|
|
|
31
33
|
import { StateDb } from "../tickets/stateDb.js";
|
|
32
34
|
import { ticketDefinitionDigest } from "../tickets/riskWaivers.js";
|
|
33
35
|
import { hasRollbackOrMitigationPlan } from "../tickets/ticketLoader.js";
|
|
36
|
+
import { migrateTrackerRules } from "../tickets/renderMarkdown.js";
|
|
34
37
|
let nestedTicketCommandDepth = 0;
|
|
35
38
|
class TicketInterviewCancelled extends Error {
|
|
36
39
|
}
|
|
37
40
|
function cancelTicketInterview() { throw new TicketInterviewCancelled("ticket interview cancelled"); }
|
|
38
|
-
/**
|
|
39
|
-
* Ticket Maker may append an explanatory note after its machine envelope.
|
|
40
|
-
* Preserve a schema-valid proposal before Foreman's strict generic marker
|
|
41
|
-
* repair path replaces the response with a status-only correction.
|
|
42
|
-
*/
|
|
43
|
-
export function acceptTicketPopulationCompletion(text) {
|
|
44
|
-
try {
|
|
45
|
-
extractTicketPopulationProposal(text);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
const status = parseStepStatus(text, { requireFinalLine: false });
|
|
51
|
-
return status.kind === "done" || status.kind === "plan_complete" ? status : undefined;
|
|
52
|
-
}
|
|
53
41
|
function fail(msg) {
|
|
54
42
|
if (nestedTicketCommandDepth > 0)
|
|
55
43
|
throw new Error(msg);
|
|
@@ -128,26 +116,6 @@ function optionWasProvided(command, name) {
|
|
|
128
116
|
}
|
|
129
117
|
const TICKET_POPULATE_ROLE = "ticket-maker";
|
|
130
118
|
const RAFI_CONFIG_FILES = ["rafi-config.yaml", "project.yaml"];
|
|
131
|
-
const RISK_NOTE_REPAIR_START = "RAFI_RISK_NOTE_REPAIR_START";
|
|
132
|
-
const RISK_NOTE_REPAIR_END = "RAFI_RISK_NOTE_REPAIR_END";
|
|
133
|
-
export function extractRiskNoteRepairProposal(output) {
|
|
134
|
-
const start = output.lastIndexOf(RISK_NOTE_REPAIR_START);
|
|
135
|
-
const end = output.lastIndexOf(RISK_NOTE_REPAIR_END);
|
|
136
|
-
if (start < 0 || end <= start)
|
|
137
|
-
throw new Error(`Ticket Maker repair output is missing ${RISK_NOTE_REPAIR_START}/${RISK_NOTE_REPAIR_END}`);
|
|
138
|
-
const raw = output.slice(start + RISK_NOTE_REPAIR_START.length, end).trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
139
|
-
let parsed;
|
|
140
|
-
try {
|
|
141
|
-
parsed = JSON.parse(raw);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
throw new Error(`Ticket Maker risk-note repair is invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
145
|
-
}
|
|
146
|
-
const validated = riskNoteRepairProposalV1Schema.safeParse(parsed);
|
|
147
|
-
if (!validated.success)
|
|
148
|
-
throw new Error(`Ticket Maker risk-note repair contract validation failed:\n${zodIssues(validated.error).map((issue) => `- ${issue}`).join("\n")}`);
|
|
149
|
-
return validated.data;
|
|
150
|
-
}
|
|
151
119
|
/** Validate a deliberately narrow repair: only exactly the missing rollback fields may change. */
|
|
152
120
|
export function applyRiskNoteRepairProposal(tickets, proposal) {
|
|
153
121
|
const targets = tickets.filter(ticket => ticket.risk !== "Low" && !hasRollbackOrMitigationPlan(ticket.rollback));
|
|
@@ -210,23 +178,13 @@ Return ticket content using Foreman's schema:
|
|
|
210
178
|
- title, area, priority, size, risk, depends_on, summary, acceptance, required_tests, likely_files, rollback, notes.
|
|
211
179
|
- Keep dependencies, acceptance criteria, testing expectations, file hints, risk notes, and implementation notes from the source material. For each ticket, use only the mapped approved slice's acceptance and required tests; do not copy plan-level acceptance criteria or add requirements about sibling slices or unfinished tickets. A ticket's acceptance must remain implementable and verifiable when its own slice is delivered.
|
|
212
180
|
- Preserve additive-slice independence without inventing an absence test: each ticket's own behavior and tests should work against the shared approved baseline and must not depend on sibling implementation. Do not add wording such as "with the sibling absent/present," "without [sibling] present," or "with this slice alone" unless the approved plan explicitly requires sibling absence testing or an isolated branch. Otherwise use positive own-ticket checks, the shared project tests, and the combined-result check.
|
|
213
|
-
-
|
|
181
|
+
- Rafi copies documentation policy and QA preparation decisions directly from the approved slice. Do not include these host-owned fields in your proposal or invent additional requirements. Preserve the approved acceptance and tests.
|
|
214
182
|
- File names and likely_files are implementation hints, not an exhaustive allowlist. Do not invent acceptance criteria or tests that require an exact changed-file list; allow other relevant files needed to complete the approved work. Preserve explicit user-established security, privacy, access, and other safety restrictions. Rafi manages ticket lifecycle state; do not require the Builder to mark tickets done in a document.
|
|
215
183
|
- For every Medium or High risk ticket, \`rollback\` is required. It must state a concrete rollback or mitigation action: what is disabled, reverted, or contained, and how recovery or impact is verified. \`notes\` may add context but never satisfies this requirement. Low-risk tickets may use \`rollback: null\`.
|
|
216
184
|
- Do not store mutable status/progress fields in .tickets/tickets.yaml.
|
|
217
185
|
- Dependencies must reference slice_ref values. Include the exact IDs of existing populated tickets whose removed slices should become obsolete.
|
|
218
186
|
|
|
219
|
-
If source content does not cleanly map to the
|
|
220
|
-
STEP_STATUS: needs_input | question="..." choices="..."
|
|
221
|
-
|
|
222
|
-
Output one JSON object between ${TICKET_POPULATION_PROPOSAL_START} and ${TICKET_POPULATION_PROPOSAL_END} with shape:
|
|
223
|
-
{"version":1,"plan_id":"approved plan ID","revision":1,"tickets":[{"slice_ref":"slc_low","title":"...","area":"...","priority":"P1","size":"S","risk":"Low","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":[],"rollback":null,"notes":null},{"slice_ref":"slc_medium","title":"...","area":"...","priority":"P1","size":"S","risk":"Medium","summary":"...","acceptance":["..."],"required_tests":["..."],"likely_files":["..."],"documentation":{"required":false,"targets":[],"criteria":[]},"depends_on":["slc_low"],"rollback":"Disable the feature flag, restore the previous handler, and verify the affected error rate returns to baseline.","notes":null}],"retirements":["exact existing ticket ID"]}
|
|
224
|
-
Triple-check that every approved slice appears exactly once.
|
|
225
|
-
|
|
226
|
-
End with exactly one marker line as the final non-empty line; do not append notes or commentary after it:
|
|
227
|
-
STEP_STATUS: done | summary="populated Foreman tickets from existing project ticket sources"
|
|
228
|
-
or
|
|
229
|
-
STEP_STATUS: blocked | reason="why ticket population cannot proceed"`;
|
|
187
|
+
If source content does not cleanly map to the approved plan, return a needs-input or blocked result instead of guessing. Return the full proposal in result.payload of the native ticket-population response. The proposal contains version, plan_id, revision, tickets, and retirements. Do not allocate final ticket IDs.`;
|
|
230
188
|
}
|
|
231
189
|
/** A bounded correction loop ended without a publishable proposal. */
|
|
232
190
|
export class TicketPopulationRiskCorrectionExhaustedError extends Error {
|
|
@@ -546,6 +504,18 @@ async function cmdSetupUpdateCli(opts) {
|
|
|
546
504
|
saveSourceRegistry(dir, patch.sourceRegistry);
|
|
547
505
|
registrySaved = true;
|
|
548
506
|
syncTicketLimits(dir, next.limits);
|
|
507
|
+
if (isTicketsInitialized(dir)) {
|
|
508
|
+
const ticketsConfig = loadTicketsConfig(dir);
|
|
509
|
+
const trackerRulesPath = join(dir, ticketsConfig.paths.trackerRules);
|
|
510
|
+
if (existsSync(trackerRulesPath)) {
|
|
511
|
+
const before = readFileSync(trackerRulesPath, "utf8");
|
|
512
|
+
const migrated = migrateTrackerRules(before, ticketsConfig.paths.progressDoc);
|
|
513
|
+
if (migrated.disposition !== "unchanged") {
|
|
514
|
+
writeFileSync(trackerRulesPath, migrated.content, "utf8");
|
|
515
|
+
console.log(`foreman tickets setup: ${migrated.disposition === "replaced-legacy" ? "upgraded" : "refreshed"} Rafi-managed tracker rules`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
549
519
|
console.log(`foreman tickets setup: updated ticket setup in ${join(dir, "rafi-config.yaml")}`);
|
|
550
520
|
if (isTicketsInitialized(dir)) {
|
|
551
521
|
const rows = cmdQueue(dir, 1);
|
|
@@ -1480,8 +1450,23 @@ export async function cmdPopulateCli(opts) {
|
|
|
1480
1450
|
if (contextSources.length) {
|
|
1481
1451
|
console.log(`foreman tickets: context sources ${contextSources.join(" ")}`);
|
|
1482
1452
|
}
|
|
1483
|
-
|
|
1484
|
-
|
|
1453
|
+
const population = (instruction, correction) => streamTicketPopulation({
|
|
1454
|
+
projectDir: dir, interviewId: planningRecord.id, correction, instruction,
|
|
1455
|
+
checkpoint: scope => { planningRecord = checkpointInterview(dir, planningRecord, { decisions: { ...planningRecord.decisions, populationArtifact: scope } }); },
|
|
1456
|
+
dispatch: (instruction, policy) => foreman.runInstruction(instruction, policy),
|
|
1457
|
+
});
|
|
1458
|
+
const populateInstruction = buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc);
|
|
1459
|
+
let initialPopulation;
|
|
1460
|
+
try {
|
|
1461
|
+
initialPopulation = await population(populateInstruction, 0);
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
// One automatic restart of a failed population before stopping for a human.
|
|
1465
|
+
console.log(`foreman tickets: ticket population failed (${String(error).slice(0, 200)}); restarting it once from the first page.`);
|
|
1466
|
+
initialPopulation = await population(populateInstruction, 0);
|
|
1467
|
+
}
|
|
1468
|
+
let turn = initialPopulation.turn;
|
|
1469
|
+
planningRecord = checkpointInterview(dir, planningRecord, { runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() }, decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, structuredResponse: turn.result.structured } });
|
|
1485
1470
|
log.write("ticket-populate", {
|
|
1486
1471
|
statusKind: turn.status.kind,
|
|
1487
1472
|
summary: turn.status.summary,
|
|
@@ -1499,7 +1484,9 @@ export async function cmdPopulateCli(opts) {
|
|
|
1499
1484
|
throw new Error(`needs human — ${turn.status.error ?? "builder did not emit done"}`);
|
|
1500
1485
|
}
|
|
1501
1486
|
const existing = loadTickets(resolveTicketPaths(ticketsConfig, dir).tickets);
|
|
1502
|
-
|
|
1487
|
+
if (!turn.result.selectedDispatch)
|
|
1488
|
+
throw new Error("Ticket population response has no sealed dispatch tuple");
|
|
1489
|
+
let proposal = initialPopulation.proposal;
|
|
1503
1490
|
let proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
|
|
1504
1491
|
let materialized;
|
|
1505
1492
|
let populationWaiver;
|
|
@@ -1525,18 +1512,22 @@ export async function cmdPopulateCli(opts) {
|
|
|
1525
1512
|
continue;
|
|
1526
1513
|
}
|
|
1527
1514
|
console.log(`foreman tickets: ${ids} need risk plans; asking Ticket Maker for a targeted correction (${correction + 1}/2)`);
|
|
1528
|
-
const
|
|
1515
|
+
const correctedPopulation = await population(`Correct the existing ticket population proposal only. Do not change plan_id, revision, slice_ref mappings, ticket IDs, dependencies, scope, ownership, acceptance criteria, tests, retirements, or any field except \`rollback\` for these tickets: ${ids}. Each listed ticket is Medium or High risk and requires a concrete rollback or mitigation plan in \`rollback\`; \`notes\` cannot satisfy this requirement. Return the complete replacement proposal through native ticket-population pages.`, correction + 1);
|
|
1516
|
+
const correctionTurn = correctedPopulation.turn;
|
|
1529
1517
|
if (correctionTurn.result.isError || (correctionTurn.status.kind !== "done" && correctionTurn.status.kind !== "plan_complete")) {
|
|
1530
1518
|
throw new Error(`Ticket Maker could not correct missing risk plans for ${ids}: ${correctionTurn.status.reason ?? correctionTurn.status.error ?? correctionTurn.result.text.slice(0, 200)}`);
|
|
1531
1519
|
}
|
|
1532
1520
|
turn = correctionTurn;
|
|
1533
1521
|
planningRecord = checkpointInterview(dir, planningRecord, {
|
|
1534
1522
|
runtime: { runtime: roleBuilder.runtime, model: roleBuilder.model, sessionId: builder.sessionId() },
|
|
1535
|
-
decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, riskPlanCorrections: correction + 1 },
|
|
1523
|
+
decisions: { ...planningRecord.decisions, proposalResponse: turn.result.text, structuredResponse: turn.result.structured, riskPlanCorrections: correction + 1 },
|
|
1536
1524
|
});
|
|
1537
|
-
const corrected =
|
|
1525
|
+
const corrected = correctedPopulation.proposal;
|
|
1538
1526
|
assertRiskPlanOnlyCorrection(proposal, corrected, err.repairableRiskSliceRefs);
|
|
1539
1527
|
proposal = corrected;
|
|
1528
|
+
if (!turn.result.selectedDispatch)
|
|
1529
|
+
throw new Error("Ticket population correction has no sealed dispatch tuple");
|
|
1530
|
+
// A correction is immutable lineage, never a rewrite of the terminal proposal.
|
|
1540
1531
|
proposalArtifact = savePopulationProposalArtifact(dir, planningRecord.id, approvedPlan, proposal);
|
|
1541
1532
|
}
|
|
1542
1533
|
}
|
|
@@ -1599,13 +1590,12 @@ export async function cmdRepairRiskNotesCli(opts) {
|
|
|
1599
1590
|
"You are Ticket Maker in read-only repair mode. Do not edit files.",
|
|
1600
1591
|
"For every listed ticket, supply a concrete rollback or mitigation plan. Do not change any other field or add/remove tickets.",
|
|
1601
1592
|
`Tickets requiring repair:\n${JSON.stringify(targets.map(ticket => ({ id: ticket.id, title: ticket.title, risk: ticket.risk, summary: ticket.summary, acceptance: ticket.acceptance, required_tests: ticket.required_tests })), null, 2)}`,
|
|
1602
|
-
`
|
|
1603
|
-
"End with STEP_STATUS: done | summary=\"risk-note repair proposed\"",
|
|
1593
|
+
`Submit the native risk-note-repair payload: {"version":1,"tickets":[{"id":"T001","rollback":"Concrete reversal or mitigation steps."}]}.`,
|
|
1604
1594
|
].join("\n\n");
|
|
1605
|
-
const turn = await foreman.runInstruction(instruction);
|
|
1595
|
+
const turn = await foreman.runInstruction(instruction, { output: riskNoteRepairOutput });
|
|
1606
1596
|
if (turn.result.isError || (turn.status.kind !== "done" && turn.status.kind !== "plan_complete"))
|
|
1607
1597
|
throw new Error(`Ticket Maker could not propose risk-plan repairs: ${turn.status.reason ?? turn.status.error ?? turn.result.text.slice(0, 300)}`);
|
|
1608
|
-
const proposal =
|
|
1598
|
+
const proposal = operationPayload(turn.result, riskNoteRepairOutput);
|
|
1609
1599
|
const repaired = applyRiskNoteRepairProposal(current, proposal);
|
|
1610
1600
|
for (const target of targets)
|
|
1611
1601
|
console.log(` ${target.id}: ${proposal.tickets.find(item => item.id === target.id).rollback.trim()}`);
|