@parabolic-technologies/banzai 2.6.4 → 2.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/banzai.mjs CHANGED
@@ -5719,6 +5719,14 @@ var CLAUDE_CATALOG = {
5719
5719
  ["claude-3-sonnet-20240229", retired(SONNET, "2025-07-21")],
5720
5720
  ["claude-3-5-haiku-20241022", retired(HAIKU, "2026-02-19")],
5721
5721
  ["claude-3-haiku-20240307", retired(HAIKU, "2026-04-20")]
5722
+ ]),
5723
+ // From Claude Code's model configuration documentation. An older client
5724
+ // refuses these models itself, before any request; tier aliases need no
5725
+ // entry because they resolve to a model the installed client serves.
5726
+ cliMinimums: /* @__PURE__ */ new Map([
5727
+ [OPUS, "2.1.280"],
5728
+ [SONNET, "2.1.284"],
5729
+ ["claude-fable-5-1", "2.1.257"]
5722
5730
  ])
5723
5731
  };
5724
5732
  var CODEX_SOL = "gpt-6.1-sol";
@@ -5737,6 +5745,14 @@ var CODEX_CATALOG = {
5737
5745
  ["gpt-5.4", retired(CODEX_SOL, "2026-08-31")],
5738
5746
  ["gpt-5.4-mini", retired("gpt-6-luna", "2026-08-31")],
5739
5747
  ["gpt-5", superseded(CODEX_SOL)]
5748
+ ]),
5749
+ // The first stable Codex CLI release whose bundled model catalog carries the
5750
+ // model, from the Codex CLI release notes.
5751
+ cliMinimums: /* @__PURE__ */ new Map([
5752
+ [CODEX_SOL, "0.159.1"],
5753
+ ["gpt-6-sol", "0.156.1"],
5754
+ ["gpt-6-luna", "0.156.1"],
5755
+ ["gpt-6-astra", "0.153.1"]
5740
5756
  ])
5741
5757
  };
5742
5758
  var FULL_EFFORTS = [
@@ -5881,6 +5897,13 @@ function consultationString(value, label, maximum) {
5881
5897
  }
5882
5898
  return value;
5883
5899
  }
