ai-foreman 1.7.16 → 1.7.17

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.
@@ -33,7 +33,10 @@ export function buildClaudeQueryOptions(opts) {
33
33
  permissionMode: qaReadOnly ? "default" : "acceptEdits",
34
34
  effort: opts.effort,
35
35
  extraArgs: opts.fast ? { fast: null } : undefined,
36
- settingSources: qaReadOnly ? [] : ["user", "project", "local"],
36
+ // Read-only roles still need the user's Claude authentication settings (for
37
+ // example, an enterprise apiKeyHelper). Do not load repository-controlled
38
+ // settings in those roles.
39
+ settingSources: qaReadOnly ? ["user"] : ["user", "project", "local"],
37
40
  ...(qaReadOnly ? { disallowedTools: ["Write", "Edit", "NotebookEdit"] } : {}),
38
41
  ...(qaReadOnly ? { sandbox: {
39
42
  enabled: true,
@@ -56,7 +59,7 @@ export function buildClaudeQueryOptions(opts) {
56
59
  return base;
57
60
  }
58
61
  function qaEnvironment(source, cwd) {
59
- const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"];
62
+ const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL", "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"];
60
63
  return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: join(dirname(cwd), "scratch") };
61
64
  }
62
65
  export async function probeClaudeSession(ref, input) {
@@ -129,7 +129,9 @@ export function mergeBranchToLocalBase(projectDir, branch, baseBranch, message,
129
129
  runGit(projectDir, ["checkout", baseBranch]);
130
130
  }
131
131
  if (method === "squash") {
132
- runGit(projectDir, ["merge", "--squash", branch]);
132
+ // A user's global `merge.ff=only` conflicts with `--squash`; pin the
133
+ // ordinary merge policy so direct delivery remains independent of it.
134
+ runGit(projectDir, ["-c", "merge.ff=true", "merge", "--squash", branch]);
133
135
  runGit(projectDir, ["commit", "-m", message]);
134
136
  }
135
137
  else if (method === "merge") {
@@ -53,10 +53,10 @@ export function originLooksLikeGitHub(cwd) {
53
53
  export function checkGitHubReadiness(cwd) {
54
54
  const ghVersion = runCommand(cwd, "gh", ["--version"], 5_000);
55
55
  if (!ghVersion.ok) {
56
- return failure("gh_missing", "GitHub CLI is not installed or is not on PATH.", [
56
+ return classifyCommandFailure(ghVersion, "gh_missing", "GitHub CLI is not installed or is not on PATH.", [
57
57
  "Install GitHub CLI: https://cli.github.com/",
58
58
  "gh --version",
59
- ], ghVersion);
59
+ ]);
60
60
  }
61
61
  const remote = inspectGitHubRemote(cwd);
62
62
  if (!remote.ok)
@@ -1,9 +1,10 @@
1
1
  import { Command } from "commander";
2
+ import type { StructuredPlanV1 } from "rafi-spec";
2
3
  import { Log } from "../log.js";
3
4
  import type { EffortLevel } from "../adapters/types.js";
4
5
  import { type RoleBuilderOptions } from "../agentRun.js";
5
6
  import type { TicketsConfig } from "../tickets/config.js";
6
- export declare function buildPopulateInstruction(sourceHints?: string[], progressDoc?: string): string;
7
+ export declare function buildPopulateInstruction(approvedPlanPath: string, contextSources?: string[], progressDoc?: string): string;
7
8
  export declare function resolvePopulateSources(projectDir: string, explicitSources: string[] | undefined, ticketsConfig: TicketsConfig): string[] | undefined;
8
9
  export declare function buildPopulateAgentRunOptions(opts: {
9
10
  projectDir: string;
@@ -26,6 +27,17 @@ interface PopulateCommandOptions {
26
27
  }
27
28
  export declare function combineRequirementSelections(selected: string[], additions: string): string[];
28
29
  export declare function cmdPopulateCli(opts: PopulateCommandOptions): Promise<void>;
30
+ export interface PopulationPlanResolution {
31
+ /** Repository-relative path when possible, for both agent instructions and diagnostics. */
32
+ path: string;
33
+ plan: StructuredPlanV1;
34
+ }
35
+ /**
36
+ * Locate the plan that Rafi approved independently of the documents the user
37
+ * wants Ticket Maker to use as context. In particular, FEATURES.md is not a
38
+ * promise that FEATURES.json is a Rafi plan.
39
+ */
40
+ export declare function resolvePopulationPlan(projectDir: string, contextSources: string[], ticketsConfig: TicketsConfig): PopulationPlanResolution;
29
41
  export interface TicketOwnershipReceipt {
30
42
  path: string;
31
43
  category: "tickets";
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  import { select, text, confirm, isCancel, multiselect } from "@clack/prompts";
6
6
  import { loadConfig } from "../config.js";
@@ -64,23 +64,27 @@ function validateEffort(effort) {
64
64
  fail(err instanceof Error ? err.message : String(err));
65
65
  }
66
66
  }
67
- export function buildPopulateInstruction(sourceHints, progressDoc = "docs/ticket-progress.md") {
68
- const sources = sourceHints ?? [];
67
+ export function buildPopulateInstruction(approvedPlanPath, contextSources, progressDoc = "docs/ticket-progress.md") {
68
+ const sources = contextSources ?? [];
69
69
  const sourceHintBlock = sources.length > 0
70
70
  ? `
71
- User-provided planning source hints:
71
+ Original context source hints:
72
72
  ${sources.map((source) => `- ${source}`).join("\n")}
73
73
 
74
74
  Treat these as files, folders, or globs to check first. Any reasonable project-planning format is acceptable because you are responsible for interpreting it. If a hinted source does not exist or is ambiguous, continue scanning the repository before asking for help.
75
75
  `
76
76
  : `
77
- No specific planning sources were provided. Scan the repository for relevant planning and ticketing documents.
77
+ No original context sources were provided. Use the approved plan and scan the repository only when it is needed to resolve its references.
78
78
  `;
79
79
  return `You are being run by Foreman to populate this repository's Foreman ticket tracker.
80
80
 
81
81
  Goal:
82
82
  - Convert every existing project ticket, task, backlog item, roadmap item, or implementation step into Foreman's structured ticket system.
83
83
  - Do not implement product/code changes. Only update ticket-tracker files.
84
+ \nApproved structured Rafi plan (authoritative for plan identity and slice mapping):
85
+ - ${approvedPlanPath}
86
+
87
+ Read this approved plan before all other sources. It determines the exact plan_id, revision, and slice_ref values that the proposal must use. The context sources below provide supporting product and implementation detail; they do not replace or redefine the approved plan.
84
88
  ${sourceHintBlock}
85
89
 
86
90
  Before proposing, read these tracker control files:
@@ -89,7 +93,7 @@ Before proposing, read these tracker control files:
89
93
  - .tickets/tracker-rules.md
90
94
  - ${progressDoc} if it exists
91
95
 
92
- Then inspect the repository for existing planning sources, beginning with the approved structured Rafi plan. You are proposal-only: do not edit any file, run tracker mutation commands, allocate final ticket IDs, or write delivery configuration. Rafi validates and performs all writes after approval.
96
+ Then inspect the repository for the context required by the approved structured Rafi plan. You are proposal-only: do not edit any file, run tracker mutation commands, allocate final ticket IDs, or write delivery configuration. Rafi validates and performs all writes after approval.
93
97
 
94
98
  Return ticket content using Foreman's schema:
95
99
  - slice_ref: the exact approved plan slice reference. Do not allocate ticket IDs.
@@ -355,13 +359,13 @@ async function collectTicketSetupPatch(dir, opts, current) {
355
359
  };
356
360
  }
357
361
  const section = await select({
358
- message: "Which ticket setup section should be configured?",
362
+ message: "What would you like to configure for tickets?",
359
363
  options: [
360
- { value: "sources", label: "Ticket sources" },
361
- { value: "populate", label: "Populate defaults" },
362
- { value: "build", label: "Build defaults" },
363
- { value: "limits", label: "Tracker limits" },
364
- { value: "all", label: "All sections" },
364
+ { value: "sources", label: "Where tickets come from", hint: "Choose planning files, issue trackers, or other sources." },
365
+ { value: "populate", label: "How Rafi creates tickets from those sources", hint: "Set import and enrichment behavior." },
366
+ { value: "build", label: "How Rafi implements tickets", hint: "Set branches, validation, and completion behavior." },
367
+ { value: "limits", label: "Ticket tracker limits", hint: "Set how many tickets Rafi can show or work on at once." },
368
+ { value: "all", label: "Configure everything", hint: "Walk through every ticket setting." },
365
369
  ],
366
370
  });
367
371
  if (isCancel(section))
@@ -387,6 +391,15 @@ async function collectTicketSetupPatch(dir, opts, current) {
387
391
  };
388
392
  }
389
393
  if (section === "populate" || section === "all") {
394
+ console.log([
395
+ "Recommended population settings:",
396
+ " • Use the saved ticket sources by default.",
397
+ " • Use the project's configured agent when both runtimes are available.",
398
+ ` • Import up to ${DEFAULT_TICKET_SETUP.populate.import_cap} tickets per run.`,
399
+ ` • Import up to ${DEFAULT_TICKET_SETUP.populate.comment_limit} comments per external ticket.`,
400
+ " • Generate deterministic recommendations during population.",
401
+ " • Recommend splitting XL tickets.",
402
+ ].join("\n"));
390
403
  const defaults = await confirm({ message: "Use the recommended population settings?", initialValue: true });
391
404
  if (isCancel(defaults))
392
405
  cancelTicketInterview();
@@ -492,7 +505,7 @@ async function collectTicketSetupPatch(dir, opts, current) {
492
505
  buildPatch.cleanup = true;
493
506
  buildPatch.auto_merge_wait = false;
494
507
  buildPatch.auto_merge_timeout_minutes = null;
495
- buildPatch.validation_checklist = await promptRequirementList("validation checklist", current.build.validation_checklist);
508
+ buildPatch.validation_checklist = await promptValidationChecklist(current.build.validation_checklist);
496
509
  return {
497
510
  sources,
498
511
  limits,
@@ -540,7 +553,7 @@ async function collectTicketSetupPatch(dir, opts, current) {
540
553
  description_sections: await promptRequirementList("required PR/MR description sections", current.build.review.description_sections),
541
554
  };
542
555
  }
543
- buildPatch.validation_checklist = await promptRequirementList("validation checklist", current.build.validation_checklist);
556
+ buildPatch.validation_checklist = await promptValidationChecklist(current.build.validation_checklist);
544
557
  if (completion === "auto-merge") {
545
558
  const wait = await confirm({
546
559
  message: "Wait for dependency PR/MRs to merge before starting dependent tickets?",
@@ -596,6 +609,44 @@ async function promptNonNegativeInteger(message, initial) {
596
609
  cancelTicketInterview();
597
610
  return Number(answer);
598
611
  }
612
+ async function promptValidationChecklist(initial) {
613
+ console.log([
614
+ "Rafi gives these project-wide checks to its QA reviewer for every ticket.",
615
+ "Current validation checklist:",
616
+ ...(initial.length ? initial.map((item) => ` • ${item}`) : [" • No checks configured."]),
617
+ ].join("\n"));
618
+ const mode = await select({
619
+ message: "Which project-wide checks should Rafi's QA reviewer verify for every ticket?",
620
+ initialValue: "keep",
621
+ options: [
622
+ { value: "keep", label: "Keep all of these checks for every ticket (Recommended)" },
623
+ { value: "choose", label: "Choose from these checks and add others" },
624
+ { value: "replace", label: "Replace these with my own checks" },
625
+ ],
626
+ });
627
+ if (isCancel(mode))
628
+ cancelTicketInterview();
629
+ if (mode === "keep")
630
+ return [...initial];
631
+ if (mode === "replace") {
632
+ const replacement = await text({ message: "Project-wide checks for QA to verify on every ticket (comma-separated):", defaultValue: "" });
633
+ if (isCancel(replacement))
634
+ cancelTicketInterview();
635
+ return splitCommaList(String(replacement));
636
+ }
637
+ const selected = initial.length ? await multiselect({
638
+ message: "Select the project-wide checks QA should verify for every ticket:",
639
+ options: initial.map((value) => ({ value, label: value })),
640
+ initialValues: initial,
641
+ required: false,
642
+ }) : [];
643
+ if (isCancel(selected))
644
+ cancelTicketInterview();
645
+ const additions = await text({ message: "Additional project-wide QA checks (comma-separated, optional):", defaultValue: "" });
646
+ if (isCancel(additions))
647
+ cancelTicketInterview();
648
+ return combineRequirementSelections(selected, String(additions));
649
+ }
599
650
  async function promptRequirementList(label, initial) {
600
651
  console.log(`Current recommended ${label}: ${initial.join("; ") || "none"}`);
601
652
  const mode = await select({
@@ -996,7 +1047,8 @@ export async function cmdPopulateCli(opts) {
996
1047
  return;
997
1048
  }
998
1049
  }
999
- const structuredPlan = loadPopulationPlan(dir, sourceHints);
1050
+ const approvedPlan = resolvePopulationPlan(dir, sourceHints, ticketsConfig);
1051
+ const contextSources = sourceHints.filter((source) => !isApprovedPlanArtifact(dir, source, approvedPlan.path));
1000
1052
  if (!opts.yes) {
1001
1053
  const action = await select({
1002
1054
  message: "Ask the read-only Ticket Maker for a structured population proposal?",
@@ -1032,10 +1084,11 @@ export async function cmdPopulateCli(opts) {
1032
1084
  console.log(`foreman tickets: project ${dir}`);
1033
1085
  console.log(`foreman tickets: role ${TICKET_POPULATE_ROLE}`);
1034
1086
  console.log(`foreman tickets: log ${logPath}\n`);
1035
- if (sourceHints.length) {
1036
- console.log(`foreman tickets: source hints ${sourceHints.join(" ")}`);
1087
+ console.log(`foreman tickets: approved plan ${approvedPlan.path}`);
1088
+ if (contextSources.length) {
1089
+ console.log(`foreman tickets: context sources ${contextSources.join(" ")}`);
1037
1090
  }
1038
- const turn = await foreman.runInstruction(buildPopulateInstruction(sourceHints, ticketsConfig.paths.progressDoc));
1091
+ const turn = await foreman.runInstruction(buildPopulateInstruction(approvedPlan.path, contextSources, ticketsConfig.paths.progressDoc));
1039
1092
  await builder.close();
1040
1093
  await viewer;
1041
1094
  log.write("ticket-populate", {
@@ -1056,7 +1109,7 @@ export async function cmdPopulateCli(opts) {
1056
1109
  }
1057
1110
  const existing = loadTickets(resolveTicketPaths(ticketsConfig, dir).tickets);
1058
1111
  const proposal = extractTicketPopulationProposal(turn.result.text);
1059
- const materialized = materializeTicketPopulation(proposal, structuredPlan, existing);
1112
+ const materialized = materializeTicketPopulation(proposal, approvedPlan.plan, existing);
1060
1113
  let retirementsConfirmed = false;
1061
1114
  if (!opts.yes) {
1062
1115
  console.log(`foreman tickets: proposal maps ${materialized.sliceToTicket.size} slice(s) and retires ${materialized.retirements.join(", ") || "none"}`);
@@ -1099,16 +1152,76 @@ async function prepareDocumentSources(projectDir, sources) {
1099
1152
  }
1100
1153
  return unique(prepared);
1101
1154
  }
1102
- function loadPopulationPlan(projectDir, sources) {
1103
- for (const source of sources) {
1104
- const candidate = source.endsWith(".json") ? resolve(projectDir, source) : source.endsWith(".md") ? resolve(projectDir, source.replace(/\.md$/, ".json")) : undefined;
1105
- if (!candidate || !existsSync(candidate))
1155
+ /**
1156
+ * Locate the plan that Rafi approved independently of the documents the user
1157
+ * wants Ticket Maker to use as context. In particular, FEATURES.md is not a
1158
+ * promise that FEATURES.json is a Rafi plan.
1159
+ */
1160
+ export function resolvePopulationPlan(projectDir, contextSources, ticketsConfig) {
1161
+ const explicit = unique(contextSources.filter((source) => source.toLowerCase().endsWith(".json")));
1162
+ const explicitResolution = findPopulationPlan(projectDir, explicit);
1163
+ if (explicitResolution)
1164
+ return explicitResolution;
1165
+ const activeCandidates = activePopulationPlanCandidates(projectDir, ticketsConfig);
1166
+ const activeResolution = findPopulationPlan(projectDir, activeCandidates);
1167
+ if (activeResolution)
1168
+ return activeResolution;
1169
+ const locations = unique([...explicit, ...activeCandidates]);
1170
+ throw new Error(`ticket population could not find a valid approved Rafi plan. Looked for: ${locations.join(", ") || "configured Rafi plan locations"}. `
1171
+ + "Run `rafi plan --validate` (or run `rafi plan` first) and retry.");
1172
+ }
1173
+ function activePopulationPlanCandidates(projectDir, ticketsConfig) {
1174
+ const configured = RAFI_CONFIG_FILES.map((file) => resolveConfiguredRafiPlanPath(projectDir, file)).filter((path) => Boolean(path));
1175
+ return unique([
1176
+ ...configured,
1177
+ join(dirname(ticketsConfig.paths.progressDoc), "rafi-plan.json"),
1178
+ "docs/rafi-plan.json",
1179
+ ]);
1180
+ }
1181
+ function resolveConfiguredRafiPlanPath(projectDir, file) {
1182
+ const configPath = join(projectDir, file);
1183
+ if (!existsSync(configPath))
1184
+ return undefined;
1185
+ const raw = parseYaml(readFileSync(configPath, "utf8"));
1186
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1187
+ throw new Error(`${file}: expected a YAML object`);
1188
+ }
1189
+ const docs = raw.docs;
1190
+ if (typeof docs?.root !== "string")
1191
+ return undefined;
1192
+ return `${validateDocsRoot(projectDir, docs.root)}/rafi-plan.json`;
1193
+ }
1194
+ function findPopulationPlan(projectDir, candidates) {
1195
+ for (const candidate of candidates) {
1196
+ const absolute = resolve(projectDir, candidate);
1197
+ if (!existsSync(absolute))
1106
1198
  continue;
1107
- const plan = JSON.parse(readFileSync(candidate, "utf8"));
1108
- if (plan.version === 1 && plan.plan_id && Number.isInteger(plan.revision) && plan.content_digest)
1109
- return plan;
1199
+ try {
1200
+ const plan = JSON.parse(readFileSync(absolute, "utf8"));
1201
+ if (plan.version === 1 && typeof plan.plan_id === "string" && plan.plan_id && Number.isInteger(plan.revision) && typeof plan.content_digest === "string" && plan.content_digest) {
1202
+ return { path: displayPath(projectDir, absolute), plan };
1203
+ }
1204
+ }
1205
+ catch {
1206
+ // A JSON context document is not necessarily a Rafi plan. Keep looking
1207
+ // for the active approved plan and report every location if none exists.
1208
+ }
1110
1209
  }
1111
- throw new Error("ticket population requires the approved paired structured plan (normally docs/rafi-plan.json); run `rafi plan --validate` first");
1210
+ return undefined;
1211
+ }
1212
+ function displayPath(projectDir, absolutePath) {
1213
+ const path = relative(projectDir, absolutePath).replace(/\\/g, "/");
1214
+ return path && !path.startsWith("../") ? path : absolutePath;
1215
+ }
1216
+ function samePath(projectDir, first, second) {
1217
+ return resolve(projectDir, first) === resolve(projectDir, second);
1218
+ }
1219
+ function isApprovedPlanArtifact(projectDir, source, approvedDataPath) {
1220
+ if (samePath(projectDir, source, approvedDataPath))
1221
+ return true;
1222
+ const dataPath = resolve(projectDir, approvedDataPath);
1223
+ return source.toLowerCase().endsWith(".md")
1224
+ && resolve(projectDir, source) === dataPath.replace(/\.json$/i, ".md");
1112
1225
  }
1113
1226
  function ticketOwnershipReceipts(projectDir, commandName, _afterWrite) {
1114
1227
  const candidates = [
@@ -28,7 +28,8 @@ export interface QaConfinementV2 {
28
28
  sourceMode: "read-only";
29
29
  scratchMode: "isolated";
30
30
  networkMode: "disabled" | "provider-required";
31
- settingsSources: "none";
31
+ /** "user" permits the machine owner's Claude authentication settings, but no project settings. */
32
+ settingsSources: "none" | "user";
32
33
  environmentDigest: string;
33
34
  policyDigest: string;
34
35
  }
package/dist/qaReview.js CHANGED
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
8
8
  import { execFileSync } from "node:child_process";
9
9
  import { resolve } from "node:path";
10
10
  import { WorkflowDb } from "./workflowDb.js";
11
+ import { canonicalSessionPath } from "./sessionIdentity.js";
11
12
  import { loadProjectAutonomyConfig, resolveAutonomyPolicy } from "./recoveryPolicy.js";
12
13
  import { appendQaRecoveryAttempt, appendQaRecoveryResource, appendQaRecoveryReviewedState, compareQaRecoveryReviewedState, createQaRecoveryPacket, ensureQaRecoveryExcluded, loadQaRecoveryPacket, materializeQaRecoveryContext, qaRecoveryInventory, qaReportDigest, renderQaRecoveryAcknowledgementInstruction, updateQaRecoveryPosition, updateQaRecoveryReviewIdentity, validateManualQaReport, validateQaRecoveryAcknowledgement, } from "./qaRecovery.js";
13
14
  import { canonicalJson, createQaFindingRefs, qaDigest } from "./qaProtocolV2.js";
@@ -1445,14 +1446,14 @@ function validateQaSessionHandle(handle, reviewedSnapshotPath) {
1445
1446
  if (identity.version !== 1 || identity.role !== "qa" || identity.stream !== "qa" || identity.provider !== handle.adapter.agent
1446
1447
  || !identity.sessionId || /^(?:unavailable|unknown)$/i.test(identity.sessionId.trim())
1447
1448
  || !Number.isSafeInteger(identity.generation) || identity.generation < 0
1448
- || resolve(identity.cwd) !== resolve(reviewedSnapshotPath) || !identity.configRoot
1449
+ || canonicalSessionPath(identity.cwd) !== canonicalSessionPath(reviewedSnapshotPath) || !identity.configRoot
1449
1450
  || Number.isNaN(Date.parse(identity.createdAt)) || !identity.validatedAt || Number.isNaN(Date.parse(identity.validatedAt))
1450
1451
  || handle.adapter.sessionId() !== identity.sessionId || !adapterIdentity
1451
1452
  || stableProviderSessionIdentity(adapterIdentity) !== stableProviderSessionIdentity(identity)) {
1452
1453
  throw new Error("QA session handle has an invalid or mismatched scoped identity");
1453
1454
  }
1454
1455
  if (!handle.effectiveRoleInstructions.trim() || confinement.version !== 2 || confinement.sourceMode !== "read-only"
1455
- || confinement.scratchMode !== "isolated" || confinement.settingsSources !== "none"
1456
+ || confinement.scratchMode !== "isolated" || !["none", "user"].includes(confinement.settingsSources)
1456
1457
  || !["disabled", "provider-required"].includes(confinement.networkMode)
1457
1458
  || !/^[a-f0-9]{64}$/.test(confinement.environmentDigest) || !/^[a-f0-9]{64}$/.test(confinement.policyDigest)
1458
1459
  || digest !== qaDigest("qa-confinement", confinementFields)) {
package/dist/qaRuntime.js CHANGED
@@ -9,10 +9,14 @@ export function frozenQaRuntimeSettings(adapter, metadata) {
9
9
  }
10
10
  export function describeQaRuntimeHandle(adapter, metadata, handoffReceipt) {
11
11
  const settings = frozenQaRuntimeSettings(adapter, metadata);
12
- const confinementPolicy = { sandboxMode: "read-only", settingsSources: "none", permissions: readOnlyPermissionConfig(), dependencyProjection: "read-only-symlink", scratch: "sibling-temp-directory" };
12
+ // `settingSources` is a Claude Agent SDK control. Claude read-only sessions
13
+ // need the machine owner's authentication settings; Codex does not receive
14
+ // that SDK option.
15
+ const settingsSources = adapter.agent === "claude" ? "user" : "none";
16
+ const confinementPolicy = { sandboxMode: "read-only", settingsSources, permissions: readOnlyPermissionConfig(), dependencyProjection: "read-only-symlink", scratch: "sibling-temp-directory" };
13
17
  const confinementBase = {
14
18
  version: 2, sourceMode: "read-only", scratchMode: "isolated",
15
- settingsSources: "none", networkMode: "provider-required",
19
+ settingsSources, networkMode: "provider-required",
16
20
  environmentDigest: qaDigest("qa-confinement-environment", { runtime: settings.make, model: settings.model, reasoning: settings.reasoning, fast: settings.fast, platform: process.platform, arch: process.arch }),
17
21
  policyDigest: qaDigest("qa-confinement-policy", confinementPolicy),
18
22
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-foreman",
3
- "version": "1.7.16",
3
+ "version": "1.7.17",
4
4
  "description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -195,7 +195,7 @@
195
195
  "scripts": {
196
196
  "build": "tsc -p tsconfig.build.json",
197
197
  "dev": "tsx src/index.ts",
198
- "test": "tsx --test test/*.test.ts",
198
+ "test": "tsx --test --test-concurrency=1 test/*.test.ts",
199
199
  "typecheck": "tsc --noEmit",
200
200
  "test:live-providers": "RAFI_LIVE_PROVIDER_SESSIONS=1 tsx --test test/providerSmoke.test.ts",
201
201
  "prepublishOnly": "tsc -p ../spec/tsconfig.build.json && tsc -p tsconfig.build.json && pnpm --filter ai-foreman test && tsc --noEmit"