@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/README.md +2 -1
- package/dist/banzai.mjs +548 -410
- package/dist/banzai.mjs.map +4 -4
- package/package.json +1 -1
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) =>
|
|
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:
|
|
5927
|
-
benefits: consultationStrings(
|
|
5928
|
-
|
|
5929
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
6051
|
-
question:
|
|
6052
|
-
why:
|
|
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" ?
|
|
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) =>
|
|
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:
|
|
12898
|
-
benefits: consultationStrings2(
|
|
12899
|
-
|
|
12900
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
13022
|
-
question:
|
|
13023
|
-
why:
|
|
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" ?
|
|
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.
|
|
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,
|
|
27122
|
+
async function requireVersion(runner, vendor, target, sourceEnvironment = process.env) {
|
|
27032
27123
|
const result = await runner({
|
|
27033
|
-
command:
|
|
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
|
-
`${
|
|
27133
|
+
`${target.executable} --version failed: ${result.stderr.trim()}`
|
|
27043
27134
|
);
|
|
27044
27135
|
}
|
|
27045
27136
|
const actual = parseVersion(
|
|
27046
27137
|
result.stdout || result.stderr,
|
|
27047
|
-
|
|
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
|
-
`${
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
27966
|
-
import
|
|
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
|
|
27971
|
-
import
|
|
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 ||
|
|
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 =
|
|
28050
|
-
const candidate =
|
|
28051
|
-
if (!candidate.startsWith(`${normalizedRoot}${
|
|
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
|
|
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 =
|
|
28600
|
+
let current = path21.resolve(repoRoot);
|
|
28248
28601
|
const segments = relativePath3.split("/");
|
|
28249
28602
|
for (const [index, part] of segments.entries()) {
|
|
28250
|
-
current =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
29939
|
-
import
|
|
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 =
|
|
29979
|
-
if (relativePath3.length === 0 || relativePath3.includes("\\") ||
|
|
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}${
|
|
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
|
|
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
|
|
30029
|
-
import
|
|
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
|
|
30034
|
-
import
|
|
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) =>
|
|
30488
|
+
(value) => path25.posix.normalize(value.replaceAll("\\", "/"))
|
|
30136
30489
|
)
|
|
30137
30490
|
);
|
|
30138
|
-
const root =
|
|
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 =
|
|
30150
|
-
|
|
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 ||
|
|
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 =
|
|
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}${
|
|
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 =
|
|
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}${
|
|
30898
|
+
if (!resolved.startsWith(`${root}${path25.sep}`)) {
|
|
30546
30899
|
throw new Error("outside repository");
|
|
30547
30900
|
}
|
|
30548
|
-
bytes = captured ?? await
|
|
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
|
|
30592
|
-
|
|
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
|
|
31028
|
-
|
|
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
|
|
31180
|
-
|
|
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
|
|
31217
|
-
|
|
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
|
|
31291
|
-
import
|
|
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
|
|
31300
|
-
|
|
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
|
|
31328
|
-
import
|
|
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
|
|
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 =
|
|
31618
|
-
const absolutePath =
|
|
31619
|
-
if (!absolutePath.startsWith(`${repositoryRoot}${
|
|
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
|
|
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
|
|
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
|
-
|
|
32245
|
+
path28.relative(repoRoot, temporaryPath)
|
|
31905
32246
|
]);
|
|
31906
32247
|
};
|
|
31907
32248
|
await verifyDestinations();
|
|
31908
|
-
await mkdir8(
|
|
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
|
|
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
|
-
|
|
32298
|
-
record7.attempt.
|
|
32299
|
-
completionError3,
|
|
32300
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
33945
|
-
|
|
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
|
|
33968
|
-
import
|
|
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
|
|
34015
|
-
|
|
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
|
|
46372
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
51750
|
-
|
|
51751
|
-
else await adapters.preflightClaude(
|
|
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
|
-
|
|
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") {
|