@wrongstack/core 0.303.0 → 0.305.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 (73) hide show
  1. package/dist/coordination/agents/index.js +530 -130
  2. package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
  3. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  4. package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
  5. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  6. package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
  7. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  8. package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
  9. package/dist/coordination/agents/types.d.ts +10 -2
  10. package/dist/coordination/director-prompts.d.ts +19 -6
  11. package/dist/coordination/director-tools.d.ts +2 -2
  12. package/dist/coordination/fleet.d.ts +0 -6
  13. package/dist/coordination/index.d.ts +1 -1
  14. package/dist/coordination/index.js +1579 -955
  15. package/dist/core/agent-types.d.ts +4 -2
  16. package/dist/core/agent.d.ts +1 -0
  17. package/dist/core/context.d.ts +15 -0
  18. package/dist/core/conversation-state.d.ts +14 -0
  19. package/dist/core/fallback-profile-manager.d.ts +70 -2
  20. package/dist/core/index.js +308 -108
  21. package/dist/core/system-prompt-blocks.d.ts +1 -1
  22. package/dist/core/system-prompt-builder.d.ts +13 -1
  23. package/dist/core/system-prompt-glossary.d.ts +73 -0
  24. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  25. package/dist/defaults/index.js +910 -693
  26. package/dist/execution/council-orchestrator.d.ts +3 -13
  27. package/dist/execution/index.js +211 -75
  28. package/dist/execution/one-shot-llm.d.ts +5 -0
  29. package/dist/hq/index.js +17 -7
  30. package/dist/hq/protocol/kanban.d.ts +21 -0
  31. package/dist/hq/protocol.js +5 -1
  32. package/dist/hq/redaction.d.ts +14 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +3482 -2538
  35. package/dist/infrastructure/index.js +247 -122
  36. package/dist/plugin/index.js +101 -3
  37. package/dist/registry/index.js +11 -0
  38. package/dist/registry/tool-registry.d.ts +8 -0
  39. package/dist/replay/hash.d.ts +9 -0
  40. package/dist/replay/index.js +14 -4
  41. package/dist/replay/replay-provider-runner.d.ts +31 -1
  42. package/dist/security/index.js +25 -20
  43. package/dist/security/secret-vault.d.ts +2 -0
  44. package/dist/session-catalog/index.js +62 -8
  45. package/dist/session-catalog/project-server.js +73 -13
  46. package/dist/session-catalog/protocol.d.ts +11 -4
  47. package/dist/session-catalog/store.d.ts +2 -2
  48. package/dist/storage/index.js +224 -67
  49. package/dist/storage/memory-consolidator.d.ts +4 -2
  50. package/dist/storage/session-resume-validation.d.ts +24 -0
  51. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  52. package/dist/storage/session-store/fork-session.d.ts +13 -1
  53. package/dist/storage/session-store/load-cache.d.ts +11 -0
  54. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  55. package/dist/storage/session-store.d.ts +18 -0
  56. package/dist/tools/index.js +174 -74
  57. package/dist/types/config/mcp-features.d.ts +31 -1
  58. package/dist/types/config/root.d.ts +12 -0
  59. package/dist/types/config/tools.d.ts +22 -0
  60. package/dist/types/default-config.d.ts +1 -0
  61. package/dist/types/index.js +3 -0
  62. package/dist/types/session.d.ts +9 -1
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.js +214 -76
  65. package/dist/utils/project-state-guard.d.ts +21 -0
  66. package/dist/utils/session-scoped-path.d.ts +17 -0
  67. package/dist/utils/todos-format.d.ts +20 -0
  68. package/instructions/leader-after-task.md +3 -4
  69. package/instructions/system-lite.md +10 -13
  70. package/instructions/system-pro.md +18 -25
  71. package/instructions/system.md +18 -23
  72. package/package.json +3 -3
  73. package/skills/wrongstack-kanban/SKILL.md +95 -124