5900
+ function consultationProse(value, label, maximum) {
5901
+ return consultationString(
5902
+ typeof value === "string" ? value.replace(/[ \t\n\r]*[\t\n\r][ \t\n\r]*/gu, " ").replace(/^ +| +$/gu, "") : value,
5903
+ label,
5904
+ maximum
5905
+ );
5906
+ }
5884
5907
  function consultationIdentifier(value, label) {
5885
5908
  const identifier22 = consultationString(value, label, 128);
5886
5909
  if (identifier22 === "." || identifier22 === ".." || !CONSULTATION_ID_PATTERN.test(identifier22)) {
@@ -5888,12 +5911,12 @@ function consultationIdentifier(value, label) {
5888
5911
  }
5889
5912
  return identifier22;
5890
5913
  }
5891
- function consultationStrings(value, label, maximumItems, maximumLength) {
5914
+ function consultationStrings(value, label, maximumItems, maximumLength, decodeItem) {
5892
5915
  if (!Array.isArray(value) || value.length > maximumItems) {
5893
5916
  return consultationError(`${label} must be a bounded string array.`);
5894
5917
  }
5895
5918
  const strings = value.map(
5896
- (item, index) => consultationString(item, `${label}[${index}]`, maximumLength)
5919
+ (item, index) => decodeItem(item, `${label}[${index}]`, maximumLength)
5897
5920
  );
5898
5921
  if (new Set(strings).size !== strings.length) {
5899
5922
  return consultationError(`${label} must not contain duplicates.`);
@@ -5923,15 +5946,34 @@ function parseConsultationOption(value, label) {
5923
5946
  ]);
5924
5947
  return {
5925
5948
  optionId: consultationIdentifier(record7.optionId, `${label}.optionId`),
5926
- title: consultationString(record7.title, `${label}.title`, 160),
5927
- benefits: consultationStrings(record7.benefits, `${label}.benefits`, 8, 500),
5928
- costs: consultationStrings(record7.costs, `${label}.costs`, 8, 500),
5929
- risks: consultationStrings(record7.risks, `${label}.risks`, 8, 500),
5949
+ title: consultationProse(record7.title, `${label}.title`, 160),
5950
+ benefits: consultationStrings(
5951
+ record7.benefits,
5952
+ `${label}.benefits`,
5953
+ 8,
5954
+ 500,
5955
+ consultationProse
5956
+ ),
5957
+ costs: consultationStrings(
5958
+ record7.costs,
5959
+ `${label}.costs`,
5960
+ 8,
5961
+ 500,
5962
+ consultationProse
5963
+ ),
5964
+ risks: consultationStrings(
5965
+ record7.risks,
5966
+ `${label}.risks`,
5967
+ 8,
5968
+ 500,
5969
+ consultationProse
5970
+ ),
5930
5971
  consequences: consultationStrings(
5931
5972
  record7.consequences,
5932
5973
  `${label}.consequences`,
5933
5974
  8,
5934
- 500
5975
+ 500,
5976
+ consultationProse
5935
5977
  )
5936
5978
  };
5937
5979
  }
@@ -5975,7 +6017,7 @@ function parseConsultationCard(value, index) {
5975
6017
  return {
5976
6018
  path: consultationPath(evidenceRecord.path, `${evidenceLabel}.path`),
5977
6019
  line,
5978
- detail: consultationString(
6020
+ detail: consultationProse(
5979
6021
  evidenceRecord.detail,
5980
6022
  `${evidenceLabel}.detail`,
5981
6023
  500
@@ -6019,7 +6061,7 @@ function parseConsultationCard(value, index) {
6019
6061
  }
6020
6062
  recommendation = {
6021
6063
  optionId,
6022
- rationale: consultationString(
6064
+ rationale: consultationProse(
6023
6065
  recommendationRecord.rationale,
6024
6066
  `${label}.recommendation.rationale`,
6025
6067
  1e3
@@ -6035,7 +6077,8 @@ function parseConsultationCard(value, index) {
6035
6077
  record7.affectedRequirements,
6036
6078
  `${label}.affectedRequirements`,
6037
6079
  16,
6038
- 120
6080
+ 120,
6081
+ consultationString
6039
6082
  );
6040
6083
  if (affectedRequirements.length === 0) {
6041
6084
  return consultationError(
@@ -6047,9 +6090,9 @@ function parseConsultationCard(value, index) {
6047
6090
  record7.decisionId,
6048
6091
  `${label}.decisionId`
6049
6092
  ),
6050
- title: consultationString(record7.title, `${label}.title`, 160),
6051
- question: consultationString(record7.question, `${label}.question`, 1e3),
6052
- why: consultationString(record7.why, `${label}.why`, 1200),
6093
+ title: consultationProse(record7.title, `${label}.title`, 160),
6094
+ question: consultationProse(record7.question, `${label}.question`, 1e3),
6095
+ why: consultationProse(record7.why, `${label}.why`, 1200),
6053
6096
  affectedRequirements,
6054
6097
  evidence: evidence2,
6055
6098
  decisionOwner: "human",
@@ -6280,7 +6323,7 @@ function parsePhaseResultFields(record7) {
6280
6323
  const decisionConsultation = parseDecisionConsultation(
6281
6324
  record7.decisionConsultation
6282
6325
  );
6283
- const summary2 = gate === "decision_consultation" ? consultationString(record7.summary, "summary", 1e3) : requireString(record7.summary, "summary");
6326
+ const summary2 = gate === "decision_consultation" ? consultationProse(record7.summary, "summary", 1e3) : requireString(record7.summary, "summary");
6284
6327
  if (gate === "decision_consultation" ? phase !== "plan" || outcome !== "needs_human_decision" || decisionConsultation === null || questions.length !== 0 : decisionConsultation !== null) {
6285
6328
  throw new BanzaiError2(
6286
6329
  "INVALID_RESULT",
@@ -12500,6 +12543,14 @@ var CLAUDE_CATALOG2 = {
12500
12543
  ["claude-3-sonnet-20240229", retired2(SONNET2, "2025-07-21")],
12501
12544
  ["claude-3-5-haiku-20241022", retired2(HAIKU2, "2026-02-19")],
12502
12545
  ["claude-3-haiku-20240307", retired2(HAIKU2, "2026-04-20")]
12546
+ ]),
12547
+ // From Claude Code's model configuration documentation. An older client
12548
+ // refuses these models itself, before any request; tier aliases need no
12549
+ // entry because they resolve to a model the installed client serves.
12550
+ cliMinimums: /* @__PURE__ */ new Map([
12551
+ [OPUS2, "2.1.280"],
12552
+ [SONNET2, "2.1.284"],
12553
+ ["claude-fable-5-1", "2.1.257"]
12503
12554
  ])
12504
12555
  };
12505
12556
  var CODEX_SOL2 = "gpt-6.1-sol";
@@ -12518,6 +12569,14 @@ var CODEX_CATALOG2 = {
12518
12569
  ["gpt-5.4", retired2(CODEX_SOL2, "2026-08-31")],
12519
12570
  ["gpt-5.4-mini", retired2("gpt-6-luna", "2026-08-31")],
12520
12571
  ["gpt-5", superseded2(CODEX_SOL2)]
12572
+ ]),
12573
+ // The first stable Codex CLI release whose bundled model catalog carries the
12574
+ // model, from the Codex CLI release notes.
12575
+ cliMinimums: /* @__PURE__ */ new Map([
12576
+ [CODEX_SOL2, "0.159.1"],
12577
+ ["gpt-6-sol", "0.156.1"],
12578
+ ["gpt-6-luna", "0.156.1"],
12579
+ ["gpt-6-astra", "0.153.1"]
12521
12580
  ])
12522
12581
  };
12523
12582
  var CATALOGS = /* @__PURE__ */ new Map([
@@ -12661,6 +12720,9 @@ function selectableModel(id3) {
12661
12720
  function isCatalogSupportedModel(vendor, id3) {
12662
12721
  return CATALOGS.get(vendor)?.supported.has(id3) ?? false;
12663
12722
  }
12723
+ function modelCliMinimum(vendor, model) {
12724
+ return CATALOGS.get(vendor)?.cliMinimums.get(model) ?? null;
12725
+ }
12664
12726
  var FALLBACK_VENDOR = {
12665
12727
  planner: "codex",
12666
12728
  implementer: "codex",
@@ -12852,6 +12914,13 @@ function consultationString2(value, label, maximum) {
12852
12914
  }
12853
12915
  return value;
12854
12916
  }
12917
+ function consultationProse2(value, label, maximum) {
12918
+ return consultationString2(
12919
+ typeof value === "string" ? value.replace(/[ \t\n\r]*[\t\n\r][ \t\n\r]*/gu, " ").replace(/^ +| +$/gu, "") : value,
12920
+ label,
12921
+ maximum
12922
+ );
12923
+ }
12855
12924
  function consultationIdentifier2(value, label) {
12856
12925
  const identifier4 = consultationString2(value, label, 128);
12857
12926
  if (identifier4 === "." || identifier4 === ".." || !CONSULTATION_ID_PATTERN2.test(identifier4)) {
@@ -12859,12 +12928,12 @@ function consultationIdentifier2(value, label) {
12859
12928
  }
12860
12929
  return identifier4;
12861
12930
  }
12862
- function consultationStrings2(value, label, maximumItems, maximumLength) {
12931
+ function consultationStrings2(value, label, maximumItems, maximumLength, decodeItem) {
12863
12932
  if (!Array.isArray(value) || value.length > maximumItems) {
12864
12933
  return consultationError2(`${label} must be a bounded string array.`);
12865
12934
  }
12866
12935
  const strings = value.map(
12867
- (item, index) => consultationString2(item, `${label}[${index}]`, maximumLength)
12936
+ (item, index) => decodeItem(item, `${label}[${index}]`, maximumLength)
12868
12937
  );
12869
12938
  if (new Set(strings).size !== strings.length) {
12870
12939
  return consultationError2(`${label} must not contain duplicates.`);
@@ -12894,15 +12963,34 @@ function parseConsultationOption2(value, label) {
12894
12963
  ]);
12895
12964
  return {
12896
12965
  optionId: consultationIdentifier2(record7.optionId, `${label}.optionId`),
12897
- title: consultationString2(record7.title, `${label}.title`, 160),
12898
- benefits: consultationStrings2(record7.benefits, `${label}.benefits`, 8, 500),
12899
- costs: consultationStrings2(record7.costs, `${label}.costs`, 8, 500),
12900
- risks: consultationStrings2(record7.risks, `${label}.risks`, 8, 500),
12966
+ title: consultationProse2(record7.title, `${label}.title`, 160),
12967
+ benefits: consultationStrings2(
12968
+ record7.benefits,
12969
+ `${label}.benefits`,
12970
+ 8,
12971
+ 500,
12972
+ consultationProse2
12973
+ ),
12974
+ costs: consultationStrings2(
12975
+ record7.costs,
12976
+ `${label}.costs`,
12977
+ 8,
12978
+ 500,
12979
+ consultationProse2
12980
+ ),
12981
+ risks: consultationStrings2(
12982
+ record7.risks,
12983
+ `${label}.risks`,
12984
+ 8,
12985
+ 500,
12986
+ consultationProse2
12987
+ ),
12901
12988
  consequences: consultationStrings2(
12902
12989
  record7.consequences,
12903
12990
  `${label}.consequences`,
12904
12991
  8,
12905
- 500
12992
+ 500,
12993
+ consultationProse2
12906
12994
  )
12907
12995
  };
12908
12996
  }
@@ -12946,7 +13034,7 @@ function parseConsultationCard2(value, index) {
12946
13034
  return {
12947
13035
  path: consultationPath2(evidenceRecord.path, `${evidenceLabel}.path`),
12948
13036
  line,
12949
- detail: consultationString2(
13037
+ detail: consultationProse2(
12950
13038
  evidenceRecord.detail,
12951
13039
  `${evidenceLabel}.detail`,
12952
13040
  500
@@ -12990,7 +13078,7 @@ function parseConsultationCard2(value, index) {
12990
13078
  }
12991
13079
  recommendation = {
12992
13080
  optionId,
12993
- rationale: consultationString2(
13081
+ rationale: consultationProse2(
12994
13082
  recommendationRecord.rationale,
12995
13083
  `${label}.recommendation.rationale`,
12996
13084
  1e3
@@ -13006,7 +13094,8 @@ function parseConsultationCard2(value, index) {
13006
13094
  record7.affectedRequirements,
13007
13095
  `${label}.affectedRequirements`,
13008
13096
  16,
13009
- 120
13097
+ 120,
13098
+ consultationString2
13010
13099
  );
13011
13100
  if (affectedRequirements.length === 0) {
13012
13101
  return consultationError2(
@@ -13018,9 +13107,9 @@ function parseConsultationCard2(value, index) {
13018
13107
  record7.decisionId,
13019
13108
  `${label}.decisionId`
13020
13109
  ),
13021
- title: consultationString2(record7.title, `${label}.title`, 160),
13022
- question: consultationString2(record7.question, `${label}.question`, 1e3),
13023
- why: consultationString2(record7.why, `${label}.why`, 1200),
13110
+ title: consultationProse2(record7.title, `${label}.title`, 160),
13111
+ question: consultationProse2(record7.question, `${label}.question`, 1e3),
13112
+ why: consultationProse2(record7.why, `${label}.why`, 1200),
13024
13113
  affectedRequirements,
13025
13114
  evidence: evidence2,
13026
13115
  decisionOwner: "human",
@@ -13337,7 +13426,7 @@ function parsePhaseResultFields2(record7) {
13337
13426
  const decisionConsultation = parseDecisionConsultation2(
13338
13427
  record7.decisionConsultation
13339
13428
  );
13340
- const summary2 = gate === "decision_consultation" ? consultationString2(record7.summary, "summary", 1e3) : requireString2(record7.summary, "summary");
13429
+ const summary2 = gate === "decision_consultation" ? consultationProse2(record7.summary, "summary", 1e3) : requireString2(record7.summary, "summary");
13341
13430
  if (gate === "decision_consultation" ? phase !== "plan" || outcome !== "needs_human_decision" || decisionConsultation === null || questions.length !== 0 : decisionConsultation !== null) {
13342
13431
  throw new BanzaiError(
13343
13432
  "INVALID_RESULT",
@@ -24360,7 +24449,7 @@ import process7 from "node:process";
24360
24449
  // package.json
24361
24450
  var package_default = {
24362
24451
  name: "@parabolic-technologies/banzai",
24363
- version: "2.6.4",
24452
+ version: "2.6.6",
24364
24453
  private: false,
24365
24454
  description: "Spec-driven AI development CLI for Codex and Claude Code, with human approvals and independent review for complex changes.",
24366
24455
  keywords: [
@@ -24495,6 +24584,7 @@ var phase_result_schema_default = {
24495
24584
  },
24496
24585
  summary: {
24497
24586
  type: "string",
24587
+ description: "With gate decision_consultation, one plain line of at most 1000 characters without control characters; the decision cards carry the detail.",
24498
24588
  minLength: 1
24499
24589
  },
24500
24590
  artifacts: {
@@ -24652,6 +24742,7 @@ var phase_result_schema_default = {
24652
24742
  },
24653
24743
  {
24654
24744
  type: "object",
24745
+ description: "Every string in this request is one plain line without control characters within its maxLength; use separate array entries instead of line breaks.",
24655
24746
  additionalProperties: false,
24656
24747
  properties: {
24657
24748
  schema: {
@@ -27028,9 +27119,9 @@ async function requireCodexStateWriteAccess(sourceEnvironment) {
27028
27119
  );
27029
27120
  }
27030
27121
  }
27031
- async function requireVersion(runner, settings, minimum, sourceEnvironment = process.env) {
27122
+ async function requireVersion(runner, vendor, target, sourceEnvironment = process.env) {
27032
27123
  const result = await runner({
27033
- command: settings.executable,
27124
+ command: target.executable,
27034
27125
  args: ["--version"],
27035
27126
  cwd: process.cwd(),
27036
27127
  env: subscriptionOnlyEnvironment(sourceEnvironment),
@@ -27039,17 +27130,25 @@ async function requireVersion(runner, settings, minimum, sourceEnvironment = pro
27039
27130
  if (result.exitCode !== 0) {
27040
27131
  throw new BanzaiError(
27041
27132
  "CLI_UNAVAILABLE",
27042
- `${settings.executable} --version failed: ${result.stderr.trim()}`
27133
+ `${target.executable} --version failed: ${result.stderr.trim()}`
27043
27134
  );
27044
27135
  }
27045
27136
  const actual = parseVersion(
27046
27137
  result.stdout || result.stderr,
27047
- settings.executable
27138
+ target.executable
27048
27139
  );
27140
+ const minimum = vendor === "claude" ? MINIMUM_CLAUDE_VERSION : MINIMUM_CODEX_VERSION;
27049
27141
  if (!versionAtLeast(actual, minimum)) {
27050
27142
  throw new BanzaiError(
27051
27143
  "CLI_TOO_OLD",
27052
- `${settings.executable} ${actual} is older than required ${minimum}.`
27144
+ `${target.executable} ${actual} is older than required ${minimum}.`
27145
+ );
27146
+ }
27147
+ const modelMinimum = target.model === null ? null : modelCliMinimum(vendor, target.model);
27148
+ if (modelMinimum !== null && !versionAtLeast(actual, modelMinimum)) {
27149
+ throw new BanzaiError(
27150
+ "CLI_TOO_OLD",
27151
+ `${target.executable} ${actual} cannot run ${target.model}, which requires ${modelMinimum} or newer. Update ${target.executable}, or choose another model with \`banzai model\`.`
27053
27152
  );
27054
27153
  }
27055
27154
  }
@@ -27527,16 +27626,11 @@ function createVendorAdapters(runner = runProcess) {
27527
27626
  runner
27528
27627
  });
27529
27628
  },
27530
- async preflightCodex(settings, sourceEnvironment = process.env) {
27629
+ async preflightCodex(target, sourceEnvironment = process.env) {
27531
27630
  await requireCodexStateWriteAccess(sourceEnvironment);
27532
- await requireVersion(
27533
- runner,
27534
- settings,
27535
- MINIMUM_CODEX_VERSION,
27536
- sourceEnvironment
27537
- );
27631
+ await requireVersion(runner, "codex", target, sourceEnvironment);
27538
27632
  const result = await runner({
27539
- command: settings.executable,
27633
+ command: target.executable,
27540
27634
  args: ["login", "status"],
27541
27635
  cwd: process.cwd(),
27542
27636
  env: subscriptionOnlyEnvironment(sourceEnvironment),
@@ -27549,15 +27643,10 @@ function createVendorAdapters(runner = runProcess) {
27549
27643
  );
27550
27644
  }
27551
27645
  },
27552
- async preflightClaude(settings, sourceEnvironment = process.env) {
27553
- await requireVersion(
27554
- runner,
27555
- settings,
27556
- MINIMUM_CLAUDE_VERSION,
27557
- sourceEnvironment
27558
- );
27646
+ async preflightClaude(target, sourceEnvironment = process.env) {
27647
+ await requireVersion(runner, "claude", target, sourceEnvironment);
27559
27648
  const result = await runner({
27560
- command: settings.executable,
27649
+ command: target.executable,
27561
27650
  args: ["auth", "status", "--json"],
27562
27651
  cwd: process.cwd(),
27563
27652
  env: subscriptionOnlyEnvironment(sourceEnvironment),
@@ -27889,6 +27978,270 @@ function createVendorAdapters(runner = runProcess) {
27889
27978
  };
27890
27979
  }
27891
27980
 
27981
+ // src/claude-permissions.ts
27982
+ import { readFile as readFile7 } from "node:fs/promises";
27983
+ import { homedir as homedir3 } from "node:os";
27984
+ import path19 from "node:path";
27985
+ var ALL_SOURCES = [
27986
+ "user",
27987
+ "project",
27988
+ "local"
27989
+ ];
27990
+ var MANAGED_SETTINGS_FILES = {
27991
+ darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
27992
+ linux: "/etc/claude-code/managed-settings.json"
27993
+ };
27994
+ var EMPTY_RULES = { allow: [], deny: [], ask: [] };
27995
+ function isRecord3(value) {
27996
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27997
+ }
27998
+ function stringArray2(value) {
27999
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
28000
+ }
28001
+ function ruleTool(rule) {
28002
+ const open14 = rule.indexOf("(");
28003
+ return (open14 === -1 ? rule : rule.slice(0, open14)).trim();
28004
+ }
28005
+ function namesTool(rule, tool) {
28006
+ return ruleTool(rule) === tool;
28007
+ }
28008
+ function isUnscoped(rule) {
28009
+ return !rule.includes("(");
28010
+ }
28011
+ function homeDirectory(env) {
28012
+ const configured = env.HOME;
28013
+ return configured === void 0 || configured.length === 0 ? homedir3() : configured;
28014
+ }
28015
+ function configDirectory(env) {
28016
+ const configured = env.CLAUDE_CONFIG_DIR;
28017
+ return configured === void 0 || configured.length === 0 ? null : configured;
28018
+ }
28019
+ function trustFile(env) {
28020
+ const configured = configDirectory(env);
28021
+ return configured === null ? path19.join(homeDirectory(env), ".claude.json") : path19.join(configured, ".claude.json");
28022
+ }
28023
+ function settingsFile(source, repoRoot, env) {
28024
+ if (source === "user") {
28025
+ return path19.join(
28026
+ configDirectory(env) ?? path19.join(homeDirectory(env), ".claude"),
28027
+ "settings.json"
28028
+ );
28029
+ }
28030
+ return path19.join(
28031
+ repoRoot,
28032
+ ".claude",
28033
+ source === "project" ? "settings.json" : "settings.local.json"
28034
+ );
28035
+ }
28036
+ async function readRules(filePath) {
28037
+ let raw;
28038
+ try {
28039
+ raw = await readFile7(filePath, "utf8");
28040
+ } catch {
28041
+ return { rules: EMPTY_RULES, intact: true };
28042
+ }
28043
+ try {
28044
+ const document = JSON.parse(raw);
28045
+ const permissions = isRecord3(document) ? document.permissions : void 0;
28046
+ if (!isRecord3(permissions)) {
28047
+ return { rules: EMPTY_RULES, intact: true };
28048
+ }
28049
+ return {
28050
+ rules: {
28051
+ allow: stringArray2(permissions.allow),
28052
+ deny: stringArray2(permissions.deny),
28053
+ ask: stringArray2(permissions.ask)
28054
+ },
28055
+ intact: true
28056
+ };
28057
+ } catch {
28058
+ return { rules: EMPTY_RULES, intact: false };
28059
+ }
28060
+ }
28061
+ async function readManagedRules(override) {
28062
+ const file = override === void 0 ? MANAGED_SETTINGS_FILES[process.platform] : override;
28063
+ return file === void 0 || file === null ? { rules: EMPTY_RULES, intact: true } : await readRules(file);
28064
+ }
28065
+ async function inspectTrust(repoRoot, env) {
28066
+ let raw;
28067
+ try {
28068
+ raw = await readFile7(trustFile(env), "utf8");
28069
+ } catch {
28070
+ return "unknown";
28071
+ }
28072
+ try {
28073
+ const document = JSON.parse(raw);
28074
+ const projects = isRecord3(document) ? document.projects : void 0;
28075
+ if (!isRecord3(projects)) {
28076
+ return "untrusted";
28077
+ }
28078
+ const entry = projects[repoRoot];
28079
+ return isRecord3(entry) && entry.hasTrustDialogAccepted === true ? "trusted" : "untrusted";
28080
+ } catch {
28081
+ return "unknown";
28082
+ }
28083
+ }
28084
+ function resolveGrant(tool, refusing, granting) {
28085
+ for (const set of refusing) {
28086
+ const refused = [...set.deny, ...set.ask];
28087
+ if (refused.some((rule) => namesTool(rule, tool) && isUnscoped(rule))) {
28088
+ return "denied";
28089
+ }
28090
+ }
28091
+ let scoped = false;
28092
+ for (const set of granting) {
28093
+ for (const rule of set.allow) {
28094
+ if (!namesTool(rule, tool)) {
28095
+ continue;
28096
+ }
28097
+ if (isUnscoped(rule)) {
28098
+ return "granted";
28099
+ }
28100
+ scoped = true;
28101
+ }
28102
+ }
28103
+ return scoped ? "scoped" : "missing";
28104
+ }
28105
+ function parseSources(settingSources) {
28106
+ if (settingSources === null) {
28107
+ return [...ALL_SOURCES];
28108
+ }
28109
+ const named = new Set(
28110
+ settingSources.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0)
28111
+ );
28112
+ return ALL_SOURCES.filter((source) => named.has(source));
28113
+ }
28114
+ function claudePermissionDemand(config) {
28115
+ const claudeRoles = new Set(
28116
+ MODEL_ROLES.filter(
28117
+ (role) => (vendorFor(config.models[role].executable) ?? FALLBACK_VENDOR[role]) === "claude"
28118
+ )
28119
+ );
28120
+ const builds = claudeRoles.has("planner") || claudeRoles.has("implementer");
28121
+ return { fileWrites: builds, shellCommands: builds };
28122
+ }
28123
+ function claudePermissionDemanded(demand) {
28124
+ return demand.fileWrites || demand.shellCommands;
28125
+ }
28126
+ async function inspectConfiguredClaudePermissions(repoRoot, config, dependencies) {
28127
+ const demand = claudePermissionDemand(config);
28128
+ if (!claudePermissionDemanded(demand)) {
28129
+ return { state: "not_demanded" };
28130
+ }
28131
+ let settingSources;
28132
+ try {
28133
+ settingSources = (await dependencies.resolveScope(config.claudeEnvironment)).scope.settingSources;
28134
+ } catch {
28135
+ return { state: "unresolved" };
28136
+ }
28137
+ const audit = await (dependencies.inspect ?? inspectClaudePermissions)({
28138
+ repoRoot,
28139
+ demand,
28140
+ settingSources
28141
+ });
28142
+ return { state: "audited", audit };
28143
+ }
28144
+ async function inspectClaudePermissions(input) {
28145
+ const env = input.env ?? process.env;
28146
+ const repoRoot = path19.resolve(input.repoRoot);
28147
+ const demand = input.demand;
28148
+ const sources = parseSources(input.settingSources);
28149
+ const [managed, trust, loaded, unloadedLocal] = await Promise.all([
28150
+ readManagedRules(input.managedSettingsFile),
28151
+ inspectTrust(repoRoot, env),
28152
+ Promise.all(
28153
+ sources.map(async (source) => ({
28154
+ source,
28155
+ ...await readRules(settingsFile(source, repoRoot, env))
28156
+ }))
28157
+ ),
28158
+ sources.includes("local") ? Promise.resolve(null) : readRules(settingsFile("local", repoRoot, env))
28159
+ ]);
28160
+ const honored = loaded.filter(
28161
+ (entry) => entry.source === "user" || trust !== "untrusted"
28162
+ );
28163
+ const granting = [managed.rules, ...honored.map((entry) => entry.rules)];
28164
+ const refusing = [managed.rules, ...loaded.map((entry) => entry.rules)];
28165
+ const audited = [
28166
+ ...demand.fileWrites ? ["Write", "Edit"] : [],
28167
+ ...demand.shellCommands ? ["Bash"] : []
28168
+ ];
28169
+ const tools = audited.map((tool) => ({
28170
+ tool,
28171
+ grant: resolveGrant(tool, refusing, granting)
28172
+ }));
28173
+ const grantOf = (tool) => tools.find((entry) => entry.tool === tool)?.grant ?? null;
28174
+ const repositoryRulesIgnored = trust === "untrusted" && loaded.some(
28175
+ (entry) => entry.source !== "user" && entry.rules.allow.length > 0
28176
+ );
28177
+ const blocking = [];
28178
+ const advisories = [];
28179
+ const refused = tools.filter((entry) => entry.tool !== "Bash" && entry.grant === "denied").map((entry) => entry.tool);
28180
+ if (refused.length > 0) {
28181
+ blocking.push(
28182
+ `A deny or ask rule refuses ${refused.join(" and ")}. No hook decision overrides it and \`--permission-mode dontAsk\` cannot escalate it to a human, so Claude planning and implementation writes are denied.`
28183
+ );
28184
+ }
28185
+ const bash = grantOf("Bash");
28186
+ if (bash === "missing" || bash === "denied") {
28187
+ advisories.push(
28188
+ "No loaded permission rule grants Bash, so every command a phase runs is denied."
28189
+ );
28190
+ } else if (bash === "scoped") {
28191
+ advisories.push(
28192
+ "Bash is granted only for the commands its scoped rules name; anything else a phase runs is denied."
28193
+ );
28194
+ }
28195
+ if (repositoryRulesIgnored) {
28196
+ advisories.push(
28197
+ "Claude Code has not been trusted in this checkout, so it ignores the repository's own allow rules."
28198
+ );
28199
+ }
28200
+ if (bash !== null && bash !== "granted" && unloadedLocal !== null && unloadedLocal.rules.allow.length > 0) {
28201
+ advisories.push(
28202
+ "Local settings hold allow rules the scoped invocation does not load; repository grants belong in the tracked project settings."
28203
+ );
28204
+ }
28205
+ if (trust === "unknown") {
28206
+ advisories.push(
28207
+ "Claude Code's workspace trust record could not be read, so repository rules were treated as honored."
28208
+ );
28209
+ }
28210
+ if (!managed.intact || loaded.some((entry) => !entry.intact)) {
28211
+ advisories.push(
28212
+ "A settings file could not be parsed, so its permission rules were treated as absent."
28213
+ );
28214
+ }
28215
+ return {
28216
+ demand,
28217
+ sources,
28218
+ trust,
28219
+ tools,
28220
+ repositoryRulesIgnored,
28221
+ blocking,
28222
+ advisories
28223
+ };
28224
+ }
28225
+ function claudePermissionRemediation(audit) {
28226
+ const steps = [];
28227
+ const refused = audit.tools.filter(
28228
+ (entry) => entry.tool !== "Bash" && entry.grant === "denied"
28229
+ );
28230
+ if (refused.length > 0) {
28231
+ steps.push(
28232
+ `remove the deny or ask rules naming ${refused.map((entry) => `"${entry.tool}"`).join(
28233
+ " and "
28234
+ )} from the settings Claude loads; a rule in administrator policy settings needs your administrator`
28235
+ );
28236
+ }
28237
+ if (audit.repositoryRulesIgnored) {
28238
+ steps.push(
28239
+ "run `claude` once in this checkout and accept the trust prompt so its repository allow rules apply"
28240
+ );
28241
+ }
28242
+ return steps.join("; ");
28243
+ }
28244
+
27892
28245
  // src/agent-control.ts
27893
28246
  import path35 from "node:path";
27894
28247
 
@@ -27962,13 +28315,13 @@ import path34 from "node:path";
27962
28315
  import process4 from "node:process";
27963
28316
 
27964
28317
  // src/review-snapshot.ts
27965
- import { lstat as lstat13, readFile as readFile8 } from "node:fs/promises";
27966
- import path20 from "node:path";
28318
+ import { lstat as lstat13, readFile as readFile9 } from "node:fs/promises";
28319
+ import path21 from "node:path";
27967
28320
 
27968
28321
  // src/git.ts
27969
28322
  import { createHash as createHash12 } from "node:crypto";
27970
- import { lstat as lstat12, readFile as readFile7 } from "node:fs/promises";
27971
- import path19 from "node:path";
28323
+ import { lstat as lstat12, readFile as readFile8 } from "node:fs/promises";
28324
+ import path20 from "node:path";
27972
28325
  var GIT_TIMEOUT_MS = 2 * 60 * 1e3;
27973
28326
  async function runGit(repoRoot, args) {
27974
28327
  return await runProcess({
@@ -28040,15 +28393,15 @@ async function currentBranch(repoRoot) {
28040
28393
  return branch.length === 0 || branch === "HEAD" ? null : branch;
28041
28394
  }
28042
28395
  function resolveRepositoryPath(repoRoot, relativePath3) {
28043
- if (relativePath3.length === 0 || path19.isAbsolute(relativePath3)) {
28396
+ if (relativePath3.length === 0 || path20.isAbsolute(relativePath3)) {
28044
28397
  throw new BanzaiError(
28045
28398
  "INVALID_ARTIFACT_PATH",
28046
28399
  `Artifact path must be repository-relative: ${relativePath3}`
28047
28400
  );
28048
28401
  }
28049
- const normalizedRoot = path19.resolve(repoRoot);
28050
- const candidate = path19.resolve(normalizedRoot, relativePath3);
28051
- if (!candidate.startsWith(`${normalizedRoot}${path19.sep}`)) {
28402
+ const normalizedRoot = path20.resolve(repoRoot);
28403
+ const candidate = path20.resolve(normalizedRoot, relativePath3);
28404
+ if (!candidate.startsWith(`${normalizedRoot}${path20.sep}`)) {
28052
28405
  throw new BanzaiError(
28053
28406
  "INVALID_ARTIFACT_PATH",
28054
28407
  `Artifact path escapes the repository: ${relativePath3}`
@@ -28065,7 +28418,7 @@ async function hashFile(repoRoot, relativePath3) {
28065
28418
  `Required artifact does not exist as a file: ${relativePath3}`
28066
28419
  );
28067
28420
  }
28068
- const contents = await readFile7(absolutePath);
28421
+ const contents = await readFile8(absolutePath);
28069
28422
  if (metadata.isSymbolicLink()) {
28070
28423
  return createHash12("sha256").update("symlink").update("\0").update(contents).digest("hex");
28071
28424
  }
@@ -28244,10 +28597,10 @@ function sealReviewSnapshot3(value) {
28244
28597
  }
28245
28598
  async function assertReviewPathOwned(repoRoot, relativePath3, allowMissing = false) {
28246
28599
  reviewScopePath4(relativePath3);
28247
- let current = path20.resolve(repoRoot);
28600
+ let current = path21.resolve(repoRoot);
28248
28601
  const segments = relativePath3.split("/");
28249
28602
  for (const [index, part] of segments.entries()) {
28250
- current = path20.join(current, part);
28603
+ current = path21.join(current, part);
28251
28604
  let stat6;
28252
28605
  try {
28253
28606
  stat6 = await lstat13(current);
@@ -28266,7 +28619,7 @@ async function reviewApprovedArtifacts(repoRoot, paths) {
28266
28619
  await assertReviewPathOwned(repoRoot, relative4);
28267
28620
  return {
28268
28621
  path: relative4,
28269
- sha256: hashArtifactBytes(await readFile8(path20.join(repoRoot, relative4)))
28622
+ sha256: hashArtifactBytes(await readFile9(path21.join(repoRoot, relative4)))
28270
28623
  };
28271
28624
  })
28272
28625
  );
@@ -28276,7 +28629,7 @@ async function reviewApprovedArtifacts(repoRoot, paths) {
28276
28629
  import { createHash as createHash13 } from "node:crypto";
28277
28630
  import { constants as constants10 } from "node:fs";
28278
28631
  import { lstat as lstat14, open as open10, realpath as realpath8 } from "node:fs/promises";
28279
- import path21 from "node:path";
28632
+ import path23 from "node:path";
28280
28633
 
28281
28634
  // src/phase-resources.ts
28282
28635
  var PHASE_RESOURCE_DEFINITIONS = {
@@ -28523,7 +28876,7 @@ async function installedDigest(directory4, id3) {
28523
28876
  const root = await realpath8(directory4);
28524
28877
  let target = root;
28525
28878
  for (const part of id3.split("/")) {
28526
- target = path21.join(target, part);
28879
+ target = path23.join(target, part);
28527
28880
  const entry = await lstat14(target);
28528
28881
  if (entry.isSymbolicLink()) throw new Error("Unsafe instruction resource");
28529
28882
  }
@@ -29935,8 +30288,8 @@ async function digestRejectionReason(context, state, sourceAttemptId, error) {
29935
30288
 
29936
30289
  // src/workflow/completion/inputs.ts
29937
30290
  import { createHash as createHash15 } from "node:crypto";
29938
- import { lstat as lstat15, readFile as readFile9, realpath as realpath9 } from "node:fs/promises";
29939
- import path23 from "node:path";
30291
+ import { lstat as lstat15, readFile as readFile10, realpath as realpath9 } from "node:fs/promises";
30292
+ import path24 from "node:path";
29940
30293
  async function observeCompletionInputs(context, state, artifacts) {
29941
30294
  const implementation = await observeReport(
29942
30295
  context,
@@ -29975,8 +30328,8 @@ async function observeReport(context, relativePath3) {
29975
30328
  return { observation: { path: null, state: "absent" }, bytes: null };
29976
30329
  let absolute;
29977
30330
  try {
29978
- absolute = path23.resolve(context.repoRoot, relativePath3);
29979
- if (relativePath3.length === 0 || relativePath3.includes("\\") || path23.posix.isAbsolute(relativePath3) || path23.posix.normalize(relativePath3) !== relativePath3 || relativePath3.startsWith("../") || !absolute.startsWith(`${path23.resolve(context.repoRoot)}${path23.sep}`))
30331
+ absolute = path24.resolve(context.repoRoot, relativePath3);
30332
+ if (relativePath3.length === 0 || relativePath3.includes("\\") || path24.posix.isAbsolute(relativePath3) || path24.posix.normalize(relativePath3) !== relativePath3 || relativePath3.startsWith("../") || !absolute.startsWith(`${path24.resolve(context.repoRoot)}${path24.sep}`))
29980
30333
  return {
29981
30334
  observation: { path: relativePath3, state: "unsafe" },
29982
30335
  bytes: null
@@ -29988,7 +30341,7 @@ async function observeReport(context, relativePath3) {
29988
30341
  bytes: null
29989
30342
  };
29990
30343
  const root = await realpath9(context.repoRoot);
29991
- if (!(await realpath9(absolute)).startsWith(`${root}${path23.sep}`))
30344
+ if (!(await realpath9(absolute)).startsWith(`${root}${path24.sep}`))
29992
30345
  return {
29993
30346
  observation: { path: relativePath3, state: "unsafe" },
29994
30347
  bytes: null
@@ -30003,7 +30356,7 @@ async function observeReport(context, relativePath3) {
30003
30356
  };
30004
30357
  }
30005
30358
  try {
30006
- const bytes = await readFile9(absolute);
30359
+ const bytes = await readFile10(absolute);
30007
30360
  return {
30008
30361
  observation: {
30009
30362
  path: relativePath3,
@@ -30025,13 +30378,13 @@ function sameObservedInputs(a, b) {
30025
30378
  }
30026
30379
 
30027
30380
  // src/workflow/completion/plan-tasks.ts
30028
- import { readFile as readFile11 } from "node:fs/promises";
30029
- import path25 from "node:path";
30381
+ import { readFile as readFile12 } from "node:fs/promises";
30382
+ import path26 from "node:path";
30030
30383
 
30031
30384
  // src/plan-task-completion.ts
30032
30385
  import { createHash as createHash16 } from "node:crypto";
30033
- import { lstat as lstat16, readFile as readFile10, realpath as realpath10 } from "node:fs/promises";
30034
- import path24 from "node:path";
30386
+ import { lstat as lstat16, readFile as readFile11, realpath as realpath10 } from "node:fs/promises";
30387
+ import path25 from "node:path";
30035
30388
  var PROJECTION_FORMAT = "banzai-plan-task-completion/v1";
30036
30389
  var SECTION_HEADING = "## Plan Task Completion";
30037
30390
  var TASK_HEADING = /^- \[ \] \*\*(T[0-9]{3}) - (.+)\*\*\s*$/u;
@@ -30132,10 +30485,10 @@ function validatePlanRunnerControlTasks(markdown, repoRoot, allowedArtifactPaths
30132
30485
  const section = markdown.split(/^## Implementation Tasks\s*$/mu)[1]?.split(/^## /mu)[0] ?? "";
30133
30486
  const allowed = new Set(
30134
30487
  allowedArtifactPaths.map(
30135
- (value) => path24.posix.normalize(value.replaceAll("\\", "/"))
30488
+ (value) => path25.posix.normalize(value.replaceAll("\\", "/"))
30136
30489
  )
30137
30490
  );
30138
- const root = path24.posix.normalize(repoRoot.replaceAll("\\", "/")).replace(/\/$/u, "");
30491
+ const root = path25.posix.normalize(repoRoot.replaceAll("\\", "/")).replace(/\/$/u, "");
30139
30492
  let previousTaskId = "";
30140
30493
  let previousPaths = [];
30141
30494
  for (const { taskId, text: text4, access: access7 } of planTaskStatements(section)) {
@@ -30146,8 +30499,8 @@ function validatePlanRunnerControlTasks(markdown, repoRoot, allowedArtifactPaths
30146
30499
  const token = raw.replaceAll("\\", "/").replace(/[.!?]+$/u, "").replace(/:\d+(?::\d+)?$/u, "");
30147
30500
  if (!token.includes("/") && !/^\.[\w-]+$/u.test(token) && !/\.[a-z0-9]+$/iu.test(token))
30148
30501
  return raw;
30149
- const normalized = path24.posix.normalize(
30150
- path24.posix.isAbsolute(token) || /^[a-z]:\//iu.test(token) ? token : `${root}/${token}`
30502
+ const normalized = path25.posix.normalize(
30503
+ path25.posix.isAbsolute(token) || /^[a-z]:\//iu.test(token) ? token : `${root}/${token}`
30151
30504
  );
30152
30505
  const relative4 = normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? normalized.slice(root.length + 1) : normalized;
30153
30506
  paths.push(relative4);
@@ -30391,7 +30744,7 @@ function parsePlanTaskCompletionProjection(markdown) {
30391
30744
  };
30392
30745
  }
30393
30746
  function normalizedRepositoryPath(relativePath3, requiredReports = /* @__PURE__ */ new Set()) {
30394
- if (relativePath3.length === 0 || relativePath3.includes("\\") || /[\u0000-\u001f\u007f-\u009f]/u.test(relativePath3) || relativePath3.length > 500 || path24.posix.isAbsolute(relativePath3) || path24.posix.normalize(relativePath3) !== relativePath3 || relativePath3 === ".." || relativePath3.startsWith("../")) {
30747
+ if (relativePath3.length === 0 || relativePath3.includes("\\") || /[\u0000-\u001f\u007f-\u009f]/u.test(relativePath3) || relativePath3.length > 500 || path25.posix.isAbsolute(relativePath3) || path25.posix.normalize(relativePath3) !== relativePath3 || relativePath3 === ".." || relativePath3.startsWith("../")) {
30395
30748
  return completionFailure(
30396
30749
  "invalid_evidence_path",
30397
30750
  "Completion evidence path must be normalized and repository-relative."
@@ -30418,7 +30771,7 @@ async function validateEvidenceFile(repoRoot, relativePath3, allowedPaths, appro
30418
30771
  );
30419
30772
  }
30420
30773
  const repositoryRoot = await realpath10(repoRoot);
30421
- const absolutePath = path24.resolve(repositoryRoot, normalized);
30774
+ const absolutePath = path25.resolve(repositoryRoot, normalized);
30422
30775
  let resolvedPath;
30423
30776
  let metadata;
30424
30777
  try {
@@ -30431,7 +30784,7 @@ async function validateEvidenceFile(repoRoot, relativePath3, allowedPaths, appro
30431
30784
  taskId
30432
30785
  );
30433
30786
  }
30434
- if (!metadata.isFile() || metadata.isSymbolicLink() || !resolvedPath.startsWith(`${repositoryRoot}${path24.sep}`)) {
30787
+ if (!metadata.isFile() || metadata.isSymbolicLink() || !resolvedPath.startsWith(`${repositoryRoot}${path25.sep}`)) {
30435
30788
  return completionFailure(
30436
30789
  "invalid_evidence_path",
30437
30790
  `${taskId} evidence must be a regular file inside the repository.`,
@@ -30533,7 +30886,7 @@ async function readRequiredReport(repoRoot, relativePath3, violation, requiredRe
30533
30886
  const normalized = normalizedRepositoryPath(relativePath3, requiredReports);
30534
30887
  if (relativePath3.startsWith(".banzai/"))
30535
30888
  await safePath3(repoRoot, relativePath3);
30536
- const absolute = path24.resolve(repoRoot, normalized);
30889
+ const absolute = path25.resolve(repoRoot, normalized);
30537
30890
  let bytes;
30538
30891
  try {
30539
30892
  const metadata = await lstat16(absolute);
@@ -30542,10 +30895,10 @@ async function readRequiredReport(repoRoot, relativePath3, violation, requiredRe
30542
30895
  }
30543
30896
  const root = await realpath10(repoRoot);
30544
30897
  const resolved = await realpath10(absolute);
30545
- if (!resolved.startsWith(`${root}${path24.sep}`)) {
30898
+ if (!resolved.startsWith(`${root}${path25.sep}`)) {
30546
30899
  throw new Error("outside repository");
30547
30900
  }
30548
- bytes = captured ?? await readFile10(absolute);
30901
+ bytes = captured ?? await readFile11(absolute);
30549
30902
  } catch {
30550
30903
  return completionFailure(
30551
30904
  violation,
@@ -30588,8 +30941,8 @@ async function validatePlanTaskCompletion(input) {
30588
30941
  "Approved artifact bundle digest or plan membership is invalid."
30589
30942
  );
30590
30943
  }
30591
- const plan = await readFile10(
30592
- path24.resolve(input.repoRoot, input.planPath),
30944
+ const plan = await readFile11(
30945
+ path25.resolve(input.repoRoot, input.planPath),
30593
30946
  "utf8"
30594
30947
  ).catch(
30595
30948
  () => completionFailure(
@@ -31024,8 +31377,8 @@ async function validateCompletionClaims(input) {
31024
31377
  "Approved artifact bundle digest or plan membership is invalid."
31025
31378
  );
31026
31379
  }
31027
- const plan = await readFile10(
31028
- path24.resolve(input.repoRoot, input.planPath),
31380
+ const plan = await readFile11(
31381
+ path25.resolve(input.repoRoot, input.planPath),
31029
31382
  "utf8"
31030
31383
  ).catch(
31031
31384
  () => completionFailure(
@@ -31176,8 +31529,8 @@ function completionPlanPath(state) {
31176
31529
  return planPath;
31177
31530
  }
31178
31531
  async function approvedTaskIds(context, state) {
31179
- const plan = await readFile11(
31180
- path25.join(context.repoRoot, completionPlanPath(state)),
31532
+ const plan = await readFile12(
31533
+ path26.join(context.repoRoot, completionPlanPath(state)),
31181
31534
  "utf8"
31182
31535
  );
31183
31536
  validateRunnerControlTasks(context, state, plan);
@@ -31213,8 +31566,8 @@ async function validateImplementationCompletion(context, state, attempt, result)
31213
31566
  async function validatePlanningResult(context, state, attempt, result) {
31214
31567
  if (result.gate !== "plan_approval" || result.artifacts.plan === null) return;
31215
31568
  try {
31216
- const markdown = await readFile11(
31217
- path25.join(context.repoRoot, result.artifacts.plan),
31569
+ const markdown = await readFile12(
31570
+ path26.join(context.repoRoot, result.artifacts.plan),
31218
31571
  "utf8"
31219
31572
  );
31220
31573
  parseApprovedPlanTaskIds(markdown);
@@ -31273,22 +31626,10 @@ async function establishPlanRemainderDigest(context, state, attempt, planPath, m
31273
31626
  }
31274
31627
  await recordResultDigestEvidence(context, state, attempt, [comparison]);
31275
31628
  }
31276
- function completionInvalidCheckpoint(context, sourceAttemptId, error, sourceAttemptKind = "implementation") {
31277
- return {
31278
- schemaVersion: 1,
31279
- status: "invalid",
31280
- sourceAttemptId,
31281
- sourceAttemptKind,
31282
- errorCode: "PLAN_TASK_COMPLETION_INVALID",
31283
- violation: error.violation,
31284
- taskId: error.taskId,
31285
- recordedAt: timestamp(context)
31286
- };
31287
- }
31288
31629
 
31289
31630
  // src/workflow/verification/required-checks.ts
31290
- import { readFile as readFile12 } from "node:fs/promises";
31291
- import path26 from "node:path";
31631
+ import { readFile as readFile13 } from "node:fs/promises";
31632
+ import path27 from "node:path";
31292
31633
  async function requiredCheckIds(context, state, config) {
31293
31634
  const ids = new Set(
31294
31635
  (taskVerificationPolicy(config)?.required ?? []).map((check2) => check2.id)
@@ -31296,8 +31637,8 @@ async function requiredCheckIds(context, state, config) {
31296
31637
  if ((state.workflowSelection?.workflow ?? "strict") !== "strict" || state.planApproval === null)
31297
31638
  return ids;
31298
31639
  for (const check2 of parseTaskVerificationObligations(
31299
- await readFile12(
31300
- path26.join(context.repoRoot, completionPlanPath(state)),
31640
+ await readFile13(
31641
+ path27.join(context.repoRoot, completionPlanPath(state)),
31301
31642
  "utf8"
31302
31643
  )
31303
31644
  ).checks)
@@ -31324,8 +31665,8 @@ import { createHash as createHash18 } from "node:crypto";
31324
31665
 
31325
31666
  // src/review-artifact.ts
31326
31667
  import { createHash as createHash17, randomUUID as randomUUID10 } from "node:crypto";
31327
- import { link as link3, mkdir as mkdir8, open as open11, readFile as readFile13, rename as rename7, unlink as unlink7 } from "node:fs/promises";
31328
- import path27 from "node:path";
31668
+ import { link as link3, mkdir as mkdir8, open as open11, readFile as readFile14, rename as rename7, unlink as unlink7 } from "node:fs/promises";
31669
+ import path28 from "node:path";
31329
31670
  var REQUIRED_SECTIONS = [
31330
31671
  "## Review Scope",
31331
31672
  "## Summary",
@@ -31348,7 +31689,7 @@ var ReviewArtifactValidationError = class extends BanzaiError {
31348
31689
  function reviewArtifactPath(slug, iteration, bound) {
31349
31690
  if (bound !== void 0)
31350
31691
  return `${bound.binding.artifacts.reviewDirectory}/review-${iteration}.md`;
31351
- return path27.posix.join(
31692
+ return path28.posix.join(
31352
31693
  "documentation",
31353
31694
  "features",
31354
31695
  slug,
@@ -31614,9 +31955,9 @@ function approvedArtifactScope(contents, diagnostics) {
31614
31955
  return paths;
31615
31956
  }
31616
31957
  function repositoryPath(repoRoot, relativePath3) {
31617
- const repositoryRoot = path27.resolve(repoRoot);
31618
- const absolutePath = path27.resolve(repositoryRoot, relativePath3);
31619
- if (!absolutePath.startsWith(`${repositoryRoot}${path27.sep}`)) {
31958
+ const repositoryRoot = path28.resolve(repoRoot);
31959
+ const absolutePath = path28.resolve(repositoryRoot, relativePath3);
31960
+ if (!absolutePath.startsWith(`${repositoryRoot}${path28.sep}`)) {
31620
31961
  throw new BanzaiError(
31621
31962
  "INVALID_REVIEW_ARTIFACT_PATH",
31622
31963
  "Review artifact path escapes the repository."
@@ -31650,7 +31991,7 @@ async function validateReviewArtifactSource(repoRoot, result, context, sourcePat
31650
31991
  "Review candidate path must differ from the canonical artifact path."
31651
31992
  );
31652
31993
  }
31653
- const contents = await readFile13(absoluteCandidatePath, "utf8").catch(() => {
31994
+ const contents = await readFile14(absoluteCandidatePath, "utf8").catch(() => {
31654
31995
  throw new BanzaiError(
31655
31996
  "REVIEW_ARTIFACT_MISSING",
31656
31997
  `Reviewer did not write candidate ${sourcePath3}.`
@@ -31879,7 +32220,7 @@ async function publishReviewArtifact(repoRoot, context, candidate) {
31879
32220
  const absolutePath = repositoryPath(repoRoot, context.path);
31880
32221
  if (context.snapshot !== void 0) {
31881
32222
  await assertReviewPathOwned(repoRoot, context.path, true);
31882
- const previous = await readFile13(absolutePath, "utf8").catch(
32223
+ const previous = await readFile14(absolutePath, "utf8").catch(
31883
32224
  (error) => {
31884
32225
  if (error.code === "ENOENT") return null;
31885
32226
  throw error;
@@ -31901,11 +32242,11 @@ async function publishReviewArtifact(repoRoot, context, candidate) {
31901
32242
  if (privateWork)
31902
32243
  await verifyIgnores4(repoRoot, binding, [
31903
32244
  context.path,
31904
- path27.relative(repoRoot, temporaryPath)
32245
+ path28.relative(repoRoot, temporaryPath)
31905
32246
  ]);
31906
32247
  };
31907
32248
  await verifyDestinations();
31908
- await mkdir8(path27.dirname(absolutePath), {
32249
+ await mkdir8(path28.dirname(absolutePath), {
31909
32250
  recursive: true,
31910
32251
  mode: privateWork ? 448 : 493
31911
32252
  });
@@ -32025,7 +32366,23 @@ function artifactWritePaths(state, phase) {
32025
32366
  }
32026
32367
 
32027
32368
  // src/workflow/attempts/lifecycle.ts
32028
- import path28 from "node:path";
32369
+ import path29 from "node:path";
32370
+
32371
+ // src/workflow/domain/completion-checkpoint.ts
32372
+ function completionInvalidCheckpoint(facts) {
32373
+ return {
32374
+ schemaVersion: 1,
32375
+ status: "invalid",
32376
+ sourceAttemptId: facts.sourceAttemptId,
32377
+ sourceAttemptKind: facts.sourceAttemptKind,
32378
+ errorCode: "PLAN_TASK_COMPLETION_INVALID",
32379
+ violation: facts.violation,
32380
+ taskId: facts.taskId,
32381
+ recordedAt: facts.recordedAt
32382
+ };
32383
+ }
32384
+
32385
+ // src/workflow/attempts/lifecycle.ts
32029
32386
  function newAttempt(context, state, kind, mutable) {
32030
32387
  return {
32031
32388
  id: `${kind}-${state.reviewIteration + 1}-${context.ids().slice(0, 8)}`,
@@ -32293,12 +32650,13 @@ async function failAttempt(context, state, record7, error) {
32293
32650
  }
32294
32651
  }
32295
32652
  if (completionError3 !== null) {
32296
- state.planTaskCompletion = completionInvalidCheckpoint(
32297
- context,
32298
- record7.attempt.id,
32299
- completionError3,
32300
- record7.attempt.kind === "fix" ? "fix" : "implementation"
32301
- );
32653
+ state.planTaskCompletion = completionInvalidCheckpoint({
32654
+ sourceAttemptId: record7.attempt.id,
32655
+ sourceAttemptKind: record7.attempt.kind === "fix" ? "fix" : "implementation",
32656
+ violation: completionError3.violation,
32657
+ taskId: completionError3.taskId,
32658
+ recordedAt: timestamp(context)
32659
+ });
32302
32660
  }
32303
32661
  blockState(
32304
32662
  state,
@@ -32359,7 +32717,7 @@ async function persistAdapterResult(context, state, record7, adapterResult) {
32359
32717
  await context.store.writeRunAttempt(state.runId, record7);
32360
32718
  }
32361
32719
  async function writeAttemptOutput(context, state, record7, stdout, stderr) {
32362
- const stdoutPath = path28.relative(
32720
+ const stdoutPath = path29.relative(
32363
32721
  context.store.runDirectory(state.runId),
32364
32722
  await context.store.writeText(
32365
32723
  state.runId,
@@ -32367,7 +32725,7 @@ async function writeAttemptOutput(context, state, record7, stdout, stderr) {
32367
32725
  redactSensitiveText(stdout)
32368
32726
  )
32369
32727
  );
32370
- const stderrPath = path28.relative(
32728
+ const stderrPath = path29.relative(
32371
32729
  context.store.runDirectory(state.runId),
32372
32730
  await context.store.writeText(
32373
32731
  state.runId,
@@ -32419,7 +32777,7 @@ function legacyPhaseResult(result) {
32419
32777
  }
32420
32778
 
32421
32779
  // src/workflow/checkpoints/terminal-checkpoints.ts
32422
- import { lstat as lstat17, readFile as readFile14 } from "node:fs/promises";
32780
+ import { lstat as lstat17, readFile as readFile15 } from "node:fs/promises";
32423
32781
  function terminalCheckpointWriter(context, state, attempt, settings, policy, completion = false) {
32424
32782
  return async (terminal) => {
32425
32783
  const current = await context.store.readState(state.runId);
@@ -32762,7 +33120,7 @@ async function establishResultSourceIdentities(context, state, attempt, result,
32762
33120
  const file = await taskVerificationPath(context.repoRoot, relativePath3);
32763
33121
  const size = (await lstat17(file)).size;
32764
33122
  return size > TASK_VERIFICATION_SOURCE_MAX_BYTES ? { observed: null, size, limit: TASK_VERIFICATION_SOURCE_MAX_BYTES } : {
32765
- observed: evidenceDigest(await readFile14(file)),
33123
+ observed: evidenceDigest(await readFile15(file)),
32766
33124
  size,
32767
33125
  limit: null
32768
33126
  };
@@ -33144,7 +33502,7 @@ async function completionRepairDiagnostic(context, state, sourceAttemptId, sourc
33144
33502
  // src/workflow/completion/publication.ts
33145
33503
  import { createHash as createHash20 } from "node:crypto";
33146
33504
  import { lstat as lstat18, rename as rename8, unlink as unlink8, writeFile as writeFile3 } from "node:fs/promises";
33147
- import path29 from "node:path";
33505
+ import path30 from "node:path";
33148
33506
  async function storeCursor(context, state, cursor) {
33149
33507
  state.completionPublication = cursor;
33150
33508
  try {
@@ -33555,7 +33913,7 @@ async function publishCapturedCompletion(context, state, record7, envelope, capt
33555
33913
  const current = await context.store.readState(state.runId);
33556
33914
  if (current.status === "cancelled")
33557
33915
  return { kind: "stopped", state: current };
33558
- const destination = path29.join(context.repoRoot, candidate.destination);
33916
+ const destination = path30.join(context.repoRoot, candidate.destination);
33559
33917
  if (!published) {
33560
33918
  if (candidate.destination.startsWith(".banzai/"))
33561
33919
  await safePath3(context.repoRoot, candidate.destination);
@@ -33938,12 +34296,13 @@ async function blockForCompletionFailure(context, state, sourceAttemptId, error)
33938
34296
  "source_attempt_invalid",
33939
34297
  errorMessage(error)
33940
34298
  );
33941
- state.planTaskCompletion = completionInvalidCheckpoint(
33942
- context,
34299
+ state.planTaskCompletion = completionInvalidCheckpoint({
33943
34300
  sourceAttemptId,
33944
- completionError3,
33945
- state.planTaskCompletion?.sourceAttemptId === sourceAttemptId ? state.planTaskCompletion.sourceAttemptKind ?? "implementation" : "implementation"
33946
- );
34301
+ sourceAttemptKind: state.planTaskCompletion?.sourceAttemptId === sourceAttemptId ? state.planTaskCompletion.sourceAttemptKind ?? "implementation" : "implementation",
34302
+ violation: completionError3.violation,
34303
+ taskId: completionError3.taskId,
34304
+ recordedAt: timestamp(context)
34305
+ });
33947
34306
  state.activeProcess = null;
33948
34307
  blockState(
33949
34308
  state,
@@ -33964,8 +34323,8 @@ async function blockForCompletionFailure(context, state, sourceAttemptId, error)
33964
34323
  }
33965
34324
 
33966
34325
  // src/workflow/verification/evidence.ts
33967
- import { readFile as readFile15 } from "node:fs/promises";
33968
- import path30 from "node:path";
34326
+ import { readFile as readFile16 } from "node:fs/promises";
34327
+ import path31 from "node:path";
33969
34328
  function verificationStorage(context, state) {
33970
34329
  if (!usesVerificationPolicy(state) || !state.artifactBinding)
33971
34330
  throw new BanzaiError(
@@ -34011,8 +34370,8 @@ async function prepareVerificationDecision(context, state, config, round, record
34011
34370
  const storage2 = verificationStorage(context, state);
34012
34371
  const configuration = parseVerificationConfiguration(config.verification);
34013
34372
  const strict = (state.workflowSelection?.workflow ?? "strict") === "strict";
34014
- const plan = strict ? await readFile15(
34015
- path30.join(context.repoRoot, completionPlanPath(state)),
34373
+ const plan = strict ? await readFile16(
34374
+ path31.join(context.repoRoot, completionPlanPath(state)),
34016
34375
  "utf8"
34017
34376
  ) : null;
34018
34377
  const current = await captureVerification(context, state, config);
@@ -34231,261 +34590,6 @@ var InvocationScopeCache = class {
34231
34590
  }
34232
34591
  };
34233
34592
 
34234
- // src/claude-permissions.ts
34235
- import { readFile as readFile16 } from "node:fs/promises";
34236
- import { homedir as homedir3 } from "node:os";
34237
- import path31 from "node:path";
34238
- var ALL_SOURCES = [
34239
- "user",
34240
- "project",
34241
- "local"
34242
- ];
34243
- var MANAGED_SETTINGS_FILES = {
34244
- darwin: "/Library/Application Support/ClaudeCode/managed-settings.json",
34245
- linux: "/etc/claude-code/managed-settings.json"
34246
- };
34247
- var EMPTY_RULES = { allow: [], deny: [], ask: [] };
34248
- function isRecord3(value) {
34249
- return typeof value === "object" && value !== null && !Array.isArray(value);
34250
- }
34251
- function stringArray2(value) {
34252
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
34253
- }
34254
- function ruleTool(rule) {
34255
- const open14 = rule.indexOf("(");
34256
- return (open14 === -1 ? rule : rule.slice(0, open14)).trim();
34257
- }
34258
- function namesTool(rule, tool) {
34259
- return ruleTool(rule) === tool;
34260
- }
34261
- function isUnscoped(rule) {
34262
- return !rule.includes("(");
34263
- }
34264
- function homeDirectory(env) {
34265
- const configured = env.HOME;
34266
- return configured === void 0 || configured.length === 0 ? homedir3() : configured;
34267
- }
34268
- function configDirectory(env) {
34269
- const configured = env.CLAUDE_CONFIG_DIR;
34270
- return configured === void 0 || configured.length === 0 ? null : configured;
34271
- }
34272
- function trustFile(env) {
34273
- const configured = configDirectory(env);
34274
- return configured === null ? path31.join(homeDirectory(env), ".claude.json") : path31.join(configured, ".claude.json");
34275
- }
34276
- function settingsFile(source, repoRoot, env) {
34277
- if (source === "user") {
34278
- return path31.join(
34279
- configDirectory(env) ?? path31.join(homeDirectory(env), ".claude"),
34280
- "settings.json"
34281
- );
34282
- }
34283
- return path31.join(
34284
- repoRoot,
34285
- ".claude",
34286
- source === "project" ? "settings.json" : "settings.local.json"
34287
- );
34288
- }
34289
- async function readRules(filePath) {
34290
- let raw;
34291
- try {
34292
- raw = await readFile16(filePath, "utf8");
34293
- } catch {
34294
- return { rules: EMPTY_RULES, intact: true };
34295
- }
34296
- try {
34297
- const document = JSON.parse(raw);
34298
- const permissions = isRecord3(document) ? document.permissions : void 0;
34299
- if (!isRecord3(permissions)) {
34300
- return { rules: EMPTY_RULES, intact: true };
34301
- }
34302
- return {
34303
- rules: {
34304
- allow: stringArray2(permissions.allow),
34305
- deny: stringArray2(permissions.deny),
34306
- ask: stringArray2(permissions.ask)
34307
- },
34308
- intact: true
34309
- };
34310
- } catch {
34311
- return { rules: EMPTY_RULES, intact: false };
34312
- }
34313
- }
34314
- async function readManagedRules(override) {
34315
- const file = override === void 0 ? MANAGED_SETTINGS_FILES[process.platform] : override;
34316
- return file === void 0 || file === null ? { rules: EMPTY_RULES, intact: true } : await readRules(file);
34317
- }
34318
- async function inspectTrust(repoRoot, env) {
34319
- let raw;
34320
- try {
34321
- raw = await readFile16(trustFile(env), "utf8");
34322
- } catch {
34323
- return "unknown";
34324
- }
34325
- try {
34326
- const document = JSON.parse(raw);
34327
- const projects = isRecord3(document) ? document.projects : void 0;
34328
- if (!isRecord3(projects)) {
34329
- return "untrusted";
34330
- }
34331
- const entry = projects[repoRoot];
34332
- return isRecord3(entry) && entry.hasTrustDialogAccepted === true ? "trusted" : "untrusted";
34333
- } catch {
34334
- return "unknown";
34335
- }
34336
- }
34337
- function resolveGrant(tool, sets) {
34338
- for (const set of sets) {
34339
- const refused = [...set.deny, ...set.ask];
34340
- if (refused.some((rule) => namesTool(rule, tool) && isUnscoped(rule))) {
34341
- return "denied";
34342
- }
34343
- }
34344
- let scoped = false;
34345
- for (const set of sets) {
34346
- for (const rule of set.allow) {
34347
- if (!namesTool(rule, tool)) {
34348
- continue;
34349
- }
34350
- if (isUnscoped(rule)) {
34351
- return "granted";
34352
- }
34353
- scoped = true;
34354
- }
34355
- }
34356
- return scoped ? "scoped" : "missing";
34357
- }
34358
- function parseSources(settingSources) {
34359
- if (settingSources === null) {
34360
- return [...ALL_SOURCES];
34361
- }
34362
- const named = new Set(
34363
- settingSources.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0)
34364
- );
34365
- return ALL_SOURCES.filter((source) => named.has(source));
34366
- }
34367
- function claudePermissionDemand(config) {
34368
- const claudeRoles = new Set(
34369
- MODEL_ROLES.filter(
34370
- (role) => (vendorFor(config.models[role].executable) ?? FALLBACK_VENDOR[role]) === "claude"
34371
- )
34372
- );
34373
- return {
34374
- planningWrites: claudeRoles.has("planner"),
34375
- shellCommands: claudeRoles.has("planner") || claudeRoles.has("implementer")
34376
- };
34377
- }
34378
- function claudePermissionDemanded(demand) {
34379
- return demand.planningWrites || demand.shellCommands;
34380
- }
34381
- async function inspectClaudePermissions(input) {
34382
- const env = input.env ?? process.env;
34383
- const repoRoot = path31.resolve(input.repoRoot);
34384
- const demand = input.demand;
34385
- const sources = parseSources(input.settingSources);
34386
- const [managed, trust, loaded, unloadedLocal] = await Promise.all([
34387
- readManagedRules(input.managedSettingsFile),
34388
- inspectTrust(repoRoot, env),
34389
- Promise.all(
34390
- sources.map(async (source) => ({
34391
- source,
34392
- ...await readRules(settingsFile(source, repoRoot, env))
34393
- }))
34394
- ),
34395
- sources.includes("local") ? Promise.resolve(null) : readRules(settingsFile("local", repoRoot, env))
34396
- ]);
34397
- const honored = loaded.filter(
34398
- (entry) => entry.source === "user" || trust !== "untrusted"
34399
- );
34400
- const sets = [managed.rules, ...honored.map((entry) => entry.rules)];
34401
- const audited = [
34402
- ...demand.planningWrites ? ["Write", "Edit"] : [],
34403
- ...demand.shellCommands ? ["Bash"] : []
34404
- ];
34405
- const tools = audited.map((tool) => ({
34406
- tool,
34407
- grant: resolveGrant(tool, sets)
34408
- }));
34409
- const grantOf = (tool) => tools.find((entry) => entry.tool === tool)?.grant ?? null;
34410
- const blocking = [];
34411
- const advisories = [];
34412
- const write = grantOf("Write");
34413
- if (write === "denied") {
34414
- blocking.push(
34415
- "A deny or ask rule refuses Write, and `--permission-mode dontAsk` cannot escalate either one to a human."
34416
- );
34417
- }
34418
- if (write === "missing") {
34419
- blocking.push(
34420
- "No loaded permission rule grants Write, so a Claude planning phase is denied the description, plan, and decision artifacts it exists to produce."
34421
- );
34422
- }
34423
- if ((write === "missing" || write === "denied") && trust === "untrusted" && sources.some((source) => source !== "user")) {
34424
- blocking.push(
34425
- "Claude Code has not been trusted in this checkout, so it ignores the repository's own permission rules."
34426
- );
34427
- }
34428
- if ((write === "missing" || write === "denied") && unloadedLocal !== null && unloadedLocal.rules.allow.length > 0) {
34429
- advisories.push(
34430
- "Local settings hold allow rules the scoped invocation does not load; repository grants belong in the tracked project settings."
34431
- );
34432
- }
34433
- if (write === "scoped") {
34434
- advisories.push(
34435
- "Every rule granting Write is path-scoped, so a planning artifact outside those paths is still denied."
34436
- );
34437
- }
34438
- const edit = grantOf("Edit");
34439
- if (edit === "missing" || edit === "denied") {
34440
- advisories.push(
34441
- "No loaded permission rule grants Edit, so a planning phase can create artifacts but not revise them."
34442
- );
34443
- }
34444
- const bash = grantOf("Bash");
34445
- if (bash === "missing" || bash === "denied") {
34446
- advisories.push(
34447
- "No loaded permission rule grants Bash, so every command a phase runs is denied."
34448
- );
34449
- } else if (bash === "scoped") {
34450
- advisories.push(
34451
- "Bash is granted only for the commands its scoped rules name; anything else a phase runs is denied."
34452
- );
34453
- }
34454
- if (trust === "unknown") {
34455
- advisories.push(
34456
- "Claude Code's workspace trust record could not be read, so repository rules were treated as honored."
34457
- );
34458
- }
34459
- if (!managed.intact || loaded.some((entry) => !entry.intact)) {
34460
- advisories.push(
34461
- "A settings file could not be parsed, so its permission rules were treated as absent."
34462
- );
34463
- }
34464
- return { demand, sources, trust, tools, blocking, advisories };
34465
- }
34466
- function claudePermissionRemediation(audit) {
34467
- const steps = [];
34468
- if (audit.trust === "untrusted" && audit.sources.includes("project")) {
34469
- steps.push(
34470
- "run `claude` once in this checkout and accept the trust prompt"
34471
- );
34472
- }
34473
- const unresolved = audit.tools.filter(
34474
- (entry) => entry.tool !== "Bash" && (entry.grant === "missing" || entry.grant === "denied")
34475
- );
34476
- if (unresolved.length > 0) {
34477
- steps.push(
34478
- `add ${unresolved.map((entry) => `"${entry.tool}"`).join(" and ")} to \`permissions.allow\` in \`.claude/settings.json\``
34479
- );
34480
- if (!audit.sources.includes("local")) {
34481
- steps.push(
34482
- "keep the grants there rather than in local settings, the one repository source a scoped invocation never loads"
34483
- );
34484
- }
34485
- }
34486
- return steps.join("; ");
34487
- }
34488
-
34489
34593
  // src/context-accounting.ts
34490
34594
  var PHASES = [
34491
34595
  "selection",
@@ -35441,6 +35545,10 @@ still genuinely unresolved and blocks sound requirements, an ADR, or the plan,
35441
35545
  stop before authoring a Proposed ADR or plan that selects an option. Return
35442
35546
  outcome=needs_human_decision, gate=decision_consultation, questions=[], and one
35443
35547
  closed decisionConsultation request containing 1-3 sequential decision cards.
35548
+ Its summary is one plain line of at most 1000 characters naming the pending
35549
+ decisions; the cards carry the detail. Every card string is also one plain line
35550
+ within its schema maxLength: put separate points in separate benefits, costs,
35551
+ risks, consequences, or evidence entries instead of line breaks or lists.
35444
35552
  Each card must have a fresh stable decisionId, a precise question and why,
35445
35553
  affected requirement IDs, repository-relative evidence, an accountable owner,
35446
35554
  the supported responseMode, explicit options with consequences when the mode is
@@ -46368,8 +46476,15 @@ async function claudePermissionsCheck(repoRoot, config, dependencies) {
46368
46476
  "Claude permission inspection requires a valid configuration."
46369
46477
  );
46370
46478
  }
46371
- const demand = claudePermissionDemand(config);
46372
- if (!claudePermissionDemanded(demand)) {
46479
+ const permissions = await inspectConfiguredClaudePermissions(
46480
+ repoRoot,
46481
+ config,
46482
+ {
46483
+ resolveScope: async (settings) => await (dependencies.resolveClaudeInvocationScope ?? resolveClaudeInvocationScope)(settings),
46484
+ inspect: async (input) => await (dependencies.inspectClaudePermissions ?? inspectClaudePermissions)(input)
46485
+ }
46486
+ );
46487
+ if (permissions.state === "not_demanded") {
46373
46488
  return check(
46374
46489
  "claude.permissions",
46375
46490
  "config",
@@ -46377,11 +46492,7 @@ async function claudePermissionsCheck(repoRoot, config, dependencies) {
46377
46492
  "No configured role runs a Claude phase that depends on repository permission rules."
46378
46493
  );
46379
46494
  }
46380
- let settingSources;
46381
- try {
46382
- const resolved = await (dependencies.resolveClaudeInvocationScope ?? resolveClaudeInvocationScope)(config.claudeEnvironment);
46383
- settingSources = resolved.scope.settingSources;
46384
- } catch {
46495
+ if (permissions.state === "unresolved") {
46385
46496
  return check(
46386
46497
  "claude.permissions",
46387
46498
  "config",
@@ -46389,13 +46500,21 @@ async function claudePermissionsCheck(repoRoot, config, dependencies) {
46389
46500
  "Claude permission inspection requires a verifiable invocation environment."
46390
46501
  );
46391
46502
  }
46392
- const audit = await (dependencies.inspectClaudePermissions ?? inspectClaudePermissions)({ repoRoot, demand, settingSources });
46503
+ const audit = permissions.audit;
46504
+ const remediation = claudePermissionRemediation(audit);
46505
+ const fixes = remediation.length === 0 ? [] : [
46506
+ {
46507
+ action: `${remediation.charAt(0).toUpperCase()}${remediation.slice(1)}.`,
46508
+ documentation: "documentation/orchestrator.md#claude-tool-permissions"
46509
+ }
46510
+ ];
46393
46511
  if (audit.blocking.length > 0) {
46394
46512
  return check(
46395
46513
  "claude.permissions",
46396
46514
  "config",
46397
46515
  "fail",
46398
- audit.blocking.join(" ")
46516
+ audit.blocking.join(" "),
46517
+ fixes
46399
46518
  );
46400
46519
  }
46401
46520
  if (audit.advisories.length > 0) {
@@ -46403,7 +46522,8 @@ async function claudePermissionsCheck(repoRoot, config, dependencies) {
46403
46522
  "claude.permissions",
46404
46523
  "config",
46405
46524
  "warning",
46406
- audit.advisories.join(" ")
46525
+ audit.advisories.join(" "),
46526
+ fixes
46407
46527
  );
46408
46528
  }
46409
46529
  return check(
@@ -46810,7 +46930,7 @@ async function inspectDoctor(repoRoot, dependencies = DEFAULT_DEPENDENCIES) {
46810
46930
  "authentication.models",
46811
46931
  "executables",
46812
46932
  "warning",
46813
- "Model authentication readiness was not checked. Doctor is offline and executable availability is not authentication proof; startup preflight remains authoritative. Run Banzai from a regular terminal, restore vendor login, or use the vendor-supported setup-token flow when unattended authentication is required."
46933
+ "Model authentication readiness and vendor CLI versions were not checked. Doctor is offline and executable availability is not authentication proof; startup preflight remains authoritative. Run Banzai from a regular terminal, restore vendor login, or use the vendor-supported setup-token flow when unattended authentication is required."
46814
46934
  ),
46815
46935
  await activeRunCheck(repoRoot),
46816
46936
  await runnerLockCheck(repoRoot, dependencies)
@@ -51746,9 +51866,9 @@ async function prepareSetupLaunch(client, repoRoot, config, sourceEnvironment =
51746
51866
  );
51747
51867
  const env = setupEnvironment(sourceEnvironment);
51748
51868
  const adapters = dependencies.adapters ?? createVendorAdapters(runner);
51749
- if (client.vendor === "codex")
51750
- await adapters.preflightCodex(fresh.settings, env);
51751
- else await adapters.preflightClaude(fresh.settings, env);
51869
+ const target = { executable: fresh.settings.executable, model: null };
51870
+ if (client.vendor === "codex") await adapters.preflightCodex(target, env);
51871
+ else await adapters.preflightClaude(target, env);
51752
51872
  const mcp = await resolveSerenaWhitelist({
51753
51873
  repoRoot,
51754
51874
  codexExecutable: client.vendor === "codex" ? fresh.settings.executable : null,
@@ -52043,6 +52163,20 @@ function authenticationGateway(adapters) {
52043
52163
  }
52044
52164
  };
52045
52165
  }
52166
+ async function reportClaudePermissionGaps(repoRoot, config) {
52167
+ const permissions = await inspectConfiguredClaudePermissions(
52168
+ repoRoot,
52169
+ config,
52170
+ { resolveScope: resolveClaudeInvocationScope }
52171
+ );
52172
+ if (permissions.state !== "audited" || permissions.audit.blocking.length === 0) {
52173
+ return;
52174
+ }
52175
+ console.log(`Claude permissions: ${permissions.audit.blocking.join(" ")}`);
52176
+ console.log(
52177
+ `Fix it before the first run: ${claudePermissionRemediation(permissions.audit)}.`
52178
+ );
52179
+ }
52046
52180
  function modelConfigurationGateway(repoRoot) {
52047
52181
  return {
52048
52182
  async load() {
@@ -52610,6 +52744,7 @@ async function main() {
52610
52744
  console.log(
52611
52745
  "Verification follows each task; no profile setup is required."
52612
52746
  );
52747
+ await reportClaudePermissionGaps(repoRoot, config);
52613
52748
  console.log(
52614
52749
  "Run banzai doctor, then review and commit your files before starting a task."
52615
52750
  );
@@ -52645,11 +52780,14 @@ async function main() {
52645
52780
  "TTY_REQUIRED",
52646
52781
  `banzai setup requires terminal input and output. Open your client in this project and explicitly invoke banzai:project-manifest, or rerun banzai setup in a terminal. ${SETUP_PLUGIN_PREREQUISITE}`
52647
52782
  );
52783
+ const config = await loadConfig(repoRoot);
52648
52784
  process7.exitCode = await runProjectSetup(
52649
52785
  repoRoot,
52650
- await loadConfig(repoRoot),
52786
+ config,
52651
52787
  new InquirerInteractiveIO()
52652
52788
  );
52789
+ if (process7.exitCode === 0)
52790
+ await reportClaudePermissionGaps(repoRoot, config);
52653
52791
  return;
52654
52792
  }
52655
52793
  if (mode2 === "interactive") {