@wrongstack/core 0.299.0 → 0.300.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 (50) hide show
  1. package/dist/coordination/director.d.ts +8 -0
  2. package/dist/coordination/fleet-manager.d.ts +48 -3
  3. package/dist/coordination/ifleet-manager.d.ts +2 -0
  4. package/dist/coordination/index.js +120 -20
  5. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  6. package/dist/core/fallback-model.d.ts +48 -0
  7. package/dist/core/index.d.ts +3 -2
  8. package/dist/core/index.js +226 -26
  9. package/dist/core/instruction-template.d.ts +80 -0
  10. package/dist/core/system-prompt-blocks.d.ts +10 -1
  11. package/dist/core/system-prompt-builder.d.ts +35 -1
  12. package/dist/defaults/index.js +238 -99
  13. package/dist/execution/autonomy-brain.d.ts +7 -0
  14. package/dist/execution/council-brain.d.ts +11 -0
  15. package/dist/execution/council-orchestrator.d.ts +23 -4
  16. package/dist/execution/council-prompts.d.ts +12 -1
  17. package/dist/execution/index.js +355 -138
  18. package/dist/fleet-notifier.d.ts +9 -2
  19. package/dist/hooks/index.js +8 -4
  20. package/dist/hq/index.js +18 -4
  21. package/dist/hq/protocol/fleet.d.ts +20 -0
  22. package/dist/hq/protocol.js +10 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1512 -707
  25. package/dist/kernel/events/brain-events.d.ts +9 -0
  26. package/dist/kernel/events/provider-events.d.ts +42 -1
  27. package/dist/models/index.js +1 -1
  28. package/dist/plugin/api.d.ts +6 -0
  29. package/dist/plugin/config.d.ts +55 -0
  30. package/dist/plugin/index.d.ts +1 -1
  31. package/dist/plugin/index.js +134 -21
  32. package/dist/security/index.d.ts +1 -1
  33. package/dist/security/index.js +157 -42
  34. package/dist/security/permission-helpers.d.ts +23 -6
  35. package/dist/security/permission-policy.d.ts +16 -0
  36. package/dist/security/totp.d.ts +14 -0
  37. package/dist/storage/director-state.d.ts +7 -0
  38. package/dist/storage/index.js +33 -8
  39. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  40. package/dist/tools/index.js +388 -102
  41. package/dist/types/council.d.ts +11 -0
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/multi-agent.d.ts +10 -0
  44. package/dist/types/one-shot-llm.d.ts +9 -0
  45. package/dist/types/plugin.d.ts +28 -0
  46. package/dist/worktree/index.js +4 -4
  47. package/instructions/system-lite.md +81 -3
  48. package/instructions/system-pro.md +275 -90
  49. package/instructions/system.md +228 -81
  50. package/package.json +3 -3
@@ -472,22 +472,29 @@ function buildCouncilJudgeUserPrompt(question, votes, opts = {}) {
472
472
  "</council-ballots>"
473
473
  ].filter(Boolean).join("\n\n");
474
474
  }