@@ -53,23 +53,13 @@ export declare class CouncilOrchestrator {
53
53
  private readonly fallbackProfileManager;
54
54
  private readonly seatCaller;
55
55
  private readonly judgeCaller;
56
- /**
57
- * Normalized ad-hoc profiles keyed by the caller's config object identity.
58
- * The Brain adapter reuses ONE profile object for every decision, so this
59
- * avoids re-validating + re-freezing it on every ask() without caching
60
- * string-keyed registry lookups (those are already O(1)).
61
- *
62
- * Hosts must treat ad-hoc profile configs as IMMUTABLE once passed to
63
- * ask(): the cache is keyed by object identity and never invalidated, so
64
- * mutating a cached profile would silently serve the first snapshot.
65
- */
66
- private readonly profileCache;
67
56
  constructor(opts: CouncilOrchestratorOptions);
68
57
  /**
69
58
  * Resolve the effective profile for a question. String ids and the default
70
59
  * go through the registry (already O(1)); ad-hoc config objects are
71
- * normalized once per stable object identity and cached, because hosts such
72
- * as the Brain adapter pass the same profile object on every ask().
60
+ * normalized on each call. Callers are allowed to reuse and mutate an
61
+ * ad-hoc object between decisions, so object-identity caching would serve a
62
+ * stale snapshot.
73
63
  */
74
64
  private resolveProfile;
75
65
  ask(question: CouncilQuestion): Promise<CouncilResult>;
@@ -480,6 +480,12 @@ function getModelFamilyRatio(calibrationKey) {
480
480
  }
481
481
 
482
482
  // src/core/conversation-state.ts
483
+ function hasToolResultBlock(message) {
484
+ return message !== void 0 && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_result");
485
+ }
486
+ function hasToolUseBlock(message) {
487
+ return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
488
+ }
483
489
  var ConversationState = class {
484
490
  ctx;
485
491
  listeners = /* @__PURE__ */ new Set();
@@ -547,15 +553,32 @@ var ConversationState = class {
547
553
  */
548
554
  overflowCount(arr) {
549
555
  let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
550
- if (Context.MAX_MESSAGE_TOKENS <= 0) return drop;
556
+ if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
551
557
  let total = 0;
552
558
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
553
- if (total <= Context.MAX_MESSAGE_TOKENS) return drop;
559
+ if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
554
560
  while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
555
561
  total -= arr[drop]?._estTokens ?? 0;
556
562
  drop++;
557
563
  }
558
- return drop;
564
+ return this.protocolSafeDropCount(arr, drop);
565
+ }
566
+ /**
567
+ * Front eviction must not retain a `tool_result` after evicting the
568
+ * immediately preceding assistant `tool_use`. Long tool-heavy sessions sit
569
+ * at the retention cap, so an unsafe boundary would create a fresh orphan on
570
+ * nearly every append and make the request-time repair discard protocol
571
+ * history continuously.
572
+ *
573
+ * Move the boundary backward to retain the complete exchange for one more
574
+ * eviction cycle. Moving it forward would also drop non-protocol text/images
575
+ * that may share either message. The temporary one-message cap overshoot is
576
+ * the minimum lossless representation; once enough newer messages exist, the
577
+ * next eviction boundary naturally moves past both halves together.
578
+ */
579
+ protocolSafeDropCount(arr, drop) {
580
+ if (drop <= 0 || drop >= arr.length) return drop;
581
+ return hasToolResultBlock(arr[drop]) && hasToolUseBlock(arr[drop - 1]) ? drop - 1 : drop;
559
582
  }
560
583
  /**
561
584
  * Append a content block to the trailing user message's content array.
@@ -960,6 +983,28 @@ var Context = class _Context {
960
983
  return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
961
984
  }
962
985
  }
986
+ _journalDropCount = 0;
987
+ _journalDropWarnAt = 0;
988
+ /** Throttled notice that a conversation event never reached the journal. */
989
+ warnConversationJournalDrop(eventType) {
990
+ this._journalDropCount++;
991
+ const now = Date.now();
992
+ if (now - this._journalDropWarnAt < 5e3) return;
993
+ this._journalDropWarnAt = now;
994
+ const dropped = this._journalDropCount;
995
+ this._journalDropCount = 0;
996
+ console.warn(
997
+ JSON.stringify({
998
+ level: "error",
999
+ event: "session.conversation_journal_drop",
1000
+ sessionId: this.session?.id,
1001
+ eventType,
1002
+ droppedEvents: dropped,
1003
+ message: "Session writer is not draining; replay of this session will be incomplete.",
1004
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1005
+ })
1006
+ );
1007
+ }
963
1008
  enqueueConversationJournal(event, writer) {
964
1009
  const bytes = this.conversationJournalBytes(event);
965
1010
  const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
@@ -977,18 +1022,21 @@ var Context = class _Context {
977
1022
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
978
1023
  this._conversationJournalQueue.splice(index, 1);
979
1024
  }
980
- if (snapshotBytes <= _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
981
- this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
982
- this._conversationJournalBytes += snapshotBytes;
983
- }
1025
+ this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
1026
+ this._conversationJournalBytes += snapshotBytes;
984
1027
  } else {
985
1028
  this._conversationJournalQueue.push({ event, bytes, writer });
986
1029
  this._conversationJournalBytes += bytes;
987
1030
  }
988
1031
  while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
989
- const dropped = this._conversationJournalQueue.shift();
1032
+ const index = this._conversationJournalQueue.findIndex(
1033
+ (queued) => queued.event.type !== "messages_replaced"
1034
+ );
1035
+ if (index === -1) break;
1036
+ const [dropped] = this._conversationJournalQueue.splice(index, 1);
990
1037
  if (!dropped) break;
991
1038
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
1039
+ this.warnConversationJournalDrop(dropped.event.type);
992
1040
  }
993
1041
  this.startConversationJournalDrain();
994
1042
  }
@@ -5042,12 +5090,97 @@ var FallbackProfileManager = class {
5042
5090
  return this.resolveRefs([ref], exclude);
5043
5091
  }
