ai-foreman 1.7.20 → 1.7.21

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.
Files changed (189) hide show
  1. package/README.md +12 -2
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. 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 { durableHumanDecision, HumanDecisionRequired } from "../humanDecision.js";
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";
@@ -55,7 +61,6 @@ import { ObservabilityStore, RunObserver } from "../observability.js";
55
61
  import { loadProjectAutonomyConfig, resolveAutonomyPolicy, resolveQaEnablement } from "../recoveryPolicy.js";
56
62
  import { loadQaRecoveryPacket, recoverPendingQaRecoveryPublications } from "../qaRecovery.js";
57
63
  import { qaDigest } from "../qaProtocolV2.js";
58
- import { loadSkill } from "special-agents";
59
64
  import { acceptQaRuntimeHandoff, describeQaRuntimeHandle, frozenQaRuntimeSettings } from "../qaRuntime.js";
60
65
  const FOREMAN_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
61
66
  function fail(message) {
@@ -147,11 +152,10 @@ export function formatResumeGuidance(executable, projectDir, steps, sessionId, r
147
152
  return [
148
153
  ...run.decisions.flatMap(decision => [
149
154
  `foreman: input required: ${decision.prompt}`,
150
- `foreman: choices: ${decision.choices.map(choice => `${choice.id} (${choice.label})`).join(", ")}`,
151
- ` rafi build:decide ${shellQuote(projectDir)} --run ${shellQuote(run.runId)} --decision ${shellQuote(decision.decisionId)} --choice <choice-id>`,
155
+ ...formatDecisionCommands(projectDir, run.runId, decision, executable),
152
156
  ]),
153
- "foreman: resume this run with:",
154
- ` ${formatRecoveryCommand(projectDir, "rafi")}`,
157
+ executable === "rafi" ? "foreman: resume this run with:" : "foreman: review recovery for this run with:",
158
+ ` ${formatExactRunRecovery(projectDir, run.runId, executable)}`,
155
159
  ];
156
160
  if (executable === "rafi") {
157
161
  return [
@@ -486,6 +490,8 @@ export function buildStartCommand() {
486
490
  .option("--auto-compact-threshold <percent>", "initial Builder context compaction threshold (1-99)")
487
491
  .option("--max-branch-depth <n>", "maximum selected branch stack depth", "5")
488
492
  .option("--pr-ready", "create ready-for-review PRs instead of draft PRs")
493
+ .option("--builder-network", "request build-scoped builder internet access with separate explicit approval; rechecked on resume")
494
+ .option("--builder-approvals", "allow the builder to request explicit per-operation sandbox approvals; QA stays restricted")
489
495
  .option("--keep-worktrees", "keep successful ticket worktrees for inspection")
490
496
  .option("--ticket <id>", "select one new ticket, or identify recovery tickets with --resume/--continue/--recover-run", collectTicket, [])
491
497
  .option("--skip-delivery-unit <id>", "skip one unfinished delivery unit for this run", collectTicket, [])
@@ -558,6 +564,34 @@ export function buildStartCommand() {
558
564
  try {
559
565
  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
566
  return;
567
+ if (recoveryRecord || opts.preparationRun) {
568
+ assertBuildAssignmentReconciled(cwd, supervisedInvocationId);
569
+ if (!await servicePendingHumanDecisions({ projectDir: cwd, runId: supervisedInvocationId, tickets: requestedTicketIds.length ? requestedTicketIds : recoveryRecord?.tickets.length ? recoveryRecord.tickets : undefined, scopeRevision: isTicketsInitialized(cwd) ? buildScopeRevision(cwd) : undefined }))
570
+ return;
571
+ }
572
+ const requestedNetwork = Boolean(opts.builderNetwork || recoveryRecord?.builderCapabilities?.requestedNetwork);
573
+ let grantedNetwork = false;
574
+ if (requestedNetwork) {
575
+ const permissionDb = new WorkflowDb(cwd);
576
+ let networkKey;
577
+ try {
578
+ networkKey = permissionDb.pendingHumanDecisions(supervisedInvocationId).find(decision => decision.interruptionId.startsWith("builder-network:"))?.interruptionId ?? `builder-network:${process.pid}:${randomUUID()}`;
579
+ }
580
+ finally {
581
+ permissionDb.close();
582
+ }
583
+ const networkDecision = await durableHumanDecision({ projectDir: cwd, runId: supervisedInvocationId, key: networkKey,
584
+ 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.",
585
+ choices: [{ id: "allow-network", label: "Allow builder internet for this build" }, { id: "stay-offline", label: "Stay offline and use a verified local bundle" }],
586
+ operation: async () => {
587
+ 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" }] });
588
+ return isCancel(action) ? undefined : action;
589
+ } });
590
+ if (networkDecision === undefined || isCancel(networkDecision))
591
+ return;
592
+ grantedNetwork = networkDecision === "allow-network";
593
+ }
594
+ const builderCapabilities = { requestedNetwork, grantedNetwork, ...(grantedNetwork ? { approvedAt: new Date().toISOString(), grantWorkerPid: process.pid } : {}) };
561
595
  const readinessOwner = { project: cwd, runId: supervisedInvocationId, authority: localBuildAuthority(cwd) };
562
596
  const autoApprovePlanUpdates = recoveryRecord
563
597
  ? recoveryRecord.recoveryDecision?.planUpdateApproval === "auto"
@@ -799,7 +833,7 @@ export function buildStartCommand() {
799
833
  }
800
834
  const recoveryTicket = !branchMode && recoveryRecord ? continueTickets[0] : undefined;
801
835
  const preferredTicket = selectedNewTicket ?? recoveryTicket;
802
- const executionTickets = !branchMode && recoveryRecord && recoveryRecord.tickets.length
836
+ let executionTickets = !branchMode && recoveryRecord && recoveryRecord.tickets.length
803
837
  ? recoveryExecutionTickets(recoveryRecord, requestedTicketIds, Boolean(opts.recoveryMode))
804
838
  : selectedNewTicket ? [selectedNewTicket] : undefined;
805
839
  const config = loadConfig(join(cwd, "foreman.yaml"));
@@ -826,11 +860,15 @@ export function buildStartCommand() {
826
860
  }
827
861
  }
828
862
  let activeObserver = observabilityStore ? new RunObserver(observabilityStore, invocationRunId) : undefined;
829
- const finishObservationOnExit = () => {
863
+ const finishObservationOnExit = (exitCode) => {
830
864
  if (!activeObserver)
831
865
  return;
832
- activeObserver.finish("interrupted");
833
- observabilityStore?.closeLogFile(logPath, "interrupted");
866
+ const reader = new WorkflowReader(cwd);
867
+ const durable = reader.getRun(invocationRunId);
868
+ reader.close();
869
+ const outcome = durable?.status === "cancelled" ? "cancelled" : durable?.checkpoint === "waiting-for-human" ? "waiting-for-human" : durable?.status === "failed" ? "failed" : "interrupted";
870
+ activeObserver.finish(outcome, { checkpoint: durable?.checkpoint, failureCategory: `process-exit:${exitCode}` });
871
+ observabilityStore?.closeLogFile(logPath, outcome);
834
872
  observabilityStore?.close();
835
873
  activeObserver = undefined;
836
874
  };
@@ -864,6 +902,8 @@ export function buildStartCommand() {
864
902
  workspaceIdentity: !branchMode ? captureCurrentWorkflowSessionIdentity(builderCwd) : captureWorkspaceIdentity(builderCwd),
865
903
  ticketId: sessionRef?.ticketId,
866
904
  deliveryUnitId: sessionRef?.deliveryUnitId,
905
+ networkAccess: grantedNetwork,
906
+ approvalPolicy: opts.builderApprovals ? "on-request" : "never",
867
907
  permission: createPermissionHandler(builderPolicy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
868
908
  onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
869
909
  onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
@@ -871,7 +911,7 @@ export function buildStartCommand() {
871
911
  fast: builderFast,
872
912
  systemPromptAppend: roleBundle.system || undefined,
873
913
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
874
- autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
914
+ autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
875
915
  observer: activeObserver,
876
916
  };
877
917
  const adapter = agent === "codex"
@@ -894,13 +934,13 @@ export function buildStartCommand() {
894
934
  role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
895
935
  reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
896
936
  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 ?? 50,
937
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
898
938
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
899
939
  } : {
900
940
  role, source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
901
941
  reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact",
902
942
  settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
903
- auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
943
+ auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
904
944
  compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
905
945
  };
906
946
  const liveRoleSettings = (role, base) => {
@@ -953,22 +993,9 @@ export function buildStartCommand() {
953
993
  });
954
994
  const qaRuntimeMetadata = new WeakMap();
955
995
  const resolveQaRuntimeMetadata = (qaCwd, settings) => {
956
- const bundle = loadRoleBundle("qa", { projectDir: qaCwd });
957
- const effectiveRoleInstructions = `${bundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`;
958
- const skills = bundle.skills.map((name) => {
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 };
996
+ const effective = resolveEffectiveQaConfiguration(cwd, settings);
997
+ const effectiveRoleInstructions = finalQaRoleInstructions(effective.qaRules);
998
+ return { settings: structuredClone(settings), effectiveRoleInstructions, skills: effective.skills.map(skill => ({ ...skill, digest: createHash("sha256").update(skill.content).digest("hex") })) };
972
999
  };
973
1000
  const createBuilderForSettings = async (builderCwd, settings) => {
974
1001
  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 +1021,8 @@ export function buildStartCommand() {
994
1021
  shutdownTimeoutMs: frozenPolicy.runtimeDeadlines?.shutdown_ms,
995
1022
  turnDeadlineMs: frozenPolicy.runtimeDeadlines?.turn_ms,
996
1023
  model: settings.model === "default" ? undefined : settings.model,
1024
+ networkAccess: grantedNetwork,
1025
+ approvalPolicy: opts.builderApprovals ? "on-request" : "never",
997
1026
  permission: createPermissionHandler(policy, log, { durable: () => ({ projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, ticketId: activeObserver?.context().ticketId }) }),
998
1027
  onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
999
1028
  onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace }),
@@ -1677,6 +1706,7 @@ export function buildStartCommand() {
1677
1706
  if (branchDefaults.createReview && branchDefaults.reviewProvider) {
1678
1707
  await ensureReviewProviderReady(cwd, branchDefaults.reviewProvider, log, Boolean(opts.yes));
1679
1708
  }
1709
+ 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
1710
  const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
1681
1711
  label: "start",
1682
1712
  yes: Boolean(opts.yes),
@@ -1755,7 +1785,7 @@ export function buildStartCommand() {
1755
1785
  try {
1756
1786
  auditBuilder = await createBuilder(cwd);
1757
1787
  auditViewer = printEvents(auditBuilder.events());
1758
- const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
1788
+ 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
1789
  if (audit.isError)
1760
1790
  throw new Error(audit.text);
1761
1791
  const auditDependencies = parseAuditDependencies(audit.text);
@@ -1860,23 +1890,23 @@ export function buildStartCommand() {
1860
1890
  make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
1861
1891
  session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
1862
1892
  display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
1863
- auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
1893
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
1864
1894
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
1865
1895
  };
1866
1896
  const capturedBranchQa = {
1867
1897
  role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
1868
1898
  reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
1869
1899
  display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
1870
- auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
1900
+ auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 65,
1871
1901
  compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
1872
1902
  };
1873
1903
  const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
1874
1904
  let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
1875
1905
  runId: invocationRunId,
1876
- tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
1906
+ builderCapabilities, tickets: plan.nodes.map((node) => node.ticket.id), deliveryUnit: selectedStacks.length ? selectedStacks.map((stack) => stack.id).join(",") : deliveryRun?.unit.id,
1877
1907
  repositoryRoot: cwd, branchMode: branchPresentation.allocationMode, baseRef: plan.baseRef, builder: capturedBranchBuilder, qa: capturedBranchQa,
1878
1908
  frozenPolicy, autonomyProfile, qaEnabled,
1879
- runDecisions: { workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
1909
+ runDecisions: { workMode: "branch-per-ticket", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBranchBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
1880
1910
  });
1881
1911
  activeContinuityRunId = masterRun.runId;
1882
1912
  if (requiresBuildApproval(cwd, plan.nodes.map(node => node.ticket.id), autoApprovePlanUpdates, approvalConsequences) && plan.issues.every((issue) => !issue.blocking)) {
@@ -1888,10 +1918,25 @@ export function buildStartCommand() {
1888
1918
  ],
1889
1919
  }) });
1890
1920
  if (isCancel(action) || action === "cancel") {
1921
+ const cancelled = action === "cancel";
1922
+ if (cancelled) {
1923
+ const decisions = new WorkflowDb(cwd);
1924
+ try {
1925
+ decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
1926
+ }
1927
+ finally {
1928
+ decisions.close();
1929
+ }
1930
+ }
1931
+ masterRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, masterRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
1932
+ activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
1891
1933
  console.log("ai-foreman: cancelled");
1892
1934
  process.exit(0);
1893
1935
  }
1894
1936
  }
1937
+ for (const node of plan.nodes)
1938
+ admitApprovedTicket(cwd, masterRun.runId, node.ticket.id, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved branch plan`);
1939
+ masterRun = checkpointBuildRun(cwd, masterRun, "capabilities-resolved", { builderCapabilities });
1895
1940
  const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, masterRun.runId, requestedTicketIds[0]) : undefined;
1896
1941
  let qaProtocolResumeTicket;
1897
1942
  if (recoveryRecord && !qaResumedRecovery) {
@@ -2149,6 +2194,17 @@ export function buildStartCommand() {
2149
2194
  fireTerminalBell(config.notifications.terminal_bell);
2150
2195
  process.exit(failed ? 2 : 0);
2151
2196
  }
2197
+ if (!recoveryRecord && isTicketsInitialized(cwd)) {
2198
+ const paths = resolveTicketPaths(loadTicketsConfig(cwd), cwd);
2199
+ const state = new StateDb(paths.stateDb);
2200
+ try {
2201
+ 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);
2202
+ }
2203
+ finally {
2204
+ state.close();
2205
+ }
2206
+ }
2207
+ 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
2208
  const ready = await ensureBuildRuntimeReadyForCommand(cwd, agent, { build: readinessOwner, durable: { projectDir: cwd, runId: activeContinuityRunId ?? invocationRunId, scopeRevision: buildScopeRevision(cwd, approvalConsequences) }, timeoutMs: frozenPolicy.runtimeDeadlines?.preparation_ms,
2153
2209
  label: "start",
2154
2210
  yes: Boolean(opts.yes),
@@ -2183,16 +2239,17 @@ export function buildStartCommand() {
2183
2239
  make: agent, model: model ?? "default", reasoning: builderEffort ?? "default", fast: Boolean(builderFast),
2184
2240
  session_strategy: roleDefaults.builder?.session_strategy ?? "compact", settings_revision: settingsRevision,
2185
2241
  display_session_cost: sessionCostOverride ?? roleDefaults.builder?.display_session_cost ?? false,
2186
- auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
2242
+ auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 65,
2187
2243
  compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
2188
2244
  };
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 ?? 50, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
2245
+ 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
2246
  let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null, expectedRecoveryDecisionDigest: opts.recoveryDecisionDigest }) : createBuildRun({
2191
2247
  runId: invocationRunId,
2192
- tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
2248
+ 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
2249
  frozenPolicy, autonomyProfile, qaEnabled,
2194
- runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
2250
+ runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 65, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
2195
2251
  });
2252
+ buildRun = checkpointBuildRun(cwd, buildRun, "capabilities-resolved", { builderCapabilities });
2196
2253
  activeObserver?.store.attachExecutionLease(activeObserver.executionId, readCurrentWorkflowLease(cwd)?.generation);
2197
2254
  activeContinuityRunId = buildRun.runId;
2198
2255
  const qaResumedRecovery = recoveryRecord ? pendingQaRecoveryPacket(cwd, buildRun.runId, requestedTicketIds[0]) : undefined;
@@ -2346,11 +2403,11 @@ export function buildStartCommand() {
2346
2403
  builderFast ? "fast" : null,
2347
2404
  qaEnabled ? null : "qa=off",
2348
2405
  ].filter(Boolean).join(" ");
2349
- console.log(`foreman: driving a ${agent} builder through ${steps} step(s)${modifiers ? ` [${modifiers}]` : ""}`);
2350
- console.log(`foreman: project ${cwd}`);
2406
+ console.log(`${accent("foreman:")} driving a ${sanitizeTerminalText(agent)} builder through ${steps} step(s)${modifiers ? ` [${sanitizeTerminalText(modifiers)}]` : ""}`);
2407
+ console.log(`${accent("foreman:")} project ${sanitizeTerminalText(cwd)}`);
2351
2408
  if (trackerRelPath)
2352
- console.log(`foreman: tracker ${trackerRelPath}`);
2353
- console.log(`foreman: log ${logPath}\n`);
2409
+ console.log(`${accent("foreman:")} tracker ${sanitizeTerminalText(trackerRelPath)}`);
2410
+ console.log(`${accent("foreman:")} log ${sanitizeTerminalText(logPath)}\n`);
2354
2411
  const viewer = printEvents(builder.events());
2355
2412
  try {
2356
2413
  if (recoveryRecord && currentBranchTicketProgress(cwd, buildRun)?.remaining.length === 0) {
@@ -2439,6 +2496,18 @@ export function buildStartCommand() {
2439
2496
  ],
2440
2497
  }) });
2441
2498
  if (isCancel(action) || action === "cancel") {
2499
+ const cancelled = action === "cancel";
2500
+ if (cancelled) {
2501
+ const decisions = new WorkflowDb(cwd);
2502
+ try {
2503
+ decisions.cancelPendingHumanDecisions(invocationRunId, "User cancelled build; pending decisions superseded");
2504
+ }
2505
+ finally {
2506
+ decisions.close();
2507
+ }
2508
+ }
2509
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, cancelled ? "cancelled" : "input-paused", { status: cancelled ? "cancelled" : "recoverable" }), cancelled ? "cancelled" : "recoverable");
2510
+ activeObserver?.finish(cancelled ? "cancelled" : "paused", { checkpoint: cancelled ? "user-cancel" : "prompt-eof-or-cancel" });
2442
2511
  console.log("ai-foreman: cancelled");
2443
2512
  statusReporter.stop("cancelled");
2444
2513
  await foreman.close();
@@ -2453,7 +2522,9 @@ export function buildStartCommand() {
2453
2522
  message: "Your feedback:",
2454
2523
  validate: (v) => (v?.trim() ? undefined : "Please enter some feedback"),
2455
2524
  }) });
2456
- if (isCancel(fb)) {
2525
+ if (isCancel(fb) || fb === undefined) {
2526
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "input-paused", { status: "recoverable" }), "recoverable");
2527
+ activeObserver?.finish("paused", { checkpoint: "feedback-eof-or-cancel" });
2457
2528
  console.log("ai-foreman: cancelled");
2458
2529
  statusReporter.stop("cancelled");
2459
2530
  await foreman.close();
@@ -2466,10 +2537,16 @@ export function buildStartCommand() {
2466
2537
  }
2467
2538
  });
2468
2539
  await ensureApproved();
2540
+ if (!isTicketsInitialized(cwd)) {
2541
+ const synthetic = initializeSyntheticWork(cwd, buildRun.runId, steps, `Explicit unticketed build invocation ${invocationRunId}`, foreman.approvedPlanText());
2542
+ buildRun = checkpointBuildRun(cwd, buildRun, "synthetic-work-admitted", { tickets: synthetic });
2543
+ }
2469
2544
  const onTicketStart = async (ticketId) => {
2470
2545
  await ensureApproved();
2546
+ admitApprovedTicket(cwd, buildRun.runId, ticketId, buildScopeRevision(cwd), `build invocation ${invocationRunId}: approved ticket selection`);
2471
2547
  const tickets = buildRun.tickets.includes(ticketId) ? buildRun.tickets : [...buildRun.tickets, ticketId];
2472
- buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId });
2548
+ buildRun = checkpointBuildRun(cwd, buildRun, "ticket-selected", { tickets, currentTicket: ticketId,
2549
+ ...(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
2550
  };
2474
2551
  const batch = () => foreman.runBatch(steps, trackerRelPath, onTicketStart, preferredTicket, executionTickets);
2475
2552
  const result = await (activeObserver ? activeObserver.span("builder_work", "Builder and QA batch", batch) : batch());
@@ -2496,8 +2573,12 @@ export function buildStartCommand() {
2496
2573
  pruneRecoveryTelemetryAtCompletion(cwd, config.observability.detail_retention_days);
2497
2574
  activeObserver = undefined;
2498
2575
  process.off("exit", finishObservationOnExit);
2499
- console.log(`\nforeman: ${result.completed}/${result.requested} step(s) completed`);
2500
- console.log(`foreman: outcome — ${result.outcome}`);
2576
+ console.log(`\n${accent("foreman:")} ${result.completed}/${result.requested} step(s) completed`);
2577
+ const safeOutcome = sanitizeTerminalText(result.outcome);
2578
+ const resultOutcome = /success|complete|pass/i.test(safeOutcome) ? success(safeOutcome)
2579
+ : /fail|error/i.test(safeOutcome) ? error(safeOutcome)
2580
+ : /warn|partial|blocked/i.test(safeOutcome) ? warning(safeOutcome) : safeOutcome;
2581
+ console.log(`${accent("foreman:")} outcome — ${resultOutcome}`);
2501
2582
  if (result.detail)
2502
2583
  console.log(`foreman: ${result.detail}`);
2503
2584
  if (buildRun.status === "completed")
@@ -2561,6 +2642,13 @@ export function buildStartCommand() {
2561
2642
  console.error(`foreman: resume this run with: ${formatRecoveryCommand(cwd)}`);
2562
2643
  process.exit(2);
2563
2644
  }
2645
+ if (err instanceof HumanDecisionCancelled) {
2646
+ buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "cancelled", { status: "cancelled" }), "cancelled");
2647
+ activeObserver?.finish("cancelled", { checkpoint: "question-menu" });
2648
+ await foreman.close().catch(() => { });
2649
+ console.log("ai-foreman: cancelled");
2650
+ process.exit(0);
2651
+ }
2564
2652
  if (err instanceof HumanDecisionRequired) {
2565
2653
  buildRun = releaseBuildLease(cwd, checkpointBuildRun(cwd, buildRun, "waiting-for-human", { status: "recoverable" }), "recoverable");
2566
2654
  await foreman.close().catch(() => { });
@@ -2585,6 +2673,16 @@ export function buildStartCommand() {
2585
2673
  fail(`run failed: ${err instanceof Error ? err.message : String(err)}`);
2586
2674
  }
2587
2675
  }
2676
+ catch (error) {
2677
+ if (error instanceof HumanDecisionCancelled) {
2678
+ console.log("ai-foreman: cancelled");
2679
+ return;
2680
+ }
2681
+ if (!(error instanceof HumanDecisionRequired))
2682
+ throw error;
2683
+ console.error(error.message);
2684
+ process.exitCode = 2;
2685
+ }
2588
2686
  finally {
2589
2687
  finishStartAdmission(cwd, supervisedInvocationId);
2590
2688
  }
@@ -2680,8 +2778,11 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2680
2778
  throw new Error("QA nonconvergence requires its owning build run");
2681
2779
  const scope = createHash("sha256").update(JSON.stringify([buildScopeRevision(projectDir), context.ticket, context.history])).digest("hex");
2682
2780
  let round = 0;
2781
+ let waiverDecisionId;
2683
2782
  const ask = (phase, choices, operation) => durableHumanDecision({
2684
2783
  projectDir, runId, ticketId: context.ticket.id, observer: observer?.(), defer: noninteractive,
2784
+ onAnswered: decision => { if (phase.startsWith("QA waiver confirmation") && decision.selectedChoiceId === "yes")
2785
+ waiverDecisionId = decision.decisionId; },
2685
2786
  key: `qa-nonconvergence:${scope}:${round}:${phase}`, prompt: `${context.ticket.id}: ${phase}`, choices, operation,
2686
2787
  });
2687
2788
  while (true) {
@@ -2702,7 +2803,10 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2702
2803
  ] }));
2703
2804
  if (approved === "yes") {
2704
2805
  const db = new WorkflowDb(projectDir);
2806
+ let authorizationRef;
2705
2807
  try {
2808
+ if (db.qaPreparationStore().policy(runId)?.mode === "enforce")
2809
+ authorizationRef = db.authorizeContractQaWaiver(runId, context.ticket.id, waiverDecisionId);
2706
2810
  const lease = db.currentLease();
2707
2811
  if (lease) {
2708
2812
  const detail = JSON.stringify({ decision: "waive", ticket: context.ticket.id, unresolved: context.history, at: new Date().toISOString() });
@@ -2714,7 +2818,7 @@ export function createQaNonconvergenceHandler(projectDir, noninteractive, planne
2714
2818
  db.close();
2715
2819
  }
2716
2820
  console.log(`foreman: QA WAIVER recorded for ${context.ticket.id}; validation_result will be failed`);
2717
- return { action: "waive" };
2821
+ return { action: "waive", ...(authorizationRef ? { authorizationRef } : {}) };
2718
2822
  }
2719
2823
  continue;
2720
2824
  }
@@ -6,8 +6,9 @@ import { 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(`foreman: ${message}`);
11
+ console.error(`${error("foreman:", { stream: process.stderr })} ${message}`);
11
12
  process.exit(1);
12
13
  }
13
14
  export function buildStatusCommand() {
@@ -24,6 +25,16 @@ 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}`);
@@ -52,8 +63,8 @@ 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
- console.log(`foreman: latest run ${logs[logs.length - 1]}`);
56
- console.log(`foreman: ${steps.length} step record(s), ${escalations.length} escalation(s)`);
66
+ console.log(`${accent("foreman:")} latest run ${sanitizeTerminalText(logs[logs.length - 1] ?? "")}`);
67
+ console.log(`${accent("foreman:")} ${steps.length} step record(s), ${escalations.length} escalation(s)`);
57
68
  if (branchEvents.length || prEvents.length) {
58
69
  const completed = branchEvents.filter((r) => r.event === "branch-complete").length;
59
70
  const issues = branchEvents.filter((r) => r.event === "branch-issue").length;
@@ -61,12 +72,16 @@ export function runStatus(project) {
61
72
  console.log(`foreman: branch mode — ${completed} completed, ${issues} issue(s), ${prs} PR(s)`);
62
73
  }
63
74
  if (batchEnd) {
64
- console.log(`foreman: outcome — ${batchEnd.outcome} (${batchEnd.completed}/${batchEnd.requested})`);
75
+ const outcome = sanitizeTerminalText(String(batchEnd.outcome ?? ""));
76
+ const styledOutcome = /success|complete|pass/i.test(outcome) ? success(outcome)
77
+ : /fail|error/i.test(outcome) ? error(outcome)
78
+ : /warn|partial|blocked/i.test(outcome) ? warning(outcome) : outcome;
79
+ console.log(`${accent("foreman:")} outcome — ${styledOutcome} (${batchEnd.completed}/${batchEnd.requested})`);
65
80
  if (batchEnd.detail)
66
81
  console.log(`foreman: ${batchEnd.detail}`);
67
82
  }
68
83
  else {
69
- console.log("ai-foreman: run is still in progress or did not finish");
84
+ console.log(`${accent("ai-foreman:")} run is still in progress or did not finish`);
70
85
  }
71
86
  if (latestFailure) {
72
87
  console.log(`foreman: latest GitHub failure — ${latestFailure.code}: ${latestFailure.message}`);
@@ -1,4 +1,5 @@
1
1
  import { Command } from "commander";
2
+ import { accent, error, sanitizeTerminalText, success, warning } from "../terminalStyle.js";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
5
  import { parse as parseYaml } from "yaml";
@@ -1240,6 +1241,7 @@ export async function cmdPopulateCli(opts) {
1240
1241
  builder = roleBuilder.builder;
1241
1242
  viewer = printEvents(builder.events());
1242
1243
  const foreman = new Foreman(builder, log, { desktop: config.notifications.enabled, terminalBell: config.notifications.terminal_bell }, false, 3, dir);
1244
+ foreman.setGraphPurpose("ticket-population");
1243
1245
  console.log(`foreman tickets: populating tickets with ${roleBuilder.runtime}`);
1244
1246
  console.log(`foreman tickets: project ${dir}`);
1245
1247
  console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
@@ -1927,7 +1929,7 @@ export function buildTicketsCommand(options = {}) {
1927
1929
  evidence: opts.evidence,
1928
1930
  lastError: opts.lastError,
1929
1931
  });
1930
- console.log(`foreman tickets: updated ${ticketId}`);
1932
+ console.log(`${accent("foreman tickets:")} updated ${sanitizeTerminalText(ticketId)}`);
1931
1933
  }
1932
1934
  catch (err) {
1933
1935
  fail(String(err instanceof Error ? err.message : err));
@@ -1954,7 +1956,7 @@ export function buildTicketsCommand(options = {}) {
1954
1956
  evidence: opts.evidence,
1955
1957
  validationNotes: opts.validationNotes,
1956
1958
  });
1957
- console.log(`foreman tickets: completed ${ticketId}`);
1959
+ console.log(`${accent("foreman tickets:")} ${success("completed")} ${sanitizeTerminalText(ticketId)}`);
1958
1960
  }
1959
1961
  catch (err) {
1960
1962
  fail(String(err instanceof Error ? err.message : err));
@@ -1979,7 +1981,7 @@ export function buildTicketsCommand(options = {}) {
1979
1981
  actor: opts.actor,
1980
1982
  unblockCriteria: opts.unblockCriteria,
1981
1983
  });
1982
- console.log(`foreman tickets: blocked ${ticketId}`);
1984
+ console.log(`${accent("foreman tickets:")} ${warning("blocked")} ${sanitizeTerminalText(ticketId)}`);
1983
1985
  }
1984
1986
  catch (err) {
1985
1987
  fail(String(err instanceof Error ? err.message : err));
@@ -1998,7 +2000,7 @@ export function buildTicketsCommand(options = {}) {
1998
2000
  summary: opts.summary,
1999
2001
  actor: opts.actor,
2000
2002
  });
2001
- console.log(`foreman tickets: unblocked ${ticketId}`);
2003
+ console.log(`${accent("foreman tickets:")} ${success("unblocked")} ${sanitizeTerminalText(ticketId)}`);
2002
2004
  }
2003
2005
  catch (err) {
2004
2006
  fail(String(err instanceof Error ? err.message : err));
@@ -2188,10 +2190,10 @@ export function buildTicketsCommand(options = {}) {
2188
2190
  try {
2189
2191
  const result = cmdValidate(cwd(opts));
2190
2192
  if (result.issues.length === 0) {
2191
- console.log("foreman tickets: validation passed — all 4 passes clean");
2193
+ console.log(`${accent("foreman tickets:")} ${success("validation passed")} — all 4 passes clean`);
2192
2194
  }
2193
2195
  else {
2194
- console.log(`foreman tickets: ${result.issues.length} issue(s) found:`);
2196
+ console.log(`${accent("foreman tickets:")} ${error(`${result.issues.length} issue(s) found:`)}`);
2195
2197
  console.log(formatValidationIssues(result.issues));
2196
2198
  if (!result.clean)
2197
2199
  process.exit(1);
@@ -2221,7 +2223,7 @@ export function buildTicketsCommand(options = {}) {
2221
2223
  console.log(line);
2222
2224
  return;
2223
2225
  }
2224
- console.log("Batches: none configured. Queue is flat.");
2226
+ console.log(`${accent("Batches:")} none configured. Queue is flat.`);
2225
2227
  const rows = cmdQueue(dir, opts.limit !== undefined ? Number(opts.limit) : undefined);
2226
2228
  if (rows.length === 0) {
2227
2229
  console.log("No remaining tickets.");
@@ -61,6 +61,14 @@ export interface ContinuityAdapterOptions {
61
61
  export declare class ContinuityAdapter implements BuilderAdapter {
62
62
  private readonly options;
63
63
  private adapter;
64
+ contractCapabilities(): {
65
+ sameSessionAcceptance: boolean;
66
+ nativeCompactionBarrier: boolean;
67
+ };
68
+ private contractEnforcing;
69
+ enableContractEnforcement(): void;
70
+ contractCompactionSequence(): number;
71
+ acceptContractDelivery(sequence: number): void;
64
72
  private readonly queue;
65
73
  private sourcePump?;
66
74
  private recoveryLeasePending;
@@ -74,7 +82,9 @@ export declare class ContinuityAdapter implements BuilderAdapter {
74
82
  handback?: boolean;
75
83
  responseOnly?: boolean;
76
84
  logicalActionId?: string;
85
+ purpose?: import("./providerPhase.js").ProviderTurnPurpose;
77
86
  }): Promise<TurnResult>;
87
+ private sendTurnOwned;
78
88
  observeEvents(listener: (event: import("./adapters/types.js").BuilderEvent) => void): () => void;
79
89
  acceptHandbackTurn(turn: TurnResult): void;
80
90
  sessionId(): string | undefined;
@@ -90,6 +100,7 @@ export declare class ContinuityAdapter implements BuilderAdapter {
90
100
  restoreNativeCompactions(compactions: NativeCompaction[]): void;
91
101
  contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
92
102
  contextUsage(): Promise<ContextUsage | undefined>;
103
+ graphRuntimeSettings(): Pick<import("./adapters/types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
93
104
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
94
105
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
95
106
  events(): AsyncIterable<import("./adapters/types.js").BuilderEvent>;