ai-foreman 1.7.20 → 1.7.22
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 +12 -2
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +27 -8
- package/dist/adapters/claude.d.ts +19 -0
- package/dist/adapters/claude.js +59 -7
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/dirtyBaseline.d.ts +5 -0
- package/dist/branch/dirtyBaseline.js +69 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/resume.js +2 -2
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +90 -4
- package/dist/buildAdmission.js +2 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +23 -17
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +215 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +235 -13
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.js +28 -8
- package/dist/cli/start.js +192 -50
- package/dist/cli/status.js +29 -7
- package/dist/cli/tickets.d.ts +4 -0
- package/dist/cli/tickets.js +37 -13
- package/dist/continuity.d.ts +11 -0
- package/dist/continuity.js +22 -6
- package/dist/foreman.d.ts +10 -2
- package/dist/foreman.js +330 -39
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -11
- package/dist/index.js +2 -0
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/interviews.d.ts +3 -1
- package/dist/interviews.js +7 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +25 -1
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.js +7 -0
- package/dist/projectDiagnostics.js +32 -8
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +234 -24
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.js +12 -5
- package/dist/recoveryGuidance.d.ts +9 -0
- package/dist/recoveryGuidance.js +13 -0
- package/dist/recoveryPolicy.js +1 -1
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +1 -0
- package/dist/runtimeReadiness.js +13 -1
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +50 -1
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.js +19 -8
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.d.ts +1 -1
- package/dist/tickets/commands.js +13 -11
- package/dist/tickets/details.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/stateDb.d.ts +4 -1
- package/dist/tickets/stateDb.js +18 -2
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/tickets/validate.js +4 -1
- package/dist/workflowDb.d.ts +51 -5
- package/dist/workflowDb.js +338 -7
- package/dist/workflowReader.d.ts +109 -2
- package/dist/workflowReader.js +48 -4
- package/package.json +85 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/dist/cli/start.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "../qaEffectiveConfig.js";
|
|
2
|
+
import { graphContext, sendGraphTurn } from "../graph/turn.js";
|
|
3
|
+
import { initializeSyntheticWork } from "../buildSyntheticWork.js";
|
|
4
|
+
import { admitApprovedTicket } from "../buildWorkAuthorization.js";
|
|
1
5
|
import { WorkflowReader } from "../workflowReader.js";
|
|
2
6
|
import { localBuildAuthority, canonicalProject, withBuildInvocation } from "../buildAdmission.js";
|
|
3
|
-
import { formatRecoveryCommand } from "../recoveryGuidance.js";
|
|
7
|
+
import { formatRecoveryCommand, formatDecisionCommands, formatExactRunRecovery } from "../recoveryGuidance.js";
|
|
4
8
|
import { durableReadOnlyProposal } from "../readOnlyProposal.js";
|
|
5
9
|
import { captureFrozenQaSource } from "../qaSnapshot.js";
|
|
6
10
|
import { finishStartAdmission, superviseStart, supervisedRunId } from "../supervisedStart.js";
|
|
7
|
-
import {
|
|
11
|
+
import { assertBuildAssignmentReconciled } from "../buildAssignment.js";
|
|
12
|
+
import { durableHumanDecision, HumanDecisionRequired, HumanDecisionCancelled, servicePendingHumanDecisions } from "../humanDecision.js";
|
|
8
13
|
import { requiresBuildApproval, buildScopeRevision, createBuildApprovalGate } from "../buildApproval.js";
|
|
9
14
|
import { Command, Option } from "commander";
|
|
15
|
+
import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
10
16
|
import { resolve, join, relative } from "node:path";
|
|
11
17
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
12
18
|
import { execFileSync } from "node:child_process";
|
|
@@ -28,6 +34,7 @@ import { loadTickets } from "../tickets/ticketLoader.js";
|
|
|
28
34
|
import { StateDb } from "../tickets/stateDb.js";
|
|
29
35
|
import { applySharedDeliveryBranch, applySizeBranchPolicy, buildBranchAuditInstruction, buildBranchPlan, parseAuditDependencies } from "../branch/planner.js";
|
|
30
36
|
import { currentGitRef, ensureCleanBaseWorktree, generatedTrackerDirtyPaths } from "../branch/git.js";
|
|
37
|
+
import { captureDirtyBaseline } from "../branch/dirtyBaseline.js";
|
|
31
38
|
import { formatGitHubFailure, preflightGh } from "../branch/github.js";
|
|
32
39
|
import { preflightGlab } from "../branch/gitlab.js";
|
|
33
40
|
import { readDeliveryUnitSession, runBranchPlan } from "../branch/runner.js";
|
|
@@ -55,7 +62,6 @@ import { ObservabilityStore, RunObserver } from "../observability.js";
|
|
|
55
62
|
import { loadProjectAutonomyConfig, resolveAutonomyPolicy, resolveQaEnablement } from "../recoveryPolicy.js";
|
|
56
63
|
import { loadQaRecoveryPacket, recoverPendingQaRecoveryPublications } from "../qaRecovery.js";
|
|
57
64
|
import { qaDigest } from "../qaProtocolV2.js";
|
|
58
|
-
import { loadSkill } from "special-agents";
|
|
59
65
|
import { acceptQaRuntimeHandoff, describeQaRuntimeHandle, frozenQaRuntimeSettings } from "../qaRuntime.js";
|
|
60
66
|
const FOREMAN_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
61
67
|
function fail(message) {
|
|
@@ -147,11 +153,10 @@ export function formatResumeGuidance(executable, projectDir, steps, sessionId, r
|
|
|
147
153
|
return [
|
|
148
154
|
...run.decisions.flatMap(decision => [
|
|
149
155
|
`foreman: input required: ${decision.prompt}`,
|
|
150
|
-
|
|
151
|
-
` rafi build:decide ${shellQuote(projectDir)} --run ${shellQuote(run.runId)} --decision ${shellQuote(decision.decisionId)} --choice <choice-id>`,
|
|
156
|
+
...formatDecisionCommands(projectDir, run.runId, decision, executable),
|
|
152
157
|
]),
|
|
153
|
-
"foreman: resume this run with:",
|
|
154
|
-
` ${
|
|
158
|
+
executable === "rafi" ? "foreman: resume this run with:" : "foreman: review recovery for this run with:",
|
|
159
|
+
` ${formatExactRunRecovery(projectDir, run.runId, executable)}`,
|
|
155
160
|
];
|
|
156
161
|
if (executable === "rafi") {
|
|
157
162
|
return [
|
|
@@ -382,6 +387,8 @@ export function savedStartArguments(command, projectDir, opts) {
|
|
|
382
387
|
}
|
|
383
388
|
/** Pure validation shared by interactive start and persisted preparation. */
|
|
384
389
|
export function validateStartOptions(opts, requireCount) {
|
|
390
|
+
if (opts.dirtyWork !== undefined && !["preserve", "include"].includes(opts.dirtyWork))
|
|
391
|
+
throw new Error("--dirty-work must be preserve or include");
|
|
385
392
|
const recovery = Boolean(opts.resume || opts.continue || opts.recoverRun);
|
|
386
393
|
const tickets = opts.ticket ?? [];
|
|
387
394
|
if (!recovery && tickets.length > 1)
|
|
@@ -486,7 +493,10 @@ export function buildStartCommand() {
|
|
|
486
493
|
.option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
|
|
487
494
|
.option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
|
|
488
495
|
.option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
|
|
496
|
+
.option("--builder-network", "request build-scoped builder internet access with separate explicit approval; rechecked on resume")
|
|
497
|
+
.option("--builder-approvals", "allow the builder to request explicit per-operation sandbox approvals; QA stays restricted")
|
|
489
498
|
.option("--keep-worktrees", "keep successful ticket worktrees for inspection")
|
|
499
|
+
.option("--dirty-work <mode>", "preserve | include: continue in isolated branches without merging into the dirty checkout")
|
|
490
500
|
.option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
|
|
491
501
|
.option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
|
|
492
502
|
.action(async (project, opts, command) => withBuildInvocation(async () => {
|
|
@@ -496,6 +506,11 @@ export function buildStartCommand() {
|
|
|
496
506
|
catch (error) {
|
|
497
507
|
fail(error.message);
|
|
498
508
|
}
|
|
509
|
+
if (opts.dirtyWork) {
|
|
510
|
+
opts.branchPerTicket = true;
|
|
511
|
+
opts.completion = "none";
|
|
512
|
+
opts.keepWorktrees = true;
|
|
513
|
+
}
|
|
499
514
|
const requestedTicketIds = opts.ticket ?? [];
|
|
500
515
|
const recoveryIntent = Boolean(opts.resume || opts.continue || opts.recoverRun);
|
|
501
516
|
if (!recoveryIntent && requestedTicketIds.length > 1)
|
|
@@ -543,6 +558,7 @@ export function buildStartCommand() {
|
|
|
543
558
|
if (!existsSync(cwd))
|
|
544
559
|
fail(`project directory not found: ${cwd}`);
|
|
545
560
|
const recoveryRecord = opts.recoverRun ? readBuildRuns(cwd).find((run) => run.runId === opts.recoverRun) : undefined;
|
|
561
|
+
let dirtyWork = recoveryRecord?.runDecisions?.dirtyWork;
|
|
546
562
|
if (opts.recoverRun && !recoveryRecord)
|
|
547
563
|
fail(`recoverable build run not found: ${opts.recoverRun}`);
|
|
548
564
|
const autonomyProfile = opts.autonomy;
|
|
@@ -558,6 +574,34 @@ export function buildStartCommand() {
|
|
|
558
574
|
try {
|
|
559
575
|
if (await superviseStart(cwd, supervisedInvocationId, opts.supervisor === false ? { ...frozenPolicy, supervisorEnabled: false } : frozenPolicy, { steps: opts.steps, stacks: opts.stacks, detach: opts.detach, launchToken: opts.launchToken, startArgs: savedStartArguments(command, cwd, opts) }))
|
|
560
576
|
return;
|
|
577
|
+
if (recoveryRecord || opts.preparationRun) {
|
|
578
|
+
assertBuildAssignmentReconciled(cwd, supervisedInvocationId);
|
|
579
|
+
if (!await servicePendingHumanDecisions({ projectDir: cwd, runId: supervisedInvocationId, tickets: requestedTicketIds.length ? requestedTicketIds : recoveryRecord?.tickets.length ? recoveryRecord.tickets : undefined, scopeRevision: isTicketsInitialized(cwd) ? buildScopeRevision(cwd) : undefined }))
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const requestedNetwork = Boolean(opts.builderNetwork || recoveryRecord?.builderCapabilities?.requestedNetwork);
|
|
583
|
+
let grantedNetwork = false;
|
|
584
|
+
if (requestedNetwork) {
|
|
585
|
+
const permissionDb = new WorkflowDb(cwd);
|
|
586
|
+
let networkKey;
|
|
587
|
+
try {
|
|
588
|
+
networkKey = permissionDb.pendingHumanDecisions(supervisedInvocationId).find(decision => decision.interruptionId.startsWith("builder-network:"))?.interruptionId ?? `builder-network:${process.pid}:${randomUUID()}`;
|
|
589
|
+
}
|
|
590
|
+
finally {
|
|
591
|
+
permissionDb.close();
|
|
592
|
+
}
|
|
593
|
+
const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey,
|
|
594
|
+
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; --yes does not approve this permission.",
|
|
595
|
+
choices: [{ id: "allow-network", label: "Allow builder internet for this build" }, { id: "stay-offline", label: "Stay offline and use a verified local bundle" }],
|
|
596
|
+
operation: async () => {
|
|
597
|
+
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" }] });
|
|
598
|
+
return isCancel(action) ? undefined : action;
|
|
599
|
+
} });
|
|
600
|
+
if (networkDecision === undefined || isCancel(networkDecision))
|
|
601
|
+
return;
|
|
602
|
+
grantedNetwork = networkDecision === "allow-network";
|
|
603
|
+
}
|
|
604
|
+
const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}) };
|
|
561
605
|
const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
|
|
562
606
|
const autoApprovePlanUpdates = recoveryRecord
|
|
563
607
|
? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
|
|
@@ -711,6 +755,12 @@ export function buildStartCommand() {
|
|
|
711
755
|
fail("current-branch build recovery supports exactly one --ticket");
|
|
712
756
|
}
|
|
713
757
|
if (branchMode) {
|
|
758
|
+
if (dirtyWork) {
|
|
759
|
+
branchDefaults.completionMode = "none";
|
|
760
|
+
branchDefaults.createReview = false;
|
|
761
|
+
branchDefaults.cleanupBranches = false;
|
|
762
|
+
opts.keepWorktrees = true;
|
|
763
|
+
}
|
|
714
764
|
if (opts.resume && continueTickets.length > 1) {
|
|
715
765
|
fail("--resume <sessionId> in branch mode supports exactly one --ticket; use --continue for multiple tickets");
|
|
716
766
|
}
|
|
@@ -799,7 +849,7 @@ export function buildStartCommand() {
|
|
|
799
849
|
}
|
|
800
850
|
const recoveryTicket = !branchMode && recoveryRecord ? continueTickets[0] : undefined;
|
|
801
851
|
const preferredTicket = selectedNewTicket ?? recoveryTicket;
|
|
802
|
-
|
|
852
|
+
let executionTickets = !branchMode && recoveryRecord && recoveryRecord.tickets.length
|
|
803
853
|
? recoveryExecutionTickets(recoveryRecord, requestedTicketIds, Boolean(opts.recoveryMode))
|
|
804
854
|
: selectedNewTicket ? [selectedNewTicket] : undefined;
|
|
805
855
|
const config = loadConfig(join(cwd, "foreman.yaml"));
|
|
@@ -826,11 +876,15 @@ export function buildStartCommand() {
|
|
|
826
876
|
}
|
|
827
877
|
}
|
|
828
878
|
let activeObserver = observabilityStore ? new RunObserver(observabilityStore, invocationRunId) : undefined;
|
|
829
|
-
const finishObservationOnExit = () => {
|
|
879
|
+
const finishObservationOnExit = (exitCode) => {
|
|
830
880
|
if (!activeObserver)
|
|
831
881
|
return;
|
|
832
|
-
|
|
833
|
-
|
|
882
|
+
const reader = new WorkflowReader(cwd);
|
|
883
|
+
const durable = reader.getRun(invocationRunId);
|
|
884
|
+
reader.close();
|
|
885
|
+
const outcome = durable?.status === "cancelled" ? "cancelled" : durable?.checkpoint === "waiting-for-human" ? "waiting-for-human" : durable?.status === "failed" ? "failed" : "interrupted";
|
|
886
|
+
activeObserver.finish(outcome, { checkpoint: durable?.checkpoint, failureCategory: `process-exit:${exitCode}` });
|
|
887
|
+
observabilityStore?.closeLogFile(logPath, outcome);
|
|
834
888
|
observabilityStore?.close();
|
|
835
889
|
activeObserver = undefined;
|
|
836
890
|
};
|
|
@@ -864,6 +918,8 @@ export function buildStartCommand() {
|
|
|
864
918
|
workspaceIdentity: !branchMode ? captureCurrentWorkflowSessionIdentity(builderCwd) : captureWorkspaceIdentity(builderCwd),
|
|
865
919
|
ticketId: sessionRef?.ticketId,
|
|
866
920
|
deliveryUnitId: sessionRef?.deliveryUnitId,
|
|
921
|
+
networkAccess: grantedNetwork,
|
|
922
|
+
approvalPolicy: opts.builderApprovals ? "on-request" : "never",
|
|
867
923
|
permission: createPermissionHandler(builderPolicy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
|
|
868
924
|
onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
|
|
869
925
|
onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
|
|
@@ -871,7 +927,7 @@ export function buildStartCommand() {
|
|
|
871
927
|
fast: builderFast,
|
|
872
928
|
systemPromptAppend: roleBundle.system || undefined,
|
|
873
929
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
874
|
-
autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ??
|
|
930
|
+
autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
|
|
875
931
|
observer: activeObserver,
|
|
876
932
|
};
|
|
877
933
|
const adapter = agent === "codex"
|
|
@@ -894,13 +950,13 @@ export function buildStartCommand() {
|
|
|
894
950
|
role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
|
|
895
951
|
reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
|
|
896
952
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
|
|
897
|
-
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ??
|
|
953
|
+
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
|
|
898
954
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
899
955
|
} : {
|
|
900
956
|
role, source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
901
957
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
902
958
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
903
|
-
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ??
|
|
959
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
|
|
904
960
|
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
905
961
|
};
|
|
906
962
|
const liveRoleSettings = (role, base) => {
|
|
@@ -953,22 +1009,9 @@ export function buildStartCommand() {
|
|
|
953
1009
|
});
|
|
954
1010
|
const qaRuntimeMetadata = new WeakMap();
|
|
955
1011
|
const resolveQaRuntimeMetadata = (qaCwd, settings) => {
|
|
956
|
-
const
|
|
957
|
-
const effectiveRoleInstructions =
|
|
958
|
-
|
|
959
|
-
const runtimeRoot = settings.make === "codex" ? ".codex" : ".claude";
|
|
960
|
-
const path = [join(qaCwd, runtimeRoot, "skills", name, "SKILL.md"), join(qaCwd, ".agents", "skills", name, "SKILL.md")].find(existsSync);
|
|
961
|
-
if (path) {
|
|
962
|
-
const content = `## ${name}\n${readFileSync(path, "utf8").trim()}`;
|
|
963
|
-
return { name, path, content, digest: createHash("sha256").update(content).digest("hex") };
|
|
964
|
-
}
|
|
965
|
-
const bundled = loadSkill(name);
|
|
966
|
-
if (!bundled.body?.trim())
|
|
967
|
-
throw new Error(`QA runtime ${settings.make} skill ${name} has no exact dispatchable content`);
|
|
968
|
-
const content = `## ${bundled.name}\n${bundled.body.trim()}`;
|
|
969
|
-
return { name, path: `special-agents:${name}`, content, digest: createHash("sha256").update(content).digest("hex") };
|
|
970
|
-
});
|
|
971
|
-
return { settings: structuredClone(settings), effectiveRoleInstructions, skills };
|
|
1012
|
+
const effective = resolveEffectiveQaConfiguration(cwd, settings);
|
|
1013
|
+
const effectiveRoleInstructions = finalQaRoleInstructions(effective.qaRules);
|
|
1014
|
+
return { settings: structuredClone(settings), effectiveRoleInstructions, skills: effective.skills.map(skill => ({ ...skill, digest: createHash("sha256").update(skill.content).digest("hex") })) };
|
|
972
1015
|
};
|
|
973
1016
|
const createBuilderForSettings = async (builderCwd, settings) => {
|
|
974
1017
|
const ready = await ensureBuildRuntimeReadyForCommand(builderCwd, settings.make, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
@@ -994,6 +1037,8 @@ export function buildStartCommand() {
|
|
|
994
1037
|
shutdownTimeoutMs: frozenPolicy.runtimeDeadlines?.shutdown_ms,
|
|
995
1038
|
turnDeadlineMs: frozenPolicy.runtimeDeadlines?.turn_ms,
|
|
996
1039
|
model: settings.model === "default" ? undefined : settings.model,
|
|
1040
|
+
networkAccess: grantedNetwork,
|
|
1041
|
+
approvalPolicy: opts.builderApprovals ? "on-request" : "never",
|
|
997
1042
|
permission: createPermissionHandler(policy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
|
|
998
1043
|
onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
|
|
999
1044
|
onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
|
|
@@ -1653,7 +1698,7 @@ export function buildStartCommand() {
|
|
|
1653
1698
|
if (branchMode) {
|
|
1654
1699
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
1655
1700
|
const allowedBaseDirtyPaths = generatedTrackerDirtyPaths(ticketsConfig.paths);
|
|
1656
|
-
|
|
1701
|
+
let baseWorktreePolicy = recoveryRecord ? "skip" : "enforce";
|
|
1657
1702
|
if (baseWorktreePolicy === "skip") {
|
|
1658
1703
|
const projection = recoveryRecord ? projectBuildRecovery(cwd, recoveryRecord) : undefined;
|
|
1659
1704
|
if (projection?.expectedChanges.length) {
|
|
@@ -1671,12 +1716,35 @@ export function buildStartCommand() {
|
|
|
1671
1716
|
ensureCleanBaseWorktree(cwd, { allowedDirtyPaths: allowedBaseDirtyPaths });
|
|
1672
1717
|
}
|
|
1673
1718
|
catch (err) {
|
|
1674
|
-
|
|
1719
|
+
let mode = opts.dirtyWork;
|
|
1720
|
+
if (!mode && process.stdin.isTTY && process.stdout.isTTY && !opts.yes) {
|
|
1721
|
+
const choice = await select({ message: "Uncommitted work found. How should the isolated build start?", options: [
|
|
1722
|
+
{ value: "preserve", label: "Preserve changes; build from committed code (Recommended)" },
|
|
1723
|
+
{ value: "include", label: "Include current files as a private build baseline" },
|
|
1724
|
+
{ value: "stop", label: "Stop for manual commit or stash" },
|
|
1725
|
+
] });
|
|
1726
|
+
if (isCancel(choice) || choice === "stop")
|
|
1727
|
+
return;
|
|
1728
|
+
mode = String(choice);
|
|
1729
|
+
}
|
|
1730
|
+
if (mode !== "preserve" && mode !== "include")
|
|
1731
|
+
fail(`${err instanceof Error ? err.message : String(err)}\nContinue safely with --dirty-work preserve (committed input) or --dirty-work include (current files). Both keep your checkout and index intact and retain isolated worktrees.`);
|
|
1732
|
+
const baseline = captureDirtyBaseline(cwd, mode);
|
|
1733
|
+
dirtyWork = { mode, ...baseline };
|
|
1734
|
+
opts.base = baseline.base;
|
|
1735
|
+
branchDefaults.completionMode = "none";
|
|
1736
|
+
branchDefaults.createReview = false;
|
|
1737
|
+
branchDefaults.cleanupBranches = false;
|
|
1738
|
+
branchDefaults.rootBaseBranches = false;
|
|
1739
|
+
opts.keepWorktrees = true;
|
|
1740
|
+
baseWorktreePolicy = "skip";
|
|
1741
|
+
console.log(`rafi: ${mode} dirty-work baseline ${baseline.base}; evidence ${baseline.recordPath}; results stay in isolated branches.`);
|
|
1675
1742
|
}
|
|
1676
1743
|
}
|
|
1677
1744
|
if (branchDefaults.createReview && branchDefaults.reviewProvider) {
|
|
1678
1745
|
await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
|
|
1679
1746
|
}
|
|
1747
|
+
console.log(`foreman: builder capability request: workspace writes, shell network ${grantedNetwork ? "approved for this build" : "off"}, approvals ${opts.builderApprovals ? "per operation" : "never"}; effective policy requires provider confirmation. Use a provenance-verified local source/dependency bundle for offline acquisition.`);
|
|
1680
1748
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
1681
1749
|
label: "start",
|
|
1682
1750
|
yes: Boolean(opts.yes),
|
|
@@ -1755,7 +1823,7 @@ export function buildStartCommand() {
|
|
|
1755
1823
|
try {
|
|
1756
1824
|
auditBuilder = await createBuilder(cwd);
|
|
1757
1825
|
auditViewer = printEvents(auditBuilder.events());
|
|
1758
|
-
const audit = await auditBuilder
|
|
1826
|
+
const audit = await sendGraphTurn(auditBuilder, buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)), graphContext(cwd, auditBuilder.sessionRef?.()?.cwd ?? cwd, "branch-dependency-audit", plan.nodes.map(node => node.ticket.id).join(" ")), { purpose: "planning" });
|
|
1759
1827
|
if (audit.isError)
|
|
1760
1828
|
throw new Error(audit.text);
|
|
1761
1829
|
const auditDependencies = parseAuditDependencies(audit.text);
|
|
@@ -1860,23 +1928,23 @@ export function buildStartCommand() {
|
|
|
1860
1928
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
1861
1929
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
1862
1930
|
display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
|
|
1863
|
-
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ??
|
|
1931
|
+
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
|
|
1864
1932
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
1865
1933
|
};
|
|
1866
1934
|
const capturedBranchQa = {
|
|
1867
1935
|
role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
1868
1936
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
1869
1937
|
display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
1870
|
-
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ??
|
|
1938
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
|
|
1871
1939
|
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
1872
1940
|
};
|
|
1873
1941
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1874
1942
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
1875
1943
|
runId: invocationRunId,
|
|
1876
|
-
tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
1944
|
+
builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
|
|
1877
1945
|
repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
|
|
1878
1946
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
1879
|
-
runDecisions: { workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ??
|
|
1947
|
+
runDecisions: { dirtyWork, workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
1880
1948
|
});
|
|
1881
1949
|
activeContinuityRunId = masterRun.runId;
|
|
1882
1950
|
if (requiresBuildApproval(cwd, plan.nodes.map(node => node.ticket.id), autoApprovePlanUpdates, approvalConsequences) && plan.issues.every((issue) => !issue.blocking)) {
|
|
@@ -1888,10 +1956,25 @@ export function buildStartCommand() {
|
|
|
1888
1956
|
],
|
|
1889
1957
|
}) });
|
|
1890
1958
|
if (isCancel(action) || action === "cancel") {
|
|
1959
|
+
const cancelled = action === "cancel";
|
|
1960
|
+
if (cancelled) {
|
|
1961
|
+
const decisions = new WorkflowDb(cwd);
|
|
1962
|
+
try {
|
|
1963
|
+
decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
|
|
1964
|
+
}
|
|
1965
|
+
finally {
|
|
1966
|
+
decisions.close();
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
masterRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
|
|
1970
|
+
activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
|
|
1891
1971
|
console.log("ai-foreman: cancelled");
|
|
1892
1972
|
process.exit(0);
|
|
1893
1973
|
}
|
|
1894
1974
|
}
|
|
1975
|
+
for (const node of plan.nodes)
|
|
1976
|
+
admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
|
|
1977
|
+
masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
|
|
1895
1978
|
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
|
|
1896
1979
|
let qaProtocolResumeTicket;
|
|
1897
1980
|
if (recoveryRecord && !qaResumedRecovery) {
|
|
@@ -2149,6 +2232,17 @@ export function buildStartCommand() {
|
|
|
2149
2232
|
fireTerminalBell(config.notifications.terminal_bell);
|
|
2150
2233
|
process.exit(failed ? 2 : 0);
|
|
2151
2234
|
}
|
|
2235
|
+
if (!recoveryRecord && isTicketsInitialized(cwd)) {
|
|
2236
|
+
const paths = resolveTicketPaths(loadTicketsConfig(cwd), cwd);
|
|
2237
|
+
const state = new StateDb(paths.stateDb);
|
|
2238
|
+
try {
|
|
2239
|
+
executionTickets = executionTickets ?? loadTickets(paths.tickets).filter(ticket => !["done", "canceled", "cancelled", "obsolete"].includes(state.getState(ticket.id)?.status ?? "planned")).sort((a, b) => a.order - b.order).slice(0, steps).map(ticket => ticket.id);
|
|
2240
|
+
}
|
|
2241
|
+
finally {
|
|
2242
|
+
state.close();
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
console.log(`foreman: builder capability request: workspace writes, shell network ${grantedNetwork ? "approved for this build" : "off"}, approvals ${opts.builderApprovals ? "per operation" : "never"}; effective policy requires provider confirmation. Use a provenance-verified local source/dependency bundle for offline acquisition.`);
|
|
2152
2246
|
const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
|
|
2153
2247
|
label: "start",
|
|
2154
2248
|
yes: Boolean(opts.yes),
|
|
@@ -2183,16 +2277,17 @@ export function buildStartCommand() {
|
|
|
2183
2277
|
make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
|
|
2184
2278
|
session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
2185
2279
|
display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
|
|
2186
|
-
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ??
|
|
2280
|
+
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
|
|
2187
2281
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
2188
2282
|
};
|
|
2189
|
-
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", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ??
|
|
2283
|
+
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", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
|
|
2190
2284
|
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
|
|
2191
2285
|
runId: invocationRunId,
|
|
2192
|
-
tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
2286
|
+
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,
|
|
2193
2287
|
frozenPolicy, autonomyProfile, qaEnabled,
|
|
2194
|
-
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ??
|
|
2288
|
+
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
2195
2289
|
});
|
|
2290
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
|
|
2196
2291
|
activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
|
|
2197
2292
|
activeContinuityRunId = buildRun.runId;
|
|
2198
2293
|
const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
|
|
@@ -2346,11 +2441,11 @@ export function buildStartCommand() {
|
|
|
2346
2441
|
builderFast ? "fast" : null,
|
|
2347
2442
|
qaEnabled ? null : "qa=off",
|
|
2348
2443
|
].filter(Boolean).join(" ");
|
|
2349
|
-
console.log(
|
|
2350
|
-
console.log(
|
|
2444
|
+
console.log(`${accent("foreman:")} driving a ${sanitizeTerminalText(agent)} builder through ${steps} step(s)${modifiers ? ` [${sanitizeTerminalText(modifiers)}]` : ""}`);
|
|
2445
|
+
console.log(`${accent("foreman:")} project ${sanitizeTerminalText(cwd)}`);
|
|
2351
2446
|
if (trackerRelPath)
|
|
2352
|
-
console.log(
|
|
2353
|
-
console.log(
|
|
2447
|
+
console.log(`${accent("foreman:")} tracker ${sanitizeTerminalText(trackerRelPath)}`);
|
|
2448
|
+
console.log(`${accent("foreman:")} log ${sanitizeTerminalText(logPath)}\n`);
|
|
2354
2449
|
const viewer = printEvents(builder.events());
|
|
2355
2450
|
try {
|
|
2356
2451
|
if (recoveryRecord && currentBranchTicketProgress(cwd, buildRun)?.remaining.length === 0) {
|
|
@@ -2439,6 +2534,18 @@ export function buildStartCommand() {
|
|
|
2439
2534
|
],
|
|
2440
2535
|
}) });
|
|
2441
2536
|
if (isCancel(action) || action === "cancel") {
|
|
2537
|
+
const cancelled = action === "cancel";
|
|
2538
|
+
if (cancelled) {
|
|
2539
|
+
const decisions = new WorkflowDb(cwd);
|
|
2540
|
+
try {
|
|
2541
|
+
decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
|
|
2542
|
+
}
|
|
2543
|
+
finally {
|
|
2544
|
+
decisions.close();
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
|
|
2548
|
+
activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
|
|
2442
2549
|
console.log("ai-foreman: cancelled");
|
|
2443
2550
|
statusReporter.stop("cancelled");
|
|
2444
2551
|
await foreman.close();
|
|
@@ -2453,7 +2560,9 @@ export function buildStartCommand() {
|
|
|
2453
2560
|
message: "Your feedback:",
|
|
2454
2561
|
validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
|
|
2455
2562
|
}) });
|
|
2456
|
-
if (isCancel(fb)) {
|
|
2563
|
+
if (isCancel(fb) || fb === undefined) {
|
|
2564
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "input-paused", { status: "recoverable" }), "recoverable");
|
|
2565
|
+
activeObserver?.finish("paused", { checkpoint: "feedback-eof-or-cancel" });
|
|
2457
2566
|
console.log("ai-foreman: cancelled");
|
|
2458
2567
|
statusReporter.stop("cancelled");
|
|
2459
2568
|
await foreman.close();
|
|
@@ -2466,10 +2575,16 @@ export function buildStartCommand() {
|
|
|
2466
2575
|
}
|
|
2467
2576
|
});
|
|
2468
2577
|
await ensureApproved();
|
|
2578
|
+
if (!isTicketsInitialized(cwd)) {
|
|
2579
|
+
const synthetic = initializeSyntheticWork(cwd, buildRun.runId, steps, `Explicit unticketed build invocation ${invocationRunId}`, foreman.approvedPlanText());
|
|
2580
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "synthetic-work-admitted", { tickets: synthetic });
|
|
2581
|
+
}
|
|
2469
2582
|
const onTicketStart = async (ticketId) => {
|
|
2470
2583
|
await ensureApproved();
|
|
2584
|
+
admitApprovedTicket(cwd, buildRun.runId, ticketId, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved ticket selection`);
|
|
2471
2585
|
const tickets = buildRun.tickets.includes(ticketId) ? buildRun.tickets : [...buildRun.tickets, ticketId];
|
|
2472
|
-
buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId
|
|
2586
|
+
buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId,
|
|
2587
|
+
...(buildRun.authorizedBatch ? { authorizedBatch: { ...buildRun.authorizedBatch, approvalRevisions: [...new Set([...(buildRun.authorizedBatch.approvalRevisions ?? [buildRun.authorizedBatch.scopeRevision]), buildScopeRevision(cwd, approvalConsequences)])], startedTickets: [...new Set([...buildRun.authorizedBatch.startedTickets, ticketId])] } } : {}) });
|
|
2473
2588
|
};
|
|
2474
2589
|
const batch = () => foreman.runBatch(steps, trackerRelPath, onTicketStart, preferredTicket, executionTickets);
|
|
2475
2590
|
const result = await (activeObserver ? activeObserver.span("builder_work", "Builder and QA batch", batch) : batch());
|
|
@@ -2496,8 +2611,12 @@ export function buildStartCommand() {
|
|
|
2496
2611
|
pruneRecoveryTelemetryAtCompletion(cwd, config.observability.detail_retention_days);
|
|
2497
2612
|
activeObserver = undefined;
|
|
2498
2613
|
process.off("exit", finishObservationOnExit);
|
|
2499
|
-
console.log(`\
|
|
2500
|
-
|
|
2614
|
+
console.log(`\n${accent("foreman:")} ${result.completed}/${result.requested} step(s) completed`);
|
|
2615
|
+
const safeOutcome = sanitizeTerminalText(result.outcome);
|
|
2616
|
+
const resultOutcome = /success|complete|pass/i.test(safeOutcome) ? success(safeOutcome)
|
|
2617
|
+
: /fail|error/i.test(safeOutcome) ? error(safeOutcome)
|
|
2618
|
+
: /warn|partial|blocked/i.test(safeOutcome) ? warning(safeOutcome) : safeOutcome;
|
|
2619
|
+
console.log(`${accent("foreman:")} outcome — ${resultOutcome}`);
|
|
2501
2620
|
if (result.detail)
|
|
2502
2621
|
console.log(`foreman: ${result.detail}`);
|
|
2503
2622
|
if (buildRun.status === "completed")
|
|
@@ -2561,6 +2680,13 @@ export function buildStartCommand() {
|
|
|
2561
2680
|
console.error(`foreman: resume this run with: ${formatRecoveryCommand(cwd)}`);
|
|
2562
2681
|
process.exit(2);
|
|
2563
2682
|
}
|
|
2683
|
+
if (err instanceof HumanDecisionCancelled) {
|
|
2684
|
+
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "cancelled", { status: "cancelled" }), "cancelled");
|
|
2685
|
+
activeObserver?.finish("cancelled", { checkpoint: "question-menu" });
|
|
2686
|
+
await foreman.close().catch(() => { });
|
|
2687
|
+
console.log("ai-foreman: cancelled");
|
|
2688
|
+
process.exit(0);
|
|
2689
|
+
}
|
|
2564
2690
|
if (err instanceof HumanDecisionRequired) {
|
|
2565
2691
|
buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "waiting-for-human", { status: "recoverable" }), "recoverable");
|
|
2566
2692
|
await foreman.close().catch(() => { });
|
|
@@ -2585,6 +2711,16 @@ export function buildStartCommand() {
|
|
|
2585
2711
|
fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2586
2712
|
}
|
|
2587
2713
|
}
|
|
2714
|
+
catch (error) {
|
|
2715
|
+
if (error instanceof HumanDecisionCancelled) {
|
|
2716
|
+
console.log("ai-foreman: cancelled");
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
if (!(error instanceof HumanDecisionRequired))
|
|
2720
|
+
throw error;
|
|
2721
|
+
console.error(error.message);
|
|
2722
|
+
process.exitCode = 2;
|
|
2723
|
+
}
|
|
2588
2724
|
finally {
|
|
2589
2725
|
finishStartAdmission(cwd, supervisedInvocationId);
|
|
2590
2726
|
}
|
|
@@ -2680,8 +2816,11 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
2680
2816
|
throw new Error("QA nonconvergence requires its owning build run");
|
|
2681
2817
|
const scope = createHash("sha256").update(JSON.stringify([buildScopeRevision(projectDir), context.ticket, context.history])).digest("hex");
|
|
2682
2818
|
let round = 0;
|
|
2819
|
+
let waiverDecisionId;
|
|
2683
2820
|
const ask = (phase, choices, operation) => durableHumanDecision({
|
|
2684
2821
|
projectDir, runId, ticketId: context.ticket.id, observer: observer?.(), defer: noninteractive,
|
|
2822
|
+
onAnswered: decision => { if (phase.startsWith("QA waiver confirmation") && decision.selectedChoiceId === "yes")
|
|
2823
|
+
waiverDecisionId = decision.decisionId; },
|
|
2685
2824
|
key: `qa-nonconvergence:${scope}:${round}:${phase}`, prompt: `${context.ticket.id}: ${phase}`, choices, operation,
|
|
2686
2825
|
});
|
|
2687
2826
|
while (true) {
|
|
@@ -2702,7 +2841,10 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
2702
2841
|
] }));
|
|
2703
2842
|
if (approved === "yes") {
|
|
2704
2843
|
const db = new WorkflowDb(projectDir);
|
|
2844
|
+
let authorizationRef;
|
|
2705
2845
|
try {
|
|
2846
|
+
if (db.qaPreparationStore().policy(runId)?.mode === "enforce")
|
|
2847
|
+
authorizationRef = db.authorizeContractQaWaiver(runId, context.ticket.id, waiverDecisionId);
|
|
2706
2848
|
const lease = db.currentLease();
|
|
2707
2849
|
if (lease) {
|
|
2708
2850
|
const detail = JSON.stringify({ decision: "waive", ticket: context.ticket.id, unresolved: context.history, at: new Date().toISOString() });
|
|
@@ -2714,7 +2856,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
|
|
|
2714
2856
|
db.close();
|
|
2715
2857
|
}
|
|
2716
2858
|
console.log(`foreman: QA WAIVER recorded for ${context.ticket.id}; validation_result will be failed`);
|
|
2717
|
-
return { action: "waive" };
|
|
2859
|
+
return { action: "waive", ...(authorizationRef ? { authorizationRef } : {}) };
|
|
2718
2860
|
}
|
|
2719
2861
|
continue;
|
|
2720
2862
|
}
|
package/dist/cli/status.js
CHANGED
|
@@ -2,12 +2,13 @@ import { Command } from "commander";
|
|
|
2
2
|
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
|
-
import { recoverableBuildRuns } from "../buildRuns.js";
|
|
5
|
+
import { readBuildRuns, recoverableBuildRuns } from "../buildRuns.js";
|
|
6
6
|
import { WORKFLOW_DB_FILE } from "../workflowDb.js";
|
|
7
7
|
import { WorkflowReader } from "../workflowReader.js";
|
|
8
8
|
import { formatWorkflowIssue } from "../outcomes.js";
|
|
9
|
+
import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
|
|
9
10
|
function fail(message) {
|
|
10
|
-
console.error(
|
|
11
|
+
console.error(`${error("foreman:", { stream: process.stderr })} ${message}`);
|
|
11
12
|
process.exit(1);
|
|
12
13
|
}
|
|
13
14
|
export function buildStatusCommand() {
|
|
@@ -24,11 +25,21 @@ export function runStatus(project) {
|
|
|
24
25
|
const workflowPath = join(projectDir, WORKFLOW_DB_FILE);
|
|
25
26
|
const workflow = existsSync(workflowPath) ? new WorkflowReader(projectDir, workflowPath) : undefined;
|
|
26
27
|
const activeWorkflows = workflow?.activeRuns() ?? [];
|
|
28
|
+
for (const run of activeWorkflows) {
|
|
29
|
+
const preparation = workflow?.qaPreparation(run.runId);
|
|
30
|
+
if (!preparation || preparation.mode === "legacy")
|
|
31
|
+
continue;
|
|
32
|
+
console.log(`foreman: QA preparation ${preparation.mode} — ${run.runId}`);
|
|
33
|
+
const metrics = workflow.qaPreparationMetrics(run.runId);
|
|
34
|
+
console.log(` admitted ${metrics.approved}; started ${metrics.started}; first reviews ${metrics.firstReviews}; passes ${metrics.firstReviewPasses}; unknown finding causes ${metrics.causes.unknown}`);
|
|
35
|
+
for (const work of preparation.works)
|
|
36
|
+
console.log(` ${work.workId}: ${work.state}; depth ${work.depth ?? "assessment pending"}; contract ${work.digest ?? "unpublished"}${work.detail ? `; ${work.detail}` : ""}`);
|
|
37
|
+
}
|
|
27
38
|
if (!existsSync(dir) && activeWorkflows.length === 0) {
|
|
28
39
|
workflow?.close();
|
|
29
40
|
fail(`no foreman runs found under ${dir}`);
|
|
30
41
|
}
|
|
31
|
-
const logs = readdirSync(dir)
|
|
42
|
+
const logs = (existsSync(dir) ? readdirSync(dir) : [])
|
|
32
43
|
.filter((f) => f.endsWith(".jsonl"))
|
|
33
44
|
.sort();
|
|
34
45
|
if (logs.length === 0) {
|
|
@@ -52,8 +63,12 @@ export function runStatus(project) {
|
|
|
52
63
|
const prEvents = records.filter((r) => ["pr-created", "pr-existing", "pr-failed"].includes(String(r.event)));
|
|
53
64
|
const latestFailure = findLatestGitHubFailure(records);
|
|
54
65
|
const batchEnd = [...records].reverse().find((r) => r.event === "batch-end");
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const retainedRuns = readBuildRuns(projectDir).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
67
|
+
const byId = new Map(retainedRuns.map(run => [run.runId, run]));
|
|
68
|
+
const recordRunId = [...records].reverse().find(record => typeof record.runId === "string" && (byId.has(record.runId) || record.event === "step" || record.event === "batch-end" || String(record.event).startsWith("branch-")))?.runId;
|
|
69
|
+
const retainedRun = typeof recordRunId === "string" ? byId.get(recordRunId) : undefined;
|
|
70
|
+
console.log(`${accent("foreman:")} latest run ${sanitizeTerminalText(logs[logs.length - 1] ?? "")}`);
|
|
71
|
+
console.log(`${accent("foreman:")} ${steps.length} step record(s), ${escalations.length} escalation(s)`);
|
|
57
72
|
if (branchEvents.length || prEvents.length) {
|
|
58
73
|
const completed = branchEvents.filter((r) => r.event === "branch-complete").length;
|
|
59
74
|
const issues = branchEvents.filter((r) => r.event === "branch-issue").length;
|
|
@@ -61,12 +76,19 @@ export function runStatus(project) {
|
|
|
61
76
|
console.log(`foreman: branch mode — ${completed} completed, ${issues} issue(s), ${prs} PR(s)`);
|
|
62
77
|
}
|
|
63
78
|
if (batchEnd) {
|
|
64
|
-
|
|
79
|
+
const outcome = sanitizeTerminalText(String(batchEnd.outcome ?? ""));
|
|
80
|
+
const styledOutcome = /success|complete|pass/i.test(outcome) ? success(outcome)
|
|
81
|
+
: /fail|error/i.test(outcome) ? error(outcome)
|
|
82
|
+
: /warn|partial|blocked/i.test(outcome) ? warning(outcome) : outcome;
|
|
83
|
+
console.log(`${accent("foreman:")} outcome — ${styledOutcome} (${batchEnd.completed}/${batchEnd.requested})`);
|
|
65
84
|
if (batchEnd.detail)
|
|
66
85
|
console.log(`foreman: ${batchEnd.detail}`);
|
|
67
86
|
}
|
|
87
|
+
else if (retainedRun?.status === "completed") {
|
|
88
|
+
console.log(`${accent("foreman:")} outcome — ${success("completed")} (durable build record ${sanitizeTerminalText(retainedRun.runId)})`);
|
|
89
|
+
}
|
|
68
90
|
else {
|
|
69
|
-
console.log("ai-foreman: run is still in progress or did not finish
|
|
91
|
+
console.log(`${accent("ai-foreman:")} run is still in progress or did not finish`);
|
|
70
92
|
}
|
|
71
93
|
if (latestFailure) {
|
|
72
94
|
console.log(`foreman: latest GitHub failure — ${latestFailure.code}: ${latestFailure.message}`);
|