5044
5092
  /**
5045
- * Resolve every usable configured target as an uncapped last-resort chain.
5093
+ * Resolve every usable configured target as a bounded last-resort chain.
5046
5094
  * Normal smart defaults stay bounded; callers append this only after the
5047
- * preferred chain and only when automatic fallback is enabled.
5095
+ * preferred chain and only when automatic fallback is enabled. The cap
5096
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
5097
+ * from producing a degenerate chain of doomed requests during a systemic
5098
+ * outage — by this point the smart default, bridge, and default profile
5099
+ * have already failed.
5048
5100
  */
5049
5101
  resolveAllConfigured(exclude) {
5050
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
5102
+ return this.smartDefault(exclude, this.lastResortCap());
5103
+ }
5104
+ /**
5105
+ * Effective cap for the last-resort append. Reads the user-configurable
5106
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
5107
+ * otherwise falls back to the compiled-in default
5108
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
5109
+ */
5110
+ lastResortCap() {
5111
+ const configured = this.config.fallbackMaxLastResortCandidates;
5112
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
5113
+ return Math.floor(configured);
5114
+ }
5115
+ return MAX_LAST_RESORT_CANDIDATES;
5116
+ }
5117
+ /**
5118
+ * Build the complete fallback candidate chain shared by the agent-loop
5119
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
5120
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
5121
+ * gate so both consumers produce identical ordering and depth semantics.
5122
+ *
5123
+ * Layering (each step deduped against all prior):
5124
+ * 1. Bridge (emergency continuity route).
5125
+ * 2. Configured primary, when the live context drifted from it.
5126
+ * 3. The selected chain (explicit refs → named profile → smart default),
5127
+ * via {@link resolveEffective}.
5128
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
5129
+ * 5. Every other configured provider — last resort, ONLY when the chain was
5130
+ * auto-derived AND `effectiveFallbackAuto` is true.
5131
+ *
5132
+ * Returns the empty chain when `closedWorld` is true and no explicit
5133
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
5134
+ *
5135
+ * @internal caller-aware options (primary, closedWorld) are accepted because
5136
+ * the agent loop has context the manager does not own; the resolution
5137
+ * pipeline itself is identical for both callers.
5138
+ */
5139
+ resolveCandidates(current, opts = {}) {
5140
+ const configuredPrimary = opts.primary ?? {
5141
+ providerId: this.config.provider,
5142
+ model: this.config.model
5143
+ };
5144
+ const configFallbackAuto = this.config.fallbackAuto;
5145
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
5146
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
5147
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
5148
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
5149
+ const fromExplicitSource = explicitUsable || profileUsable;
5150
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
5151
+ fallbackModels: explicitRefs,
5152
+ fallbackProfile: opts.fallbackProfile,
5153
+ fallbackAuto: effectiveFallbackAuto,
5154
+ exclude: current
5155
+ });
5156
+ const candidates = [];
5157
+ if (opts.closedWorld) {
5158
+ candidates.push(...selectedChain);
5159
+ } else {
5160
+ candidates.push(...this.resolveBridge(current));
5161
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
5162
+ candidates.push({
5163
+ providerId: configuredPrimary.providerId,
5164
+ model: configuredPrimary.model,
5165
+ providerSwitched: configuredPrimary.providerId !== current.providerId
5166
+ });
5167
+ }
5168
+ candidates.push(...selectedChain);
5169
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
5170
+ candidates.push(...this.resolve("default", { exclude: current }));
5171
+ }
5172
+ if (!fromExplicitSource && effectiveFallbackAuto) {
5173
+ const cap = this.lastResortCap();
5174
+ if (cap > 0) {
5175
+ const usedKeys = new Set(
5176
+ candidates.map((c) => `${c.providerId}/${c.model}`)
5177
+ );
5178
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
5179
+ candidates.push(...lastResort);
5180
+ }
5181
+ }
5182
+ }
5183
+ return dedupeChain(candidates, current);
5051
5184
  }
5052
5185
  // ── Provider availability (read-only) ──────────────────────────────────
5053
5186
  checkProvider(providerId) {
@@ -5171,7 +5304,20 @@ var FallbackProfileManager = class {
5171
5304
  );
5172
5305
  }
5173
5306
  };
5307
+ var MAX_LAST_RESORT_CANDIDATES = 12;
5174
5308
  var FREEZER_EMPTY = Object.freeze([]);
5309
+ function dedupeChain(entries, current) {
5310
+ const seen = /* @__PURE__ */ new Set();
5311
+ const currentKey = `${current.providerId}/${current.model}`;
5312
+ return Object.freeze(
5313
+ entries.filter((entry) => {
5314
+ const key = `${entry.providerId}/${entry.model}`;
5315
+ if (key === currentKey || seen.has(key)) return false;
5316
+ seen.add(key);
5317
+ return true;
5318
+ })
5319
+ );
5320
+ }
5175
5321
 
5176
5322
  // src/core/fallback-model.ts
