@wrongstack/core 0.298.3 → 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 (70) hide show
  1. package/dist/chronicle/index.js +4 -1
  2. package/dist/coordination/agents/index.js +4 -1
  3. package/dist/coordination/director.d.ts +8 -0
  4. package/dist/coordination/fleet-manager.d.ts +48 -3
  5. package/dist/coordination/ifleet-manager.d.ts +2 -0
  6. package/dist/coordination/index.js +127 -24
  7. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  8. package/dist/core/fallback-model.d.ts +48 -0
  9. package/dist/core/index.d.ts +3 -2
  10. package/dist/core/index.js +288 -34
  11. package/dist/core/instruction-template.d.ts +80 -0
  12. package/dist/core/system-prompt-blocks.d.ts +10 -1
  13. package/dist/core/system-prompt-builder.d.ts +35 -1
  14. package/dist/defaults/index.js +358 -117
  15. package/dist/design/index.js +4 -1
  16. package/dist/execution/autonomy-brain.d.ts +7 -0
  17. package/dist/execution/council-brain.d.ts +17 -2
  18. package/dist/execution/council-orchestrator.d.ts +23 -4
  19. package/dist/execution/council-personas.d.ts +10 -0
  20. package/dist/execution/council-prompts.d.ts +12 -1
  21. package/dist/execution/index.d.ts +1 -1
  22. package/dist/execution/index.js +412 -145
  23. package/dist/fleet-notifier.d.ts +9 -2
  24. package/dist/goal/index.js +4 -1
  25. package/dist/hooks/index.js +140 -10
  26. package/dist/hq/exposure.d.ts +0 -11
  27. package/dist/hq/index.js +34 -8
  28. package/dist/hq/protocol/client.d.ts +14 -1
  29. package/dist/hq/protocol/fleet.d.ts +22 -0
  30. package/dist/hq/protocol.js +12 -1
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +1718 -753
  33. package/dist/infrastructure/index.js +50 -2
  34. package/dist/infrastructure/mcp-servers.d.ts +35 -0
  35. package/dist/kernel/events/brain-events.d.ts +9 -0
  36. package/dist/kernel/events/provider-events.d.ts +49 -2
  37. package/dist/kernel/events/sdd-events.d.ts +2 -0
  38. package/dist/models/index.js +1 -1
  39. package/dist/plugin/api.d.ts +6 -0
  40. package/dist/plugin/config.d.ts +55 -0
  41. package/dist/plugin/index.d.ts +1 -1
  42. package/dist/plugin/index.js +138 -22
  43. package/dist/security/index.d.ts +1 -1
  44. package/dist/security/index.js +157 -42
  45. package/dist/security/permission-helpers.d.ts +23 -6
  46. package/dist/security/permission-policy.d.ts +16 -0
  47. package/dist/security/totp.d.ts +14 -0
  48. package/dist/storage/director-state.d.ts +7 -0
  49. package/dist/storage/index.js +46 -9
  50. package/dist/tools/council-tool.d.ts +1 -1
  51. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  52. package/dist/tools/index.js +449 -112
  53. package/dist/types/config/skills-fleet-brain.d.ts +4 -2
  54. package/dist/types/config/tools.d.ts +99 -0
  55. package/dist/types/council.d.ts +11 -0
  56. package/dist/types/index.d.ts +3 -2
  57. package/dist/types/index.js +3 -3
  58. package/dist/types/multi-agent.d.ts +10 -0
  59. package/dist/types/one-shot-llm.d.ts +31 -3
  60. package/dist/types/plugin.d.ts +28 -0
  61. package/dist/types/session.d.ts +5 -1
  62. package/dist/utils/index.js +4 -1
  63. package/dist/utils/wstack-paths.d.ts +2 -0
  64. package/dist/worktree/index.js +47 -25
  65. package/dist/worktree/worktree-manager.d.ts +16 -10
  66. package/instructions/coordination/subagent-baseline.md +8 -0
  67. package/instructions/system-lite.md +83 -3
  68. package/instructions/system-pro.md +286 -97
  69. package/instructions/system.md +236 -85
  70. package/package.json +3 -3
@@ -46,6 +46,9 @@ var BUILTIN_COUNCIL_PERSONAS = Object.freeze([
46
46
  tags: ["users", "usability", "accessibility"]
47
47
  })
48
48
  ]);
