@brainbase-labs/cli 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1267 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -35184,7 +35184,7 @@ var require_dist2 = __commonJS((exports, module) => {
35184
35184
  // src/index.ts
35185
35185
  var import_picocolors81 = __toESM(require_picocolors(), 1);
35186
35186
  import process14 from "node:process";
35187
- import fs94 from "node:fs";
35187
+ import fs96 from "node:fs";
35188
35188
 
35189
35189
  // src/cli/template.ts
35190
35190
  var import_picocolors12 = __toESM(require_picocolors(), 1);
@@ -36091,7 +36091,7 @@ function padStart(s, n) {
36091
36091
  // package.json
36092
36092
  var package_default = {
36093
36093
  name: "@brainbase-labs/cli",
36094
- version: "0.54.0",
36094
+ version: "0.55.0",
36095
36095
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36096
36096
  type: "module",
36097
36097
  bin: {
@@ -82445,6 +82445,10 @@ var SyncedScheduleTriggerSchema = exports_external.object({
82445
82445
  node_id: exports_external.string(),
82446
82446
  hash: exports_external.string()
82447
82447
  });
82448
+ var SyncedDecisionSchema = exports_external.object({
82449
+ id: exports_external.string(),
82450
+ hash: exports_external.string()
82451
+ });
82448
82452
  var SyncedOrchestrationMetaSchema = exports_external.object({
82449
82453
  name: exports_external.string().optional(),
82450
82454
  description: exports_external.string().optional(),
@@ -82460,6 +82464,7 @@ var OrchestrationSyncStateSchema = exports_external.object({
82460
82464
  members: exports_external.array(SyncedMemberSchema),
82461
82465
  edges: exports_external.array(SyncedEdgeSchema),
82462
82466
  triggers: exports_external.array(SyncedScheduleTriggerSchema).optional(),
82467
+ decisions: exports_external.array(SyncedDecisionSchema).optional(),
82463
82468
  meta: SyncedOrchestrationMetaSchema.optional()
82464
82469
  });
82465
82470
  function orchLinkPath(cwd2) {
@@ -82881,6 +82886,9 @@ var TriggerEdgeSchema = exports_external.object({
82881
82886
  payload_schema: exports_external.record(exports_external.unknown()).optional()
82882
82887
  });
82883
82888
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
82889
+ function canonicalUuid2(value2) {
82890
+ return UUID_RE.test(value2) ? value2.toLowerCase() : value2;
82891
+ }
82884
82892
  var ScheduleTriggerSchema = exports_external.object({
82885
82893
  type: exports_external.literal("schedule"),
82886
82894
  node_id: exports_external.string().regex(UUID_RE, "must be a UUID"),
@@ -82898,12 +82906,52 @@ var ReadOnlyTriggerSchema = exports_external.object({
82898
82906
  to: exports_external.array(TriggerEdgeSchema).default([])
82899
82907
  });
82900
82908
  var TriggerSchema = exports_external.union([ScheduleTriggerSchema, ReadOnlyTriggerSchema]);
82909
+ var DECISION_MODELS = [
82910
+ "jev",
82911
+ "kev-4b",
82912
+ "clef",
82913
+ "clef-flash",
82914
+ "gpt-6-luna"
82915
+ ];
82916
+ var DEFAULT_DECISION_MODEL = "clef-flash";
82917
+ var DECISION_LIMITS = {
82918
+ name: 200,
82919
+ model: 100,
82920
+ instructions: 4000,
82921
+ answersMin: 2,
82922
+ answersMax: 20,
82923
+ answer: 200,
82924
+ criterion: 1000
82925
+ };
82926
+ var DecisionTargetSchema = exports_external.object({
82927
+ agent: exports_external.string().min(1).optional(),
82928
+ decision: exports_external.string().regex(UUID_RE, "must be a UUID").optional(),
82929
+ description: exports_external.string().optional()
82930
+ }).refine((t) => t.agent === undefined !== (t.decision === undefined), {
82931
+ message: "set exactly one of agent or decision"
82932
+ });
82933
+ var DecisionAnswerSchema = exports_external.object({
82934
+ answer: exports_external.string(),
82935
+ criterion: exports_external.string().optional(),
82936
+ to: exports_external.array(DecisionTargetSchema).default([])
82937
+ });
82938
+ var DecisionSchema = exports_external.object({
82939
+ id: exports_external.string().regex(UUID_RE, "must be a UUID"),
82940
+ name: exports_external.string(),
82941
+ model: exports_external.string().optional(),
82942
+ instructions: exports_external.string().optional(),
82943
+ confidence_threshold: exports_external.number().min(0).max(1).nullable().optional(),
82944
+ from: exports_external.array(exports_external.object({ trigger: exports_external.string().regex(UUID_RE, "must be a UUID") })).default([]),
82945
+ answers: exports_external.array(DecisionAnswerSchema),
82946
+ fallback: exports_external.array(DecisionTargetSchema).default([])
82947
+ });
82901
82948
  var OrchestrationManifestSchema = exports_external.object({
82902
82949
  schema: exports_external.literal(1),
82903
82950
  orchestration: OrchMetaSchema,
82904
82951
  members: exports_external.array(MemberSchema).default([]),
82905
82952
  edges: exports_external.array(EdgeSchema).default([]),
82906
- triggers: exports_external.array(TriggerSchema).optional()
82953
+ triggers: exports_external.array(TriggerSchema).optional(),
82954
+ decisions: exports_external.array(DecisionSchema).optional()
82907
82955
  });
82908
82956
  function orchManifestPath(cwd2) {
82909
82957
  return path96.join(cwd2, ORCH_MANIFEST_FILE);
@@ -83328,6 +83376,132 @@ function syncedScheduleTriggers(triggers) {
83328
83376
  hash: scheduleTriggerHash(t)
83329
83377
  }));
83330
83378
  }
83379
+ var DECISION = "decision";
83380
+ function manifestDecisions(manifest, slugToAgentId) {
83381
+ const target = (t) => {
83382
+ const agentId = t.agent !== undefined ? slugToAgentId?.get(t.agent) : undefined;
83383
+ return {
83384
+ ...t.agent !== undefined ? { agent: t.agent, ...agentId ? { agent_id: agentId } : {} } : { decision: canonicalUuid2(t.decision) },
83385
+ description: t.description ?? ""
83386
+ };
83387
+ };
83388
+ return (manifest?.decisions ?? []).map((d3) => ({
83389
+ id: canonicalUuid2(d3.id),
83390
+ name: d3.name.trim(),
83391
+ model: (d3.model ?? DEFAULT_DECISION_MODEL).trim(),
83392
+ instructions: d3.instructions ?? "",
83393
+ confidence_threshold: d3.confidence_threshold ?? null,
83394
+ triggers: d3.from.map((f4) => canonicalUuid2(f4.trigger)),
83395
+ answers: d3.answers.map((a3) => ({
83396
+ answer: a3.answer.trim(),
83397
+ criterion: (a3.criterion ?? "").trim(),
83398
+ to: a3.to.map(target)
83399
+ })),
83400
+ fallback: d3.fallback.map(target)
83401
+ }));
83402
+ }
83403
+ function liveDecisionLinks(cloud) {
83404
+ const agents = new Set(cloud.members.map((m3) => m3.agent_id));
83405
+ const decisions = new Set((cloud.decisions ?? []).map((d3) => canonicalUuid2(d3.id)));
83406
+ const triggers = new Set((cloud.triggers ?? []).map((t) => canonicalUuid2(t.node_id)));
83407
+ return (d3) => {
83408
+ const answers = new Set(d3.answers.map((a3) => a3.answer));
83409
+ return {
83410
+ routes: d3.routes.filter((r2) => (r2.is_fallback || r2.answer != null && answers.has(r2.answer)) && (r2.to_agent_id ? agents.has(r2.to_agent_id) : r2.to_decision_id != null && decisions.has(canonicalUuid2(r2.to_decision_id)))),
83411
+ inputs: d3.inputs.filter((i) => triggers.has(canonicalUuid2(i.from_trigger_node_id)))
83412
+ };
83413
+ };
83414
+ }
83415
+ function cloudDecisions(cloud, slugForAgent) {
83416
+ if (cloud.decisions == null)
83417
+ return null;
83418
+ const live = liveDecisionLinks(cloud);
83419
+ return cloud.decisions.map((d3) => {
83420
+ const { routes, inputs } = live(d3);
83421
+ const target = (r2) => ({
83422
+ ...r2.to_agent_id ? {
83423
+ agent: slugForAgent(r2.to_agent_id, r2.to_slug ?? r2.to_agent_id),
83424
+ agent_id: r2.to_agent_id
83425
+ } : { decision: canonicalUuid2(r2.to_decision_id) },
83426
+ description: r2.description ?? ""
83427
+ });
83428
+ return {
83429
+ id: canonicalUuid2(d3.id),
83430
+ name: d3.name.trim(),
83431
+ model: d3.model.trim(),
83432
+ instructions: d3.instructions ?? "",
83433
+ confidence_threshold: d3.confidence_threshold ?? null,
83434
+ triggers: inputs.map((i) => canonicalUuid2(i.from_trigger_node_id)),
83435
+ answers: d3.answers.map((a3) => ({
83436
+ answer: a3.answer.trim(),
83437
+ criterion: (a3.criterion ?? "").trim(),
83438
+ to: routes.filter((r2) => !r2.is_fallback && r2.answer === a3.answer).map(target)
83439
+ })),
83440
+ fallback: routes.filter((r2) => r2.is_fallback).map(target)
83441
+ };
83442
+ });
83443
+ }
83444
+ function deadDecisionLinks(cloud) {
83445
+ const live = liveDecisionLinks(cloud);
83446
+ const out = new Map;
83447
+ for (const d3 of cloud.decisions ?? []) {
83448
+ const { routes, inputs } = live(d3);
83449
+ const dead = d3.routes.length - routes.length + d3.inputs.length - inputs.length;
83450
+ if (dead > 0)
83451
+ out.set(d3.id, dead);
83452
+ }
83453
+ return out;
83454
+ }
83455
+ function decisionHash(d3) {
83456
+ const targets = (ts2) => ts2.map(({ agent, agent_id, ...rest2 }) => stableJson(agent_id !== undefined ? { agent_id, ...rest2 } : { agent, ...rest2 })).sort();
83457
+ const canonical = {
83458
+ name: d3.name,
83459
+ model: d3.model,
83460
+ instructions: d3.instructions,
83461
+ confidence_threshold: d3.confidence_threshold,
83462
+ triggers: [...d3.triggers].sort(),
83463
+ answers: d3.answers.map((a3) => ({
83464
+ answer: a3.answer,
83465
+ criterion: a3.criterion,
83466
+ to: targets(a3.to)
83467
+ })),
83468
+ fallback: targets(d3.fallback)
83469
+ };
83470
+ return crypto6.createHash("sha256").update(stableJson(canonical)).digest("hex");
83471
+ }
83472
+ function decisionLabel(d3) {
83473
+ return `decision "${d3.name}" (${d3.id})`;
83474
+ }
83475
+ function decideDecisionReconcile(input) {
83476
+ const rows = threeWayDiff({
83477
+ local: input.local.map((d3) => ({
83478
+ type: DECISION,
83479
+ slug: d3.id,
83480
+ hash: decisionHash(d3)
83481
+ })),
83482
+ lock: (input.baseline ?? []).map((d3) => ({
83483
+ type: DECISION,
83484
+ slug: d3.id,
83485
+ hash: d3.hash,
83486
+ installedPaths: []
83487
+ })),
83488
+ cloud: input.cloud.map((d3) => ({
83489
+ type: DECISION,
83490
+ slug: d3.id,
83491
+ hash: decisionHash(d3),
83492
+ files: []
83493
+ }))
83494
+ });
83495
+ return decideCollectionReconcile({
83496
+ rows,
83497
+ type: DECISION,
83498
+ claims: input.local.length > 0,
83499
+ force: input.force
83500
+ });
83501
+ }
83502
+ function syncedDecisions(decisions) {
83503
+ return decisions.map((d3) => ({ id: d3.id, hash: decisionHash(d3) }));
83504
+ }
83331
83505
  var ORCH_META_FIELDS = [
83332
83506
  "name",
83333
83507
  "description",
@@ -83410,6 +83584,26 @@ function triggersForManifest(triggers, slugFor) {
83410
83584
  };
83411
83585
  });
83412
83586
  }
83587
+ function decisionsForManifest(decisions) {
83588
+ const target = (t) => ({
83589
+ ...t.agent !== undefined ? { agent: t.agent } : { decision: t.decision },
83590
+ ...t.description ? { description: t.description } : {}
83591
+ });
83592
+ return decisions.map((d3) => ({
83593
+ id: d3.id,
83594
+ name: d3.name,
83595
+ model: d3.model,
83596
+ ...d3.instructions ? { instructions: d3.instructions } : {},
83597
+ ...d3.confidence_threshold != null ? { confidence_threshold: d3.confidence_threshold } : {},
83598
+ ...d3.triggers.length ? { from: d3.triggers.map((trigger) => ({ trigger })) } : {},
83599
+ answers: d3.answers.map((a3) => ({
83600
+ answer: a3.answer,
83601
+ ...a3.criterion ? { criterion: a3.criterion } : {},
83602
+ ...a3.to.length ? { to: a3.to.map(target) } : {}
83603
+ })),
83604
+ ...d3.fallback.length ? { fallback: d3.fallback.map(target) } : {}
83605
+ }));
83606
+ }
83413
83607
  async function runOrchestrationPull(cwd2, args) {
83414
83608
  banner("orchestration pull — fetch orchestration + all member agents");
83415
83609
  let orchId = null;
@@ -83460,6 +83654,14 @@ async function runOrchestrationPull(cwd2, args) {
83460
83654
  planLines.push(` ${import_picocolors52.default.cyan(from)} ${import_picocolors52.default.dim("→")} ${import_picocolors52.default.cyan(to2)}${desc}`);
83461
83655
  }
83462
83656
  }
83657
+ const pulledDecisions = cloudDecisions(cloud, (agentId, fallback) => slugByAgent.get(agentId) ?? fallback);
83658
+ if (pulledDecisions?.length) {
83659
+ planLines.push("");
83660
+ planLines.push(` ${import_picocolors52.default.dim("decisions:")}`);
83661
+ for (const d3 of pulledDecisions) {
83662
+ planLines.push(` ${import_picocolors52.default.cyan("◆")} ${import_picocolors52.default.bold(d3.name)} ${import_picocolors52.default.dim(`(${d3.model}, ${d3.answers.length} answers)`)}`);
83663
+ }
83664
+ }
83463
83665
  planLines.push("");
83464
83666
  console.log(planLines.join(`
83465
83667
  `));
@@ -83525,6 +83727,19 @@ async function runOrchestrationPull(cwd2, args) {
83525
83727
  }
83526
83728
  }
83527
83729
  const manifestTriggers = triggersForManifest(cloud.triggers ?? [], slugFor);
83730
+ let manifestDecisions2 = pulledDecisions ? decisionsForManifest(pulledDecisions) : undefined;
83731
+ let decisionBaseline = pulledDecisions ? syncedDecisions(pulledDecisions) : undefined;
83732
+ if (!pulledDecisions) {
83733
+ try {
83734
+ manifestDecisions2 = readOrchManifest(cwd2)?.decisions;
83735
+ } catch {}
83736
+ const lock = readOrchSyncState(cwd2);
83737
+ decisionBaseline = lock?.orchestration_id === cloud.id ? lock.decisions : undefined;
83738
+ }
83739
+ for (const [decisionId, count] of deadDecisionLinks(cloud)) {
83740
+ const name = pulledDecisions?.find((d3) => d3.id === decisionId)?.name;
83741
+ f2.warn(`Decision ${import_picocolors52.default.bold(name ?? decisionId)} has ${count} route${count === 1 ? "" : "s"} or input${count === 1 ? "" : "s"} that lead${count === 1 ? "s" : ""} nowhere: on an answer it no longer has, or to an agent, decision or trigger no longer in this orchestration. They never run and are not in ${import_picocolors52.default.bold("brainbase-orchestration.yaml")}; the next push of decisions removes them.`);
83742
+ }
83528
83743
  const manifest = {
83529
83744
  schema: 1,
83530
83745
  orchestration: {
@@ -83548,7 +83763,8 @@ async function runOrchestrationPull(cwd2, args) {
83548
83763
  ...e2.payload_schema && Object.keys(e2.payload_schema).length ? { payload_schema: e2.payload_schema } : {},
83549
83764
  ...e2.settings && Object.keys(e2.settings).length ? { settings: e2.settings } : {}
83550
83765
  })),
83551
- ...manifestTriggers.length ? { triggers: manifestTriggers } : {}
83766
+ ...manifestTriggers.length ? { triggers: manifestTriggers } : {},
83767
+ ...manifestDecisions2?.length ? { decisions: manifestDecisions2 } : {}
83552
83768
  };
83553
83769
  writeOrchManifest(cwd2, manifest);
83554
83770
  writeOrchLink(cwd2, {
@@ -83574,6 +83790,7 @@ async function runOrchestrationPull(cwd2, args) {
83574
83790
  payload_schema: e2.payload_schema ?? {}
83575
83791
  })),
83576
83792
  triggers: syncedScheduleTriggers(cloudScheduleTriggers(cloud, (agentId, fallback) => slugFor(agentId) || fallback)),
83793
+ ...decisionBaseline ? { decisions: decisionBaseline } : {},
83577
83794
  meta: syncedOrchestrationMeta(cloud)
83578
83795
  });
83579
83796
  if (missingMembers.length > 0) {
@@ -83601,6 +83818,83 @@ function handleApiError5(err) {
83601
83818
  var import_picocolors53 = __toESM(require_picocolors(), 1);
83602
83819
 
83603
83820
  // src/core/orchestration-outgoing.ts
83821
+ function charLength(value2) {
83822
+ return Array.from(value2).length;
83823
+ }
83824
+ function validateDecisions(decisions, memberSlugs, triggerNodeIds) {
83825
+ const ids = new Set;
83826
+ for (const d3 of decisions) {
83827
+ if (ids.has(d3.id))
83828
+ throw new Error(`Decision ${d3.id} is duplicated.`);
83829
+ ids.add(d3.id);
83830
+ }
83831
+ for (const d3 of decisions) {
83832
+ const label2 = `Decision "${d3.name || d3.id}"`;
83833
+ const fail3 = (message) => {
83834
+ throw new Error(`${label2}: ${message}`);
83835
+ };
83836
+ if (!d3.name || charLength(d3.name) > DECISION_LIMITS.name) {
83837
+ fail3(`name must be 1 to ${DECISION_LIMITS.name} characters.`);
83838
+ }
83839
+ if (!d3.model || charLength(d3.model) > DECISION_LIMITS.model) {
83840
+ fail3(`model must be 1 to ${DECISION_LIMITS.model} characters.`);
83841
+ }
83842
+ if (charLength(d3.instructions) > DECISION_LIMITS.instructions) {
83843
+ fail3(`instructions must be ${DECISION_LIMITS.instructions} characters or fewer.`);
83844
+ }
83845
+ const count = d3.answers.length;
83846
+ if (count < DECISION_LIMITS.answersMin || count > DECISION_LIMITS.answersMax) {
83847
+ fail3(`needs ${DECISION_LIMITS.answersMin} to ${DECISION_LIMITS.answersMax} answers (has ${count}).`);
83848
+ }
83849
+ const answers = new Set;
83850
+ for (const a3 of d3.answers) {
83851
+ if (!a3.answer || charLength(a3.answer) > DECISION_LIMITS.answer) {
83852
+ fail3(`each answer must be 1 to ${DECISION_LIMITS.answer} characters.`);
83853
+ }
83854
+ if (answers.has(a3.answer))
83855
+ fail3(`answer "${a3.answer}" appears twice.`);
83856
+ answers.add(a3.answer);
83857
+ if (charLength(a3.criterion) > DECISION_LIMITS.criterion) {
83858
+ fail3(`the criterion for "${a3.answer}" must be ${DECISION_LIMITS.criterion} characters or fewer.`);
83859
+ }
83860
+ }
83861
+ const fed = new Set;
83862
+ for (const nodeId of d3.triggers) {
83863
+ if (!triggerNodeIds.has(nodeId)) {
83864
+ fail3(`from trigger ${nodeId}, which isn't in triggers.`);
83865
+ }
83866
+ if (fed.has(nodeId))
83867
+ fail3(`trigger ${nodeId} is listed twice in from.`);
83868
+ fed.add(nodeId);
83869
+ }
83870
+ const checkTargets = (targets, branch) => {
83871
+ const seen = new Set;
83872
+ for (const t of targets) {
83873
+ if (t.agent !== undefined && !memberSlugs.has(t.agent)) {
83874
+ fail3(`${branch} routes to "${t.agent}", which isn't in members.`);
83875
+ }
83876
+ if (t.decision !== undefined) {
83877
+ if (t.decision === d3.id)
83878
+ fail3(`${branch} routes to the decision itself.`);
83879
+ if (!ids.has(t.decision)) {
83880
+ fail3(`${branch} routes to decision ${t.decision}, which isn't in decisions.`);
83881
+ }
83882
+ }
83883
+ const key2 = t.agent !== undefined ? `agent:${t.agent}` : `decision:${t.decision}`;
83884
+ if (seen.has(key2))
83885
+ fail3(`${branch} routes to ${key2.replace(":", " ")} twice.`);
83886
+ seen.add(key2);
83887
+ }
83888
+ };
83889
+ for (const a3 of d3.answers)
83890
+ checkTargets(a3.to, `answer "${a3.answer}"`);
83891
+ checkTargets(d3.fallback, "fallback");
83892
+ }
83893
+ }
83894
+ function unknownDecisionModels(manifest) {
83895
+ const known = new Set(DECISION_MODELS);
83896
+ return manifestDecisions(manifest).filter((d3) => !known.has(d3.model)).map((d3) => ({ name: d3.name, model: d3.model }));
83897
+ }
83604
83898
  function buildOrchestrationGraphPayload(manifest, slugToAgentId) {
83605
83899
  const memberSlugs = new Set(manifest.members.map((m3) => m3.slug));
83606
83900
  for (const e2 of manifest.edges) {
@@ -83661,6 +83955,32 @@ function buildOrchestrationGraphPayload(manifest, slugToAgentId) {
83661
83955
  }))
83662
83956
  };
83663
83957
  }
83958
+ function buildDecisionsPayload(manifest, slugToAgentId) {
83959
+ const decisions = manifestDecisions(manifest, slugToAgentId);
83960
+ validateDecisions(decisions, new Set(manifest.members.map((m3) => m3.slug)), new Set((manifest.triggers ?? []).map((t) => canonicalUuid2(t.node_id))));
83961
+ return decisions.map((d3) => decisionPayload(d3, slugToAgentId));
83962
+ }
83963
+ function decisionPayload(d3, slugToAgentId) {
83964
+ const route = (answer, t) => ({
83965
+ answer,
83966
+ is_fallback: answer === null,
83967
+ ...t.agent !== undefined ? { to_agent_id: slugToAgentId.get(t.agent) } : { to_decision_id: t.decision },
83968
+ description: t.description
83969
+ });
83970
+ return {
83971
+ id: d3.id,
83972
+ name: d3.name,
83973
+ model: d3.model,
83974
+ instructions: d3.instructions,
83975
+ answers: d3.answers.map((a3) => ({ answer: a3.answer, criterion: a3.criterion })),
83976
+ confidence_threshold: d3.confidence_threshold,
83977
+ inputs: d3.triggers.map((nodeId) => ({ from_trigger_node_id: nodeId })),
83978
+ routes: [
83979
+ ...d3.answers.flatMap((a3) => a3.to.map((t) => route(a3.answer, t))),
83980
+ ...d3.fallback.map((t) => route(null, t))
83981
+ ]
83982
+ };
83983
+ }
83664
83984
 
83665
83985
  // src/cli/orchestration-push.ts
83666
83986
  function joinList(items) {
@@ -83788,7 +84108,21 @@ async function runOrchestrationPush(cwd2, args) {
83788
84108
  const metaOverwrites = metaDecisions.filter((d3) => d3.kind === "push");
83789
84109
  const metaKept = metaDecisions.filter((d3) => d3.kind === "keep");
83790
84110
  const blockedTriggers = triggerDecision.kind === "blocked" ? triggerDecision : null;
83791
- if (blockedTriggers || metaConflicts.length > 0) {
84111
+ const localDecisions = manifestDecisions(manifest, slugToAgentId);
84112
+ const cloudDecisionList = cloudDecisions(cloud, slugForAgent);
84113
+ const decisionPlan = cloudDecisionList ? decideDecisionReconcile({
84114
+ local: localDecisions,
84115
+ cloud: cloudDecisionList,
84116
+ baseline: lock?.decisions,
84117
+ force: !!args.force
84118
+ }) : null;
84119
+ const decisionById = new Map([...cloudDecisionList ?? [], ...localDecisions].map((d3) => [d3.id, d3]));
84120
+ const decisionLabelFor = (id) => {
84121
+ const d3 = decisionById.get(id);
84122
+ return d3 ? decisionLabel(d3) : id;
84123
+ };
84124
+ const blockedDecisions = decisionPlan?.kind === "blocked" ? decisionPlan : null;
84125
+ if (blockedTriggers || blockedDecisions || metaConflicts.length > 0) {
83792
84126
  const pullTargets = [];
83793
84127
  const forceEffects = [];
83794
84128
  if (blockedTriggers) {
@@ -83807,6 +84141,22 @@ async function runOrchestrationPush(cwd2, args) {
83807
84141
  pullTargets.push(`${total} schedule trigger${total === 1 ? "" : "s"}`);
83808
84142
  forceEffects.push("make your local schedule triggers authoritative (deleting the cloud ones above)");
83809
84143
  }
84144
+ if (blockedDecisions) {
84145
+ const { unseen, cloudModified, staleArchives } = blockedDecisions;
84146
+ f2.error(unseen.length > 0 ? "Cannot push: the cloud has decisions this folder has never seen." : "Cannot push: decisions changed both locally and in the cloud.");
84147
+ for (const id of unseen) {
84148
+ console.error(` ${import_picocolors53.default.red("!")} ${import_picocolors53.default.bold(decisionLabelFor(id))} ${import_picocolors53.default.dim("(only in the cloud — pushing would delete it)")}`);
84149
+ }
84150
+ for (const id of cloudModified) {
84151
+ console.error(` ${import_picocolors53.default.red("!")} ${import_picocolors53.default.bold(decisionLabelFor(id))} ${import_picocolors53.default.dim("(edited in the cloud — pushing would overwrite that edit)")}`);
84152
+ }
84153
+ for (const id of staleArchives) {
84154
+ console.error(` ${import_picocolors53.default.red("!")} ${import_picocolors53.default.bold(decisionLabelFor(id))} ${import_picocolors53.default.dim("(deleted here, edited in the cloud since — pushing would delete that edit)")}`);
84155
+ }
84156
+ const total = unseen.length + cloudModified.length + staleArchives.length;
84157
+ pullTargets.push(`${total} decision${total === 1 ? "" : "s"}`);
84158
+ forceEffects.push("make your local decisions authoritative (deleting or overwriting the cloud ones above)");
84159
+ }
83810
84160
  if (metaConflicts.length > 0) {
83811
84161
  f2.error("Cannot push: orchestration metadata changed both locally and in the cloud.");
83812
84162
  for (const d3 of metaConflicts) {
@@ -83839,9 +84189,30 @@ async function runOrchestrationPush(cwd2, args) {
83839
84189
  f2.warn(`${cloudTriggers.length} cloud schedule trigger${cloudTriggers.length === 1 ? "" : "s"} ${cloudTriggers.length === 1 ? "is" : "are"} not in ${import_picocolors53.default.bold(ORCH_MANIFEST_FILE)} and will be left untouched. Run ${import_picocolors53.default.cyan("brainbase orchestration pull")} to manage ${cloudTriggers.length === 1 ? "it" : "them"} from here.`);
83840
84190
  }
83841
84191
  const triggerDeletes = triggerDecision.kind === "reconcile" ? triggerDecision.archives : [];
84192
+ const decisionDeletes = decisionPlan?.kind === "reconcile" ? decisionPlan.archives : [];
84193
+ const sendDecisions = decisionPlan?.kind === "reconcile" && (localDecisions.length > 0 || decisionDeletes.length > 0);
84194
+ if (!cloudDecisionList && localDecisions.length > 0) {
84195
+ f2.warn(`The cloud did not return decision nodes for this account, so the ${localDecisions.length} decision${localDecisions.length === 1 ? "" : "s"} in ${import_picocolors53.default.bold(ORCH_MANIFEST_FILE)} ${localDecisions.length === 1 ? "is" : "are"} left out of this push and the cloud's are left alone.`);
84196
+ } else if (decisionPlan?.kind === "skip") {
84197
+ const left = decisionPlan.unseen.length + decisionPlan.cloudModified.length;
84198
+ f2.warn(`${left} cloud decision${left === 1 ? "" : "s"} ${left === 1 ? "is" : "are"} not in ${import_picocolors53.default.bold(ORCH_MANIFEST_FILE)} and will be left untouched. Run ${import_picocolors53.default.cyan("brainbase orchestration pull")} to manage ${left === 1 ? "it" : "them"} from here.`);
84199
+ }
84200
+ let decisionsPayload = [];
84201
+ if (sendDecisions) {
84202
+ try {
84203
+ decisionsPayload = buildDecisionsPayload(manifest, slugToAgentId);
84204
+ } catch (err) {
84205
+ f2.error(err.message);
84206
+ process.exitCode = 1;
84207
+ return;
84208
+ }
84209
+ for (const { name, model } of unknownDecisionModels(manifest)) {
84210
+ f2.warn(`Decision ${import_picocolors53.default.bold(name)} uses model ${import_picocolors53.default.bold(model)}, which is not one the canvas offers. It is pushed as written; if the gateway does not know it, the decision fails when its trigger fires.`);
84211
+ }
84212
+ }
83842
84213
  const plan = [""];
83843
84214
  plan.push(` ${import_picocolors53.default.bold(link2.name)} ${import_picocolors53.default.dim(`(${link2.orchestration_id})`)}`);
83844
- plan.push(` ${import_picocolors53.default.dim(`${manifest.members.length} member${manifest.members.length === 1 ? "" : "s"}, ${manifest.edges.length} edge${manifest.edges.length === 1 ? "" : "s"}, ${graph.triggers.length} trigger${graph.triggers.length === 1 ? "" : "s"}`)}`);
84215
+ plan.push(` ${import_picocolors53.default.dim(`${manifest.members.length} member${manifest.members.length === 1 ? "" : "s"}, ${manifest.edges.length} edge${manifest.edges.length === 1 ? "" : "s"}, ${graph.triggers.length} trigger${graph.triggers.length === 1 ? "" : "s"}${sendDecisions ? `, ${decisionsPayload.length} decision${decisionsPayload.length === 1 ? "" : "s"}` : ""}`)}`);
83845
84216
  plan.push("");
83846
84217
  if (!args.graphOnly) {
83847
84218
  plan.push(` ${import_picocolors53.default.dim("per-member agent push:")}`);
@@ -83857,6 +84228,13 @@ async function runOrchestrationPush(cwd2, args) {
83857
84228
  }
83858
84229
  plan.push("");
83859
84230
  }
84231
+ if (decisionDeletes.length) {
84232
+ plan.push(` ${import_picocolors53.default.bold("decisions this push will delete")} ${import_picocolors53.default.dim(`(${decisionDeletes.length})`)}`);
84233
+ for (const id of decisionDeletes) {
84234
+ plan.push(` ${import_picocolors53.default.yellow("⨯")} ${decisionLabelFor(id)}`);
84235
+ }
84236
+ plan.push("");
84237
+ }
83860
84238
  if (metaOverwrites.length) {
83861
84239
  plan.push(` ${import_picocolors53.default.bold("orchestration metadata this push will overwrite")}`);
83862
84240
  for (const m3 of metaOverwrites) {
@@ -83875,9 +84253,13 @@ async function runOrchestrationPush(cwd2, args) {
83875
84253
  console.log(plan.join(`
83876
84254
  `));
83877
84255
  if (!autoProceed(args.yes)) {
84256
+ const deletions = [
84257
+ ...triggerDeletes.length ? [`${triggerDeletes.length} schedule trigger${triggerDeletes.length === 1 ? "" : "s"}`] : [],
84258
+ ...decisionDeletes.length ? [`${decisionDeletes.length} decision${decisionDeletes.length === 1 ? "" : "s"}`] : []
84259
+ ];
83878
84260
  const ok = await se({
83879
- message: triggerDeletes.length ? `Push, deleting ${triggerDeletes.length} schedule trigger${triggerDeletes.length === 1 ? "" : "s"}?` : args.graphOnly ? "Push graph (members, edges + triggers) only?" : "Push each member, then update the graph?",
83880
- initialValue: triggerDeletes.length === 0
84261
+ message: deletions.length ? `Push, deleting ${joinList(deletions)}?` : args.graphOnly ? "Push graph (members, edges + triggers) only?" : "Push each member, then update the graph?",
84262
+ initialValue: deletions.length === 0
83881
84263
  });
83882
84264
  if (!ensureNotCancelled(ok)) {
83883
84265
  $e("Aborted.");
@@ -83923,9 +84305,11 @@ async function runOrchestrationPush(cwd2, args) {
83923
84305
  members: graph.memberIds,
83924
84306
  edges: graph.edges,
83925
84307
  triggers: graph.triggers,
84308
+ ...sendDecisions ? { decisions: decisionsPayload } : {},
83926
84309
  ...lock?.revision != null ? { base_revision: lock.revision } : {}
83927
84310
  });
83928
84311
  sp.stop(`Graph updated. New cloud revision ${updated.revision}.`);
84312
+ const settledDecisions = sendDecisions ? cloudDecisions(updated, slugForAgent) : decisionPlan?.kind === "reconcile" ? cloudDecisionList : null;
83929
84313
  writeOrchSyncState(cwd2, {
83930
84314
  schemaVersion: 1,
83931
84315
  orchestration_id: updated.id,
@@ -83943,6 +84327,7 @@ async function runOrchestrationPush(cwd2, args) {
83943
84327
  payload_schema: e2.payload_schema ?? {}
83944
84328
  })),
83945
84329
  triggers: triggerDecision.kind === "skip" ? lock?.triggers ?? [] : syncedScheduleTriggers(cloudScheduleTriggers(updated, slugForAgent)),
84330
+ ...settledDecisions ? { decisions: syncedDecisions(settledDecisions) } : lock?.decisions ? { decisions: lock.decisions } : {},
83946
84331
  meta: nextOrchestrationMeta({
83947
84332
  applied: updated,
83948
84333
  decisions: metaDecisions,
@@ -84115,6 +84500,64 @@ async function runOrchestrationStatus(cwd2) {
84115
84500
  lines.push(` ${import_picocolors54.default.dim(`${left.length} cloud schedule trigger${left.length === 1 ? "" : "s"} not managed by this manifest — push leaves ${left.length === 1 ? "it" : "them"} untouched`)}`);
84116
84501
  lines.push("");
84117
84502
  }
84503
+ const localDecisions = manifestDecisions(localManifest, new Map([...localSlugByAgentId].map(([agentId, slug]) => [slug, agentId])));
84504
+ const cloudDecisionList = cloudDecisions(cloud, slugForAgent);
84505
+ let decisionsToPush = [];
84506
+ let decisionsToPull = [];
84507
+ if (!cloudDecisionList) {
84508
+ if (localDecisions.length) {
84509
+ lines.push(` ${import_picocolors54.default.dim(`${localDecisions.length} decision${localDecisions.length === 1 ? "" : "s"} in yaml, but the cloud does not show decision nodes to this account — push leaves ${localDecisions.length === 1 ? "it" : "them"} alone`)}`);
84510
+ lines.push("");
84511
+ }
84512
+ } else {
84513
+ const decisionPlan = decideDecisionReconcile({
84514
+ local: localDecisions,
84515
+ cloud: cloudDecisionList,
84516
+ baseline: lock?.decisions,
84517
+ force: false
84518
+ });
84519
+ const byId = new Map([...cloudDecisionList, ...localDecisions].map((d3) => [d3.id, d3]));
84520
+ const label2 = (id) => {
84521
+ const d3 = byId.get(id);
84522
+ return d3 ? decisionLabel(d3) : id;
84523
+ };
84524
+ const localIds = new Set(localDecisions.map((d3) => d3.id));
84525
+ const cloudById = new Map(cloudDecisionList.map((d3) => [d3.id, d3]));
84526
+ decisionsToPush = localDecisions.filter((d3) => {
84527
+ const remote = cloudById.get(d3.id);
84528
+ return !remote || decisionHash(remote) !== decisionHash(d3);
84529
+ });
84530
+ decisionsToPull = cloudDecisionList.filter((d3) => !localIds.has(d3.id));
84531
+ const decisionDeletes = decisionPlan.kind === "reconcile" ? decisionPlan.archives : [];
84532
+ if (decisionsToPush.length || decisionsToPull.length) {
84533
+ lines.push(` ${import_picocolors54.default.bold("decisions")}`);
84534
+ for (const d3 of decisionsToPush) {
84535
+ lines.push(` ${import_picocolors54.default.yellow("→ push")} added/changed in yaml: ${decisionLabel(d3)}`);
84536
+ }
84537
+ for (const d3 of decisionsToPull) {
84538
+ lines.push(decisionDeletes.includes(d3.id) ? ` ${import_picocolors54.default.red("⨯ push deletes")} ${decisionLabel(d3)}` : ` ${import_picocolors54.default.cyan("← pull")} only on cloud: ${decisionLabel(d3)}`);
84539
+ }
84540
+ lines.push("");
84541
+ }
84542
+ if (decisionPlan.kind === "blocked") {
84543
+ lines.push(` ${import_picocolors54.default.bold(import_picocolors54.default.red("decisions: push blocked"))}`);
84544
+ for (const id of decisionPlan.unseen) {
84545
+ lines.push(` ${import_picocolors54.default.red("!")} ${label2(id)} ${import_picocolors54.default.dim("— only in the cloud; pushing would delete it")}`);
84546
+ }
84547
+ for (const id of decisionPlan.cloudModified) {
84548
+ lines.push(` ${import_picocolors54.default.red("!")} ${label2(id)} ${import_picocolors54.default.dim("— edited in the cloud; pushing would overwrite that edit")}`);
84549
+ }
84550
+ for (const id of decisionPlan.staleArchives) {
84551
+ lines.push(` ${import_picocolors54.default.red("!")} ${label2(id)} ${import_picocolors54.default.dim("— deleted here, edited in the cloud since; pushing would delete that edit")}`);
84552
+ }
84553
+ lines.push(` ${import_picocolors54.default.dim("run")} ${import_picocolors54.default.cyan("brainbase orchestration pull")} ${import_picocolors54.default.dim("first")}`);
84554
+ lines.push("");
84555
+ } else if (decisionPlan.kind === "skip") {
84556
+ const left = untouchedRows(decisionPlan);
84557
+ lines.push(` ${import_picocolors54.default.dim(`${left.length} cloud decision${left.length === 1 ? "" : "s"} not managed by this manifest — push leaves ${left.length === 1 ? "it" : "them"} untouched`)}`);
84558
+ lines.push("");
84559
+ }
84560
+ }
84118
84561
  const lockByAgentId = new Map((lock?.members ?? []).map((m3) => [m3.agent_id, m3]));
84119
84562
  const memberDrift = [];
84120
84563
  for (const m3 of cloud.members) {
@@ -84148,7 +84591,7 @@ async function runOrchestrationStatus(cwd2) {
84148
84591
  lines.push(` ${import_picocolors54.default.cyan("← pull")} cloud changed since last pull: ${import_picocolors54.default.dim(`lock ${lock.revision} → cloud ${cloud.revision}`)}`);
84149
84592
  lines.push("");
84150
84593
  }
84151
- if (!membersAdded.length && !membersRemoved.length && !edgesAdded.length && !edgesRemoved.length && !triggersToPush.length && !triggersToPull.length && !metaDecisions.length && !memberDrift.length && !revisionDrift) {
84594
+ if (!membersAdded.length && !membersRemoved.length && !edgesAdded.length && !edgesRemoved.length && !triggersToPush.length && !triggersToPull.length && !decisionsToPush.length && !decisionsToPull.length && !metaDecisions.length && !memberDrift.length && !revisionDrift) {
84152
84595
  lines.push(` ${import_picocolors54.default.green("✓")} everything is in sync`);
84153
84596
  lines.push("");
84154
84597
  console.log(lines.join(`
@@ -84503,6 +84946,10 @@ async function runOrchestrationCreate(cwd2, args) {
84503
84946
  payload_schema: e2.payload_schema ?? {}
84504
84947
  }))
84505
84948
  });
84949
+ const decisionCount = manifest.decisions?.length ?? 0;
84950
+ if (decisionCount) {
84951
+ f2.info(`Run ${import_picocolors57.default.cyan("brainbase orchestration push")} to add the ${decisionCount} decision${decisionCount === 1 ? "" : "s"} in this manifest.`);
84952
+ }
84506
84953
  $e(`Created ${created.name} at revision ${created.revision}.`);
84507
84954
  } catch (err) {
84508
84955
  sp.stop("Failed.");
@@ -97544,6 +97991,806 @@ async function runBenchmark(sub, args, write3 = (value2) => process.stdout.write
97544
97991
  }
97545
97992
  }
97546
97993
 
97994
+ // src/cli/decide.ts
97995
+ import fs95 from "node:fs";
97996
+ import path107 from "node:path";
97997
+
97998
+ // src/core/decide-api.ts
97999
+ import path105 from "node:path";
98000
+ function decideKeysFile() {
98001
+ return path105.join(process.env.BRAINBASE_HOME ?? BRAINBASE_HOME, "decide-keys.json");
98002
+ }
98003
+ var ATTEMPTS = 3;
98004
+ var RETRY_STATUSES = new Set([429, 502, 503, 504]);
98005
+ var MAX_RETRY_DELAY_MS = 30000;
98006
+ var REQUEST_TIMEOUT_MS3 = 5 * 60000;
98007
+
98008
+ class DecideHttpError extends Error {
98009
+ status;
98010
+ constructor(message3, status) {
98011
+ super(message3);
98012
+ this.status = status;
98013
+ this.name = "DecideHttpError";
98014
+ }
98015
+ }
98016
+ function cacheKey(base2, agentId) {
98017
+ return `${base2}|${agentId}`;
98018
+ }
98019
+ async function resolveDecideCredential(cwd2, agentFlag) {
98020
+ const base2 = proxyBaseUrl(authStatus().session ?? null);
98021
+ const taskKey = process.env.BB_TASK_KEY?.trim();
98022
+ if (taskKey)
98023
+ return { key: taskKey, base: base2, source: "task_key" };
98024
+ const agentId = agentFlag?.trim() || readLink(cwd2)?.agent_id;
98025
+ if (!agentId) {
98026
+ throw new DecideHttpError("No agent to bill. Run inside a linked folder, or pass --agent <id> (see `brainbase agent list`).", null);
98027
+ }
98028
+ const cached2 = readJsonOr(decideKeysFile(), {})[cacheKey(base2, agentId)];
98029
+ if (cached2?.key)
98030
+ return { key: cached2.key, base: base2, source: "agent_key", agentId };
98031
+ return mintAgentKey(base2, agentId);
98032
+ }
98033
+ async function mintAgentKey(base2, agentId) {
98034
+ const created = await api.createCliKey(agentId, { name: "brainbase decide" });
98035
+ const cache3 = readJsonOr(decideKeysFile(), {});
98036
+ cache3[cacheKey(base2, agentId)] = { key: created.key, key_id: created.id };
98037
+ writeJsonAtomic(decideKeysFile(), cache3);
98038
+ return { key: created.key, base: base2, source: "agent_key", agentId };
98039
+ }
98040
+ async function replaceAgentKey(credential) {
98041
+ if (credential.source !== "agent_key" || !credential.agentId)
98042
+ return credential;
98043
+ const cache3 = readJsonOr(decideKeysFile(), {});
98044
+ delete cache3[cacheKey(credential.base, credential.agentId)];
98045
+ writeJsonAtomic(decideKeysFile(), cache3);
98046
+ return mintAgentKey(credential.base, credential.agentId);
98047
+ }
98048
+ function retryDelayMs(attempt2, retryAfter) {
98049
+ const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
98050
+ const base2 = Number.isFinite(seconds) ? seconds * 1000 : 1000 * 2 ** attempt2;
98051
+ return Math.min(Math.max(base2, 0), MAX_RETRY_DELAY_MS) + Math.random() * 500;
98052
+ }
98053
+ function errorMessage2(status, text4) {
98054
+ try {
98055
+ const body = JSON.parse(text4);
98056
+ const message3 = body.error?.message ?? body.detail;
98057
+ if (typeof message3 === "string" && message3)
98058
+ return message3;
98059
+ } catch {}
98060
+ return `HTTP ${status}`;
98061
+ }
98062
+ async function postDecide(credential, body, options = {}) {
98063
+ const sleep3 = options.sleep ?? ((ms2) => new Promise((resolve) => setTimeout(resolve, ms2)));
98064
+ const payload = JSON.stringify(body);
98065
+ let failure = new DecideHttpError("The decide request failed", null);
98066
+ for (let attempt2 = 0;attempt2 < ATTEMPTS; attempt2 += 1) {
98067
+ let retryAfter = null;
98068
+ try {
98069
+ const response = await fetch(`${credential.base}/v1/decide`, {
98070
+ method: "POST",
98071
+ headers: {
98072
+ Authorization: `Bearer ${credential.key}`,
98073
+ "Content-Type": "application/json"
98074
+ },
98075
+ body: payload,
98076
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
98077
+ });
98078
+ const text4 = await response.text();
98079
+ if (response.ok) {
98080
+ try {
98081
+ return JSON.parse(text4);
98082
+ } catch {
98083
+ throw new DecideHttpError("The server sent an unreadable response", response.status);
98084
+ }
98085
+ }
98086
+ failure = new DecideHttpError(errorMessage2(response.status, text4), response.status);
98087
+ if (!RETRY_STATUSES.has(response.status))
98088
+ throw failure;
98089
+ retryAfter = response.headers.get("retry-after");
98090
+ } catch (error2) {
98091
+ if (error2 instanceof DecideHttpError)
98092
+ throw error2;
98093
+ failure = new DecideHttpError(`Network error: ${error2.message}`, null);
98094
+ }
98095
+ if (attempt2 + 1 < ATTEMPTS)
98096
+ await sleep3(retryDelayMs(attempt2, retryAfter));
98097
+ }
98098
+ throw failure;
98099
+ }
98100
+
98101
+ // src/core/decide-items.ts
98102
+ import fs94 from "node:fs";
98103
+ import path106 from "node:path";
98104
+
98105
+ class DecideInputError extends Error {
98106
+ constructor(message3) {
98107
+ super(message3);
98108
+ this.name = "DecideInputError";
98109
+ }
98110
+ }
98111
+ var GLOB_CHARS = /[*?[{]/;
98112
+ function fileId(cwd2, file) {
98113
+ return path106.relative(cwd2, file).split(path106.sep).join("/") || path106.basename(file);
98114
+ }
98115
+ function isHidden2(name) {
98116
+ return name.startsWith(".") || name === "node_modules";
98117
+ }
98118
+ function appendAll(into, from) {
98119
+ for (const value2 of from)
98120
+ into.push(value2);
98121
+ }
98122
+ function walk(dir, out = []) {
98123
+ for (const entry of fs94.readdirSync(dir, { withFileTypes: true })) {
98124
+ if (isHidden2(entry.name))
98125
+ continue;
98126
+ const full = path106.join(dir, entry.name);
98127
+ if (entry.isDirectory())
98128
+ walk(full, out);
98129
+ else if (entry.isFile())
98130
+ out.push(full);
98131
+ else if (entry.isSymbolicLink()) {
98132
+ try {
98133
+ if (fs94.statSync(full).isFile())
98134
+ out.push(full);
98135
+ } catch {}
98136
+ }
98137
+ }
98138
+ return out;
98139
+ }
98140
+ function walkSorted(dir) {
98141
+ return walk(dir).sort();
98142
+ }
98143
+ function globToRegExp(pattern2) {
98144
+ let source = "";
98145
+ for (let i = 0;i < pattern2.length; i += 1) {
98146
+ const char = pattern2[i];
98147
+ if (char === "*") {
98148
+ if (pattern2[i + 1] === "*") {
98149
+ const slashFollows = pattern2[i + 2] === "/";
98150
+ source += slashFollows ? "(?:.*/)?" : ".*";
98151
+ i += slashFollows ? 2 : 1;
98152
+ } else {
98153
+ source += "[^/]*";
98154
+ }
98155
+ } else if (char === "?") {
98156
+ source += "[^/]";
98157
+ } else if (char === "{") {
98158
+ const close = pattern2.indexOf("}", i);
98159
+ if (close === -1) {
98160
+ source += "\\{";
98161
+ continue;
98162
+ }
98163
+ const choices = pattern2.slice(i + 1, close).split(",");
98164
+ source += `(?:${choices.map((choice) => globToRegExp(choice).source.slice(1, -1)).join("|")})`;
98165
+ i = close;
98166
+ } else if (char === "[") {
98167
+ const close = pattern2.indexOf("]", i + 1);
98168
+ if (close === -1) {
98169
+ source += "\\[";
98170
+ continue;
98171
+ }
98172
+ let body = pattern2.slice(i + 1, close);
98173
+ if (body.startsWith("!"))
98174
+ body = `^${body.slice(1)}`;
98175
+ source += `[${body.replace(/\\/g, "\\\\")}]`;
98176
+ i = close;
98177
+ } else {
98178
+ source += char.replace(/[.+^$()|\\/]/g, "\\$&");
98179
+ }
98180
+ }
98181
+ return new RegExp(`^${source}$`);
98182
+ }
98183
+ function expandGlob(cwd2, pattern2) {
98184
+ const absolute = path106.isAbsolute(pattern2);
98185
+ const segments = pattern2.split("/");
98186
+ const fixed = [];
98187
+ for (const segment of segments) {
98188
+ if (GLOB_CHARS.test(segment))
98189
+ break;
98190
+ fixed.push(segment);
98191
+ }
98192
+ if (fixed.length === segments.length) {
98193
+ return [path106.resolve(cwd2, pattern2)];
98194
+ }
98195
+ const root = path106.resolve(cwd2, fixed.join("/") || ".");
98196
+ if (!fs94.existsSync(root) || !fs94.statSync(root).isDirectory())
98197
+ return [];
98198
+ const matcher = globToRegExp(absolute ? pattern2 : path106.posix.normalize(segments.join("/")));
98199
+ return walkSorted(root).filter((file) => {
98200
+ const candidate = absolute ? file.split(path106.sep).join("/") : fileId(cwd2, file);
98201
+ return matcher.test(candidate);
98202
+ });
98203
+ }
98204
+ function readFileItem(cwd2, file) {
98205
+ const id = fileId(cwd2, file);
98206
+ let bytes;
98207
+ try {
98208
+ bytes = fs94.readFileSync(file);
98209
+ } catch (error2) {
98210
+ return { id, error: `Could not read the file: ${error2.message}` };
98211
+ }
98212
+ if (bytes.includes(0))
98213
+ return { id, error: "Not a text file" };
98214
+ let text4;
98215
+ try {
98216
+ text4 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
98217
+ } catch {
98218
+ return { id, error: "Not UTF-8 text" };
98219
+ }
98220
+ if (!text4.trim())
98221
+ return { id, error: "Empty file" };
98222
+ return { id, text: text4 };
98223
+ }
98224
+ function fileItems(cwd2, paths, globs) {
98225
+ const files = [];
98226
+ for (const entry of paths) {
98227
+ const full = path106.resolve(cwd2, entry);
98228
+ let stat;
98229
+ try {
98230
+ stat = fs94.statSync(full);
98231
+ } catch {
98232
+ throw new DecideInputError(`No such file or directory: ${entry}`);
98233
+ }
98234
+ if (stat.isDirectory())
98235
+ appendAll(files, walkSorted(full));
98236
+ else
98237
+ files.push(full);
98238
+ }
98239
+ for (const pattern2 of globs) {
98240
+ const matched = expandGlob(cwd2, pattern2);
98241
+ if (matched.length === 0 || !matched.some((file) => fs94.existsSync(file))) {
98242
+ throw new DecideInputError(`No files match ${pattern2}`);
98243
+ }
98244
+ appendAll(files, matched);
98245
+ }
98246
+ return [...new Set(files)];
98247
+ }
98248
+ function readFileItems(cwd2, files) {
98249
+ const set3 = { items: [], failures: [] };
98250
+ for (const file of files) {
98251
+ const item = readFileItem(cwd2, file);
98252
+ if ("error" in item)
98253
+ set3.failures.push(item);
98254
+ else
98255
+ set3.items.push(item);
98256
+ }
98257
+ return set3;
98258
+ }
98259
+ function isRecord2(value2) {
98260
+ return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
98261
+ }
98262
+ function rowItem(row, options, fallbackId) {
98263
+ const idField = options.idField ?? "id";
98264
+ const rawId = isRecord2(row) ? row[idField] : undefined;
98265
+ let id = fallbackId;
98266
+ if (rawId !== undefined && rawId !== null && rawId !== "")
98267
+ id = String(rawId);
98268
+ else if (options.idField)
98269
+ return { id: fallbackId, error: `Missing ${options.idField}` };
98270
+ if (options.textFields.length === 0)
98271
+ return { id, json: row };
98272
+ if (!isRecord2(row))
98273
+ return { id, error: "Not an object, so --text-field has nothing to select" };
98274
+ if (options.textFields.length === 1) {
98275
+ const field2 = options.textFields[0];
98276
+ const value2 = row[field2];
98277
+ if (value2 === undefined || value2 === null || value2 === "")
98278
+ return { id, error: `Empty ${field2}` };
98279
+ return { id, text: typeof value2 === "string" ? value2 : JSON.stringify(value2) };
98280
+ }
98281
+ const missing = options.textFields.filter((field2) => row[field2] === undefined);
98282
+ if (missing.length)
98283
+ return { id, error: `Missing ${missing.join(", ")}` };
98284
+ const picked = {};
98285
+ for (const field2 of options.textFields)
98286
+ picked[field2] = row[field2];
98287
+ return { id, json: picked };
98288
+ }
98289
+ function sourceLabel(cwd2, file) {
98290
+ return fileId(cwd2, path106.resolve(cwd2, file));
98291
+ }
98292
+ function readSource(cwd2, file) {
98293
+ try {
98294
+ return fs94.readFileSync(path106.resolve(cwd2, file), "utf8");
98295
+ } catch (error2) {
98296
+ throw new DecideInputError(`Could not read ${file}: ${error2.message}`);
98297
+ }
98298
+ }
98299
+ function jsonlItems(cwd2, file, options) {
98300
+ const set3 = { items: [], failures: [] };
98301
+ const label3 = sourceLabel(cwd2, file);
98302
+ readSource(cwd2, file).split(`
98303
+ `).forEach((line3, index) => {
98304
+ if (!line3.trim())
98305
+ return;
98306
+ const fallbackId = `${label3}:${index + 1}`;
98307
+ let row;
98308
+ try {
98309
+ row = JSON.parse(line3);
98310
+ } catch {
98311
+ set3.failures.push({ id: fallbackId, error: "Not valid JSON" });
98312
+ return;
98313
+ }
98314
+ const item = rowItem(row, options, fallbackId);
98315
+ if ("error" in item)
98316
+ set3.failures.push(item);
98317
+ else
98318
+ set3.items.push(item);
98319
+ });
98320
+ return set3;
98321
+ }
98322
+ function parseCsv(text4) {
98323
+ const rows = [];
98324
+ let row = [];
98325
+ let field2 = "";
98326
+ let quoted = false;
98327
+ for (let i = 0;i < text4.length; i += 1) {
98328
+ const char = text4[i];
98329
+ if (quoted) {
98330
+ if (char === '"' && text4[i + 1] === '"') {
98331
+ field2 += '"';
98332
+ i += 1;
98333
+ } else if (char === '"') {
98334
+ quoted = false;
98335
+ } else {
98336
+ field2 += char;
98337
+ }
98338
+ } else if (char === '"' && field2 === "") {
98339
+ quoted = true;
98340
+ } else if (char === ",") {
98341
+ row.push(field2);
98342
+ field2 = "";
98343
+ } else if (char === `
98344
+ ` || char === "\r") {
98345
+ if (char === "\r" && text4[i + 1] === `
98346
+ `)
98347
+ i += 1;
98348
+ row.push(field2);
98349
+ rows.push(row);
98350
+ row = [];
98351
+ field2 = "";
98352
+ } else {
98353
+ field2 += char;
98354
+ }
98355
+ }
98356
+ if (field2 !== "" || row.length > 0) {
98357
+ row.push(field2);
98358
+ rows.push(row);
98359
+ }
98360
+ return rows.filter((cells) => cells.some((cell) => cell !== ""));
98361
+ }
98362
+ function csvItems(cwd2, file, options) {
98363
+ const set3 = { items: [], failures: [] };
98364
+ const label3 = sourceLabel(cwd2, file);
98365
+ const [header, ...rows] = parseCsv(readSource(cwd2, file).replace(/^/, ""));
98366
+ if (!header)
98367
+ return set3;
98368
+ rows.forEach((cells, index) => {
98369
+ const row = {};
98370
+ header.forEach((name, column) => {
98371
+ row[name] = cells[column] ?? "";
98372
+ });
98373
+ const item = rowItem(row, options, `${label3}:${index + 2}`);
98374
+ if ("error" in item)
98375
+ set3.failures.push(item);
98376
+ else
98377
+ set3.items.push(item);
98378
+ });
98379
+ return set3;
98380
+ }
98381
+ function readResults(file) {
98382
+ const rows = new Map;
98383
+ if (!fs94.existsSync(file))
98384
+ return rows;
98385
+ for (const line3 of fs94.readFileSync(file, "utf8").split(`
98386
+ `)) {
98387
+ if (!line3.trim())
98388
+ continue;
98389
+ try {
98390
+ const row = JSON.parse(line3);
98391
+ if (row && typeof row.id === "string")
98392
+ rows.set(row.id, row);
98393
+ } catch {}
98394
+ }
98395
+ return rows;
98396
+ }
98397
+ function repairResultsTail(file) {
98398
+ if (!fs94.existsSync(file))
98399
+ return;
98400
+ const text4 = fs94.readFileSync(file, "utf8");
98401
+ if (!text4 || text4.endsWith(`
98402
+ `))
98403
+ return;
98404
+ const cut = text4.lastIndexOf(`
98405
+ `) + 1;
98406
+ const tail2 = text4.slice(cut);
98407
+ let complete = false;
98408
+ try {
98409
+ const row = JSON.parse(tail2);
98410
+ complete = Boolean(row) && typeof row.id === "string";
98411
+ } catch {
98412
+ complete = false;
98413
+ }
98414
+ if (complete)
98415
+ fs94.appendFileSync(file, `
98416
+ `);
98417
+ else
98418
+ fs94.truncateSync(file, Buffer.byteLength(text4.slice(0, cut)));
98419
+ }
98420
+ function parseWhere(value2) {
98421
+ if (value2 === "failed")
98422
+ return { kind: "failed" };
98423
+ if (value2 === "uncertain")
98424
+ return { kind: "uncertain" };
98425
+ const split2 = value2.indexOf("=");
98426
+ if (split2 > 0 && split2 < value2.length - 1) {
98427
+ return { kind: "answer", question: value2.slice(0, split2), answer: value2.slice(split2 + 1) };
98428
+ }
98429
+ throw new DecideInputError(`--where takes failed, uncertain or <question>=<answer>, not ${value2}`);
98430
+ }
98431
+ function rowMatches(row, where) {
98432
+ switch (where.kind) {
98433
+ case "failed":
98434
+ return row.error !== undefined;
98435
+ case "uncertain":
98436
+ return row.error === undefined && row.uncertain === true;
98437
+ case "answer":
98438
+ return row.error === undefined && row.answers?.[where.question]?.choice === where.answer;
98439
+ }
98440
+ }
98441
+
98442
+ // src/cli/decide.ts
98443
+ var DEFAULT_OUT = "decide-results.jsonl";
98444
+ var MAX_ITEMS_PER_REQUEST = 200;
98445
+ var MAX_REQUEST_BYTES = 6 * 1024 * 1024;
98446
+ var DEFAULT_CONCURRENCY = 4;
98447
+ var MAX_CONCURRENCY = 16;
98448
+ var SUMMARY_UNCERTAIN_IDS = 20;
98449
+ var SUMMARY_FAILED = 10;
98450
+ var STOP_STATUSES = new Set([401, 402, 403, 404]);
98451
+ var OPTIONS = [
98452
+ { key: "questions", names: ["--questions", "-q"], kind: "value" },
98453
+ { key: "items", names: ["--items"], kind: "value", multiple: true },
98454
+ { key: "jsonl", names: ["--jsonl"], kind: "value", multiple: true },
98455
+ { key: "csv", names: ["--csv"], kind: "value", multiple: true },
98456
+ { key: "idField", names: ["--id-field"], kind: "value" },
98457
+ { key: "textField", names: ["--text-field"], kind: "value", multiple: true },
98458
+ { key: "model", names: ["--model"], kind: "value" },
98459
+ { key: "minConfidence", names: ["--min-confidence"], kind: "value" },
98460
+ { key: "out", names: ["--out", "-o"], kind: "value" },
98461
+ { key: "resume", names: ["--resume"], kind: "boolean" },
98462
+ { key: "overwrite", names: ["--overwrite"], kind: "boolean" },
98463
+ { key: "selectFrom", names: ["--select-from"], kind: "value" },
98464
+ { key: "where", names: ["--where"], kind: "value" },
98465
+ { key: "agent", names: ["--agent"], kind: "value" },
98466
+ { key: "concurrency", names: ["--concurrency"], kind: "value" },
98467
+ { key: "help", names: ["--help", "-h"], kind: "boolean" }
98468
+ ];
98469
+ var DECIDE_USAGE = `Usage: brainbase decide --questions <file.json> <sources...> [options]
98470
+
98471
+ Answer the same decision-model questions over many items. Items are read on
98472
+ this machine and never pass through your agent's context.
98473
+
98474
+ Sources (at least one):
98475
+ <path>... files, or directories (every file under them)
98476
+ --items <glob> files matching a glob; quote it ('docs/**/*.md')
98477
+ --jsonl <file> one item per line
98478
+ --csv <file> one item per row; the first row names the fields
98479
+ --id-field <name> JSONL/CSV field holding each item's id
98480
+ (default: id when present, else file:line)
98481
+ --text-field <name> field to judge; repeat for several (a row
98482
+ missing any of them fails without being sent)
98483
+ (default: the whole row)
98484
+
98485
+ Questions:
98486
+ --questions, -q <file> JSON object of System One questions, e.g.
98487
+ {"team": {"type": "choice", "instructions": "Which team?",
98488
+ "criteria": {"billing": "...", "infra": "..."}}}
98489
+ --model <slug> jev, kev-4b, clef, clef-flash or gpt-6-luna
98490
+ (default: the server's default)
98491
+ --min-confidence <0-1> mark answers below this confidence as uncertain
98492
+
98493
+ Results:
98494
+ --out, -o <file> one JSON line per item (default: ${DEFAULT_OUT})
98495
+ --resume skip ids already answered in --out; append the rest
98496
+ --overwrite replace an existing --out
98497
+ --select-from <file> only items whose row in this earlier results file
98498
+ --where <filter> matches: failed, uncertain or <question>=<answer>
98499
+
98500
+ Other:
98501
+ --agent <id> agent to bill outside a Brainbase task
98502
+ (default: this folder's linked agent)
98503
+ --concurrency <n> requests in flight, 1-${MAX_CONCURRENCY} (default: ${DEFAULT_CONCURRENCY})
98504
+
98505
+ Prints one JSON summary on stdout. Exits 0 when every item was sent (check
98506
+ counts.failed), 1 when the run stopped early (rerun with --resume), 2 for a
98507
+ usage error.
98508
+ `;
98509
+ function usageError2(stderr, message3) {
98510
+ stderr(`brainbase decide: ${message3}
98511
+ Run \`brainbase decide --help\` for usage.
98512
+ `);
98513
+ return 2;
98514
+ }
98515
+ function readQuestions(cwd2, file) {
98516
+ let parsed;
98517
+ try {
98518
+ parsed = JSON.parse(fs95.readFileSync(path107.resolve(cwd2, file), "utf8"));
98519
+ } catch (error2) {
98520
+ throw new DecideInputError(`Could not read questions from ${file}: ${error2.message}`);
98521
+ }
98522
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !Object.keys(parsed).length) {
98523
+ throw new DecideInputError(`${file} must hold a JSON object of questions`);
98524
+ }
98525
+ return parsed;
98526
+ }
98527
+ function mergeSets(sets) {
98528
+ const merged = { items: [], failures: [] };
98529
+ for (const set3 of sets) {
98530
+ appendAll(merged.items, set3.items);
98531
+ appendAll(merged.failures, set3.failures);
98532
+ }
98533
+ const seen = new Set;
98534
+ for (const { id } of [...merged.items, ...merged.failures]) {
98535
+ if (seen.has(id)) {
98536
+ throw new DecideInputError(`Two items share the id ${JSON.stringify(id)}; pass --id-field naming a unique field`);
98537
+ }
98538
+ seen.add(id);
98539
+ }
98540
+ return merged;
98541
+ }
98542
+ function chunk2(items, failures) {
98543
+ const chunks = [];
98544
+ let current = [];
98545
+ let bytes = 0;
98546
+ for (const item of items) {
98547
+ const size2 = Buffer.byteLength(JSON.stringify(item));
98548
+ if (size2 > MAX_REQUEST_BYTES) {
98549
+ failures.push({ id: item.id, error: "Larger than one request can carry (6 MB)" });
98550
+ continue;
98551
+ }
98552
+ if (current.length === MAX_ITEMS_PER_REQUEST || bytes + size2 > MAX_REQUEST_BYTES) {
98553
+ chunks.push(current);
98554
+ current = [];
98555
+ bytes = 0;
98556
+ }
98557
+ current.push(item);
98558
+ bytes += size2;
98559
+ }
98560
+ if (current.length)
98561
+ chunks.push(current);
98562
+ return chunks;
98563
+ }
98564
+
98565
+ class Tally {
98566
+ done = 0;
98567
+ uncertainIds = [];
98568
+ failed = [];
98569
+ answers = {};
98570
+ credits = 0;
98571
+ model = null;
98572
+ addRow(row) {
98573
+ if (row.error !== undefined) {
98574
+ this.failed.push({ id: row.id, reason: row.error });
98575
+ return;
98576
+ }
98577
+ this.done += 1;
98578
+ if (row.uncertain)
98579
+ this.uncertainIds.push(row.id);
98580
+ }
98581
+ addResponse(response) {
98582
+ this.model ??= response.model;
98583
+ this.credits += response.credits;
98584
+ for (const [question, counts] of Object.entries(response.answers)) {
98585
+ const into = this.answers[question] ??= {};
98586
+ for (const [answer, count] of Object.entries(counts)) {
98587
+ into[answer] = (into[answer] ?? 0) + count;
98588
+ }
98589
+ }
98590
+ }
98591
+ }
98592
+ async function runDecide(cwd2, argv, deps = {}) {
98593
+ const stdout = deps.stdout ?? ((text4) => void process.stdout.write(text4));
98594
+ const stderr = deps.stderr ?? ((text4) => void process.stderr.write(text4));
98595
+ const started = Date.now();
98596
+ let parsed;
98597
+ try {
98598
+ parsed = parseArgv(argv, OPTIONS);
98599
+ } catch (error2) {
98600
+ if (error2 instanceof ArgvParseError)
98601
+ return usageError2(stderr, error2.message);
98602
+ throw error2;
98603
+ }
98604
+ if (booleanOption(parsed, "help")) {
98605
+ stdout(DECIDE_USAGE);
98606
+ return 0;
98607
+ }
98608
+ const questionsFile = stringOption(parsed, "questions");
98609
+ if (!questionsFile)
98610
+ return usageError2(stderr, "--questions <file.json> is required");
98611
+ const paths = parsed.positionals;
98612
+ const globs = stringOptions(parsed, "items");
98613
+ const jsonlFiles = stringOptions(parsed, "jsonl");
98614
+ const csvFiles = stringOptions(parsed, "csv");
98615
+ if (!paths.length && !globs.length && !jsonlFiles.length && !csvFiles.length) {
98616
+ return usageError2(stderr, "name at least one source: paths, --items, --jsonl or --csv");
98617
+ }
98618
+ const minConfidenceRaw = stringOption(parsed, "minConfidence");
98619
+ const minConfidence = minConfidenceRaw === undefined ? undefined : Number(minConfidenceRaw);
98620
+ if (minConfidence !== undefined && !(minConfidence >= 0 && minConfidence <= 1)) {
98621
+ return usageError2(stderr, "--min-confidence takes a number from 0 to 1");
98622
+ }
98623
+ const concurrencyRaw = stringOption(parsed, "concurrency");
98624
+ const concurrency = concurrencyRaw === undefined ? DEFAULT_CONCURRENCY : Number(concurrencyRaw);
98625
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
98626
+ return usageError2(stderr, `--concurrency takes a whole number from 1 to ${MAX_CONCURRENCY}`);
98627
+ }
98628
+ const selectFrom = stringOption(parsed, "selectFrom");
98629
+ const whereRaw = stringOption(parsed, "where");
98630
+ if (Boolean(selectFrom) !== Boolean(whereRaw)) {
98631
+ return usageError2(stderr, "--select-from and --where go together");
98632
+ }
98633
+ const resume = booleanOption(parsed, "resume");
98634
+ const overwrite = booleanOption(parsed, "overwrite");
98635
+ if (resume && overwrite)
98636
+ return usageError2(stderr, "--resume and --overwrite cannot be combined");
98637
+ const outArg = stringOption(parsed, "out") ?? DEFAULT_OUT;
98638
+ const outPath = path107.resolve(cwd2, outArg);
98639
+ let questions;
98640
+ let set3;
98641
+ let skipped = 0;
98642
+ try {
98643
+ questions = readQuestions(cwd2, questionsFile);
98644
+ const rowOptions = {
98645
+ idField: stringOption(parsed, "idField"),
98646
+ textFields: stringOptions(parsed, "textField")
98647
+ };
98648
+ set3 = mergeSets([
98649
+ readFileItems(cwd2, fileItems(cwd2, paths, globs)),
98650
+ ...jsonlFiles.map((file) => jsonlItems(cwd2, file, rowOptions)),
98651
+ ...csvFiles.map((file) => csvItems(cwd2, file, rowOptions))
98652
+ ]);
98653
+ if (selectFrom && whereRaw) {
98654
+ const where = parseWhere(whereRaw);
98655
+ const earlier = path107.resolve(cwd2, selectFrom);
98656
+ if (!fs95.existsSync(earlier))
98657
+ throw new DecideInputError(`No such results file: ${selectFrom}`);
98658
+ const rows = readResults(earlier);
98659
+ const keep = (id) => {
98660
+ const row = rows.get(id);
98661
+ return row !== undefined && rowMatches(row, where);
98662
+ };
98663
+ set3 = { items: set3.items.filter((item) => keep(item.id)), failures: set3.failures.filter((f4) => keep(f4.id)) };
98664
+ }
98665
+ const outHasRows = fs95.existsSync(outPath) && fs95.statSync(outPath).size > 0;
98666
+ if (outHasRows && !resume && !overwrite) {
98667
+ throw new DecideInputError(`${outArg} already has results; pass --resume to continue it or --overwrite to replace it`);
98668
+ }
98669
+ if (resume) {
98670
+ const answered = new Set([...readResults(outPath).values()].filter((row) => row.error === undefined).map((row) => row.id));
98671
+ const before2 = set3.items.length + set3.failures.length;
98672
+ set3 = {
98673
+ items: set3.items.filter((item) => !answered.has(item.id)),
98674
+ failures: set3.failures.filter((failure) => !answered.has(failure.id))
98675
+ };
98676
+ skipped = before2 - set3.items.length - set3.failures.length;
98677
+ }
98678
+ } catch (error2) {
98679
+ if (error2 instanceof DecideInputError)
98680
+ return usageError2(stderr, error2.message);
98681
+ throw error2;
98682
+ }
98683
+ fs95.mkdirSync(path107.dirname(outPath), { recursive: true });
98684
+ if (resume)
98685
+ repairResultsTail(outPath);
98686
+ else
98687
+ fs95.writeFileSync(outPath, "");
98688
+ const append = (rows) => {
98689
+ if (rows.length)
98690
+ fs95.appendFileSync(outPath, rows.map((row) => JSON.stringify(row)).join(`
98691
+ `) + `
98692
+ `);
98693
+ };
98694
+ const tally = new Tally;
98695
+ const localFailures = [...set3.failures];
98696
+ const chunks = chunk2(set3.items, localFailures);
98697
+ const localRows = localFailures.map((failure) => ({ id: failure.id, error: failure.error }));
98698
+ append(localRows);
98699
+ localRows.forEach((row) => tally.addRow(row));
98700
+ const total = set3.items.length + set3.failures.length;
98701
+ let stopError;
98702
+ let unsent = 0;
98703
+ let next = 0;
98704
+ if (chunks.length) {
98705
+ let credential;
98706
+ try {
98707
+ credential = await resolveDecideCredential(cwd2, stringOption(parsed, "agent"));
98708
+ } catch (error2) {
98709
+ stopError = error2.message;
98710
+ }
98711
+ if (credential) {
98712
+ const modelFlag = stringOption(parsed, "model");
98713
+ const body = (items) => ({
98714
+ questions,
98715
+ items,
98716
+ ...modelFlag ? { model: modelFlag } : {},
98717
+ ...minConfidence !== undefined ? { min_confidence: minConfidence } : {},
98718
+ output: "full"
98719
+ });
98720
+ const progress2 = process.stderr.isTTY && !deps.stderr;
98721
+ let answeredSoFar = localRows.length;
98722
+ let replacement;
98723
+ const send = async (items) => {
98724
+ const used = credential;
98725
+ try {
98726
+ return await postDecide(used, body(items), deps);
98727
+ } catch (error2) {
98728
+ if (!(error2 instanceof DecideHttpError && error2.status === 401 && used.source === "agent_key")) {
98729
+ throw error2;
98730
+ }
98731
+ replacement ??= replaceAgentKey(used).then((fresh2) => credential = fresh2);
98732
+ const fresh = await replacement;
98733
+ if (fresh.key === used.key)
98734
+ throw error2;
98735
+ return postDecide(fresh, body(items), deps);
98736
+ }
98737
+ };
98738
+ const worker = async () => {
98739
+ while (next < chunks.length && stopError === undefined) {
98740
+ const items = chunks[next++];
98741
+ let rows;
98742
+ try {
98743
+ const response = await send(items);
98744
+ tally.addResponse(response);
98745
+ rows = response.rows;
98746
+ } catch (error2) {
98747
+ const message3 = error2.message;
98748
+ const refusal = error2 instanceof DecideHttpError;
98749
+ if (!refusal || error2.status !== null && STOP_STATUSES.has(error2.status)) {
98750
+ stopError ??= refusal && error2.status === 404 ? `/v1/decide is not available on ${credential.base} (${message3})` : message3;
98751
+ unsent += items.length;
98752
+ continue;
98753
+ }
98754
+ rows = items.map((item) => ({ id: item.id, error: `Request failed: ${message3}` }));
98755
+ }
98756
+ append(rows);
98757
+ rows.forEach((row) => tally.addRow(row));
98758
+ answeredSoFar += rows.length;
98759
+ if (progress2)
98760
+ process.stderr.write(`\rdecided ${answeredSoFar}/${total}`);
98761
+ }
98762
+ };
98763
+ await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, worker));
98764
+ if (progress2)
98765
+ process.stderr.write(`
98766
+ `);
98767
+ }
98768
+ unsent += chunks.slice(next).reduce((count, items) => count + items.length, 0);
98769
+ }
98770
+ const summary = {
98771
+ status: stopError === undefined ? "done" : "stopped",
98772
+ model: tally.model ?? stringOption(parsed, "model") ?? null,
98773
+ counts: {
98774
+ total,
98775
+ done: tally.done,
98776
+ uncertain: tally.uncertainIds.length,
98777
+ failed: tally.failed.length,
98778
+ skipped,
98779
+ unsent
98780
+ },
98781
+ answers: tally.answers,
98782
+ uncertain_ids: tally.uncertainIds.slice(0, SUMMARY_UNCERTAIN_IDS),
98783
+ failed: tally.failed.slice(0, SUMMARY_FAILED),
98784
+ credits: tally.credits,
98785
+ elapsed_ms: Date.now() - started,
98786
+ out: path107.relative(cwd2, outPath) || outArg,
98787
+ ...stopError !== undefined ? { error: `${stopError}. Answers so far are in ${outArg}; rerun with --resume to continue.` } : {}
98788
+ };
98789
+ stdout(JSON.stringify(summary, null, 2) + `
98790
+ `);
98791
+ return stopError === undefined ? 0 : 1;
98792
+ }
98793
+
97547
98794
  // src/ui/console-streams.ts
97548
98795
  import { format as format3 } from "node:util";
97549
98796
  function routeConsoleThroughStreams() {
@@ -97601,6 +98848,7 @@ var COMMANDS3 = [
97601
98848
  "status",
97602
98849
  "mcp",
97603
98850
  "integrations",
98851
+ "decide",
97604
98852
  "run",
97605
98853
  "help",
97606
98854
  "version"
@@ -97728,6 +98976,10 @@ function help() {
97728
98976
  out.push(` ${import_picocolors81.default.cyan("token rename")} ${import_picocolors81.default.dim("<id>")} ${import_picocolors81.default.dim("relabel a token")}`);
97729
98977
  out.push(` ${import_picocolors81.default.cyan("token revoke")} ${import_picocolors81.default.dim("<id>")} ${import_picocolors81.default.dim("revoke a token")}`);
97730
98978
  out.push("");
98979
+ out.push(divider("DECISIONS"));
98980
+ out.push("");
98981
+ out.push(` ${import_picocolors81.default.cyan("decide")} ${import_picocolors81.default.dim("-q <questions.json> <sources...>")} ${import_picocolors81.default.dim("answer decision-model questions over many files or rows in a few requests (`decide --help`)")}`);
98982
+ out.push("");
97731
98983
  out.push(divider("MCP"));
97732
98984
  out.push("");
97733
98985
  out.push(` ${import_picocolors81.default.cyan("mcp check")} ${import_picocolors81.default.dim("[--json]")} ${import_picocolors81.default.dim("verify MCP server connectivity through the brainbase proxy (runs at sandbox bootstrap)")}`);
@@ -97988,7 +99240,7 @@ async function main() {
97988
99240
  const rawCwd = process14.cwd();
97989
99241
  const cwd2 = (() => {
97990
99242
  try {
97991
- return fs94.realpathSync(rawCwd);
99243
+ return fs96.realpathSync(rawCwd);
97992
99244
  } catch {
97993
99245
  return rawCwd;
97994
99246
  }
@@ -98005,7 +99257,7 @@ async function main() {
98005
99257
  await runRun(cwd2, argv);
98006
99258
  return;
98007
99259
  }
98008
- const ownsArgs = new Set(["task", "benchmark", "interface", "interfaces"]);
99260
+ const ownsArgs = new Set(["task", "benchmark", "interface", "interfaces", "decide"]);
98009
99261
  if (cmd === "agent")
98010
99262
  argv.splice(0, argv.length, ...routeAgentSurfaceArgs(argv));
98011
99263
  const sharedArgs = ownsArgs.has(cmd) || cmd === "agent" && ["versions", "surface", "connect", "disconnect", "connections"].includes(argv[0] ?? "") ? [] : argv;
@@ -98242,6 +99494,10 @@ async function main() {
98242
99494
  process14.exitCode = code;
98243
99495
  break;
98244
99496
  }
99497
+ case "decide": {
99498
+ process14.exitCode = await runDecide(cwd2, argv);
99499
+ break;
99500
+ }
98245
99501
  case "integration":
98246
99502
  case "integrations": {
98247
99503
  const sub = argv.shift();