475
- function normalizeOptions(options) {
476
- if (!options) return [];
475
+ function validateCouncilOptions(options) {
476
+ const errors = [];
477
477
  const seen = /* @__PURE__ */ new Set();
478
- return options.map((option) => {
478
+ for (const option of options ?? []) {
479
479
  const id = option.id.trim();
480
- const label = option.label.trim();
481
- if (!id) throw new Error("buildCouncilQuestionPrompt: option id must not be empty.");
482
- if (!label) throw new Error(`buildCouncilQuestionPrompt: option "${id}" needs a label.`);
483
- if (seen.has(id)) throw new Error(`buildCouncilQuestionPrompt: duplicate option id "${id}".`);
480
+ if (!id) errors.push("Every option must have a non-empty `id`.");
481
+ if (!option.label.trim()) errors.push(`Option "${id || "<empty>"}" must have a label.`);
482
+ if (seen.has(id)) errors.push(`Duplicate option id "${id}".`);
484
483
  seen.add(id);
485
- return {
486
- id,
487
- label,
488
- ...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
489
- };
490
- });
484
+ }
485
+ return errors;
486
+ }
487
+ function normalizeOptions(options) {
488
+ if (!options) return [];
489
+ const errors = validateCouncilOptions(options);
490
+ if (errors.length > 0) {
491
+ throw new Error(`buildCouncilQuestionPrompt: ${errors.join("; ")}`);
492
+ }
493
+ return options.map((option) => ({
494
+ id: option.id.trim(),
495
+ label: option.label.trim(),
496
+ ...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
497
+ }));
491
498
  }
492
499
  function requiredInstruction(path7) {
493
500
  const text = readBundledInstructionText(path7);
@@ -595,6 +602,8 @@ function requireFraction(value, label) {
595
602
  var COUNCIL_REFUSAL_OPTION_ID = "council_refuse";
596
603
  var DEFAULT_COUNCIL_MAX_CONCURRENCY = 3;
597
604
  var MAX_COUNCIL_CONCURRENCY = 8;
605
+ var OVERALL_TIMEOUT_REASON = "Council overall timeout exceeded.";
606
+ var CALL_CANCELLED_REASON = "Cancelled.";
598
607
  var CouncilOrchestrator = class {
599
608
  caller;
600
609
  personas;
@@ -605,6 +614,17 @@ var CouncilOrchestrator = class {
605
614
  fallbackProfileManager;
606
615
  seatCaller;
607
616
  judgeCaller;
617
+ /**
618
+ * Normalized ad-hoc profiles keyed by the caller's config object identity.
619
+ * The Brain adapter reuses ONE profile object for every decision, so this
620
+ * avoids re-validating + re-freezing it on every ask() without caching
621
+ * string-keyed registry lookups (those are already O(1)).
622
+ *
623
+ * Hosts must treat ad-hoc profile configs as IMMUTABLE once passed to
624
+ * ask(): the cache is keyed by object identity and never invalidated, so
625
+ * mutating a cached profile would silently serve the first snapshot.
626
+ */
627
+ profileCache = /* @__PURE__ */ new WeakMap();
608
628
  constructor(opts) {
609
629
  if (!opts.caller && !opts.seatCaller && !opts.judgeCaller) {
610
630
  throw new Error(
@@ -623,13 +643,33 @@ var CouncilOrchestrator = class {
623
643
  this.seatCaller = opts.seatCaller;
624
644
  this.judgeCaller = opts.judgeCaller;
625
645
  }
626
- async ask(question) {
627
- const startedAt = Date.now();
628
- const profile = resolveCouncilProfile(question.profile, {
646
+ /**
647
+ * Resolve the effective profile for a question. String ids and the default
648
+ * go through the registry (already O(1)); ad-hoc config objects are
649
+ * normalized once per stable object identity and cached, because hosts such
650
+ * as the Brain adapter pass the same profile object on every ask().
651
+ */
652
+ resolveProfile(profile) {
653
+ if (typeof profile === "string" || profile === void 0) {
654
+ return resolveCouncilProfile(profile, {
655
+ registry: this.profiles,
656
+ personas: this.personas,
657
+ defaultProfile: this.defaultProfile
658
+ });
659
+ }
660
+ const cached = this.profileCache.get(profile);
661
+ if (cached) return cached;
662
+ const resolved = resolveCouncilProfile(profile, {
629
663
  registry: this.profiles,
630
664
  personas: this.personas,
631
665
  defaultProfile: this.defaultProfile
632
666
  });
667
+ this.profileCache.set(profile, resolved);
668
+ return resolved;
669
+ }
670
+ async ask(question) {
671
+ const startedAt = Date.now();
672
+ const profile = this.resolveProfile(question.profile);
633
673
  validateRefusalCollision(question, this.refusalOptionId);
634
674
  const timeoutSignal = AbortSignal.timeout(profile.overallTimeoutMs);
635
675
  const signal = question.signal ? AbortSignal.any([question.signal, timeoutSignal]) : timeoutSignal;
@@ -646,11 +686,16 @@ var CouncilOrchestrator = class {
646
686
  try {
647
687
  return await this.callSeat(question, profile, seat, i, signal, usage);
648
688
  } catch (error) {
689
+ const timedOut = signal.aborted && !question.signal?.aborted;
649
690
  return {
650
691
  seatId: seat.id,
651
692
  persona: seat.persona,
652
- status: signal.aborted ? "cancelled" : "failed",
653
- error: errorMessage(error)
693
+ // Only the caller's own cancel is a "cancelled" vote; the overall
694
+ // budget expiring is a failure (timeout), matching the envelope.
695
+ status: question.signal?.aborted ? "cancelled" : "failed",
696
+ // Canonical text for aborted-by-budget or cancelled seats, so one
697
+ // event does not surface a different string per code path.
698
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : errorMessage(error)
654
699
  };
655
700
  }
656
701
  }
@@ -660,7 +705,7 @@ var CouncilOrchestrator = class {
660
705
  if (question.signal?.aborted) {
661
706
  return resultEnvelope({
662
707
  status: "cancelled",
663
- reason: "Council call cancelled.",
708
+ reason: CALL_CANCELLED_REASON,
664
709
  resolution: "none",
665
710
  votes,
666
711
  profile,
@@ -673,14 +718,17 @@ var CouncilOrchestrator = class {
673
718
  if (timeoutSignal.aborted) {
674
719
  return resultEnvelope({
675
720
  status: "failed",
676
- reason: "Council overall timeout exceeded.",
721
+ reason: OVERALL_TIMEOUT_REASON,
677
722
  resolution: "none",
678
723
  votes,
679
724
  profile,
680
725
  usage,
681
726
  startedAt,
682
727
  warnings,
683
- errors: [...errors, "Council overall timeout exceeded."]
728
+ // Seat-prefixed errors normally carry the canonical timeout text, but
729
+ // a signal-blind caller can resolve valid votes even after the budget
730
+ // expired — append the standalone entry only when nothing carries it.
731
+ errors: errors.some((entry) => entry.includes(OVERALL_TIMEOUT_REASON)) ? errors : [...errors, OVERALL_TIMEOUT_REASON]
684
732
  });
685
733
  }
686
734
  if (!question.options || question.options.length === 0) {
@@ -707,7 +755,17 @@ var CouncilOrchestrator = class {
707
755
  );
708
756
  }
709
757
  async callSeat(question, profile, seat, seatIndex, signal, usage) {
710
- if (signal.aborted) return cancelledVote(seat);
758
+ if (signal.aborted) {
759
+ return question.signal?.aborted ? cancelledVote(seat) : {
760
+ seatId: seat.id,
761
+ persona: seat.persona,
762
+ status: "failed",
763
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
764
+ ...seat.target?.model ? { model: seat.target.model } : {},
765
+ durationMs: 0,
766
+ error: OVERALL_TIMEOUT_REASON
767
+ };
768
+ }
711
769
  let persona;
712
770
  try {
713
771
  persona = this.personas.require(seat.persona);
@@ -733,12 +791,15 @@ var CouncilOrchestrator = class {
733
791
  });
734
792
  const metadata = callMetadata(result);
735
793
  if (result.error) {
794
+ const timedOut = signal.aborted && !question.signal?.aborted;
736
795
  return {
737
796
  seatId: seat.id,
738
797
  persona: seat.persona,
739
- status: signal.aborted ? "cancelled" : "failed",
798
+ status: question.signal?.aborted ? "cancelled" : "failed",
740
799
  ...metadata,
741
- error: result.error
800
+ // Canonical text for cancelled or aborted-by-budget seats, so one
801
+ // cancel/timeout event does not surface a raw provider string.
802
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : result.error
742
803
  };
743
804
  }
744
805
  const parsed = parseVote(result.text, question, this.refusalOptionId);
@@ -837,9 +898,27 @@ var CouncilOrchestrator = class {
837
898
  signal,
838
899
  usage
839
900
  );
901
+ if (signal.aborted) {
902
+ const cancelled = question.signal?.aborted === true;
903
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
904
+ return resultEnvelope({
905
+ status: cancelled ? "cancelled" : "failed",
906
+ reason,
907
+ resolution: "none",
908
+ votes,
909
+ profile,
910
+ usage,
911
+ startedAt,
912
+ warnings,
913
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
914
+ judgeUsed: true
915
+ });
916
+ }
840
917
  if (!judged.ok) {
841
918
  return resultEnvelope({
842
- status: signal.aborted ? "cancelled" : "abstained",
919
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
920
+ // otherwise the judge simply failed -> abstained (can't decide).
921
+ status: question.signal?.aborted ? "cancelled" : signal.aborted ? "failed" : "abstained",
843
922
  reason: judged.error,
844
923
  resolution: "none",
845
924
  votes,
@@ -899,6 +978,19 @@ var CouncilOrchestrator = class {
899
978
  });
900
979
  }
901
980
  if (!profile.judge) {
981
+ if (divergentStances(valid)) {
982
+ return resultEnvelope({
983
+ status: "abstained",
984
+ reason: "Council produced multiple distinct stances and has no judge to reconcile them.",
985
+ resolution: "none",
986
+ votes,
987
+ profile,
988
+ usage,
989
+ startedAt,
990
+ warnings,
991
+ errors
992
+ });
993
+ }
902
994
  const first = valid[0];
903
995
  if (!first) {
904
996
  return resultEnvelope({
@@ -935,9 +1027,30 @@ var CouncilOrchestrator = class {
935
1027
  signal,
936
1028
  usage
937
1029
  );
1030
+ if (signal.aborted) {
1031
+ const cancelled = question.signal?.aborted === true;
1032
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
1033
+ return resultEnvelope({
1034
+ status: cancelled ? "cancelled" : "failed",
1035
+ reason,
1036
+ resolution: "none",
1037
+ votes,
1038
+ profile,
1039
+ usage,
1040
+ startedAt,
1041
+ warnings,
1042
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
1043
+ judgeUsed: true
1044
+ });
1045
+ }
938
1046
  if (!judged.ok) {
939
1047
  return resultEnvelope({
940
- status: signal.aborted ? "cancelled" : "failed",
1048
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
1049
+ // otherwise the judge simply failed -> failed (open questions cannot
1050
+ // abstain for a judge failure — 'abstained' is reserved for quorum
1051
+ // failure and stance divergence; the option path maps this same
1052
+ // plain-judge-failure case to 'abstained' instead).
1053
+ status: question.signal?.aborted ? "cancelled" : "failed",
941
1054
  reason: judged.error,
942
1055
  resolution: "none",
943
1056
  votes,
@@ -976,18 +1089,28 @@ var CouncilOrchestrator = class {
976
1089
  signal,
977
1090
  usage
978
1091
  });
979
- if (result.error) return { ok: false, error: result.error };
1092
+ if (result.error) {
1093
+ if (signal.aborted && !question.signal?.aborted) {
1094
+ return { ok: false, error: OVERALL_TIMEOUT_REASON };
1095
+ }
1096
+ if (question.signal?.aborted) {
1097
+ return { ok: false, error: CALL_CANCELLED_REASON };
1098
+ }
1099
+ return { ok: false, error: result.error };
1100
+ }
980
1101
  return parseJudge(result.text, question, this.refusalOptionId);
981
1102
  }
982
1103
  /**
983
- * Resolve the effective LLM caller for a call. Voter seats use
984
- * `seatCaller(seatIndex)` when wired. Judge seats (seatIndex undefined)
985
- * use `judgeCaller` if set, otherwise `seatCaller(0)` if set, otherwise
986
- * the shared `caller`.
1104
+ * Resolve the effective LLM caller for a call. Voter seats (defined
1105
+ * seatIndex) use `seatCaller(seatIndex)` when wired, otherwise the shared
1106
+ * `caller` a seat never falls through to the judge caller. Judge seats
1107
+ * (seatIndex undefined) use `judgeCaller` if set, otherwise `seatCaller(0)`
1108
+ * if set, otherwise the shared `caller`.
987
1109
  */
988
1110
  resolveCaller(seatIndex) {
989
- if (seatIndex !== void 0 && this.seatCaller) {
990
- return this.seatCaller(seatIndex);
1111
+ if (seatIndex !== void 0) {
1112
+ if (this.seatCaller) return this.seatCaller(seatIndex);
1113
+ return this.caller ?? this.judgeCaller;
991
1114
  }
992
1115
  if (this.judgeCaller) return this.judgeCaller;
993
1116
  if (this.seatCaller) return this.seatCaller(0);
@@ -996,6 +1119,7 @@ var CouncilOrchestrator = class {
996
1119
  async safeCall(input) {
997
1120
  const effectiveCaller = this.resolveCaller(input.seatIndex);
998
1121
  const resolvedTarget = this.resolveCouncilTarget(input.target);
1122
+ const startedAt = Date.now();
999
1123
  try {
1000
1124
  const result = await effectiveCaller.call({
1001
1125
  system: input.system,
@@ -1012,8 +1136,17 @@ var CouncilOrchestrator = class {
1012
1136
  addUsage(input.usage, result);
1013
1137
  return result;
1014
1138
  } catch (error) {
1015
- input.usage.calls += 1;
1016
- return emptyCallResult(errorMessage(error));
1139
+ const failed = {
1140
+ text: "",
1141
+ model: resolvedTarget?.model ?? "",
1142
+ provider: resolvedTarget?.providerId ?? "",
1143
+ tokens: { input: 0, output: 0, total: 0 },
1144
+ durationMs: Math.max(0, Date.now() - startedAt),
1145
+ fromFallback: false,
1146
+ error: errorMessage(error)
1147
+ };
1148
+ addUsage(input.usage, failed);
1149
+ return failed;
1017
1150
  }
1018
1151
  }
1019
1152
  /**
@@ -1045,47 +1178,77 @@ var CouncilOrchestrator = class {
1045
1178
  };
1046
1179
  }
1047
1180
  };
1048
- function parseVote(text, question, refusalOptionId) {
1181
+ function parseCouncilResponse(text, question, refusalOptionId, opts) {
1182
+ const roleLabel = opts.role === "judge" ? "Judge" : "Voter";
1049
1183
  const parsed = parseObject(text);
1050
1184
  if (!parsed.ok && (!question.options || question.options.length === 0)) {
1051
1185
  const fallback = text.trim();
1052
- if (fallback) return { ok: true, vote: { stance: fallback } };
1053
- return { ok: false, error: "Voter returned an empty response." };
1186
+ if (fallback) return { ok: true, value: { [opts.freeTextField]: fallback } };
1187
+ return { ok: false, error: `${roleLabel} returned an empty response.` };
1054
1188
  }
1055
1189
  if (!parsed.ok) return parsed;
1056
1190
  const rationale = optionalString(parsed.value["rationale"]);
1057
1191
  if (question.options && question.options.length > 0) {
1058
1192
  const optionId = optionalString(parsed.value["optionId"]);
1059
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
1193
+ const allowed = /* @__PURE__ */ new Set([
1194
+ ...question.options.map((option) => option.id.trim()),
1195
+ refusalOptionId
1196
+ ]);
1060
1197
  if (!optionId || !allowed.has(optionId)) {
1061
- return { ok: false, error: "Voter returned an unknown or missing optionId." };
1198
+ return { ok: false, error: `${roleLabel} returned an unknown or missing optionId.` };
1062
1199
  }
1063
- return { ok: true, vote: { optionId, ...rationale ? { rationale } : {} } };
1200
+ return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
1064
1201
  }
1065
- const stance = optionalString(parsed.value["stance"]);
1066
- if (!stance) return { ok: false, error: "Voter returned an empty or missing stance." };
1067
- return { ok: true, vote: { stance, ...rationale ? { rationale } : {} } };
1202
+ const freeText = optionalString(parsed.value[opts.freeTextField]);
1203
+ if (!freeText) {
1204
+ return {
1205
+ ok: false,
1206
+ error: `${roleLabel} returned an empty or missing ${opts.freeTextField}.`
1207
+ };
1208
+ }
1209
+ return { ok: true, value: { [opts.freeTextField]: freeText, ...rationale ? { rationale } : {} } };
1068
1210
  }
1069
- function parseJudge(text, question, refusalOptionId) {
1070
- const parsed = parseObject(text);
1071
- if (!parsed.ok && (!question.options || question.options.length === 0)) {
1072
- const fallback = text.trim();
1073
- if (fallback) return { ok: true, value: { answer: fallback } };
1074
- return { ok: false, error: "Judge returned an empty response." };
1211
+ function divergentStances(valid) {
1212
+ const seen = /* @__PURE__ */ new Set();
1213
+ for (const vote of valid) {
1214
+ const normalized = vote.stance.trim().replace(/^["'`]+|["'`]+$/g, "").toLowerCase().replace(/[.!?;:,]+$/g, "").replace(/\s+/g, " ").trim();
1215
+ if (!normalized) return true;
1216
+ seen.add(normalized);
1217
+ if (seen.size > 1) return true;
1075
1218
  }
1219
+ return false;
1220
+ }
1221
+ function parseVote(text, question, refusalOptionId) {
1222
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
1223
+ role: "voter",
1224
+ freeTextField: "stance"
1225
+ });
1076
1226
  if (!parsed.ok) return parsed;
1077
- const rationale = optionalString(parsed.value["rationale"]);
1078
- if (question.options && question.options.length > 0) {
1079
- const optionId = optionalString(parsed.value["optionId"]);
1080
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
1081
- if (!optionId || !allowed.has(optionId)) {
1082
- return { ok: false, error: "Judge returned an unknown or missing optionId." };
1227
+ const { optionId, stance, rationale } = parsed.value;
1228
+ return {
1229
+ ok: true,
1230
+ vote: {
1231
+ ...optionId ? { optionId } : {},
1232
+ ...stance ? { stance } : {},
1233
+ ...rationale ? { rationale } : {}
1083
1234
  }
1084
- return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
1085
- }
1086
- const answer = optionalString(parsed.value["answer"]);
1087
- if (!answer) return { ok: false, error: "Judge returned an empty or missing answer." };
1088
- return { ok: true, value: { answer, ...rationale ? { rationale } : {} } };
1235
+ };
1236
+ }
1237
+ function parseJudge(text, question, refusalOptionId) {
1238
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
1239
+ role: "judge",
1240
+ freeTextField: "answer"
1241
+ });
1242
+ if (!parsed.ok) return parsed;
1243
+ const { optionId, answer, rationale } = parsed.value;
1244
+ return {
1245
+ ok: true,
1246
+ value: {
1247
+ ...optionId ? { optionId } : {},
1248
+ ...answer ? { answer } : {},
1249
+ ...rationale ? { rationale } : {}
1250
+ }
1251
+ };
1089
1252
  }
1090
1253
  function parseObject(text) {
1091
1254
  const trimmed = text.trim();
@@ -1129,7 +1292,7 @@ function callMetadata(result) {
1129
1292
  };
1130
1293
  }
1131
1294
  function addUsage(usage, result) {
1132
- usage.calls += 1;
1295
+ usage.calls += Math.max(1, result.attempts ?? 1);
1133
1296
  usage.inputTokens += result.tokens.input;
1134
1297
  usage.outputTokens += result.tokens.output;
1135
1298
  usage.totalTokens += result.tokens.total;
@@ -1138,21 +1301,40 @@ function usageResult(usage, startedAt) {
1138
1301
  return Object.freeze({ ...usage, durationMs: Math.max(0, Date.now() - startedAt) });
1139
1302
  }
1140
1303
  function cancelledVote(seat) {
1141
- return { seatId: seat.id, persona: seat.persona, status: "cancelled", error: "Cancelled." };
1304
+ return {
1305
+ seatId: seat.id,
1306
+ persona: seat.persona,
1307
+ status: "cancelled",
1308
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
1309
+ ...seat.target?.model ? { model: seat.target.model } : {},
1310
+ durationMs: 0,
1311
+ error: CALL_CANCELLED_REASON
1312
+ };
1142
1313
  }
1143
1314
  function distinctTargetCount(votes, profile) {
1144
- const keys = votes.filter((vote) => vote.status === "valid").map(
1145
- (vote) => profile.distinctness === "provider" ? vote.provider : `${vote.provider ?? ""}/${vote.model ?? ""}`
1146
- ).filter(Boolean);
1147
- return new Set(keys).size;
1315
+ return new Set(distinctTargetKeys(votes, profile)).size;
1316
+ }
1317
+ function distinctTargetKeys(votes, profile) {
1318
+ const keys = [];
1319
+ for (const vote of votes) {
1320
+ if (vote.status !== "valid") continue;
1321
+ const provider = vote.provider?.trim() ?? "";
1322
+ const model = vote.model?.trim() ?? "";
1323
+ if (profile.distinctness === "provider") {
1324
+ if (provider) keys.push(provider);
1325
+ } else if (provider || model) {
1326
+ keys.push(`${provider}/${model}`);
1327
+ }
1328
+ }
1329
+ return keys;
1148
1330
  }
1149
1331
  function distinctnessWarnings(votes, profile) {
1150
1332
  if (profile.distinctness === "none") return [];
1151
- const valid = votes.filter((vote) => vote.status === "valid");
1152
- const distinct = distinctTargetCount(valid, profile);
1153
- if (valid.length > 1 && distinct < valid.length) {
1333
+ const keys = distinctTargetKeys(votes, profile);
1334
+ const distinct = new Set(keys).size;
1335
+ if (keys.length > 1 && distinct < keys.length) {
1154
1336
  return [
1155
- `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${valid.length} valid vote(s).`
1337
+ `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${keys.length} valid vote(s).`
1156
1338
  ];
1157
1339
  }
1158
1340
  return [];
@@ -1188,17 +1370,6 @@ async function mapConcurrent(items, concurrency, worker) {
1188
1370
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => run()));
1189
1371
  return results;
1190
1372
  }
1191
- function emptyCallResult(error) {
1192
- return {
1193
- text: "",
1194
- model: "",
1195
- provider: "",
1196
- tokens: { input: 0, output: 0, total: 0 },
1197
- durationMs: 0,
1198
- fromFallback: false,
1199
- error
1200
- };
1201
- }
1202
1373
  function optionalString(value) {
1203
1374
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1204
1375
  }
@@ -1297,14 +1468,7 @@ function validateCouncilToolInput(input, profileIds) {
1297
1468
  if ((input.options?.length ?? 0) > MAX_COUNCIL_TOOL_OPTIONS) {
1298
1469
  errors.push(`\`options\` must not contain more than ${MAX_COUNCIL_TOOL_OPTIONS} items.`);
1299
1470
  }
1300
- const ids = /* @__PURE__ */ new Set();
1301
- for (const option of input.options ?? []) {
1302
- const id = option.id.trim();
1303
- if (!id) errors.push("Every option must have a non-empty `id`.");
1304
- if (!option.label.trim()) errors.push(`Option "${id || "<empty>"}" must have a label.`);
1305
- if (ids.has(id)) errors.push(`Duplicate option id "${id}".`);
1306
- ids.add(id);
1307
- }
1471
+ errors.push(...validateCouncilOptions(input.options));
1308
1472
  return errors;
1309
1473
  }
1310
1474
 
@@ -6199,8 +6363,18 @@ var SYSTEM_CONFIG_VIEW_SCHEMA = {
6199
6363
  properties: {
6200
6364
  section: {
6201
6365
  type: "string",
6202
- enum: ["all", "providers", "models", "fallbacks", "matrix", "agents", "refiner", "doctor"],
6203
- description: "Which section to show: all (everything), providers (configured providers + keys), models (favorites + leader), fallbacks (chain + profiles + toggles), matrix (per-role assignments), agents (every catalog agent with resolved model), refiner (goal refinement config), doctor (validate config and show issues/warnings). Default: all."
6366
+ enum: [
6367
+ "all",
6368
+ "providers",
6369
+ "models",
6370
+ "fallbacks",
6371
+ "matrix",
6372
+ "agents",
6373
+ "fleet",
6374
+ "refiner",
6375
+ "doctor"
6376
+ ],
6377
+ description: "Which section to show: all (everything), providers (configured providers + keys), models (favorites + leader), fallbacks (chain + profiles + toggles), matrix (per-role assignments), agents (every catalog agent with resolved model), fleet (concurrency + lifetime spawn/token/cost budgets), refiner (goal refinement config), doctor (validate config and show issues/warnings). Default: all."
6204
6378
  }
6205
6379
  },
6206
6380
  additionalProperties: false
@@ -6302,6 +6476,7 @@ ${content}`);
6302
6476
  const profiles = config.fallbackProfiles ?? {};
6303
6477
  const chain = config.fallbackModels ?? [];
6304
6478
  const matrix = config.modelMatrix ?? {};
6479
+ const fleetBudget = config.fleet?.budget;
6305
6480
  const bridge = config.fallbackBridge?.trim();
6306
6481
  if (bridge) {
6307
6482
  const parsed = parseModelRef(bridge);
@@ -6371,6 +6546,26 @@ ${content}`);
6371
6546
  }
6372
6547
  }
6373
6548
  }
6549
+ if (typeof config.maxConcurrent === "number") {
6550
+ if (!Number.isFinite(config.maxConcurrent) || config.maxConcurrent < 0) {
6551
+ issues.push(`maxConcurrent must be a non-negative number (got ${config.maxConcurrent})`);
6552
+ } else if (config.maxConcurrent === 0) {
6553
+ warnings.push("maxConcurrent is 0 \u2014 subagent concurrency effectively disabled");
6554
+ } else {
6555
+ ok.push(`maxConcurrent ${config.maxConcurrent}`);
6556
+ }
6557
+ }
6558
+ if (fleetBudget && typeof fleetBudget === "object") {
6559
+ for (const key of ["maxSpawns", "maxTokens", "maxCostUsd"]) {
6560
+ const v = fleetBudget[key];
6561
+ if (v === void 0) continue;
6562
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
6563
+ issues.push(`fleet.budget.${key} must be a non-negative number (got ${JSON.stringify(v)})`);
6564
+ } else {
6565
+ ok.push(`fleet.budget.${key} ${v}`);
6566
+ }
6567
+ }
6568
+ }
6374
6569
  for (const [key, entry] of Object.entries(matrix)) {
6375
6570
  const eProvider = entry.provider ?? config.provider;
6376
6571
  const eModel = entry.model;
@@ -6411,6 +6606,27 @@ ${content}`);
6411
6606
  }
6412
6607
  addSection("Configuration Doctor", lines.join("\n"));
6413
6608
  }
6609
+ if (section === "all" || section === "fleet") {
6610
+ const fleet = config.fleet;
6611
+ const budget = fleet?.budget;
6612
+ const lifecycle = fleet?.lifecycle;
6613
+ const fmt = (n, unit = "") => typeof n === "number" && Number.isFinite(n) ? `${n}${unit}` : "(default)";
6614
+ addSection(
6615
+ "Fleet Budgets (configured ceilings)",
6616
+ [
6617
+ ` maxConcurrent: ${typeof config.maxConcurrent === "number" ? config.maxConcurrent : "(default 4)"}`,
6618
+ ` fleet.budget.maxSpawns: ${fmt(budget?.maxSpawns)} ${typeof budget?.maxSpawns !== "number" ? "\u2192 default 64" : ""}`,
6619
+ ` fleet.budget.maxTokens: ${fmt(budget?.maxTokens)}`,
6620
+ ` fleet.budget.maxCostUsd: ${fmt(budget?.maxCostUsd)}`,
6621
+ ` fleet.lifecycle.idleTimeoutMs: ${fmt(lifecycle?.idleTimeoutMs, "ms")}`,
6622
+ ` fleet.lifecycle.retireOnTaskComplete: ${lifecycle?.retireOnTaskComplete === void 0 ? "(default true)" : String(lifecycle.retireOnTaskComplete)}`,
6623
+ "",
6624
+ " Live used/remaining spawns are on /fleet status (not static config).",
6625
+ " Override ceilings: --max-concurrent / WRONGSTACK_MAX_CONCURRENT,",
6626
+ " --max-spawns / WRONGSTACK_MAX_SPAWNS, or fleet.budget.maxSpawns in profile."
6627
+ ].join("\n")
6628
+ );
6629
+ }
6414
6630
  if (section === "all" || section === "refiner") {
6415
6631
  const ref = config.autonomy;
6416
6632
  addSection(
@@ -7732,6 +7948,7 @@ var OneShotOrchestrator = class {
7732
7948
  tokens: { input: 0, output: 0, total: 0 },
7733
7949
  durationMs: Math.round(performance.now() - startedAt),
7734
7950
  fromFallback: false,
7951
+ attempts: 0,
7735
7952
  error: "No provider or model could be resolved. Check your config."
7736
7953
  };
7737
7954
  }
@@ -7746,6 +7963,7 @@ var OneShotOrchestrator = class {
7746
7963
  tokens: { input: 0, output: 0, total: 0 },
7747
7964
  durationMs: Math.round(performance.now() - startedAt),
7748
7965
  fromFallback: false,
7966
+ attempts: 0,
7749
7967
  error: `Cannot build provider "${target.providerId}": ${err instanceof Error ? err.message : String(err)}`
7750
7968
  };
7751
7969
  }
@@ -7758,11 +7976,13 @@ var OneShotOrchestrator = class {
7758
7976
  let fromFallback = false;
7759
7977
  let lastError;
7760
7978
  let fallbackEligible = false;
7979
+ let attempts = 0;
7761
7980
  if (tracker && !tracker.isAvailable(target.providerId, target.model) || !evaluateModelCalendar(config.modelAvailabilitySchedule, target.providerId, target.model).allowed) {
7762
7981
  this.opts.logger?.debug(
7763
7982
  `one-shot: primary "${target.providerId}/${target.model}" is blocked \u2014 trying fallback`
7764
7983
  );
7765
7984
  } else {
7985
+ attempts += 1;
7766
7986
  const primaryAttempt = await this.tryCall(
7767
7987
  provider,
7768
7988
  request,
@@ -7777,10 +7997,17 @@ var OneShotOrchestrator = class {
7777
7997
  tracker?.recordSuccess(target.providerId, target.model);
7778
7998
  servingProviderId = provider.id;
7779
7999
  servingModel = target.model;
7780
- return this.buildResult(result, servingProviderId, servingModel, false, startedAt);
8000
+ return this.buildResult(result, servingProviderId, servingModel, false, startedAt, attempts);
7781
8001
  }
7782
8002
  if (!fallbackEligible || chain.length === 0) {
7783
- return this.buildErrorResult(lastError, target.providerId, target.model, false, startedAt);
8003
+ return this.buildErrorResult(
8004
+ lastError,
8005
+ target.providerId,
8006
+ target.model,
8007
+ false,
8008
+ startedAt,
8009
+ attempts
8010
+ );
7784
8011
  }
7785
8012
  }
7786
8013
  const estimatedTokens = estimateRequestTokens(request.messages, request.system, []).total;
@@ -7808,6 +8035,7 @@ var OneShotOrchestrator = class {
7808
8035
  }
7809
8036
  servingProviderId = fbProvider.id;
7810
8037
  servingModel = entry.model;
8038
+ attempts += 1;
7811
8039
  const attempt = await this.tryCall(
7812
8040
  fbProvider,
7813
8041
  this.buildRequest(input, entry.model),
@@ -7818,7 +8046,14 @@ var OneShotOrchestrator = class {
7818
8046
  if (attempt.response) {
7819
8047
  tracker?.recordSuccess(entry.providerId, entry.model);
7820
8048
  fromFallback = true;
7821
- return this.buildResult(attempt.response, servingProviderId, servingModel, true, startedAt);
8049
+ return this.buildResult(
8050
+ attempt.response,
8051
+ servingProviderId,
8052
+ servingModel,
8053
+ true,
8054
+ startedAt,
8055
+ attempts
8056
+ );
7822
8057
  }
7823
8058
  lastError = attempt.error;
7824
8059
  }
@@ -7827,7 +8062,8 @@ var OneShotOrchestrator = class {
7827
8062
  servingProviderId,
7828
8063
  servingModel,
7829
8064
  fromFallback,
7830
- startedAt
8065
+ startedAt,
8066
+ attempts
7831
8067
  );
7832
8068
  }
7833
8069
  // ── Private helpers ─────────────────────────────────────────────
@@ -7951,7 +8187,7 @@ var OneShotOrchestrator = class {
7951
8187
  }
7952
8188
  }
7953
8189
  /** Build a success result from a provider Response. */
7954
- buildResult(response, servingProviderId, servingModel, fromFallback, startedAt) {
8190
+ buildResult(response, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7955
8191
  const textBlocks = response.content.filter(isTextBlock);
7956
8192
  const text = textBlocks.map((b) => b.text).join("\n").trim();
7957
8193
  return {
@@ -7965,11 +8201,12 @@ var OneShotOrchestrator = class {
7965
8201
  },
7966
8202
  durationMs: Math.round(performance.now() - startedAt),
7967
8203
  fromFallback,
8204
+ attempts,
7968
8205
  stopReason: response.stopReason
7969
8206
  };
7970
8207
  }
7971
8208
  /** Build a total-failure error result. */
7972
- buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt) {
8209
+ buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7973
8210
  return {
7974
8211
  text: "",
7975
8212
  model: servingModel,
@@ -7977,6 +8214,7 @@ var OneShotOrchestrator = class {
7977
8214
  tokens: { input: 0, output: 0, total: 0 },
7978
8215
  durationMs: Math.round(performance.now() - startedAt),
7979
8216
  fromFallback,
8217
+ attempts,
7980
8218
  error: error instanceof Error ? error.message : String(error ?? "Unknown error")
7981
8219
  };
7982
8220
  }
@@ -8107,6 +8345,44 @@ function createOneShotLLMTool(opts) {
8107
8345
  };
8108
8346
  }
8109
8347
 
8348
+ // src/plugin/config.ts
8349
+ function pluginEntryMatchesName(configuredName, name, aliases = []) {
8350
+ for (const candidate of [name, ...aliases]) {
8351
+ if (configuredName === candidate) return true;
8352
+ if (configuredName === `@wrongstack/plugins/${candidate}`) return true;
8353
+ }
8354
+ return false;
8355
+ }
8356
+ function resolvePluginEnablement(input) {
8357
+ if (input.config?.features?.plugins === false) {
8358
+ return { enabled: false, source: "feature-flag" };
8359
+ }
8360
+ const names = [.../* @__PURE__ */ new Set([input.name, ...input.aliases ?? []])];
8361
+ const matches = input.matches ?? ((configuredName) => pluginEntryMatchesName(configuredName, input.name, input.aliases ?? []));
8362
+ const plugins = input.config?.plugins;
8363
+ if (Array.isArray(plugins)) {
8364
+ for (const candidate of plugins) {
8365
+ if (typeof candidate === "string") {
8366
+ if (matches(candidate)) return { enabled: true, source: "plugin-entry" };
8367
+ continue;
8368
+ }
8369
+ if (!isPluginEntry(candidate) || !matches(candidate.name)) continue;
8370
+ return { enabled: candidate.enabled !== false, source: "plugin-entry" };
8371
+ }
8372
+ }
8373
+ for (const name of names) {
8374
+ const enabled = input.config?.extensions?.[name]?.["enabled"];
8375
+ if (typeof enabled === "boolean") return { enabled, source: "extension" };
8376
+ }
8377
+ return { enabled: input.defaultState === "active", source: "default" };
8378
+ }
8379
+ function isPluginEntry(value) {
8380
+ return isRecord2(value) && typeof value["name"] === "string";
8381
+ }
8382
+ function isRecord2(value) {
8383
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8384
+ }
8385
+
8110
8386
  // src/utils/json-schema-validate.ts
8111
8387
  function validateAgainstSchema(value, schema) {
8112
8388
  const errors = [];
@@ -8230,6 +8506,12 @@ function deepEqual(a, b) {
8230
8506
 
8231
8507
  // src/tools/plugin-manager.ts
8232
8508
  var PLUGIN_MANAGER_TOOL_NAME = "plugin_manager";
8509
+ var PLUGIN_VIEW_STATE_SOURCE = {
8510
+ "feature-flag": "feature_flag",
8511
+ "plugin-entry": "config",
8512
+ extension: "extension",
8513
+ default: "default"
8514
+ };
8233
8515
  var INPUT_SCHEMA3 = {
8234
8516
  type: "object",
8235
8517
  properties: {
@@ -8461,10 +8743,14 @@ function buildPluginViews(opts) {
8461
8743
  return catalog.map((entry) => {
8462
8744
  const aliases = [...entry.aliases ?? []];
8463
8745
  const names = /* @__PURE__ */ new Set([entry.name, ...aliases]);
8464
- const configuredEntry = configured.find((item) => names.has(pluginConfigName(item)));
8465
- const globallyDisabled = config.features?.plugins === false;
8466
- const enabled = globallyDisabled ? false : configuredEntry === void 0 ? entry.defaultState === "active" : typeof configuredEntry === "string" || configuredEntry.enabled !== false;
8467
- const stateSource = globallyDisabled ? "feature_flag" : configuredEntry === void 0 ? "default" : "config";
8746
+ const { enabled, source } = resolvePluginEnablement({
8747
+ name: entry.name,
8748
+ aliases,
8749
+ defaultState: entry.defaultState,
8750
+ config,
8751
+ matches: (spec) => names.has(spec)
8752
+ });
8753
+ const stateSource = PLUGIN_VIEW_STATE_SOURCE[source];
8468
8754
  const tools = pluginTools(opts.toolRegistry, entry.name, aliases);
8469
8755
  const managerControl = isManagerLocked(config, entry.name, aliases) ? "locked" : "allowed";
8470
8756
  return {