49
+ var BUILTIN_COUNCIL_PERSONA_IDS = Object.freeze(
50
+ BUILTIN_COUNCIL_PERSONAS.map((persona) => persona.id)
51
+ );
49
52
  var CouncilPersonaRegistry = class _CouncilPersonaRegistry {
50
53
  byId;
51
54
  constructor(personas = []) {
@@ -469,22 +472,29 @@ function buildCouncilJudgeUserPrompt(question, votes, opts = {}) {
469
472
  "</council-ballots>"
470
473
  ].filter(Boolean).join("\n\n");
471
474
  }
472
- function normalizeOptions(options) {
473
- if (!options) return [];
475
+ function validateCouncilOptions(options) {
476
+ const errors = [];
474
477
  const seen = /* @__PURE__ */ new Set();
475
- return options.map((option) => {
478
+ for (const option of options ?? []) {
476
479
  const id = option.id.trim();
477
- const label = option.label.trim();
478
- if (!id) throw new Error("buildCouncilQuestionPrompt: option id must not be empty.");
479
- if (!label) throw new Error(`buildCouncilQuestionPrompt: option "${id}" needs a label.`);
480
- 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}".`);
481
483
  seen.add(id);
482
- return {
483
- id,
484
- label,
485
- ...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
486
- };
487
- });
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
+ }));
488
498
  }