5177
5323
  function effectiveFallbackChain(config) {
@@ -5762,17 +5908,6 @@ var CouncilOrchestrator = class {
5762
5908
  fallbackProfileManager;
5763
5909
  seatCaller;
5764
5910
  judgeCaller;
5765
- /**
5766
- * Normalized ad-hoc profiles keyed by the caller's config object identity.
5767
- * The Brain adapter reuses ONE profile object for every decision, so this
5768
- * avoids re-validating + re-freezing it on every ask() without caching
5769
- * string-keyed registry lookups (those are already O(1)).
5770
- *
5771
- * Hosts must treat ad-hoc profile configs as IMMUTABLE once passed to
5772
- * ask(): the cache is keyed by object identity and never invalidated, so
5773
- * mutating a cached profile would silently serve the first snapshot.
5774
- */
5775
- profileCache = /* @__PURE__ */ new WeakMap();
5776
5911
  constructor(opts) {
5777
5912
  if (!opts.caller && !opts.seatCaller && !opts.judgeCaller) {
5778
5913
  throw new Error("CouncilOrchestrator: provide `caller`, `seatCaller`, or `judgeCaller`.");
@@ -5792,8 +5927,9 @@ var CouncilOrchestrator = class {
5792
5927
  /**
5793
5928
  * Resolve the effective profile for a question. String ids and the default
5794
5929
  * go through the registry (already O(1)); ad-hoc config objects are
5795
- * normalized once per stable object identity and cached, because hosts such
5796
- * as the Brain adapter pass the same profile object on every ask().
5930
+ * normalized on each call. Callers are allowed to reuse and mutate an
5931
+ * ad-hoc object between decisions, so object-identity caching would serve a
5932
+ * stale snapshot.
5797
5933
  */
5798
5934
  resolveProfile(profile) {
5799
5935
  if (typeof profile === "string" || profile === void 0) {
@@ -5803,15 +5939,11 @@ var CouncilOrchestrator = class {
5803
5939
  defaultProfile: this.defaultProfile
5804
5940
  });
5805
5941
  }
5806
- const cached = this.profileCache.get(profile);
5807
- if (cached) return cached;
5808
- const resolved = resolveCouncilProfile(profile, {
5942
+ return resolveCouncilProfile(profile, {
5809
5943
  registry: this.profiles,
5810
5944
  personas: this.personas,
5811
5945
  defaultProfile: this.defaultProfile
5812
5946
  });
5813
- this.profileCache.set(profile, resolved);
5814
- return resolved;
5815
5947
  }
5816
5948
  async ask(question) {
5817
5949
  const startedAt = Date.now();
@@ -11643,52 +11775,24 @@ var OneShotOrchestrator = class {
11643
11775
  }
11644
11776
  /**
11645
11777
  * Build the fallback model chain from input + config + current target.
11778
+ * Delegates to {@link FallbackProfileManager.resolveCandidates} — the
11779
+ * shared constructor used by both the agent loop and the one-shot path,
11780
+ * so both produce identical ordering, depth, and fromExplicitSource
11781
+ * semantics.
11782
+ *
11646
11783
  * The injected {@link OneShotOrchestratorOptions.fallbackProfileManager}
11647
11784
  * is the only allowed manager — OneShot never owns a private snapshot
11648
11785
  * so a live `ConfigStore` change reaches every call without rebuilding
11649
11786
  * the manager.
11650
11787
  */
11651
11788
  resolveFallbackChain(input, config, target) {
11652
- const mgr = this.opts.fallbackProfileManager;
11653
- let fromExplicitSource = false;
11654
- let selected = mgr.resolveEffective({
11655
- fallbackAuto: false,
11656
- exclude: target
11789
+ return this.opts.fallbackProfileManager.resolveCandidates(target, {
11790
+ fallbackModels: input.fallbackModels && input.fallbackModels.length > 0 ? input.fallbackModels : config.fallbackModels,
11791
+ // The one-shot already tried `target` as the primary before entering
11792
+ // the chain. Passing it as `primary` suppresses the primary-insertion
11793
+ // step in resolveCandidates (primary === current → not pushed).
11794
+ primary: target
11657
11795
  });
11658
- if (input.fallbackModels && input.fallbackModels.length > 0) {
11659
- const resolved = mgr.resolveEffective({
11660
- fallbackModels: input.fallbackModels,
11661
- fallbackAuto: false,
11662
- exclude: target
11663
- });
11664
- selected = resolved;
11665
- fromExplicitSource = resolved.length > 0;
11666
- } else if (config.fallbackModels && config.fallbackModels.length > 0) {
11667
- const resolved = mgr.resolveEffective({
11668
- fallbackModels: config.fallbackModels,
11669
- fallbackAuto: false,
11670
- exclude: target
11671
- });
11672
- selected = resolved;
11673
- fromExplicitSource = resolved.length > 0;
11674
- } else if (config.fallbackAuto !== false) {
11675
- selected = mgr.resolveEffective({
11676
- fallbackAuto: true,
11677
- exclude: target
11678
- });
11679
- }
11680
- if (config.fallbackAuto === false) return selected;
11681
- if (fromExplicitSource) return selected;
11682
- const combined = [...selected, ...mgr.resolveAllConfigured(target)];
11683
- const seen = /* @__PURE__ */ new Set();
11684
- return Object.freeze(
11685
- combined.filter((entry) => {
11686
- const key = `${entry.providerId}/${entry.model}`;
11687
- if (seen.has(key)) return false;
11688
- seen.add(key);
11689
- return true;
11690
- })
11691
- );
11692
11796
  }
11693
11797
  /** Attempt a provider call while preserving the actual failure for callers. */
11694
11798
  async tryCall(provider, request, signal, providerId, model) {
@@ -12972,6 +13076,12 @@ function normalizeForComparison(text) {
12972
13076
  }
12973
13077
 
12974
13078
  // src/coordination/agents/project-agent-learning-structured.ts
13079
+ function directiveTrials(entry) {
13080
+ const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
13081
+ const applied = count(entry.applied);
13082
+ const wins = Math.min(applied, count(entry.wins));
13083
+ return { applied, wins, losses: applied - wins };
13084
+ }
12975
13085
  function parseLearnedEntryStamp(entry) {
12976
13086
  const structuredMatch = entry.match(
12977
13087
  /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
@@ -13090,6 +13200,10 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
13090
13200
  const category = parseLearnedCategory(attributes["category"]) ?? "fact";
13091
13201
  const capturedAt = attributes["capturedAt"] ?? "";
13092
13202
  const skill = attributes["skill"];
13203
+ const trials = directiveTrials({
13204
+ applied: Number(attributes["applied"]),
13205
+ wins: Number(attributes["wins"])
13206
+ });
13093
13207
  const start = (stamp.index ?? 0) + stamp[0].length;
13094
13208
  const end = stamps[index + 1]?.index ?? raw.length;
13095
13209
  const parsed = parseEntryBody(raw.slice(start, end));
@@ -13101,7 +13215,8 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
13101
13215
  why: parsed.why || WHY_BY_CATEGORY[category],
13102
13216
  how: parsed.how,
13103
13217
  capturedAt,
13104
- ...skill ? { skill } : {}
13218
+ ...skill ? { skill } : {},
13219
+ ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
13105
13220
  });
13106
13221
  }
13107
13222
  if (structured.length === 0) {
@@ -13231,7 +13346,8 @@ function loadProjectAgentLearningPolicy(role, projectRoot) {
13231
13346
  enabled: parsed.enabled !== false,
13232
13347
  lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
13233
13348
  ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
13234
- ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {}
13349
+ ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
13350
+ ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
13235
13351
  };
13236
13352
  } catch {
13237
13353
  return { ...DEFAULT_LEARNING_POLICY };
@@ -13641,6 +13757,8 @@ The file below stores **learning data for this project's "${role}" agent** \u201
13641
13757
  - **Self-contained** \u2014 understandable without the surrounding session context.
13642
13758
  - **Front-load concrete anchors** \u2014 commands in backticks, package names like \`@wrongstack/core\`, file paths like \`packages/core/src/.../foo.ts\`. The structured-list renderer extracts these as the "how" for the entry.
13643
13759
 
13760
+ **Your directives are scored against real outcomes.** After every task the runtime checks which stored directives were actually exercised \u2014 it matches their anchors against the report \u2014 and folds that task's success or failure into each one's record. A directive that keeps correlating with success outlives newer arrivals and survives rewording; one that has been exercised repeatedly and kept correlating with failure is retired and stops being injected. Two consequences for how you write them: anchors are what make a directive *measurable*, not just runnable, so an anchorless directive can never earn a record; and a directive you are unsure about costs nothing to write, because the loop will find out.
13761
+
13644
13762
  **Tag the skill you are developing.** When a directive refines one of your skills, mark it: \`## LEARNED [skill: testing]\`. Tagged directives are distilled into that skill's project addendum, so the lesson arrives as part of the skill itself on every future run instead of as a loose fact. Untagged directives are routed automatically when the wording makes the target obvious, and stay role-level otherwise.
13645
13763
 
13646
13764
  **Bad** (session log \u2014 rejected at capture time):
@@ -13925,8 +14043,16 @@ var TOOLS = {
13925
14043
  "test",
13926
14044
  "mailbox"
13927
14045
  ],
13928
- /** Version control. */
13929
- vcs: ["read", "grep", "glob", "git", "diff"],
14046
+ /**
14047
+ * Version control.
14048
+ *
14049
+ * `mailbox` is in every preset on purpose: a subagent that hits a wall must
14050
+ * be able to say so. This was the one preset without it, which left the `git`
14051
+ * and `release` roles able to fail but not to ask — the two roles whose work
14052
+ * most often needs a decision from the leader (force-push, tag collision,
14053
+ * dirty tree) and least often has a safe default.
14054
+ */
14055
+ vcs: ["read", "grep", "glob", "git", "diff", "mailbox"],
13930
14056
  /** Dependency management + CVE audit. */
13931
14057
  deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
13932
14058
  /** Documentation authoring. */
@@ -16610,6 +16736,14 @@ var SHADOW_AGENT = {
16610
16736
  };
16611
16737
  var CRITIC_AGENT = defineAgent("critic", "Critic");
16612
16738
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
16739
+ function withDispatchMetadata(definition) {
16740
+ const summary = definition.capability?.summary?.trim();
16741
+ if (!summary) return definition.config;
16742
+ return {
16743
+ ...definition.config,
16744
+ dispatch: { summary, keywords: [...definition.capability.keywords ?? []] }
16745
+ };
16746
+ }
16613
16747
  var FLEET_ROSTER = {
16614
16748
  "audit-log": AUDIT_LOG_AGENT,
16615
16749
  "bug-hunter": BUG_HUNTER_AGENT,
@@ -16619,7 +16753,7 @@ var FLEET_ROSTER = {
16619
16753
  generic: GENERIC_AGENT,
16620
16754
  "shadow-agent": SHADOW_AGENT,
16621
16755
  ...Object.fromEntries(
16622
- ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
16756
+ ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
16623
16757
  )
16624
16758
  };
16625
16759
  var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -21244,7 +21378,7 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
21244
21378
  } : { decision: "allow" };
21245
21379
  }
21246
21380
  const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
21247
- if (governanceRequired) {
21381
+ if (governanceRequired && board.lifecycle?.mode === "managed") {
21248
21382
  if (!identity.taskId) {
21249
21383
  return {
21250
21384
  decision: "block",
@@ -21270,9 +21404,11 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
21270
21404
  };
21271
21405
  }
21272
21406
  if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
21407
+ const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
21408
+ const assignmentStatus = task.assignment?.status ?? "missing";
21273
21409
  return {
21274
21410
  decision: "block",
21275
- reason: "Active card must be in Running with a live assignment before product mutation. Call kanban start_task after completing the required card details.",
21411
+ reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
21276
21412
  boardId: board.id,
21277
21413
  taskId: task.id
21278
21414
  };
@@ -44,6 +44,11 @@ export declare class OneShotOrchestrator {
44
44
  private resolveSignal;
45
45
  /**
46
46
  * Build the fallback model chain from input + config + current target.
47
+ * Delegates to {@link FallbackProfileManager.resolveCandidates} — the
48
+ * shared constructor used by both the agent loop and the one-shot path,
49
+ * so both produce identical ordering, depth, and fromExplicitSource
50
+ * semantics.
51
+ *
47
52
  * The injected {@link OneShotOrchestratorOptions.fallbackProfileManager}
48
53
  * is the only allowed manager — OneShot never owns a private snapshot
49
54
  * so a live `ConfigStore` change reaches every call without rebuilding
package/dist/hq/index.js CHANGED
@@ -86,7 +86,7 @@ var MAX_HQ_KANBAN_BOARD_BYTES = 75e4;
86
86
  function isHqKanbanSnapshotPayload(value) {
87
87
  if (typeof value !== "object" || value === null) return false;
88
88
  const payload = value;
89
- if (typeof payload.projectId !== "string" || payload.projectId.length === 0 || typeof payload.generatedAt !== "string" || !Number.isFinite(Date.parse(payload.generatedAt)) || !Array.isArray(payload.boards) || !Array.isArray(payload.tombstones) || payload.boards.length > MAX_HQ_KANBAN_BOARDS || payload.tombstones.length > MAX_HQ_KANBAN_BOARDS) {
89
+ if (typeof payload.projectId !== "string" || payload.projectId.length === 0 || typeof payload.generatedAt !== "string" || !Number.isFinite(Date.parse(payload.generatedAt)) || !Array.isArray(payload.boards) || !Array.isArray(payload.tombstones) || payload.boards.length > MAX_HQ_KANBAN_BOARDS || payload.tombstones.length > MAX_HQ_KANBAN_BOARDS || !isOptionalChunkOrdinal(payload.chunkIndex) || !isOptionalChunkOrdinal(payload.chunkCount)) {
90
90
  return false;
91
91
  }
92
92
  const seen = /* @__PURE__ */ new Set();
@@ -100,6 +100,9 @@ function isHqKanbanSnapshotPayload(value) {
100
100
  }
101
101
  return true;
102
102
  }
103
+ function isOptionalChunkOrdinal(value) {
104
+ return value === void 0 || Number.isSafeInteger(value) && value >= 0;
105
+ }
103
106
  function isBoardRecord(value) {
104
107
  if (typeof value !== "object" || value === null) return false;
105
108
  const record = value;
@@ -1140,12 +1143,11 @@ function redactHqValueInternal(value, options, preserveRawContentKeys) {
1140
1143
  function redactHqValue(value, options = {}) {
1141
1144
  return redactHqValueInternal(value, options, false);
1142
1145
  }
1146
+ function redactHqEventPayload(type, payload, options = {}) {
1147
+ return redactHqValueInternal(payload, options, HQ_PROJECT_STATE_EVENT_TYPES.has(type));
1148
+ }
1143
1149
  function redactHqEvent(event, options = {}) {
1144
- const payload = redactHqValueInternal(
1145
- event.payload,
1146
- options,
1147
- HQ_PROJECT_STATE_EVENT_TYPES.has(event.type)
1148
- );
1150
+ const payload = redactHqEventPayload(event.type, event.payload, options);
1149
1151
  const nextEvent = {
1150
1152
  ...event,
1151
1153
  payload: payload.value
@@ -1322,7 +1324,9 @@ function queuedFrameCoalesceKey(frame) {
1322
1324
  if (frame.type !== "client.event" || !frame.event.type.endsWith(".snapshot")) {
1323
1325
  return void 0;
1324
1326
  }
1325
- return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? ""].join("|");
1327
+ const payload = frame.event.payload;
1328
+ const chunk = typeof payload?.chunkIndex === "number" && Number.isFinite(payload.chunkIndex) ? String(payload.chunkIndex) : "";
1329
+ return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? "", chunk].join("|");
1326
1330
  }
1327
1331
  function defaultSocketFactory(url) {
1328
1332
  const WebSocketCtor = globalThis.WebSocket;
@@ -1655,6 +1659,10 @@ var HqPublisher = class {
1655
1659
  if (bytes > this.maxQueuedBytes) {
1656
1660
  this.droppedFrames += 1;
1657
1661
  this.droppedBytes += bytes;
1662
+ process.emitWarning(
1663
+ `HQ telemetry frame of ${bytes} bytes exceeds the ${this.maxQueuedBytes}-byte offline queue cap and was dropped.`,
1664
+ { code: "WRONGSTACK_HQ_FRAME_TOO_LARGE" }
1665
+ );
1658
1666
  return;
1659
1667
  }
1660
1668
  if (coalesceKey !== void 0) {
@@ -5086,6 +5094,7 @@ export {
5086
5094
  HqSimpleLog,
5087
5095
  HqSnapshotStore,
5088
5096
  HqTimeseriesStore,
5097
+ MAX_HQ_KANBAN_BOARD_BYTES,
5089
5098
  appendHqAuthAudit,
5090
5099
  assessHqExposure,
5091
5100
  buildTranscriptFromEvents,
@@ -5134,6 +5143,7 @@ export {
5134
5143
  readHqAuthFile,
5135
5144
  readHqRuntimeFileSync,
5136
5145
  redactHqEvent,
5146
+ redactHqEventPayload,
5137
5147
  redactHqValue,
5138
5148
  resolveHqConfig,
5139
5149
  resolveHqConfigFromEnv,
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Largest single board record HQ will accept, in bytes of serialized board JSON.
3
+ *
4
+ * Exported because the sender has to know it. `isHqKanbanSnapshotPayload`
5
+ * rejects the *whole* snapshot when one board is over the limit, and the
6
+ * rejection is silent from the sender's point of view — so a publisher that
7
+ * does not check this first can lose every board in the chunk, permanently.
8
+ */
9
+ export declare const MAX_HQ_KANBAN_BOARD_BYTES = 750000;
1
10
  export interface HqKanbanBoardRecord {
2
11
  boardId: string;
3
12
  revision: number;
@@ -16,6 +25,18 @@ export interface HqKanbanSnapshotPayload {
16
25
  generatedAt: string;
17
26
  boards: HqKanbanBoardRecord[];
18
27
  tombstones: HqKanbanTombstone[];
28
+ /**
29
+ * Position of this payload within a multi-part publish, present only when the
30
+ * publish was split. It exists so the offline frame queue can tell chunks of
31
+ * one snapshot apart from successive versions of the same snapshot — coalesce
32
+ * on `chunkIndex`, not on the event type alone, or every chunk but the last
33
+ * is evicted before the socket reconnects.
34
+ *
35
+ * Receivers do not need it: chunks are independent record sets, and each
36
+ * record carries its own revision.
37
+ */
38
+ chunkIndex?: number | undefined;
39
+ chunkCount?: number | undefined;
19
40
  }
20
41
  /** Server-to-client snapshot after HQ has merged all known project writers. */
21
42
  export interface HqServerKanbanSnapshotMessage {
@@ -86,7 +86,7 @@ var MAX_HQ_KANBAN_BOARD_BYTES = 75e4;
86
86
  function isHqKanbanSnapshotPayload(value) {
87
87
  if (typeof value !== "object" || value === null) return false;
88
88
  const payload = value;
89
- if (typeof payload.projectId !== "string" || payload.projectId.length === 0 || typeof payload.generatedAt !== "string" || !Number.isFinite(Date.parse(payload.generatedAt)) || !Array.isArray(payload.boards) || !Array.isArray(payload.tombstones) || payload.boards.length > MAX_HQ_KANBAN_BOARDS || payload.tombstones.length > MAX_HQ_KANBAN_BOARDS) {
89
+ if (typeof payload.projectId !== "string" || payload.projectId.length === 0 || typeof payload.generatedAt !== "string" || !Number.isFinite(Date.parse(payload.generatedAt)) || !Array.isArray(payload.boards) || !Array.isArray(payload.tombstones) || payload.boards.length > MAX_HQ_KANBAN_BOARDS || payload.tombstones.length > MAX_HQ_KANBAN_BOARDS || !isOptionalChunkOrdinal(payload.chunkIndex) || !isOptionalChunkOrdinal(payload.chunkCount)) {
90
90
  return false;
91
91
  }
92
92
  const seen = /* @__PURE__ */ new Set();
@@ -100,6 +100,9 @@ function isHqKanbanSnapshotPayload(value) {
100
100
  }
101
101
  return true;
102
102
  }
103
+ function isOptionalChunkOrdinal(value) {
104
+ return value === void 0 || Number.isSafeInteger(value) && value >= 0;
105
+ }
103
106
  function isBoardRecord(value) {
104
107
  if (typeof value !== "object" || value === null) return false;
105
108
  const record = value;
@@ -673,6 +676,7 @@ export {
673
676
  HQ_RESUME_GAP_MAX_ENVELOPES,
674
677
  HQ_RESUME_GAP_MAX_STALE_MS,
675
678
  HQ_TRANSCRIPT_TEXT_CAP,
679
+ MAX_HQ_KANBAN_BOARD_BYTES,
676
680
  createHqEventEnvelope,
677
681
  isHqGovernanceSnapshotPayload,
678
682
  isHqKanbanSnapshotPayload,
@@ -25,6 +25,20 @@ export declare function tightenHqRedactionPolicy(publisherPolicy: HqRedactionPol
25
25
  */
26
26
  export declare function scrubAndTruncateHqPreview(value: unknown, maxLength?: number): string | undefined;
27
27
  export declare function redactHqValue<T>(value: T, options?: HqRedactOptions): HqRedactionResult<T>;
28
+ /**
29
+ * Redact a payload exactly as it would be redacted when published under `type`.
30
+ *
31
+ * Exported because a sender sometimes needs to know what will go on the wire
32
+ * before it commits to sending — the Kanban→HQ sync measures a board against
33
+ * HQ's per-board byte limit, and HQ drops an over-limit frame without a reply.
34
+ * That measurement has to run the SAME transform as the send, because the
35
+ * event type decides whether raw-content keys survive: `kanban.snapshot` is a
36
+ * project-state event and keeps them, while the generic `redactHqValue` path
37
+ * replaces them with a short marker. Measuring through the generic path made
38
+ * the estimate smaller than reality, so an oversized board could pass the check
39
+ * and still be rejected — the exact silent loss the check exists to prevent.
40
+ */
41
+ export declare function redactHqEventPayload<TPayload>(type: string, payload: TPayload, options?: HqRedactOptions): HqRedactionResult<TPayload>;
28
42
  export declare function redactHqEvent<TPayload>(event: HqEventEnvelope<TPayload>, options?: HqRedactOptions): HqRedactionResult<HqEventEnvelope<TPayload>>;
29
43
  export declare function summarizeHqToolArgs(value: unknown, options?: HqRedactOptions): unknown;
30
44
  //# sourceMappingURL=redaction.d.ts.map
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ export { runProviderWithRetry } from './core/provider-runner.js';
60
60
  export { buildQueuedMessagesBlock, consumeQueuedMessagesUpdate, peekQueuedMessages, setQueuedMessagesSnapshot, } from './core/queued-messages.js';
61
61
  export { extractRunEnv, type RunEnv } from './core/run-env.js';
62
62
  export { DefaultSystemPromptBuilder, type DefaultSystemPromptBuilderOptions, LAYER_1_IDENTITY, SYSTEM_BLOCK_SOURCE, type SystemBlockSource, } from './core/system-prompt-builder.js';
63
+ export { type DomainGlossary, type DomainGlossaryOptions, makeDomainGlossaryContributor, renderDomainGlossary, } from './core/system-prompt-glossary.js';
63
64
  export * from './defaults/index.js';
64
65
  export { AutoCompactionMiddleware, type CompactorOptions, type CompactorStrategy, createStrategyCompactor, HybridCompactor, IntelligentCompactor, type IntelligentCompactorOptions, installSubagentAutoCompaction, SelectiveCompactor, type SelectiveCompactorOptions, type StrategyCompactorOptions, } from './defaults/index.js';
65
66
  export { type AutonomyBrainOptions, type BrainAutoRisk, type BrainLlmTarget, buildBrainUserMessage, completeBrainLlm, createAutonomyBrain, createTieredBrainArbiter, formatDecisionSummary, parseOptionDecision, type TieredBrainArbiterOptions, } from './execution/autonomy-brain.js';