@wrongstack/core 0.299.0 → 0.301.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 (165) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +10 -2
  13. package/dist/coordination/fleet-manager.d.ts +48 -3
  14. package/dist/coordination/ifleet-manager.d.ts +2 -0
  15. package/dist/coordination/index.js +6880 -6440
  16. package/dist/coordination/mail-tools.d.ts +3 -3
  17. package/dist/coordination/mailbox-project-server.js +3 -4
  18. package/dist/coordination/mailbox-types.d.ts +6 -0
  19. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  20. package/dist/core/agent-response.d.ts +5 -1
  21. package/dist/core/agent-tools.d.ts +3 -0
  22. package/dist/core/context.d.ts +20 -0
  23. package/dist/core/fallback-model.d.ts +48 -0
  24. package/dist/core/index.d.ts +11 -9
  25. package/dist/core/index.js +1119 -310
  26. package/dist/core/instruction-template.d.ts +83 -0
  27. package/dist/core/next-steps-slot.d.ts +88 -0
  28. package/dist/core/system-prompt-blocks.d.ts +10 -1
  29. package/dist/core/system-prompt-builder.d.ts +40 -4
  30. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  31. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  32. package/dist/defaults/index.js +9805 -8767
  33. package/dist/design/index.js +11 -3
  34. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  35. package/dist/execution/autonomy-brain.d.ts +15 -2
  36. package/dist/execution/brain-runtime.d.ts +3 -1
  37. package/dist/execution/compaction-core.d.ts +41 -2
  38. package/dist/execution/council-brain.d.ts +47 -1
  39. package/dist/execution/council-orchestrator.d.ts +25 -6
  40. package/dist/execution/council-prompts.d.ts +12 -1
  41. package/dist/execution/index.d.ts +11 -10
  42. package/dist/execution/index.js +10507 -9189
  43. package/dist/execution/tool-executor.d.ts +4 -1
  44. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  45. package/dist/extension/index.js +8 -2
  46. package/dist/extension/registry.d.ts +8 -1
  47. package/dist/fleet-notifier.d.ts +9 -2
  48. package/dist/goal/index.js +11 -3
  49. package/dist/hooks/index.js +50 -13
  50. package/dist/hooks/runner.d.ts +12 -1
  51. package/dist/hq/index.js +59 -21
  52. package/dist/hq/protocol/fleet.d.ts +20 -0
  53. package/dist/hq/protocol.js +10 -0
  54. package/dist/hq/publisher.d.ts +21 -3
  55. package/dist/index.d.ts +22 -20
  56. package/dist/index.js +12211 -8572
  57. package/dist/infrastructure/index.js +108 -21
  58. package/dist/kernel/events/brain-events.d.ts +9 -0
  59. package/dist/kernel/events/provider-events.d.ts +42 -1
  60. package/dist/kernel/events/session-events.d.ts +13 -0
  61. package/dist/kernel/events/tool-events.d.ts +3 -3
  62. package/dist/models/index.d.ts +1 -0
  63. package/dist/models/index.js +18 -1
  64. package/dist/models/provider-credentials.d.ts +54 -0
  65. package/dist/plugin/api.d.ts +6 -0
  66. package/dist/plugin/config.d.ts +55 -0
  67. package/dist/plugin/index.d.ts +2 -1
  68. package/dist/plugin/index.js +2179 -544
  69. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  70. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  71. package/dist/plugins/review-claim-registry.d.ts +23 -8
  72. package/dist/plugins/review-types.d.ts +2 -0
  73. package/dist/registry/index.js +109 -74
  74. package/dist/registry/tool-registry.d.ts +15 -0
  75. package/dist/security/capabilities.d.ts +2 -0
  76. package/dist/security/index.d.ts +2 -2
  77. package/dist/security/index.js +166 -44
  78. package/dist/security/permission-helpers.d.ts +23 -6
  79. package/dist/security/permission-policy.d.ts +16 -0
  80. package/dist/security/readonly-permission-policy.d.ts +20 -0
  81. package/dist/security/totp.d.ts +14 -0
  82. package/dist/session-registry-atomic-file.d.ts +32 -5
  83. package/dist/skills/frontmatter.d.ts +6 -0
  84. package/dist/skills/index.js +22 -5
  85. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  86. package/dist/storage/cloud-config-sync.d.ts +87 -0
  87. package/dist/storage/director-state.d.ts +7 -0
  88. package/dist/storage/index.d.ts +1 -0
  89. package/dist/storage/index.js +887 -74
  90. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +898 -335
  93. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  94. package/dist/tools/plugin-manager.d.ts +27 -0
  95. package/dist/types/config/mcp-features.d.ts +18 -11
  96. package/dist/types/config/root.d.ts +8 -1
  97. package/dist/types/config/runtime.d.ts +20 -6
  98. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  99. package/dist/types/config/tools.d.ts +16 -0
  100. package/dist/types/context-window.d.ts +1 -0
  101. package/dist/types/council.d.ts +11 -0
  102. package/dist/types/hooks.d.ts +14 -0
  103. package/dist/types/index.d.ts +3 -3
  104. package/dist/types/index.js +1 -0
  105. package/dist/types/multi-agent.d.ts +12 -0
  106. package/dist/types/one-shot-llm.d.ts +25 -0
  107. package/dist/types/plugin.d.ts +28 -0
  108. package/dist/types/provider.d.ts +16 -0
  109. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  110. package/dist/types/skill.d.ts +5 -0
  111. package/dist/types/system-prompt.d.ts +3 -1
  112. package/dist/types/tool-executor.d.ts +8 -1
  113. package/dist/utils/context-breakdown.d.ts +8 -2
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +141 -31
  116. package/dist/utils/regex-guard.d.ts +16 -2
  117. package/dist/utils/sage-output-block.d.ts +7 -10
  118. package/dist/utils/wstack-paths.d.ts +11 -3
  119. package/dist/worktree/index.js +4 -4
  120. package/instructions/agents/browser.md +1 -4
  121. package/instructions/agents/e2e.md +17 -15
  122. package/instructions/agents/ios.md +3 -3
  123. package/instructions/agents/search.md +1 -1
  124. package/instructions/autonomy/goal-preamble.md +4 -4
  125. package/instructions/coordination/director-preamble.md +2 -3
  126. package/instructions/coordination/subagent-baseline.md +3 -1
  127. package/instructions/leader-after-task.md +12 -0
  128. package/instructions/llm/chimera-review.md +1 -1
  129. package/instructions/modes/audit-lite.md +1 -1
  130. package/instructions/sections/tool/common-patterns.md +18 -2
  131. package/instructions/sections/tool/mailbox-compact.md +1 -1
  132. package/instructions/sections/tool/mailbox-full.md +1 -1
  133. package/instructions/system-lite.md +91 -3
  134. package/instructions/system-pro.md +296 -102
  135. package/instructions/system.md +248 -92
  136. package/package.json +3 -3
  137. package/skills/api-design/SKILL.md +3 -0
  138. package/skills/audit-log/SKILL.md +2 -0
  139. package/skills/auto-review/SKILL.md +6 -1
  140. package/skills/bug-hunter/SKILL.md +4 -1
  141. package/skills/chimera/SKILL.md +3 -0
  142. package/skills/data-governance/SKILL.md +3 -0
  143. package/skills/design-system/SKILL.md +5 -2
  144. package/skills/docker-deploy/SKILL.md +2 -0
  145. package/skills/git-flow/SKILL.md +2 -0
  146. package/skills/mailbox-bridge/SKILL.md +3 -0
  147. package/skills/mnemosyne/SKILL.md +2 -0
  148. package/skills/multi-agent/SKILL.md +4 -1
  149. package/skills/node-modern/SKILL.md +4 -1
  150. package/skills/observability/SKILL.md +3 -0
  151. package/skills/output-standards/SKILL.md +2 -0
  152. package/skills/plugin-author/SKILL.md +4 -1
  153. package/skills/prompt-engineering/SKILL.md +3 -1
  154. package/skills/react-modern/SKILL.md +6 -3
  155. package/skills/refactor-planner/SKILL.md +2 -0
  156. package/skills/research-web/SKILL.md +3 -0
  157. package/skills/sdd/SKILL.md +3 -1
  158. package/skills/security-scanner/SKILL.md +3 -0
  159. package/skills/skill-creator/SKILL.md +2 -0
  160. package/skills/tech-stack/SKILL.md +3 -0
  161. package/skills/testing/SKILL.md +4 -1
  162. package/skills/typescript-strict/SKILL.md +4 -1
  163. package/skills/wrongstack-kanban/SKILL.md +6 -3
  164. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  165. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -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,11 +614,20 @@ 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
- throw new Error(
611
- "CouncilOrchestrator: provide `caller`, `seatCaller`, or `judgeCaller`."
612
- );
630
+ throw new Error("CouncilOrchestrator: provide `caller`, `seatCaller`, or `judgeCaller`.");
613
631
  }
614
632
  this.caller = opts.caller;
615
633
  this.personas = opts.personas ?? DEFAULT_COUNCIL_PERSONA_REGISTRY;
@@ -623,13 +641,33 @@ var CouncilOrchestrator = class {
623
641
  this.seatCaller = opts.seatCaller;
624
642
  this.judgeCaller = opts.judgeCaller;
625
643
  }