489
499
  function requiredInstruction(path7) {
490
500
  const text = readBundledInstructionText(path7);
@@ -592,6 +602,8 @@ function requireFraction(value, label) {
592
602
  var COUNCIL_REFUSAL_OPTION_ID = "council_refuse";
593
603
  var DEFAULT_COUNCIL_MAX_CONCURRENCY = 3;
594
604
  var MAX_COUNCIL_CONCURRENCY = 8;
605
+ var OVERALL_TIMEOUT_REASON = "Council overall timeout exceeded.";
606
+ var CALL_CANCELLED_REASON = "Cancelled.";
595
607
  var CouncilOrchestrator = class {
596
608
  caller;
597
609
  personas;
@@ -602,6 +614,17 @@ var CouncilOrchestrator = class {
602
614
  fallbackProfileManager;
603
615
  seatCaller;
604
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();
605
628
  constructor(opts) {
606
629
  if (!opts.caller && !opts.seatCaller && !opts.judgeCaller) {
607
630
  throw new Error(
@@ -620,13 +643,33 @@ var CouncilOrchestrator = class {
620
643
  this.seatCaller = opts.seatCaller;
621
644
  this.judgeCaller = opts.judgeCaller;
622
645
  }
623
- async ask(question) {
624
- const startedAt = Date.now();
625
- 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, {
626
663
  registry: this.profiles,
627
664
  personas: this.personas,
628
665
  defaultProfile: this.defaultProfile
629
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);
630
673
  validateRefusalCollision(question, this.refusalOptionId);
631
674
  const timeoutSignal = AbortSignal.timeout(profile.overallTimeoutMs);
632
675
  const signal = question.signal ? AbortSignal.any([question.signal, timeoutSignal]) : timeoutSignal;
@@ -643,11 +686,16 @@ var CouncilOrchestrator = class {
643
686
  try {
644
687
  return await this.callSeat(question, profile, seat, i, signal, usage);
645
688
  } catch (error) {
689
+ const timedOut = signal.aborted && !question.signal?.aborted;
646
690
  return {
647
691
  seatId: seat.id,
648
692
  persona: seat.persona,
649
- status: signal.aborted ? "cancelled" : "failed",
650
- 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)
651
699
  };
652
700
  }
653
701
  }
@@ -657,7 +705,7 @@ var CouncilOrchestrator = class {
657
705
  if (question.signal?.aborted) {
658
706
  return resultEnvelope({
659
707
  status: "cancelled",
660
- reason: "Council call cancelled.",
708
+ reason: CALL_CANCELLED_REASON,
661
709
  resolution: "none",
662
710
  votes,
663
711
  profile,
@@ -670,14 +718,17 @@ var CouncilOrchestrator = class {
670
718
  if (timeoutSignal.aborted) {
671
719
  return resultEnvelope({
672
720
  status: "failed",
673
- reason: "Council overall timeout exceeded.",
721
+ reason: OVERALL_TIMEOUT_REASON,
674
722
  resolution: "none",
675
723
  votes,
676
724
  profile,
677
725
  usage,
678
726
  startedAt,
679
727
  warnings,
680
- 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]
681
732
  });
682
733
  }
683
734
  if (!question.options || question.options.length === 0) {
@@ -704,7 +755,17 @@ var CouncilOrchestrator = class {
704
755
  );
705
756
  }
706
757
  async callSeat(question, profile, seat, seatIndex, signal, usage) {
707
- 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
+ }
708
769
  let persona;
709
770
  try {
710
771
  persona = this.personas.require(seat.persona);
@@ -730,12 +791,15 @@ var CouncilOrchestrator = class {
730
791
  });
731
792
  const metadata = callMetadata(result);
732
793
  if (result.error) {
794
+ const timedOut = signal.aborted && !question.signal?.aborted;
733
795
  return {
734
796
  seatId: seat.id,
735
797
  persona: seat.persona,
736
- status: signal.aborted ? "cancelled" : "failed",
798
+ status: question.signal?.aborted ? "cancelled" : "failed",
737
799
  ...metadata,
738
- 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
739
803
  };
740
804
  }
741
805
  const parsed = parseVote(result.text, question, this.refusalOptionId);
@@ -834,9 +898,27 @@ var CouncilOrchestrator = class {
834
898
  signal,
835
899
  usage
836
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
+ }
837
917
  if (!judged.ok) {
838
918
  return resultEnvelope({
839
- 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",
840
922
  reason: judged.error,
841
923
  resolution: "none",
842
924
  votes,
@@ -896,6 +978,19 @@ var CouncilOrchestrator = class {
896
978
  });
897
979
  }
898
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
+ }
899
994
  const first = valid[0];
900
995
  if (!first) {
901
996
  return resultEnvelope({
@@ -932,9 +1027,30 @@ var CouncilOrchestrator = class {
932
1027
  signal,
933
1028
  usage
934
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
+ }
935
1046
  if (!judged.ok) {
936
1047
  return resultEnvelope({
937
- 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",
938
1054
  reason: judged.error,
939
1055
  resolution: "none",
940
1056
  votes,
@@ -973,18 +1089,28 @@ var CouncilOrchestrator = class {
973
1089
  signal,
974
1090
  usage
975
1091
  });
976
- 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
+ }
977
1101
  return parseJudge(result.text, question, this.refusalOptionId);
978
1102
  }
979
1103
  /**
980
- * Resolve the effective LLM caller for a call. Voter seats use
981
- * `seatCaller(seatIndex)` when wired. Judge seats (seatIndex undefined)
982
- * use `judgeCaller` if set, otherwise `seatCaller(0)` if set, otherwise
983
- * 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`.
984
1109
  */
985
1110
  resolveCaller(seatIndex) {
986
- if (seatIndex !== void 0 && this.seatCaller) {
987
- return this.seatCaller(seatIndex);
1111
+ if (seatIndex !== void 0) {
1112
+ if (this.seatCaller) return this.seatCaller(seatIndex);
1113
+ return this.caller ?? this.judgeCaller;
988
1114
  }
989
1115
  if (this.judgeCaller) return this.judgeCaller;
990
1116
  if (this.seatCaller) return this.seatCaller(0);
@@ -993,6 +1119,7 @@ var CouncilOrchestrator = class {
993
1119
  async safeCall(input) {
994
1120
  const effectiveCaller = this.resolveCaller(input.seatIndex);
995
1121
  const resolvedTarget = this.resolveCouncilTarget(input.target);
1122
+ const startedAt = Date.now();
996
1123
  try {
997
1124
  const result = await effectiveCaller.call({
998
1125
  system: input.system,
@@ -1009,8 +1136,17 @@ var CouncilOrchestrator = class {
1009
1136
  addUsage(input.usage, result);
1010
1137
  return result;
1011
1138
  } catch (error) {
1012
- input.usage.calls += 1;
1013
- 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;
1014
1150
  }
1015
1151
  }
1016
1152
  /**
@@ -1042,47 +1178,77 @@ var CouncilOrchestrator = class {
1042
1178
  };
1043
1179
  }
1044
1180
  };
1045
- function parseVote(text, question, refusalOptionId) {
1181
+ function parseCouncilResponse(text, question, refusalOptionId, opts) {
1182
+ const roleLabel = opts.role === "judge" ? "Judge" : "Voter";
1046
1183
  const parsed = parseObject(text);
1047
1184
  if (!parsed.ok && (!question.options || question.options.length === 0)) {
1048
1185
  const fallback = text.trim();
1049
- if (fallback) return { ok: true, vote: { stance: fallback } };
1050
- 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.` };
1051
1188
  }
1052
1189
  if (!parsed.ok) return parsed;
1053
1190
  const rationale = optionalString(parsed.value["rationale"]);
1054
1191
  if (question.options && question.options.length > 0) {
1055
1192
  const optionId = optionalString(parsed.value["optionId"]);
1056
- 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
+ ]);
1057
1197
  if (!optionId || !allowed.has(optionId)) {
1058
- return { ok: false, error: "Voter returned an unknown or missing optionId." };
1198
+ return { ok: false, error: `${roleLabel} returned an unknown or missing optionId.` };
1059
1199
  }
1060
- return { ok: true, vote: { optionId, ...rationale ? { rationale } : {} } };
1200
+ return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
1201
+ }
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
+ };
1061
1208
  }
1062
- const stance = optionalString(parsed.value["stance"]);
1063
- if (!stance) return { ok: false, error: "Voter returned an empty or missing stance." };
1064
- return { ok: true, vote: { stance, ...rationale ? { rationale } : {} } };
1209
+ return { ok: true, value: { [opts.freeTextField]: freeText, ...rationale ? { rationale } : {} } };
1065
1210
  }
1066
- function parseJudge(text, question, refusalOptionId) {
1067
- const parsed = parseObject(text);
1068
- if (!parsed.ok && (!question.options || question.options.length === 0)) {
1069
- const fallback = text.trim();
1070
- if (fallback) return { ok: true, value: { answer: fallback } };
1071
- 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;
1072
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
+ });
1073
1226
  if (!parsed.ok) return parsed;
1074
- const rationale = optionalString(parsed.value["rationale"]);
1075
- if (question.options && question.options.length > 0) {
1076
- const optionId = optionalString(parsed.value["optionId"]);
1077
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
1078
- if (!optionId || !allowed.has(optionId)) {
1079
- 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 } : {}
1080
1234
  }
1081
- return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
1082
- }
1083
- const answer = optionalString(parsed.value["answer"]);
1084
- if (!answer) return { ok: false, error: "Judge returned an empty or missing answer." };
1085
- 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
+ };
1086
1252
  }
1087
1253
  function parseObject(text) {
1088
1254
  const trimmed = text.trim();
@@ -1126,7 +1292,7 @@ function callMetadata(result) {
1126
1292
  };
1127
1293
  }
1128
1294
  function addUsage(usage, result) {
1129
- usage.calls += 1;
1295
+ usage.calls += Math.max(1, result.attempts ?? 1);
1130
1296
  usage.inputTokens += result.tokens.input;
1131
1297
  usage.outputTokens += result.tokens.output;
1132
1298
  usage.totalTokens += result.tokens.total;
@@ -1135,21 +1301,40 @@ function usageResult(usage, startedAt) {
1135
1301
  return Object.freeze({ ...usage, durationMs: Math.max(0, Date.now() - startedAt) });
1136
1302
  }
1137
1303
  function cancelledVote(seat) {
1138
- 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
+ };
1139
1313
  }
1140
1314
  function distinctTargetCount(votes, profile) {
1141
- const keys = votes.filter((vote) => vote.status === "valid").map(
1142
- (vote) => profile.distinctness === "provider" ? vote.provider : `${vote.provider ?? ""}/${vote.model ?? ""}`
1143
- ).filter(Boolean);
1144
- 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;
1145
1330
  }
1146
1331
  function distinctnessWarnings(votes, profile) {
1147
1332
  if (profile.distinctness === "none") return [];
1148
- const valid = votes.filter((vote) => vote.status === "valid");
1149
- const distinct = distinctTargetCount(valid, profile);
1150
- 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) {
1151
1336
  return [
1152
- `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).`
1153
1338
  ];
1154
1339
  }
1155
1340
  return [];
@@ -1185,17 +1370,6 @@ async function mapConcurrent(items, concurrency, worker) {
1185
1370
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => run()));
1186
1371
  return results;
1187
1372
  }
1188
- function emptyCallResult(error) {
1189
- return {
1190
- text: "",
1191
- model: "",
1192
- provider: "",
1193
- tokens: { input: 0, output: 0, total: 0 },
1194
- durationMs: 0,
1195
- fromFallback: false,
1196
- error
1197
- };
1198
- }
1199
1373
  function optionalString(value) {
1200
1374
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1201
1375
  }
@@ -1238,7 +1412,7 @@ var INPUT_SCHEMA = {
1238
1412
  },
1239
1413
  profile: {
1240
1414
  type: "string",
1241
- description: "Registered Council profile id. Defaults to the host-configured profile."
1415
+ description: 'Registered Council profile id (e.g. "balanced", "fast", "risk-review"). Defaults to the host-configured profile.'
1242
1416
  }
1243
1417
  },
1244
1418
  required: ["question"],
@@ -1249,10 +1423,12 @@ function createCouncilTool(opts) {
1249
1423
  ...opts,
1250
1424
  fallbackProfileManager: opts.fallbackProfileManager
1251
1425
  });
1426
+ const profiles = opts.profiles ?? DEFAULT_COUNCIL_PROFILE_REGISTRY;
1427
+ const profileIds = profiles.list().map((profile) => profile.id);
1252
1428
  return {
1253
1429
  name: COUNCIL_TOOL_NAME,
1254
1430
  description: "Ask an independent, multi-persona Council to evaluate a decision or synthesize an answer. Uses bounded parallel voters, quorum/veto/weighted resolution, optional judging, model routing, fallback chains, and cancellation.",
1255
- usageHint: "Use for consequential or disputed decisions that benefit from independent lenses. Provide `options` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data.",
1431
+ usageHint: `Use for consequential or disputed decisions that benefit from independent lenses. Provide \`options\` for a vote or omit them for an open answer. Keep context evidence-focused; the Council treats it as untrusted data. Available profiles: ${profileIds.join(", ")}.`,
1256
1432
  category: "meta",
1257
1433
  inputSchema: INPUT_SCHEMA,
1258
1434
  permission: "auto",
@@ -1270,11 +1446,17 @@ function createCouncilTool(opts) {
1270
1446
  };
1271
1447
  return orchestrator.ask(question);
1272
1448
  },
1273
- validate: validateCouncilToolInput
1449
+ validate: (input) => validateCouncilToolInput(input, profileIds)
1274
1450
  };
1275
1451
  }
1276
- function validateCouncilToolInput(input) {
1452
+ function validateCouncilToolInput(input, profileIds) {
1277
1453
  const errors = [];
1454
+ if (typeof input.profile === "string" && input.profile.trim()) {
1455
+ const id = input.profile.trim();
1456
+ if (!profileIds.includes(id)) {
1457
+ errors.push(`Unknown \`profile\` "${id}". Available: ${profileIds.join(", ")}.`);
1458
+ }
1459
+ }
1278
1460
  const question = input.question?.trim() ?? "";
1279
1461
  if (!question) errors.push("`question` must not be empty.");
1280
1462
  if (question.length > MAX_COUNCIL_QUESTION_CHARS) {
@@ -1286,14 +1468,7 @@ function validateCouncilToolInput(input) {
1286
1468
  if ((input.options?.length ?? 0) > MAX_COUNCIL_TOOL_OPTIONS) {
1287
1469
  errors.push(`\`options\` must not contain more than ${MAX_COUNCIL_TOOL_OPTIONS} items.`);
1288
1470
  }
1289
- const ids = /* @__PURE__ */ new Set();
1290
- for (const option of input.options ?? []) {
1291
- const id = option.id.trim();
1292
- if (!id) errors.push("Every option must have a non-empty `id`.");
1293
- if (!option.label.trim()) errors.push(`Option "${id || "<empty>"}" must have a label.`);
1294
- if (ids.has(id)) errors.push(`Duplicate option id "${id}".`);
1295
- ids.add(id);
1296
- }
1471
+ errors.push(...validateCouncilOptions(input.options));
1297
1472
  return errors;
1298
1473
  }
1299
1474
 
@@ -1418,7 +1593,7 @@ var FsError = class extends WrongStackError {
1418
1593
  var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
1419
1594
 
1420
1595
  // src/types/provider.ts
1421
- var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|prompt is too long|too long|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|input.{0,12}too (?:large|long)|context_length_exceeded/i;
1596
+ var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|(?:prompt|request|input|messages?).{0,12}too (?:large|long)|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|context_length_exceeded/i;
1422
1597
  var CONTENT_FILTER_RE = /content.(filter|policy|moderation)|safety (system|filter)/i;
1423
1598
  var RATE_LIMIT_EXCEEDED_RE = /rate[-_\s]*limit[-_\s]*exceeded/i;
1424
1599
  function classifyProviderError(status, body, message) {
@@ -1428,7 +1603,7 @@ function classifyProviderError(status, body, message) {
1428
1603
  if (status === 408) return "timeout";
1429
1604
  if (status === 599) return "stream_hang";
1430
1605
  if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
1431
- if (status === 429 && body?.message && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
1606
+ if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
1432
1607
  return "quota_exhausted";
1433
1608
  }
1434
1609
  if (type === "rate_limit_error" || status === 429) return "rate_limit";
@@ -1488,7 +1663,7 @@ var ProviderError = class extends WrongStackError {
1488
1663
  const e = err;
1489
1664
  const name = e.name;
1490
1665
  if (typeof name !== "string" || !name.endsWith("Error")) return false;
1491
- return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string";
1666
+ return typeof e.status === "number" && typeof e.retryable === "boolean" && typeof e.kind === "string" && typeof e.describe === "function";
1492
1667
  }
1493
1668
  constructor(message, status, retryable, providerId, opts = {}) {
1494
1669
  const kind = opts.kind ?? classifyProviderError(status, opts.body, message);
@@ -2216,7 +2391,9 @@ function safeProfileName(name) {
2216
2391
  function activeProfileName(globalRoot) {
2217
2392
  try {
2218
2393
  const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
2219
- return safeProfileName(typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0);
2394
+ return safeProfileName(
2395
+ typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2396
+ );
2220
2397
  } catch {
2221
2398
  return "default";
2222
2399
  }
@@ -2296,6 +2473,7 @@ function resolveWstackPaths(opts) {
2296
2473
  projectPlan: path2.join(projectDir, "plan.json"),
2297
2474
  projectAutophase: path2.join(projectDir, "autophase"),
2298
2475
  projectSddBoards: path2.join(projectDir, "sdd-boards"),
2476
+ projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
2299
2477
  syncConfig: path2.join(profileDir, "sync.json"),
2300
2478
  configHistoryDir: path2.join(globalRoot, "config-history"),
2301
2479
  projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
@@ -6185,8 +6363,18 @@ var SYSTEM_CONFIG_VIEW_SCHEMA = {
6185
6363
  properties: {
6186
6364
  section: {
6187
6365
  type: "string",
6188
- enum: ["all", "providers", "models", "fallbacks", "matrix", "agents", "refiner", "doctor"],
6189
- 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."
6190
6378
  }
6191
6379
  },
6192
6380
  additionalProperties: false
@@ -6288,6 +6476,7 @@ ${content}`);
6288
6476
  const profiles = config.fallbackProfiles ?? {};
6289
6477
  const chain = config.fallbackModels ?? [];
6290
6478
  const matrix = config.modelMatrix ?? {};
6479
+ const fleetBudget = config.fleet?.budget;
6291
6480
  const bridge = config.fallbackBridge?.trim();
6292
6481
  if (bridge) {
6293
6482
  const parsed = parseModelRef(bridge);
@@ -6357,6 +6546,26 @@ ${content}`);
6357
6546
  }
6358
6547
  }
6359
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
+ }
6360
6569
  for (const [key, entry] of Object.entries(matrix)) {
6361
6570
  const eProvider = entry.provider ?? config.provider;
6362
6571
  const eModel = entry.model;
@@ -6397,6 +6606,27 @@ ${content}`);
6397
6606
  }
6398
6607
  addSection("Configuration Doctor", lines.join("\n"));
6399
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
+ }
6400
6630
  if (section === "all" || section === "refiner") {
6401
6631
  const ref = config.autonomy;
6402
6632
  addSection(
@@ -7187,6 +7417,38 @@ var sshManagerServer = () => ({
7187
7417
  permission: "confirm",
7188
7418
  requestTimeoutMs: 18e4
7189
7419
  });
7420
+ var requirementIntakeServer = () => ({
7421
+ name: "requirement-intake",
7422
+ description: "WrongStack Requirements Intake \u2014 list intake records and file new ones (project-scoped, --writable)",
7423
+ transport: "stdio",
7424
+ command: "wstack-requirement-intake-mcp",
7425
+ args: ["--project-root", ".", "--writable"],
7426
+ permission: "auto"
7427
+ });
7428
+ var kanbanServer = () => ({
7429
+ name: "kanban",
7430
+ description: "WrongStack Kanban \u2014 inspect and manage project work boards (project-scoped, manage tier, no destructive ops)",
7431
+ transport: "stdio",
7432
+ command: "wstack-kanban-mcp",
7433
+ args: ["--project-root", ".", "--writable"],
7434
+ permission: "confirm"
7435
+ });
7436
+ var mailboxServer = () => ({
7437
+ name: "mailbox",
7438
+ description: "WrongStack Mailbox \u2014 read and send project agent mail (project-scoped, no admin/credentials)",
7439
+ transport: "stdio",
7440
+ command: "wstack-mailbox-mcp",
7441
+ args: ["--project-root", ".", "--actor", "external-agent", "--writable"],
7442
+ permission: "auto"
7443
+ });
7444
+ var codebaseIndexServer = () => ({
7445
+ name: "codebase-index",
7446
+ description: "WrongStack Codebase Index \u2014 symbol search and dependency graphs (project-scoped, --writable)",
7447
+ transport: "stdio",
7448
+ command: "wstack-codebase-index-mcp",
7449
+ args: ["--project-root", ".", "--writable"],
7450
+ permission: "auto"
7451
+ });
7190
7452
  var allServers = () => ({
7191
7453
  filesystem: { ...filesystemServer(), enabled: false },
7192
7454
  github: { ...githubServer(), enabled: false },
@@ -7201,7 +7463,11 @@ var allServers = () => ({
7201
7463
  "zai-vision": { ...zaiVisionServer(), enabled: false },
7202
7464
  "minimax-vision": { ...miniMaxVisionServer(), enabled: false },
7203
7465
  playwright: { ...playwrightServer(), enabled: false },
7204
- ssh: { ...sshManagerServer(), enabled: false }
7466
+ ssh: { ...sshManagerServer(), enabled: false },
7467
+ kanban: { ...kanbanServer(), enabled: false },
7468
+ mailbox: { ...mailboxServer(), enabled: false },
7469
+ "codebase-index": { ...codebaseIndexServer(), enabled: false },
7470
+ "requirement-intake": { ...requirementIntakeServer(), enabled: false }
7205
7471
  });
7206
7472
 
7207
7473
  // src/utils/config-json.ts
@@ -7682,6 +7948,7 @@ var OneShotOrchestrator = class {
7682
7948
  tokens: { input: 0, output: 0, total: 0 },
7683
7949
  durationMs: Math.round(performance.now() - startedAt),
7684
7950
  fromFallback: false,
7951
+ attempts: 0,
7685
7952
  error: "No provider or model could be resolved. Check your config."
7686
7953
  };
7687
7954
  }
@@ -7696,6 +7963,7 @@ var OneShotOrchestrator = class {
7696
7963
  tokens: { input: 0, output: 0, total: 0 },
7697
7964
  durationMs: Math.round(performance.now() - startedAt),
7698
7965
  fromFallback: false,
7966
+ attempts: 0,
7699
7967
  error: `Cannot build provider "${target.providerId}": ${err instanceof Error ? err.message : String(err)}`
7700
7968
  };
7701
7969
  }
@@ -7708,11 +7976,13 @@ var OneShotOrchestrator = class {
7708
7976
  let fromFallback = false;
7709
7977
  let lastError;
7710
7978
  let fallbackEligible = false;
7979
+ let attempts = 0;
7711
7980
  if (tracker && !tracker.isAvailable(target.providerId, target.model) || !evaluateModelCalendar(config.modelAvailabilitySchedule, target.providerId, target.model).allowed) {
7712
7981
  this.opts.logger?.debug(
7713
7982
  `one-shot: primary "${target.providerId}/${target.model}" is blocked \u2014 trying fallback`
7714
7983
  );
7715
7984
  } else {
7985
+ attempts += 1;
7716
7986
  const primaryAttempt = await this.tryCall(
7717
7987
  provider,
7718
7988
  request,
@@ -7727,10 +7997,17 @@ var OneShotOrchestrator = class {
7727
7997
  tracker?.recordSuccess(target.providerId, target.model);
7728
7998
  servingProviderId = provider.id;
7729
7999
  servingModel = target.model;
7730
- return this.buildResult(result, servingProviderId, servingModel, false, startedAt);
8000
+ return this.buildResult(result, servingProviderId, servingModel, false, startedAt, attempts);
7731
8001
  }
7732
8002
  if (!fallbackEligible || chain.length === 0) {
7733
- 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
+ );
7734
8011
  }
7735
8012
  }
7736
8013
  const estimatedTokens = estimateRequestTokens(request.messages, request.system, []).total;
@@ -7758,6 +8035,7 @@ var OneShotOrchestrator = class {
7758
8035
  }
7759
8036
  servingProviderId = fbProvider.id;
7760
8037
  servingModel = entry.model;
8038
+ attempts += 1;
7761
8039
  const attempt = await this.tryCall(
7762
8040
  fbProvider,
7763
8041
  this.buildRequest(input, entry.model),
@@ -7768,7 +8046,14 @@ var OneShotOrchestrator = class {
7768
8046
  if (attempt.response) {
7769
8047
  tracker?.recordSuccess(entry.providerId, entry.model);
7770
8048
  fromFallback = true;
7771
- 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
+ );
7772
8057
  }
7773
8058
  lastError = attempt.error;
7774
8059
  }
@@ -7777,7 +8062,8 @@ var OneShotOrchestrator = class {
7777
8062
  servingProviderId,
7778
8063
  servingModel,
7779
8064
  fromFallback,
7780
- startedAt
8065
+ startedAt,
8066
+ attempts
7781
8067
  );
7782
8068
  }
7783
8069
  // ── Private helpers ─────────────────────────────────────────────
@@ -7788,7 +8074,8 @@ var OneShotOrchestrator = class {
7788
8074
  resolveTarget(input, config) {
7789
8075
  if (input.role && this.opts.modelRouter) {
7790
8076
  const pick = this.opts.modelRouter.pickForTask(input.role, "");
7791
- if (pick) {
8077
+ const hasExplicitTarget = Boolean(input.providerId || input.model);
8078
+ if (pick && (pick.fromMatrix === true || !hasExplicitTarget)) {
7792
8079
  return { providerId: pick.provider, model: pick.model };
7793
8080
  }
7794
8081
  }
@@ -7900,7 +8187,7 @@ var OneShotOrchestrator = class {
7900
8187
  }
7901
8188
  }
7902
8189
  /** Build a success result from a provider Response. */
7903
- buildResult(response, servingProviderId, servingModel, fromFallback, startedAt) {
8190
+ buildResult(response, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7904
8191
  const textBlocks = response.content.filter(isTextBlock);
7905
8192
  const text = textBlocks.map((b) => b.text).join("\n").trim();
7906
8193
  return {
@@ -7914,11 +8201,12 @@ var OneShotOrchestrator = class {
7914
8201
  },
7915
8202
  durationMs: Math.round(performance.now() - startedAt),
7916
8203
  fromFallback,
8204
+ attempts,
7917
8205
  stopReason: response.stopReason
7918
8206
  };
7919
8207
  }
7920
8208
  /** Build a total-failure error result. */
7921
- buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt) {
8209
+ buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7922
8210
  return {
7923
8211
  text: "",
7924
8212
  model: servingModel,
@@ -7926,6 +8214,7 @@ var OneShotOrchestrator = class {
7926
8214
  tokens: { input: 0, output: 0, total: 0 },
7927
8215
  durationMs: Math.round(performance.now() - startedAt),
7928
8216
  fromFallback,
8217
+ attempts,
7929
8218
  error: error instanceof Error ? error.message : String(error ?? "Unknown error")
7930
8219
  };
7931
8220
  }
@@ -8056,6 +8345,44 @@ function createOneShotLLMTool(opts) {
8056
8345
  };
8057
8346
  }
8058
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
+
8059
8386
  // src/utils/json-schema-validate.ts
8060
8387
  function validateAgainstSchema(value, schema) {
8061
8388
  const errors = [];
@@ -8179,6 +8506,12 @@ function deepEqual(a, b) {
8179
8506
 
8180
8507
  // src/tools/plugin-manager.ts
8181
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
+ };
8182
8515
  var INPUT_SCHEMA3 = {
8183
8516
  type: "object",
8184
8517
  properties: {
@@ -8410,10 +8743,14 @@ function buildPluginViews(opts) {
8410
8743
  return catalog.map((entry) => {
8411
8744
  const aliases = [...entry.aliases ?? []];
8412
8745
  const names = /* @__PURE__ */ new Set([entry.name, ...aliases]);
8413
- const configuredEntry = configured.find((item) => names.has(pluginConfigName(item)));
8414
- const globallyDisabled = config.features?.plugins === false;
8415
- const enabled = globallyDisabled ? false : configuredEntry === void 0 ? entry.defaultState === "active" : typeof configuredEntry === "string" || configuredEntry.enabled !== false;
8416
- 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];
8417
8754
  const tools = pluginTools(opts.toolRegistry, entry.name, aliases);
8418
8755
  const managerControl = isManagerLocked(config, entry.name, aliases) ? "locked" : "allowed";
8419
8756
  return {