626
- async ask(question) {
627
- const startedAt = Date.now();
628
- const profile = resolveCouncilProfile(question.profile, {
644
+ /**
645
+ * Resolve the effective profile for a question. String ids and the default
646
+ * go through the registry (already O(1)); ad-hoc config objects are
647
+ * normalized once per stable object identity and cached, because hosts such
648
+ * as the Brain adapter pass the same profile object on every ask().
649
+ */
650
+ resolveProfile(profile) {
651
+ if (typeof profile === "string" || profile === void 0) {
652
+ return resolveCouncilProfile(profile, {
653
+ registry: this.profiles,
654
+ personas: this.personas,
655
+ defaultProfile: this.defaultProfile
656
+ });
657
+ }
658
+ const cached = this.profileCache.get(profile);
659
+ if (cached) return cached;
660
+ const resolved = resolveCouncilProfile(profile, {
629
661
  registry: this.profiles,
630
662
  personas: this.personas,
631
663
  defaultProfile: this.defaultProfile
632
664
  });
665
+ this.profileCache.set(profile, resolved);
666
+ return resolved;
667
+ }
668
+ async ask(question) {
669
+ const startedAt = Date.now();
670
+ const profile = this.resolveProfile(question.profile);
633
671
  validateRefusalCollision(question, this.refusalOptionId);
634
672
  const timeoutSignal = AbortSignal.timeout(profile.overallTimeoutMs);
635
673
  const signal = question.signal ? AbortSignal.any([question.signal, timeoutSignal]) : timeoutSignal;
@@ -646,11 +684,16 @@ var CouncilOrchestrator = class {
646
684
  try {
647
685
  return await this.callSeat(question, profile, seat, i, signal, usage);
648
686
  } catch (error) {
687
+ const timedOut = signal.aborted && !question.signal?.aborted;
649
688
  return {
650
689
  seatId: seat.id,
651
690
  persona: seat.persona,
652
- status: signal.aborted ? "cancelled" : "failed",
653
- error: errorMessage(error)
691
+ // Only the caller's own cancel is a "cancelled" vote; the overall
692
+ // budget expiring is a failure (timeout), matching the envelope.
693
+ status: question.signal?.aborted ? "cancelled" : "failed",
694
+ // Canonical text for aborted-by-budget or cancelled seats, so one
695
+ // event does not surface a different string per code path.
696
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : errorMessage(error)
654
697
  };
655
698
  }
656
699
  }
@@ -660,7 +703,7 @@ var CouncilOrchestrator = class {
660
703
  if (question.signal?.aborted) {
661
704
  return resultEnvelope({
662
705
  status: "cancelled",
663
- reason: "Council call cancelled.",
706
+ reason: CALL_CANCELLED_REASON,
664
707
  resolution: "none",
665
708
  votes,
666
709
  profile,
@@ -673,14 +716,17 @@ var CouncilOrchestrator = class {
673
716
  if (timeoutSignal.aborted) {
674
717
  return resultEnvelope({
675
718
  status: "failed",
676
- reason: "Council overall timeout exceeded.",
719
+ reason: OVERALL_TIMEOUT_REASON,
677
720
  resolution: "none",
678
721
  votes,
679
722
  profile,
680
723
  usage,
681
724
  startedAt,
682
725
  warnings,
683
- errors: [...errors, "Council overall timeout exceeded."]
726
+ // Seat-prefixed errors normally carry the canonical timeout text, but
727
+ // a signal-blind caller can resolve valid votes even after the budget
728
+ // expired — append the standalone entry only when nothing carries it.
729
+ errors: errors.some((entry) => entry.includes(OVERALL_TIMEOUT_REASON)) ? errors : [...errors, OVERALL_TIMEOUT_REASON]
684
730
  });
685
731
  }
686
732
  if (!question.options || question.options.length === 0) {
@@ -707,7 +753,17 @@ var CouncilOrchestrator = class {
707
753
  );
708
754
  }
709
755
  async callSeat(question, profile, seat, seatIndex, signal, usage) {
710
- if (signal.aborted) return cancelledVote(seat);
756
+ if (signal.aborted) {
757
+ return question.signal?.aborted ? cancelledVote(seat) : {
758
+ seatId: seat.id,
759
+ persona: seat.persona,
760
+ status: "failed",
761
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
762
+ ...seat.target?.model ? { model: seat.target.model } : {},
763
+ durationMs: 0,
764
+ error: OVERALL_TIMEOUT_REASON
765
+ };
766
+ }
711
767
  let persona;
712
768
  try {
713
769
  persona = this.personas.require(seat.persona);
@@ -733,12 +789,15 @@ var CouncilOrchestrator = class {
733
789
  });
734
790
  const metadata = callMetadata(result);
735
791
  if (result.error) {
792
+ const timedOut = signal.aborted && !question.signal?.aborted;
736
793
  return {
737
794
  seatId: seat.id,
738
795
  persona: seat.persona,
739
- status: signal.aborted ? "cancelled" : "failed",
796
+ status: question.signal?.aborted ? "cancelled" : "failed",
740
797
  ...metadata,
741
- error: result.error
798
+ // Canonical text for cancelled or aborted-by-budget seats, so one
799
+ // cancel/timeout event does not surface a raw provider string.
800
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : result.error
742
801
  };
743
802
  }
744
803
  const parsed = parseVote(result.text, question, this.refusalOptionId);
@@ -748,7 +807,7 @@ var CouncilOrchestrator = class {
748
807
  persona: seat.persona,
749
808
  status: "invalid",
750
809
  ...metadata,
751
- error: parsed.error
810
+ error: withTruncationNote(parsed.error, result, profile.voterMaxTokens)
752
811
  };
753
812
  }
754
813
  return {
@@ -837,9 +896,27 @@ var CouncilOrchestrator = class {
837
896
  signal,
838
897
  usage
839
898
  );
899
+ if (signal.aborted) {
900
+ const cancelled = question.signal?.aborted === true;
901
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
902
+ return resultEnvelope({
903
+ status: cancelled ? "cancelled" : "failed",
904
+ reason,
905
+ resolution: "none",
906
+ votes,
907
+ profile,
908
+ usage,
909
+ startedAt,
910
+ warnings,
911
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
912
+ judgeUsed: true
913
+ });
914
+ }
840
915
  if (!judged.ok) {
841
916
  return resultEnvelope({
842
- status: signal.aborted ? "cancelled" : "abstained",
917
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
918
+ // otherwise the judge simply failed -> abstained (can't decide).
919
+ status: question.signal?.aborted ? "cancelled" : signal.aborted ? "failed" : "abstained",
843
920
  reason: judged.error,
844
921
  resolution: "none",
845
922
  votes,
@@ -899,6 +976,19 @@ var CouncilOrchestrator = class {
899
976
  });
900
977
  }
901
978
  if (!profile.judge) {
979
+ if (divergentStances(valid)) {
980
+ return resultEnvelope({
981
+ status: "abstained",
982
+ reason: "Council produced multiple distinct stances and has no judge to reconcile them.",
983
+ resolution: "none",
984
+ votes,
985
+ profile,
986
+ usage,
987
+ startedAt,
988
+ warnings,
989
+ errors
990
+ });
991
+ }
902
992
  const first = valid[0];
903
993
  if (!first) {
904
994
  return resultEnvelope({
@@ -935,9 +1025,30 @@ var CouncilOrchestrator = class {
935
1025
  signal,
936
1026
  usage
937
1027
  );
1028
+ if (signal.aborted) {
1029
+ const cancelled = question.signal?.aborted === true;
1030
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
1031
+ return resultEnvelope({
1032
+ status: cancelled ? "cancelled" : "failed",
1033
+ reason,
1034
+ resolution: "none",
1035
+ votes,
1036
+ profile,
1037
+ usage,
1038
+ startedAt,
1039
+ warnings,
1040
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
1041
+ judgeUsed: true
1042
+ });
1043
+ }
938
1044
  if (!judged.ok) {
939
1045
  return resultEnvelope({
940
- status: signal.aborted ? "cancelled" : "failed",
1046
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
1047
+ // otherwise the judge simply failed -> failed (open questions cannot
1048
+ // abstain for a judge failure — 'abstained' is reserved for quorum
1049
+ // failure and stance divergence; the option path maps this same
1050
+ // plain-judge-failure case to 'abstained' instead).
1051
+ status: question.signal?.aborted ? "cancelled" : "failed",
941
1052
  reason: judged.error,
942
1053
  resolution: "none",
943
1054
  votes,
@@ -976,18 +1087,35 @@ var CouncilOrchestrator = class {
976
1087
  signal,
977
1088
  usage
978
1089
  });
979
- if (result.error) return { ok: false, error: result.error };
980
- return parseJudge(result.text, question, this.refusalOptionId);
1090
+ if (result.error) {
1091
+ if (signal.aborted && !question.signal?.aborted) {
1092
+ return { ok: false, error: OVERALL_TIMEOUT_REASON };
1093
+ }
1094
+ if (question.signal?.aborted) {
1095
+ return { ok: false, error: CALL_CANCELLED_REASON };
1096
+ }
1097
+ return { ok: false, error: result.error };
1098
+ }
1099
+ const judged = parseJudge(result.text, question, this.refusalOptionId);
1100
+ if (!judged.ok) {
1101
+ return {
1102
+ ok: false,
1103
+ error: withTruncationNote(judged.error, result, profile.judgeMaxTokens)
1104
+ };
1105
+ }
1106
+ return judged;
981
1107
  }
982
1108
  /**
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`.
1109
+ * Resolve the effective LLM caller for a call. Voter seats (defined
1110
+ * seatIndex) use `seatCaller(seatIndex)` when wired, otherwise the shared
1111
+ * `caller` a seat never falls through to the judge caller. Judge seats
1112
+ * (seatIndex undefined) use `judgeCaller` if set, otherwise `seatCaller(0)`
1113
+ * if set, otherwise the shared `caller`.
987
1114
  */
988
1115
  resolveCaller(seatIndex) {
989
- if (seatIndex !== void 0 && this.seatCaller) {
990
- return this.seatCaller(seatIndex);
1116
+ if (seatIndex !== void 0) {
1117
+ if (this.seatCaller) return this.seatCaller(seatIndex);
1118
+ return this.caller ?? this.judgeCaller;
991
1119
  }
992
1120
  if (this.judgeCaller) return this.judgeCaller;
993
1121
  if (this.seatCaller) return this.seatCaller(0);
@@ -996,6 +1124,7 @@ var CouncilOrchestrator = class {
996
1124
  async safeCall(input) {
997
1125
  const effectiveCaller = this.resolveCaller(input.seatIndex);
998
1126
  const resolvedTarget = this.resolveCouncilTarget(input.target);
1127
+ const startedAt = Date.now();
999
1128
  try {
1000
1129
  const result = await effectiveCaller.call({
1001
1130
  system: input.system,
@@ -1012,8 +1141,17 @@ var CouncilOrchestrator = class {
1012
1141
  addUsage(input.usage, result);
1013
1142
  return result;
1014
1143
  } catch (error) {
1015
- input.usage.calls += 1;
1016
- return emptyCallResult(errorMessage(error));
1144
+ const failed = {
1145
+ text: "",
1146
+ model: resolvedTarget?.model ?? "",
1147
+ provider: resolvedTarget?.providerId ?? "",
1148
+ tokens: { input: 0, output: 0, total: 0 },
1149
+ durationMs: Math.max(0, Date.now() - startedAt),
1150
+ fromFallback: false,
1151
+ error: errorMessage(error)
1152
+ };
1153
+ addUsage(input.usage, failed);
1154
+ return failed;
1017
1155
  }
1018
1156
  }
1019
1157
  /**
@@ -1045,47 +1183,84 @@ var CouncilOrchestrator = class {
1045
1183
  };
1046
1184
  }
1047
1185
  };
1048
- function parseVote(text, question, refusalOptionId) {
1186
+ function parseCouncilResponse(text, question, refusalOptionId, opts) {
1187
+ const roleLabel = opts.role === "judge" ? "Judge" : "Voter";
1049
1188
  const parsed = parseObject(text);
1050
1189
  if (!parsed.ok && (!question.options || question.options.length === 0)) {
1051
1190
  const fallback = text.trim();
1052
- if (fallback) return { ok: true, vote: { stance: fallback } };
1053
- return { ok: false, error: "Voter returned an empty response." };
1191
+ if (fallback) return { ok: true, value: { [opts.freeTextField]: fallback } };
1192
+ return { ok: false, error: `${roleLabel} returned an empty response.` };
1054
1193
  }
1055
1194
  if (!parsed.ok) return parsed;
1056
1195
  const rationale = optionalString(parsed.value["rationale"]);
1057
1196
  if (question.options && question.options.length > 0) {
1058
1197
  const optionId = optionalString(parsed.value["optionId"]);
1059
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
1198
+ const allowed = /* @__PURE__ */ new Set([
1199
+ ...question.options.map((option) => option.id.trim()),
1200
+ refusalOptionId
1201
+ ]);
1060
1202
  if (!optionId || !allowed.has(optionId)) {
1061
- return { ok: false, error: "Voter returned an unknown or missing optionId." };
1203
+ return { ok: false, error: `${roleLabel} returned an unknown or missing optionId.` };
1062
1204
  }
1063
- return { ok: true, vote: { optionId, ...rationale ? { rationale } : {} } };
1205
+ return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
1206
+ }
1207
+ const freeText = optionalString(parsed.value[opts.freeTextField]);
1208
+ if (!freeText) {
1209
+ return {
1210
+ ok: false,
1211
+ error: `${roleLabel} returned an empty or missing ${opts.freeTextField}.`
1212
+ };
1064
1213
  }
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 } : {} } };
1214
+ return {
1215
+ ok: true,
1216
+ value: { [opts.freeTextField]: freeText, ...rationale ? { rationale } : {} }
1217
+ };
1068
1218
  }
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." };
1219
+ function divergentStances(valid) {
1220
+ const seen = /* @__PURE__ */ new Set();
1221
+ for (const vote of valid) {
1222
+ const normalized = vote.stance.trim().replace(/^["'`]+|["'`]+$/g, "").toLowerCase().replace(/[.!?;:,]+$/g, "").replace(/\s+/g, " ").trim();
1223
+ if (!normalized) return true;
1224
+ seen.add(normalized);
1225
+ if (seen.size > 1) return true;
1075
1226
  }
1227
+ return false;
1228
+ }
1229
+ function parseVote(text, question, refusalOptionId) {
1230
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
1231
+ role: "voter",
1232
+ freeTextField: "stance"
1233
+ });
1076
1234
  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." };
1235
+ const { optionId, stance, rationale } = parsed.value;
1236
+ return {
1237
+ ok: true,
1238
+ vote: {
1239
+ ...optionId ? { optionId } : {},
1240
+ ...stance ? { stance } : {},
1241
+ ...rationale ? { rationale } : {}
1083
1242
  }
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 } : {} } };
1243
+ };
1244
+ }
1245
+ function parseJudge(text, question, refusalOptionId) {
1246
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
1247
+ role: "judge",
1248
+ freeTextField: "answer"
1249
+ });
1250
+ if (!parsed.ok) return parsed;
1251
+ const { optionId, answer, rationale } = parsed.value;
1252
+ return {
1253
+ ok: true,
1254
+ value: {
1255
+ ...optionId ? { optionId } : {},
1256
+ ...answer ? { answer } : {},
1257
+ ...rationale ? { rationale } : {}
1258
+ }
1259
+ };
1260
+ }
1261
+ function withTruncationNote(error, result, maxTokens) {
1262
+ if (result.stopReason !== "max_tokens") return error;
1263
+ return `${error} (response truncated at maxTokens=${maxTokens} \u2014 reasoning models may need a larger budget)`;
1089
1264
  }
1090
1265
  function parseObject(text) {
1091
1266
  const trimmed = text.trim();
@@ -1129,7 +1304,7 @@ function callMetadata(result) {
1129
1304
  };
1130
1305
  }
1131
1306
  function addUsage(usage, result) {
1132
- usage.calls += 1;
1307
+ usage.calls += Math.max(1, result.attempts ?? 1);
1133
1308
  usage.inputTokens += result.tokens.input;
1134
1309
  usage.outputTokens += result.tokens.output;
1135
1310
  usage.totalTokens += result.tokens.total;
@@ -1138,21 +1313,40 @@ function usageResult(usage, startedAt) {
1138
1313
  return Object.freeze({ ...usage, durationMs: Math.max(0, Date.now() - startedAt) });
1139
1314
  }
1140
1315
  function cancelledVote(seat) {
1141
- return { seatId: seat.id, persona: seat.persona, status: "cancelled", error: "Cancelled." };
1316
+ return {
1317
+ seatId: seat.id,
1318
+ persona: seat.persona,
1319
+ status: "cancelled",
1320
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
1321
+ ...seat.target?.model ? { model: seat.target.model } : {},
1322
+ durationMs: 0,
1323
+ error: CALL_CANCELLED_REASON
1324
+ };
1142
1325
  }
1143
1326
  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;
1327
+ return new Set(distinctTargetKeys(votes, profile)).size;
1328
+ }
1329
+ function distinctTargetKeys(votes, profile) {
1330
+ const keys = [];
1331
+ for (const vote of votes) {
1332
+ if (vote.status !== "valid") continue;
1333
+ const provider = vote.provider?.trim() ?? "";
1334
+ const model = vote.model?.trim() ?? "";
1335
+ if (profile.distinctness === "provider") {
1336
+ if (provider) keys.push(provider);
1337
+ } else if (provider || model) {
1338
+ keys.push(`${provider}/${model}`);
1339
+ }
1340
+ }
1341
+ return keys;
1148
1342
  }
1149
1343
  function distinctnessWarnings(votes, profile) {
1150
1344
  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) {
1345
+ const keys = distinctTargetKeys(votes, profile);
1346
+ const distinct = new Set(keys).size;
1347
+ if (keys.length > 1 && distinct < keys.length) {
1154
1348
  return [
1155
- `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${valid.length} valid vote(s).`
1349
+ `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${keys.length} valid vote(s).`
1156
1350
  ];
1157
1351
  }
1158
1352
  return [];
@@ -1188,17 +1382,6 @@ async function mapConcurrent(items, concurrency, worker) {
1188
1382
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => run()));
1189
1383
  return results;
1190
1384
  }
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
1385
  function optionalString(value) {
1203
1386
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
1204
1387
  }
@@ -1297,14 +1480,7 @@ function validateCouncilToolInput(input, profileIds) {
1297
1480
  if ((input.options?.length ?? 0) > MAX_COUNCIL_TOOL_OPTIONS) {
1298
1481
  errors.push(`\`options\` must not contain more than ${MAX_COUNCIL_TOOL_OPTIONS} items.`);
1299
1482
  }
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
- }
1483
+ errors.push(...validateCouncilOptions(input.options));
1308
1484
  return errors;
1309
1485
  }
1310
1486
 
@@ -2127,59 +2303,6 @@ function fallbackProfileChain(config, profileName) {
2127
2303
  }
2128
2304
  var DEFAULT_PRIMARY_COOLDOWN_MAX_MS = 10 * 6e4;
2129
2305
 
2130
- // src/coordination/agents/types.ts
2131
- var HOUR = 60 * 60 * 1e3;
2132
- var LIGHT_BUDGET = {
2133
- timeoutMs: 3 * HOUR,
2134
- maxIterations: 3e3,
2135
- maxToolCalls: 8e3
2136
- };
2137
- var MEDIUM_BUDGET = {
2138
- timeoutMs: 5 * HOUR,
2139
- maxIterations: 5e3,
2140
- maxToolCalls: 14e3
2141
- };
2142
- var HEAVY_BUDGET = {
2143
- timeoutMs: 10 * HOUR,
2144
- maxIterations: 8e3,
2145
- maxToolCalls: 2e4
2146
- };
2147
- var TOOLS = {
2148
- /** Pure read/inspect — safe for analysis and review agents. */
2149
- read: ["read", "grep", "glob", "search", "tree", "mailbox"],
2150
- /** Read + structured inspection (logs, diffs, json, dependency audit). */
2151
- inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
2152
- /** Read + edit (no shell). For agents that write code/docs but don't run it. */
2153
- write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
2154
- /** Full build loop: edit + run (lint/format/typecheck/test/bash). */
2155
- build: [
2156
- "read",
2157
- "grep",
2158
- "glob",
2159
- "search",
2160
- "tree",
2161
- "write",
2162
- "edit",
2163
- "replace",
2164
- "patch",
2165
- "bash",
2166
- "exec",
2167
- "lint",
2168
- "format",
2169
- "typecheck",
2170
- "test",
2171
- "mailbox"
2172
- ],
2173
- /** Version control. */
2174
- vcs: ["read", "grep", "glob", "git", "diff"],
2175
- /** Dependency management + CVE audit. */
2176
- deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
2177
- /** Documentation authoring. */
2178
- docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
2179
- /** Web research. */
2180
- research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
2181
- };
2182
-
2183
2306
  // src/coordination/agents/agent-prompts.ts
2184
2307
  import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
2185
2308
  import * as path6 from "node:path";
@@ -2202,6 +2325,9 @@ function canonicalProjectRoot(absRoot) {
2202
2325
  const commonDirFile = path2.join(gitDir, "commondir");
2203
2326
  if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
2204
2327
  const commonDir = path2.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
2328
+ const worktreesDir = path2.dirname(gitDir);
2329
+ if (path2.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2330
+ if (path2.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2205
2331
  if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2206
2332
  return path2.dirname(commonDir);
2207
2333
  } catch {
@@ -2224,7 +2350,7 @@ function safeProfileName(name) {
2224
2350
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2225
2351
  return safe || "default";
2226
2352
  }
2227
- function activeProfileName(globalRoot) {
2353
+ function bootstrapProfileName(globalRoot) {
2228
2354
  try {
2229
2355
  const parsed = JSON.parse(fs.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
2230
2356
  return safeProfileName(
@@ -2242,7 +2368,7 @@ function wstackGlobalRoot() {
2242
2368
  function resolveWstackPaths(opts) {
2243
2369
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2244
2370
  const homeDir = opts.userHome ?? os.homedir();
2245
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
2371
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2246
2372
  const profileDir = path2.join(globalRoot, "profiles", profileName);
2247
2373
  const hash = projectHash(opts.projectRoot);
2248
2374
  const slug = projectSlug(opts.projectRoot);
@@ -2312,7 +2438,12 @@ function resolveWstackPaths(opts) {
2312
2438
  projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
2313
2439
  syncConfig: path2.join(profileDir, "sync.json"),
2314
2440
  configHistoryDir: path2.join(globalRoot, "config-history"),
2315
- projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
2441
+ projectStatus: (statusProjectSlug) => {
2442
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
2443
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
2444
+ }
2445
+ return path2.join(globalRoot, "projects", statusProjectSlug, "status.json");
2446
+ }
2316
2447
  };
2317
2448
  }
2318
2449
 
@@ -2357,6 +2488,8 @@ var ToolCapabilities = {
2357
2488
  NET_OUTBOUND: "net.outbound",
2358
2489
  /** Can mutate in-memory session todos only. */
2359
2490
  SESSION_TODO: "session.todo",
2491
+ /** Can park after-task `<nextsteps>` suggestions for the current turn. */
2492
+ SESSION_NEXTSTEPS: "session.nextsteps",
2360
2493
  /** Can mutate in-memory session mode only. */
2361
2494
  SESSION_MODE: "session.mode",
2362
2495
  /** Can inspect registered tool metadata. */
@@ -2418,6 +2551,334 @@ var WIDE_SUBAGENT_CAPABILITIES = [
2418
2551
  ToolCapabilities.COORDINATION_FLEET_READ,
2419
2552
  ToolCapabilities.COORDINATION_RESULT_SUBMIT
2420
2553
  ];
2554
+ function getDangerousCapabilities(toolOrCaps) {
2555
+ if (!toolOrCaps) return [];
2556
+ const input = toolOrCaps;
2557
+ const caps = Array.isArray(toolOrCaps) ? toolOrCaps : input.capabilities ?? [];
2558
+ return caps.filter(
2559
+ (c) => DANGEROUS_FOR_SUBAGENTS.includes(c)
2560
+ );
2561
+ }
2562
+
2563
+ // src/types/runtime-capability-manifest.ts
2564
+ var PLAYWRIGHT_ALIASES = {
2565
+ playwright_navigate: "browser_navigate",
2566
+ playwright_screenshot: "browser_screenshot",
2567
+ playwright_click: "browser_click",
2568
+ playwright_type: "browser_type",
2569
+ playwright_evaluate: "browser_evaluate",
2570
+ playwright_select_option: "browser_select",
2571
+ playwright_hover: "browser_hover",
2572
+ playwright_fill_form: "browser_type",
2573
+ playwright_wait_for: "browser_wait",
2574
+ playwright_press_key: "browser_press",
2575
+ playwright_drag: "browser_drag"
2576
+ };
2577
+ var RUNTIME_CAPABILITY_MANIFEST = [
2578
+ {
2579
+ id: "filesystem.read",
2580
+ pack: "core",
2581
+ exposure: "direct",
2582
+ tools: ["read", "grep", "glob", "tree"]
2583
+ },
2584
+ {
2585
+ id: "code.inspect",
2586
+ pack: "core",
2587
+ exposure: "direct",
2588
+ tools: [
2589
+ "codebase-stats",
2590
+ "codebase-search",
2591
+ "codebase-incoming-calls",
2592
+ "codebase-outgoing-calls",
2593
+ "codebase-index",
2594
+ "dead-code-scan",
2595
+ "diff",
2596
+ "json",
2597
+ "logs"
2598
+ ]
2599
+ },
2600
+ {
2601
+ id: "filesystem.write",
2602
+ pack: "development",
2603
+ exposure: "direct",
2604
+ tools: ["write", "edit", "replace", "patch"]
2605
+ },
2606
+ {
2607
+ id: "execution.shell",
2608
+ pack: "development",
2609
+ exposure: "direct",
2610
+ tools: ["bash", "exec", "language", "language_info", "language_package"]
2611
+ },
2612
+ {
2613
+ id: "verification.run",
2614
+ pack: "development",
2615
+ exposure: "direct",
2616
+ tools: ["lint", "format", "typecheck", "test", "e2e_plan"]
2617
+ },
2618
+ {
2619
+ id: "version-control.manage",
2620
+ pack: "development",
2621
+ exposure: "direct",
2622
+ tools: ["git"]
2623
+ },
2624
+ {
2625
+ id: "dependencies.manage",
2626
+ pack: "development",
2627
+ exposure: "on-demand",
2628
+ tools: ["install", "outdated", "audit"]
2629
+ },
2630
+ {
2631
+ id: "documentation.author",
2632
+ pack: "development",
2633
+ exposure: "on-demand",
2634
+ tools: ["document", "design", "scaffold"]
2635
+ },
2636
+ {
2637
+ id: "web.research",
2638
+ pack: "core",
2639
+ exposure: "direct",
2640
+ tools: ["search", "fetch"]
2641
+ },
2642
+ {
2643
+ id: "work.plan",
2644
+ pack: "core",
2645
+ exposure: "direct",
2646
+ tools: ["todo", "plan", "task", "kanban", "nextsteps"]
2647
+ },
2648
+ {
2649
+ id: "coordination.mailbox",
2650
+ pack: "fleet",
2651
+ exposure: "direct",
2652
+ tools: ["mailbox", "mail_send", "mail_inbox", "fleet_status"]
2653
+ },
2654
+ {
2655
+ id: "fleet.delegate",
2656
+ pack: "fleet",
2657
+ exposure: "on-demand",
2658
+ tools: [
2659
+ "delegate",
2660
+ "spawn_subagent",
2661
+ "assign_task",
2662
+ "kanban_queue",
2663
+ "await_tasks",
2664
+ "ask_subagent",
2665
+ "ask_result",
2666
+ "roll_up",
2667
+ "quality_gate",
2668
+ "terminate_subagent",
2669
+ "terminate_all",
2670
+ "work_complete",
2671
+ "collab_debug",
2672
+ "fleet_emit",
2673
+ "fleet"
2674
+ ]
2675
+ },
2676
+ {
2677
+ id: "reasoning.oneshot",
2678
+ pack: "core",
2679
+ exposure: "on-demand",
2680
+ tools: ["llm", "council"]
2681
+ },
2682
+ {
2683
+ id: "memory.manage",
2684
+ pack: "memory",
2685
+ exposure: "on-demand",
2686
+ tools: ["remember", "search_memory", "find_related_memories", "forget"]
2687
+ },
2688
+ {
2689
+ id: "memory.curate",
2690
+ pack: "memory",
2691
+ exposure: "on-demand",
2692
+ tools: [
2693
+ "memory_candidates",
2694
+ "memory_update",
2695
+ "memory_delete",
2696
+ "memory_recover",
2697
+ "memory_backfill_recoverable",
2698
+ "memory_for_file",
2699
+ "memory_for_path",
2700
+ "memory_search",
2701
+ "memory_graph",
2702
+ "memory_gather_batch",
2703
+ "memory_verify",
2704
+ "memory_hygiene"
2705
+ ]
2706
+ },
2707
+ {
2708
+ id: "browser.interact",
2709
+ pack: "browser",
2710
+ exposure: "on-demand",
2711
+ tools: [
2712
+ "browser_open",
2713
+ "browser_status",
2714
+ "browser_list",
2715
+ "browser_navigate",
2716
+ "browser_snapshot",
2717
+ "browser_screenshot",
2718
+ "browser_click",
2719
+ "browser_type",
2720
+ "browser_select",
2721
+ "browser_press",
2722
+ "browser_hover",
2723
+ "browser_drag",
2724
+ "browser_wait",
2725
+ "browser_evaluate",
2726
+ "browser_upload",
2727
+ "browser_close"
2728
+ ],
2729
+ aliases: PLAYWRIGHT_ALIASES
2730
+ },
2731
+ {
2732
+ id: "mcp.dynamic",
2733
+ pack: "mcp",
2734
+ exposure: "on-demand",
2735
+ tools: ["mcp_use"]
2736
+ },
2737
+ {
2738
+ id: "automation.manage",
2739
+ pack: "admin",
2740
+ exposure: "on-demand",
2741
+ tools: ["cron_schedule", "cron_list", "cron_cancel", "watch_start", "watch_list", "watch_stop"]
2742
+ },
2743
+ {
2744
+ id: "context.pin",
2745
+ pack: "admin",
2746
+ exposure: "on-demand",
2747
+ tools: ["pin_add", "pin_list", "pin_remove"]
2748
+ },
2749
+ {
2750
+ id: "quality.analyze",
2751
+ pack: "development",
2752
+ exposure: "on-demand",
2753
+ tools: ["dead_code_scan", "detect_duplicate_code", "secret_scanner_test", "error_lens_history"]
2754
+ },
2755
+ {
2756
+ id: "release.manage",
2757
+ pack: "development",
2758
+ exposure: "on-demand",
2759
+ tools: ["git_autocommit", "semver_current", "semver_bump", "semver_changelog"]
2760
+ },
2761
+ {
2762
+ id: "messaging.telegram",
2763
+ pack: "fleet",
2764
+ exposure: "on-demand",
2765
+ tools: ["telegram_send", "telegram_read", "telegram_approve"]
2766
+ },
2767
+ {
2768
+ id: "tools.discover",
2769
+ pack: "admin",
2770
+ exposure: "direct",
2771
+ tools: ["tool_search", "tool_use", "tool_help", "batch_tool_use"]
2772
+ },
2773
+ {
2774
+ id: "runtime.admin",
2775
+ pack: "admin",
2776
+ exposure: "internal",
2777
+ tools: ["set_working_dir", "context_manager", "mode", "skill", "mcp_control"]
2778
+ }
2779
+ ];
2780
+ var CAPABILITY_BY_ID = new Map(
2781
+ RUNTIME_CAPABILITY_MANIFEST.map((definition) => [definition.id, definition])
2782
+ );
2783
+ var CAPABILITY_BY_TOOL = /* @__PURE__ */ new Map();
2784
+ var TOOL_ALIASES = /* @__PURE__ */ new Map();
2785
+ for (const definition of RUNTIME_CAPABILITY_MANIFEST) {
2786
+ for (const tool of definition.tools) {
2787
+ CAPABILITY_BY_TOOL.set(tool, definition.id);
2788
+ }
2789
+ for (const [alias, tool] of Object.entries(definition.aliases ?? {})) {
2790
+ TOOL_ALIASES.set(alias, tool);
2791
+ }
2792
+ }
2793
+ function toolsForRuntimeCapabilities(capabilityIds) {
2794
+ return [
2795
+ ...new Set(
2796
+ capabilityIds.flatMap((id) => {
2797
+ const definition = CAPABILITY_BY_ID.get(id);
2798
+ if (!definition) throw new Error(`Unknown runtime capability: "${id}"`);
2799
+ return definition.tools;
2800
+ })
2801
+ )
2802
+ ];
2803
+ }
2804
+ function normalizeRuntimeToolName(name) {
2805
+ return TOOL_ALIASES.get(name) ?? name;
2806
+ }
2807
+ function runtimeCapabilityForTool(name) {
2808
+ return CAPABILITY_BY_TOOL.get(normalizeRuntimeToolName(name));
2809
+ }
2810
+ function inferRuntimeCapabilities(toolNames) {
2811
+ const ids = /* @__PURE__ */ new Set();
2812
+ for (const rawName of toolNames) {
2813
+ const id = runtimeCapabilityForTool(rawName);
2814
+ if (id) ids.add(id);
2815
+ }
2816
+ return [...ids];
2817
+ }
2818
+
2819
+ // src/coordination/agents/project-agent-consolidation.ts
2820
+ import {
2821
+ existsSync,
2822
+ mkdirSync,
2823
+ readFileSync as readFileSync3,
2824
+ rmSync
2825
+ } from "node:fs";
2826
+ import * as path4 from "node:path";
2827
+
2828
+ // src/coordination/agents/project-agent-learning-entries.ts
2829
+ function splitLearnedEntries(body) {
2830
+ return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
2831
+ (entry) => entry.replace(/^# Learned wisdom for .+$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
2832
+ ).filter(Boolean);
2833
+ }
2834
+
2835
+ // src/coordination/agents/project-agent-paths.ts
2836
+ import * as path3 from "node:path";
2837
+ var AGENT_ROLE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,95}$/i;
2838
+ function assertProjectAgentRole(role) {
2839
+ const normalized = role.trim();
2840
+ if (!AGENT_ROLE_PATTERN.test(normalized) || normalized === "." || normalized === ".." || normalized.includes("/") || normalized.includes("\\")) {
2841
+ throw new Error(`Invalid project agent role: "${role}".`);
2842
+ }
2843
+ return normalized;
2844
+ }
2845
+ function agentsDir(projectRoot) {
2846
+ const root = projectRoot ?? process.cwd();
2847
+ return path3.join(root, ".wrongstack", "agents");
2848
+ }
2849
+ function roleDir(role, projectRoot) {
2850
+ return path3.join(agentsDir(projectRoot), assertProjectAgentRole(role));
2851
+ }
2852
+ function learningPolicyPath(role, projectRoot) {
2853
+ return path3.join(roleDir(role, projectRoot), "learning.json");
2854
+ }
2855
+
2856
+ // src/coordination/agents/project-agent-consolidation.ts
2857
+ function consolidationPath(role, projectRoot) {
2858
+ return path4.join(roleDir(role, projectRoot), "consolidated.md");
2859
+ }
2860
+ function consolidationMetaPath(role, projectRoot) {
2861
+ return path4.join(roleDir(role, projectRoot), "consolidation.json");
2862
+ }
2863
+ function loadProjectAgentConsolidated(role, projectRoot) {
2864
+ try {
2865
+ return readFileSync3(consolidationPath(role, projectRoot), "utf8").trim();
2866
+ } catch {
2867
+ return "";
2868
+ }
2869
+ }
2870
+ function loadConsolidationMetadata(role, projectRoot) {
2871
+ try {
2872
+ const raw = readFileSync3(consolidationMetaPath(role, projectRoot), "utf8");
2873
+ const parsed = JSON.parse(raw);
2874
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.consolidatedAt === "string" && typeof parsed.sourceEntryCount === "number") {
2875
+ return parsed;
2876
+ }
2877
+ return void 0;
2878
+ } catch {
2879
+ return void 0;
2880
+ }
2881
+ }
2421
2882
 
2422
2883
  // src/coordination/agents/project-agent-knowledge-manifests.ts
2423
2884
  var BUILT_IN_KNOWLEDGE_MANIFESTS = {
@@ -2485,30 +2946,7 @@ var BUILT_IN_KNOWLEDGE_MANIFESTS = {
2485
2946
  };
2486
2947
 
2487
2948
  // src/coordination/agents/project-agent-learning-policy.ts
2488
- import { readFileSync as readFileSync3 } from "node:fs";
2489
-
2490
- // src/coordination/agents/project-agent-paths.ts
2491
- import * as path3 from "node:path";
2492
- var AGENT_ROLE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,95}$/i;
2493
- function assertProjectAgentRole(role) {
2494
- const normalized = role.trim();
2495
- if (!AGENT_ROLE_PATTERN.test(normalized) || normalized === "." || normalized === ".." || normalized.includes("/") || normalized.includes("\\")) {
2496
- throw new Error(`Invalid project agent role: "${role}".`);
2497
- }
2498
- return normalized;
2499
- }
2500
- function agentsDir(projectRoot) {
2501
- const root = projectRoot ?? process.cwd();
2502
- return path3.join(root, ".wrongstack", "agents");
2503
- }
2504
- function roleDir(role, projectRoot) {
2505
- return path3.join(agentsDir(projectRoot), assertProjectAgentRole(role));
2506
- }
2507
- function learningPolicyPath(role, projectRoot) {
2508
- return path3.join(roleDir(role, projectRoot), "learning.json");
2509
- }
2510
-
2511
- // src/coordination/agents/project-agent-learning-policy.ts
2949
+ import { readFileSync as readFileSync4 } from "node:fs";
2512
2950
  var DEFAULT_LEARNING_POLICY = {
2513
2951
  enabled: true,
2514
2952
  lifetimeCaptureCount: 0
@@ -2516,7 +2954,7 @@ var DEFAULT_LEARNING_POLICY = {
2516
2954
  function loadProjectAgentLearningPolicy(role, projectRoot) {
2517
2955
  try {
2518
2956
  const parsed = JSON.parse(
2519
- readFileSync3(learningPolicyPath(role, projectRoot), "utf8")
2957
+ readFileSync4(learningPolicyPath(role, projectRoot), "utf8")
2520
2958
  );
2521
2959
  return {
2522
2960
  enabled: parsed.enabled !== false,
@@ -2529,47 +2967,6 @@ function loadProjectAgentLearningPolicy(role, projectRoot) {
2529
2967
  }
2530
2968
  }
2531
2969
 
2532
- // src/coordination/agents/project-agent-learning-entries.ts
2533
- function splitLearnedEntries(body) {
2534
- return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
2535
- (entry) => entry.replace(/^# Learned wisdom for .+$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
2536
- ).filter(Boolean);
2537
- }
2538
-
2539
- // src/coordination/agents/project-agent-consolidation.ts
2540
- import {
2541
- existsSync,
2542
- mkdirSync,
2543
- readFileSync as readFileSync4,
2544
- rmSync
2545
- } from "node:fs";
2546
- import * as path4 from "node:path";
2547
- function consolidationPath(role, projectRoot) {
2548
- return path4.join(roleDir(role, projectRoot), "consolidated.md");
2549
- }
2550
- function consolidationMetaPath(role, projectRoot) {
2551
- return path4.join(roleDir(role, projectRoot), "consolidation.json");
2552
- }
2553
- function loadProjectAgentConsolidated(role, projectRoot) {
2554
- try {
2555
- return readFileSync4(consolidationPath(role, projectRoot), "utf8").trim();
2556
- } catch {
2557
- return "";
2558
- }
2559
- }
2560
- function loadConsolidationMetadata(role, projectRoot) {
2561
- try {
2562
- const raw = readFileSync4(consolidationMetaPath(role, projectRoot), "utf8");
2563
- const parsed = JSON.parse(raw);
2564
- if (typeof parsed === "object" && parsed !== null && typeof parsed.consolidatedAt === "string" && typeof parsed.sourceEntryCount === "number") {
2565
- return parsed;
2566
- }
2567
- return void 0;
2568
- } catch {
2569
- return void 0;
2570
- }
2571
- }
2572
-
2573
2970
  // src/coordination/agents/project-agent-identity.ts
2574
2971
  function loadProjectAgentIdentity(role, projectRoot) {
2575
2972
  const identityPath = path5.join(roleDir(role, projectRoot), "identity.md");
@@ -2898,6 +3295,64 @@ function isDirectory2(candidate) {
2898
3295
  }
2899
3296
  }
2900
3297
 
3298
+ // src/coordination/agents/types.ts
3299
+ var HOUR = 60 * 60 * 1e3;
3300
+ var LIGHT_BUDGET = {
3301
+ timeoutMs: 3 * HOUR,
3302
+ maxIterations: 3e3,
3303
+ maxToolCalls: 8e3
3304
+ };
3305
+ var MEDIUM_BUDGET = {
3306
+ timeoutMs: 5 * HOUR,
3307
+ maxIterations: 5e3,
3308
+ maxToolCalls: 14e3
3309
+ };
3310
+ var HEAVY_BUDGET = {
3311
+ timeoutMs: 10 * HOUR,
3312
+ maxIterations: 8e3,
3313
+ maxToolCalls: 2e4
3314
+ };
3315
+ var TOOLS = {
3316
+ /** Pure read/inspect — safe for analysis and review agents. */
3317
+ read: ["read", "grep", "glob", "search", "tree", "mailbox"],
3318
+ /** Read + structured inspection (logs, diffs, json, dependency audit). */
3319
+ inspect: ["read", "grep", "glob", "search", "tree", "json", "diff", "logs", "audit", "mailbox"],
3320
+ /** Read + edit (no shell). For agents that write code/docs but don't run it. */
3321
+ write: ["read", "grep", "glob", "search", "tree", "write", "edit", "replace", "patch", "mailbox"],
3322
+ /** Full build loop: edit + run (lint/format/typecheck/test/bash). */
3323
+ build: [
3324
+ "read",
3325
+ "grep",
3326
+ "glob",
3327
+ "search",
3328
+ "tree",
3329
+ "write",
3330
+ "edit",
3331
+ "replace",
3332
+ "patch",
3333
+ "bash",
3334
+ "exec",
3335
+ "lint",
3336
+ "format",
3337
+ "typecheck",
3338
+ "test",
3339
+ "mailbox"
3340
+ ],
3341
+ /** Version control. */
3342
+ vcs: ["read", "grep", "glob", "git", "diff"],
3343
+ /** Dependency management + CVE audit. */
3344
+ deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
3345
+ /** Documentation authoring. */
3346
+ docs: ["read", "grep", "glob", "search", "tree", "write", "edit", "document", "mailbox"],
3347
+ /** Web research. */
3348
+ research: ["read", "grep", "glob", "search", "fetch", "mailbox"]
3349
+ };
3350
+ var SPECIALIST_TOOLS = {
3351
+ browser: toolsForRuntimeCapabilities(["browser.interact"]),
3352
+ memory: toolsForRuntimeCapabilities(["memory.manage"]),
3353
+ mcp: toolsForRuntimeCapabilities(["mcp.dynamic"])
3354
+ };
3355
+
2901
3356
  // src/coordination/agents/phase1-discovery.ts
2902
3357
  var DISCOVERY_AGENTS = [
2903
3358
  {
@@ -2933,7 +3388,7 @@ var DISCOVERY_AGENTS = [
2933
3388
  id: "search",
2934
3389
  name: "Search",
2935
3390
  role: "search",
2936
- tools: [...TOOLS.read],
3391
+ tools: [...TOOLS.read, "codebase-search"],
2937
3392
  prompt: agentPrompt("search")
2938
3393
  },
2939
3394
  budget: MEDIUM_BUDGET,
@@ -3395,14 +3850,7 @@ var WAVE1_AGENTS = [
3395
3850
  capability: {
3396
3851
  phase: "domain",
3397
3852
  summary: "Android platform specialist: Kotlin, Jetpack Compose, Android SDK, lifecycle and Play Store delivery.",
3398
- keywords: [
3399
- ...ANDROID_META.signals,
3400
- "android",
3401
- "kotlin",
3402
- "compose",
3403
- "gradle",
3404
- "playstore"
3405
- ]
3853
+ keywords: [...ANDROID_META.signals, "android", "kotlin", "compose", "gradle", "playstore"]
3406
3854
  }
3407
3855
  },
3408
3856
  {
@@ -3417,14 +3865,7 @@ var WAVE1_AGENTS = [
3417
3865
  capability: {
3418
3866
  phase: "domain",
3419
3867
  summary: "Desktop application specialist: Electron or Tauri, native packaging, IPC and OS integration.",
3420
- keywords: [
3421
- ...DESKTOP_META.signals,
3422
- "desktop",
3423
- "electron",
3424
- "tauri",
3425
- "ipc",
3426
- "tray"
3427
- ]
3868
+ keywords: [...DESKTOP_META.signals, "desktop", "electron", "tauri", "ipc", "tray"]
3428
3869
  }
3429
3870
  },
3430
3871
  {
@@ -3500,7 +3941,7 @@ var WAVE1_AGENTS = [
3500
3941
  id: "platform-engineer",
3501
3942
  name: "Platform Engineer",
3502
3943
  role: "platform-engineer",
3503
- tools: [...TOOLS.build, "install", "git", "node-modern"],
3944
+ tools: [...TOOLS.build, "install", "git"],
3504
3945
  prompt: agentPrompt("platform-engineer")
3505
3946
  },
3506
3947
  budget: MEDIUM_BUDGET,
@@ -3744,8 +4185,7 @@ var WAVE2_AGENTS = [
3744
4185
  id: "resilience-engineer",
3745
4186
  name: "Resilience Engineer",
3746
4187
  role: "resilience-engineer",
3747
- tools: [...TOOLS.build, "logs", "observability"],
3748
- // uniqueness guaranteed by 'observability' vs chaos-engineer's 'test'
4188
+ tools: [...TOOLS.build, "logs"],
3749
4189
  prompt: agentPrompt("resilience-engineer")
3750
4190
  },
3751
4191
  budget: MEDIUM_BUDGET,
@@ -3768,8 +4208,7 @@ var WAVE2_AGENTS = [
3768
4208
  id: "chaos-engineer",
3769
4209
  name: "Chaos Engineer",
3770
4210
  role: "chaos-engineer",
3771
- tools: [...TOOLS.build, "logs", "observability"],
3772
- // uniqueness guaranteed by 'observability' vs chaos-engineer's 'test'
4211
+ tools: [...TOOLS.build, "logs"],
3773
4212
  prompt: agentPrompt("chaos-engineer")
3774
4213
  },
3775
4214
  budget: MEDIUM_BUDGET,
@@ -3910,21 +4349,7 @@ var VERIFY_AGENTS = [
3910
4349
  id: "e2e",
3911
4350
  name: "E2E",
3912
4351
  role: "e2e",
3913
- tools: [
3914
- ...TOOLS.build,
3915
- "fetch",
3916
- "playwright_navigate",
3917
- "playwright_screenshot",
3918
- "playwright_click",
3919
- "playwright_type",
3920
- "playwright_evaluate",
3921
- "playwright_select_option",
3922
- "playwright_hover",
3923
- "playwright_fill_form",
3924
- "playwright_wait_for",
3925
- "playwright_press_key",
3926
- "playwright_drag"
3927
- ],
4352
+ tools: [...TOOLS.build, "fetch", ...SPECIALIST_TOOLS.browser],
3928
4353
  prompt: agentPrompt("e2e")
3929
4354
  },
3930
4355
  budget: HEAVY_BUDGET,
@@ -3959,21 +4384,7 @@ var VERIFY_AGENTS = [
3959
4384
  id: "browser",
3960
4385
  name: "Browser",
3961
4386
  role: "browser",
3962
- tools: [
3963
- ...TOOLS.read,
3964
- "fetch",
3965
- "playwright_navigate",
3966
- "playwright_screenshot",
3967
- "playwright_click",
3968
- "playwright_type",
3969
- "playwright_evaluate",
3970
- "playwright_select_option",
3971
- "playwright_hover",
3972
- "playwright_fill_form",
3973
- "playwright_wait_for",
3974
- "playwright_press_key",
3975
- "playwright_drag"
3976
- ],
4387
+ tools: [...TOOLS.read, "fetch", ...SPECIALIST_TOOLS.browser],
3977
4388
  prompt: agentPrompt("browser")
3978
4389
  },
3979
4390
  budget: MEDIUM_BUDGET,
@@ -4674,27 +5085,7 @@ var DELIVERY_AGENTS = [
4674
5085
  id: "devops",
4675
5086
  name: "DevOps",
4676
5087
  role: "devops",
4677
- tools: [
4678
- ...TOOLS.build,
4679
- "mcp__ssh__ssh_list_servers",
4680
- "mcp__ssh__ssh_connection_status",
4681
- "mcp__ssh__ssh_execute",
4682
- "mcp__ssh__ssh_execute_sudo",
4683
- "mcp__ssh__ssh_upload",
4684
- "mcp__ssh__ssh_download",
4685
- "mcp__ssh__ssh_sync",
4686
- "mcp__ssh__ssh_deploy",
4687
- "mcp__ssh__ssh_health_check",
4688
- "mcp__ssh__ssh_service_status",
4689
- "mcp__ssh__ssh_process_manager",
4690
- "mcp__ssh__ssh_tunnel",
4691
- "mcp__ssh__ssh_backup_create",
4692
- "mcp__ssh__ssh_backup_list",
4693
- "mcp__ssh__ssh_backup_restore",
4694
- "mcp__ssh__ssh_db_list",
4695
- "mcp__ssh__ssh_db_query",
4696
- "mcp__ssh__ssh_profile"
4697
- ],
5088
+ tools: [...TOOLS.build, ...SPECIALIST_TOOLS.mcp],
4698
5089
  prompt: agentPrompt("devops")
4699
5090
  },
4700
5091
  budget: MEDIUM_BUDGET,
@@ -4895,7 +5286,7 @@ var WAVE3_AGENTS = [
4895
5286
  id: "messaging",
4896
5287
  name: "Messaging",
4897
5288
  role: "messaging",
4898
- tools: [...TOOLS.build, "logs", "observability"],
5289
+ tools: [...TOOLS.build, "logs"],
4899
5290
  prompt: agentPrompt("messaging")
4900
5291
  },
4901
5292
  budget: HEAVY_BUDGET,
@@ -5336,7 +5727,7 @@ var WAVE4_AGENTS = [
5336
5727
  id: "memory-curator",
5337
5728
  name: "Memory Curator",
5338
5729
  role: "memory-curator",
5339
- tools: [...TOOLS.read, "memory"],
5730
+ tools: [...TOOLS.read, ...SPECIALIST_TOOLS.memory],
5340
5731
  prompt: agentPrompt("memory-curator")
5341
5732
  },
5342
5733
  budget: LIGHT_BUDGET,
@@ -5358,7 +5749,7 @@ var WAVE4_AGENTS = [
5358
5749
  id: "fleet-coordinator",
5359
5750
  name: "Fleet Coordinator",
5360
5751
  role: "fleet-coordinator",
5361
- tools: [...TOOLS.read, "plan", "multi-agent"],
5752
+ tools: [...TOOLS.read, "plan"],
5362
5753
  prompt: agentPrompt("fleet-coordinator")
5363
5754
  },
5364
5755
  budget: MEDIUM_BUDGET,
@@ -5458,8 +5849,20 @@ var ROLE_SKILL_SETS = {
5458
5849
  cost: skillSet("audit-log", "tech-stack", "observability", "research-web"),
5459
5850
  "tech-stack": skillSet("tech-stack", "research-web", "security-scanner", "node-modern"),
5460
5851
  // ── Wave 1: Platform and systems engineering ──────────────────────────
5461
- android: skillSet("react-modern", "typescript-strict", "testing", "security-scanner", "tech-stack"),
5462
- desktop: skillSet("node-modern", "typescript-strict", "testing", "security-scanner", "tech-stack"),
5852
+ android: skillSet(
5853
+ "react-modern",
5854
+ "typescript-strict",
5855
+ "testing",
5856
+ "security-scanner",
5857
+ "tech-stack"
5858
+ ),
5859
+ desktop: skillSet(
5860
+ "node-modern",
5861
+ "typescript-strict",
5862
+ "testing",
5863
+ "security-scanner",
5864
+ "tech-stack"
5865
+ ),
5463
5866
  realtime: skillSet(
5464
5867
  "api-design",
5465
5868
  "observability",
@@ -5563,26 +5966,9 @@ var ROLE_SKILL_SETS = {
5563
5966
  "security-scanner",
5564
5967
  "tech-stack"
5565
5968
  ),
5566
- "benchmark-engineer": skillSet(
5567
- "observability",
5568
- "testing",
5569
- "output-standards",
5570
- "tech-stack"
5571
- ),
5572
- "memory-curator": skillSet(
5573
- "mnemosyne",
5574
- "audit-log",
5575
- "testing",
5576
- "output-standards",
5577
- "tech-stack"
5578
- ),
5579
- "fleet-coordinator": skillSet(
5580
- "multi-agent",
5581
- "sdd",
5582
- "output-standards",
5583
- "testing",
5584
- "tech-stack"
5585
- ),
5969
+ "benchmark-engineer": skillSet("observability", "testing", "output-standards", "tech-stack"),
5970
+ "memory-curator": skillSet("mnemosyne", "audit-log", "testing", "output-standards", "tech-stack"),
5971
+ "fleet-coordinator": skillSet("multi-agent", "sdd", "output-standards", "testing", "tech-stack"),
5586
5972
  // ── Wave 2: Security, resilience, governance and agent platform ────────
5587
5973
  "threat-modeler": skillSet(
5588
5974
  "security-scanner",
@@ -5634,6 +6020,7 @@ var SHADOW_AGENT_SKILLS = skillSet(
5634
6020
  "observability",
5635
6021
  "security-scanner"
5636
6022
  );
6023
+ var MAX_EAGER_ROSTER_SKILLS = 3;
5637
6024
  function assignSkillsToAgents(definitions) {
5638
6025
  return definitions.map((definition) => {
5639
6026
  const role = definition.config.role;
@@ -5647,7 +6034,8 @@ function assignSkillsToAgents(definitions) {
5647
6034
  ...definition,
5648
6035
  config: {
5649
6036
  ...definition.config,
5650
- skillNames: [...skills]
6037
+ capabilities: inferRuntimeCapabilities(definition.config.tools ?? []),
6038
+ skillNames: skills.slice(0, MAX_EAGER_ROSTER_SKILLS)
5651
6039
  }
5652
6040
  };
5653
6041
  });
@@ -6199,8 +6587,18 @@ var SYSTEM_CONFIG_VIEW_SCHEMA = {
6199
6587
  properties: {
6200
6588
  section: {
6201
6589
  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."
6590
+ enum: [
6591
+ "all",
6592
+ "providers",
6593
+ "models",
6594
+ "fallbacks",
6595
+ "matrix",
6596
+ "agents",
6597
+ "fleet",
6598
+ "refiner",
6599
+ "doctor"
6600
+ ],
6601
+ 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
6602
  }
6205
6603
  },
6206
6604
  additionalProperties: false
@@ -6302,6 +6700,7 @@ ${content}`);
6302
6700
  const profiles = config.fallbackProfiles ?? {};
6303
6701
  const chain = config.fallbackModels ?? [];
6304
6702
  const matrix = config.modelMatrix ?? {};
6703
+ const fleetBudget = config.fleet?.budget;
6305
6704
  const bridge = config.fallbackBridge?.trim();
6306
6705
  if (bridge) {
6307
6706
  const parsed = parseModelRef(bridge);
@@ -6371,6 +6770,26 @@ ${content}`);
6371
6770
  }
6372
6771
  }
6373
6772
  }
6773
+ if (typeof config.maxConcurrent === "number") {
6774
+ if (!Number.isFinite(config.maxConcurrent) || config.maxConcurrent < 0) {
6775
+ issues.push(`maxConcurrent must be a non-negative number (got ${config.maxConcurrent})`);
6776
+ } else if (config.maxConcurrent === 0) {
6777
+ warnings.push("maxConcurrent is 0 \u2014 subagent concurrency effectively disabled");
6778
+ } else {
6779
+ ok.push(`maxConcurrent ${config.maxConcurrent}`);
6780
+ }
6781
+ }
6782
+ if (fleetBudget && typeof fleetBudget === "object") {
6783
+ for (const key of ["maxSpawns", "maxTokens", "maxCostUsd"]) {
6784
+ const v = fleetBudget[key];
6785
+ if (v === void 0) continue;
6786
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
6787
+ issues.push(`fleet.budget.${key} must be a non-negative number (got ${JSON.stringify(v)})`);
6788
+ } else {
6789
+ ok.push(`fleet.budget.${key} ${v}`);
6790
+ }
6791
+ }
6792
+ }
6374
6793
  for (const [key, entry] of Object.entries(matrix)) {
6375
6794
  const eProvider = entry.provider ?? config.provider;
6376
6795
  const eModel = entry.model;
@@ -6411,6 +6830,27 @@ ${content}`);
6411
6830
  }
6412
6831
  addSection("Configuration Doctor", lines.join("\n"));
6413
6832
  }
6833
+ if (section === "all" || section === "fleet") {
6834
+ const fleet = config.fleet;
6835
+ const budget = fleet?.budget;
6836
+ const lifecycle = fleet?.lifecycle;
6837
+ const fmt = (n, unit = "") => typeof n === "number" && Number.isFinite(n) ? `${n}${unit}` : "(default)";
6838
+ addSection(
6839
+ "Fleet Budgets (configured ceilings)",
6840
+ [
6841
+ ` maxConcurrent: ${typeof config.maxConcurrent === "number" ? config.maxConcurrent : "(default 4)"}`,
6842
+ ` fleet.budget.maxSpawns: ${fmt(budget?.maxSpawns)} ${typeof budget?.maxSpawns !== "number" ? "\u2192 default 64" : ""}`,
6843
+ ` fleet.budget.maxTokens: ${fmt(budget?.maxTokens)}`,
6844
+ ` fleet.budget.maxCostUsd: ${fmt(budget?.maxCostUsd)}`,
6845
+ ` fleet.lifecycle.idleTimeoutMs: ${fmt(lifecycle?.idleTimeoutMs, "ms")}`,
6846
+ ` fleet.lifecycle.retireOnTaskComplete: ${lifecycle?.retireOnTaskComplete === void 0 ? "(default true)" : String(lifecycle.retireOnTaskComplete)}`,
6847
+ "",
6848
+ " Live used/remaining spawns are on /fleet status (not static config).",
6849
+ " Override ceilings: --max-concurrent / WRONGSTACK_MAX_CONCURRENT,",
6850
+ " --max-spawns / WRONGSTACK_MAX_SPAWNS, or fleet.budget.maxSpawns in profile."
6851
+ ].join("\n")
6852
+ );
6853
+ }
6414
6854
  if (section === "all" || section === "refiner") {
6415
6855
  const ref = config.autonomy;
6416
6856
  addSection(
@@ -7732,6 +8172,7 @@ var OneShotOrchestrator = class {
7732
8172
  tokens: { input: 0, output: 0, total: 0 },
7733
8173
  durationMs: Math.round(performance.now() - startedAt),
7734
8174
  fromFallback: false,
8175
+ attempts: 0,
7735
8176
  error: "No provider or model could be resolved. Check your config."
7736
8177
  };
7737
8178
  }
@@ -7746,6 +8187,7 @@ var OneShotOrchestrator = class {
7746
8187
  tokens: { input: 0, output: 0, total: 0 },
7747
8188
  durationMs: Math.round(performance.now() - startedAt),
7748
8189
  fromFallback: false,
8190
+ attempts: 0,
7749
8191
  error: `Cannot build provider "${target.providerId}": ${err instanceof Error ? err.message : String(err)}`
7750
8192
  };
7751
8193
  }
@@ -7758,11 +8200,13 @@ var OneShotOrchestrator = class {
7758
8200
  let fromFallback = false;
7759
8201
  let lastError;
7760
8202
  let fallbackEligible = false;
8203
+ let attempts = 0;
7761
8204
  if (tracker && !tracker.isAvailable(target.providerId, target.model) || !evaluateModelCalendar(config.modelAvailabilitySchedule, target.providerId, target.model).allowed) {
7762
8205
  this.opts.logger?.debug(
7763
8206
  `one-shot: primary "${target.providerId}/${target.model}" is blocked \u2014 trying fallback`
7764
8207
  );
7765
8208
  } else {
8209
+ attempts += 1;
7766
8210
  const primaryAttempt = await this.tryCall(
7767
8211
  provider,
7768
8212
  request,
@@ -7777,10 +8221,17 @@ var OneShotOrchestrator = class {
7777
8221
  tracker?.recordSuccess(target.providerId, target.model);
7778
8222
  servingProviderId = provider.id;
7779
8223
  servingModel = target.model;
7780
- return this.buildResult(result, servingProviderId, servingModel, false, startedAt);
8224
+ return this.buildResult(result, servingProviderId, servingModel, false, startedAt, attempts);
7781
8225
  }
7782
8226
  if (!fallbackEligible || chain.length === 0) {
7783
- return this.buildErrorResult(lastError, target.providerId, target.model, false, startedAt);
8227
+ return this.buildErrorResult(
8228
+ lastError,
8229
+ target.providerId,
8230
+ target.model,
8231
+ false,
8232
+ startedAt,
8233
+ attempts
8234
+ );
7784
8235
  }
7785
8236
  }
7786
8237
  const estimatedTokens = estimateRequestTokens(request.messages, request.system, []).total;
@@ -7808,6 +8259,7 @@ var OneShotOrchestrator = class {
7808
8259
  }
7809
8260
  servingProviderId = fbProvider.id;
7810
8261
  servingModel = entry.model;
8262
+ attempts += 1;
7811
8263
  const attempt = await this.tryCall(
7812
8264
  fbProvider,
7813
8265
  this.buildRequest(input, entry.model),
@@ -7818,7 +8270,14 @@ var OneShotOrchestrator = class {
7818
8270
  if (attempt.response) {
7819
8271
  tracker?.recordSuccess(entry.providerId, entry.model);
7820
8272
  fromFallback = true;
7821
- return this.buildResult(attempt.response, servingProviderId, servingModel, true, startedAt);
8273
+ return this.buildResult(
8274
+ attempt.response,
8275
+ servingProviderId,
8276
+ servingModel,
8277
+ true,
8278
+ startedAt,
8279
+ attempts
8280
+ );
7822
8281
  }
7823
8282
  lastError = attempt.error;
7824
8283
  }
@@ -7827,7 +8286,8 @@ var OneShotOrchestrator = class {
7827
8286
  servingProviderId,
7828
8287
  servingModel,
7829
8288
  fromFallback,
7830
- startedAt
8289
+ startedAt,
8290
+ attempts
7831
8291
  );
7832
8292
  }
7833
8293
  // ── Private helpers ─────────────────────────────────────────────
@@ -7929,10 +8389,9 @@ var OneShotOrchestrator = class {
7929
8389
  /** Attempt a provider call while preserving the actual failure for callers. */
7930
8390
  async tryCall(provider, request, signal, providerId, model) {
7931
8391
  try {
7932
- return {
7933
- response: await provider.complete(request, { signal }),
7934
- fallbackEligible: false
7935
- };
8392
+ const direct = (req) => provider.complete(req, { signal });
8393
+ const response = this.opts.wrapProviderCall ? await this.opts.wrapProviderCall(request, direct) : await direct(request);
8394
+ return { response, fallbackEligible: false };
7936
8395
  } catch (err) {
7937
8396
  if (err instanceof ProviderError && providerId && model) {
7938
8397
  this.opts.statusTracker?.recordFailure(
@@ -7946,12 +8405,17 @@ var OneShotOrchestrator = class {
7946
8405
  }
7947
8406
  return {
7948
8407
  error: err,
7949
- fallbackEligible: !signal.aborted && (!(err instanceof ProviderError) || isFallbackWorthy(err.kind))
8408
+ // Provider failures retain their normal fallback policy. A plain error
8409
+ // thrown by the host wrapper is a local refusal (for example,
8410
+ // prompt-firewall block mode), not evidence that another provider may
8411
+ // succeed; retrying would only replay the same blocked request and
8412
+ // misattribute the final error to the last fallback.
8413
+ fallbackEligible: !signal.aborted && (err instanceof ProviderError ? isFallbackWorthy(err.kind) : this.opts.wrapProviderCall === void 0)
7950
8414
  };
7951
8415
  }
7952
8416
  }
7953
8417
  /** Build a success result from a provider Response. */
7954
- buildResult(response, servingProviderId, servingModel, fromFallback, startedAt) {
8418
+ buildResult(response, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7955
8419
  const textBlocks = response.content.filter(isTextBlock);
7956
8420
  const text = textBlocks.map((b) => b.text).join("\n").trim();
7957
8421
  return {
@@ -7965,11 +8429,12 @@ var OneShotOrchestrator = class {
7965
8429
  },
7966
8430
  durationMs: Math.round(performance.now() - startedAt),
7967
8431
  fromFallback,
8432
+ attempts,
7968
8433
  stopReason: response.stopReason
7969
8434
  };
7970
8435
  }
7971
8436
  /** Build a total-failure error result. */
7972
- buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt) {
8437
+ buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
7973
8438
  return {
7974
8439
  text: "",
7975
8440
  model: servingModel,
@@ -7977,6 +8442,7 @@ var OneShotOrchestrator = class {
7977
8442
  tokens: { input: 0, output: 0, total: 0 },
7978
8443
  durationMs: Math.round(performance.now() - startedAt),
7979
8444
  fromFallback,
8445
+ attempts,
7980
8446
  error: error instanceof Error ? error.message : String(error ?? "Unknown error")
7981
8447
  };
7982
8448
  }
@@ -8075,7 +8541,8 @@ function createOneShotLLMTool(opts) {
8075
8541
  getConfig: opts.getConfig,
8076
8542
  fallbackProfileManager: opts.fallbackProfileManager,
8077
8543
  modelRouter: opts.modelRouter,
8078
- logger: opts.logger
8544
+ logger: opts.logger,
8545
+ wrapProviderCall: opts.wrapProviderCall
8079
8546
  });
8080
8547
  return {
8081
8548
  name: ONE_SHOT_LLM_TOOL_NAME,
@@ -8107,6 +8574,63 @@ function createOneShotLLMTool(opts) {
8107
8574
  };
8108
8575
  }
8109
8576
 
8577
+ // src/plugin/config.ts
8578
+ function pluginEntryMatchesName(configuredName, name, aliases = []) {
8579
+ for (const candidate of [name, ...aliases]) {
8580
+ if (configuredName === candidate) return true;
8581
+ if (configuredName === `@wrongstack/plugins/${candidate}`) return true;
8582
+ }
8583
+ return false;
8584
+ }
8585
+ function resolvePluginEnablement(input) {
8586
+ if (input.config?.features?.plugins === false) {
8587
+ return { enabled: false, source: "feature-flag" };
8588
+ }
8589
+ const names = [.../* @__PURE__ */ new Set([input.name, ...input.aliases ?? []])];
8590
+ const matches = input.matches ?? ((configuredName) => pluginEntryMatchesName(configuredName, input.name, input.aliases ?? []));
8591
+ const plugins = input.config?.plugins;
8592
+ if (Array.isArray(plugins)) {
8593
+ for (const candidate of plugins) {
8594
+ if (typeof candidate === "string") {
8595
+ if (matches(candidate)) return { enabled: true, source: "plugin-entry" };
8596
+ continue;
8597
+ }
8598
+ if (!isPluginEntry(candidate) || !matches(candidate.name)) continue;
8599
+ return { enabled: candidate.enabled !== false, source: "plugin-entry" };
8600
+ }
8601
+ }
8602
+ for (const name of names) {
8603
+ const enabled = input.config?.extensions?.[name]?.["enabled"];
8604
+ if (typeof enabled === "boolean") return { enabled, source: "extension" };
8605
+ }
8606
+ return { enabled: input.defaultState === "active", source: "default" };
8607
+ }
8608
+ function isPluginEntry(value) {
8609
+ return isRecord2(value) && typeof value["name"] === "string";
8610
+ }
8611
+ function isRecord2(value) {
8612
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8613
+ }
8614
+
8615
+ // src/security/readonly-permission-policy.ts
8616
+ var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
8617
+ ToolCapabilities.FS_WRITE,
8618
+ ToolCapabilities.FS_WRITE_OUTSIDE_PROJECT,
8619
+ ToolCapabilities.MEMORY_WRITE,
8620
+ ToolCapabilities.MEMORY_DELETE,
8621
+ ToolCapabilities.SHELL_ARBITRARY,
8622
+ ToolCapabilities.SHELL_RESTRICTED,
8623
+ ToolCapabilities.SHELL_EXEC,
8624
+ ToolCapabilities.TOOL_MUTATE_ANY,
8625
+ ToolCapabilities.CONFIG_MUTATE,
8626
+ ToolCapabilities.PACKAGE_INSTALL,
8627
+ ToolCapabilities.SUBAGENT_SPAWN
8628
+ ]);
8629
+ function toolMutates(tool) {
8630
+ if (tool.mutating === true) return true;
8631
+ return (tool.capabilities ?? []).some((c) => MUTATION_CAPABILITIES.has(c));
8632
+ }
8633
+
8110
8634
  // src/utils/json-schema-validate.ts
8111
8635
  function validateAgainstSchema(value, schema) {
8112
8636
  const errors = [];
@@ -8230,6 +8754,12 @@ function deepEqual(a, b) {
8230
8754
 
8231
8755
  // src/tools/plugin-manager.ts
8232
8756
  var PLUGIN_MANAGER_TOOL_NAME = "plugin_manager";
8757
+ var PLUGIN_VIEW_STATE_SOURCE = {
8758
+ "feature-flag": "feature_flag",
8759
+ "plugin-entry": "config",
8760
+ extension: "extension",
8761
+ default: "default"
8762
+ };
8233
8763
  var INPUT_SCHEMA3 = {
8234
8764
  type: "object",
8235
8765
  properties: {
@@ -8416,7 +8946,36 @@ function createPluginManagerTool(opts) {
8416
8946
  directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
8417
8947
  };
8418
8948
  }
8419
- const nestedInput = input.input ?? {};
8949
+ const dangerousCaps = getDangerousCapabilities(tool);
8950
+ if (dangerousCaps.length > 0) {
8951
+ return {
8952
+ status: "needs_direct_call",
8953
+ message: `Tool "${tool.name}" declares dangerous capabilities (${dangerousCaps.join(", ")}); the permission pipeline must rule on it. Call it directly.`,
8954
+ directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
8955
+ };
8956
+ }
8957
+ if (ctx.meta["readOnly"] === true && toolMutates(tool)) {
8958
+ return {
8959
+ status: "needs_direct_call",
8960
+ message: `Session is in read-only mode and "${tool.name}" mutates. Call it directly so read-only mode can rule on it.`,
8961
+ directCall: { tool: tool.name, input: input.input ?? {}, inputSchema: tool.inputSchema }
8962
+ };
8963
+ }
8964
+ let nestedInput = input.input ?? {};
8965
+ const hooks = opts.getHookRunner?.();
8966
+ if (hooks) {
8967
+ const pre = await hooks.preToolUse(tool.name, nestedInput, ctx, {
8968
+ capabilities: tool.capabilities,
8969
+ mutating: tool.mutating
8970
+ });
8971
+ if (pre.block) {
8972
+ return {
8973
+ status: "error",
8974
+ message: `A policy hook blocked "${tool.name}"` + (pre.reason ? `: ${pre.reason}` : ".")
8975
+ };
8976
+ }
8977
+ if (pre.input) nestedInput = pre.input;
8978
+ }
8420
8979
  const validation = validateAgainstSchema(nestedInput, tool.inputSchema);
8421
8980
  if (!validation.ok) {
8422
8981
  return {
@@ -8461,10 +9020,14 @@ function buildPluginViews(opts) {
8461
9020
  return catalog.map((entry) => {
8462
9021
  const aliases = [...entry.aliases ?? []];
8463
9022
  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";
9023
+ const { enabled, source } = resolvePluginEnablement({
9024
+ name: entry.name,
9025
+ aliases,
9026
+ defaultState: entry.defaultState,
9027
+ config,
9028
+ matches: (spec) => names.has(spec)
9029
+ });
9030
+ const stateSource = PLUGIN_VIEW_STATE_SOURCE[source];
8468
9031
  const tools = pluginTools(opts.toolRegistry, entry.name, aliases);
8469
9032
  const managerControl = isManagerLocked(config, entry.name, aliases) ? "locked" : "allowed";
8470
9033
  return {