@wrongstack/core 0.303.0 → 0.305.1

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 (76) hide show
  1. package/dist/chronicle/project-server.js +18 -48
  2. package/dist/coordination/agents/index.js +530 -130
  3. package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
  4. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  5. package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
  6. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  7. package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
  8. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  9. package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
  10. package/dist/coordination/agents/types.d.ts +10 -2
  11. package/dist/coordination/director-prompts.d.ts +19 -6
  12. package/dist/coordination/director-tools.d.ts +2 -2
  13. package/dist/coordination/fleet.d.ts +0 -6
  14. package/dist/coordination/index.d.ts +2 -1
  15. package/dist/coordination/index.js +1581 -955
  16. package/dist/coordination/mailbox-project-server.js +28 -57
  17. package/dist/core/agent-types.d.ts +4 -2
  18. package/dist/core/agent.d.ts +1 -0
  19. package/dist/core/context.d.ts +15 -0
  20. package/dist/core/conversation-state.d.ts +14 -0
  21. package/dist/core/fallback-profile-manager.d.ts +70 -2
  22. package/dist/core/index.js +308 -108
  23. package/dist/core/system-prompt-blocks.d.ts +1 -1
  24. package/dist/core/system-prompt-builder.d.ts +13 -1
  25. package/dist/core/system-prompt-glossary.d.ts +73 -0
  26. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  27. package/dist/defaults/index.js +910 -693
  28. package/dist/execution/council-orchestrator.d.ts +3 -13
  29. package/dist/execution/index.js +211 -75
  30. package/dist/execution/one-shot-llm.d.ts +5 -0
  31. package/dist/hq/index.js +17 -7
  32. package/dist/hq/protocol/kanban.d.ts +21 -0
  33. package/dist/hq/protocol.js +5 -1
  34. package/dist/hq/redaction.d.ts +14 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +3505 -2539
  37. package/dist/infrastructure/index.js +247 -122
  38. package/dist/plugin/index.js +101 -3
  39. package/dist/registry/index.js +11 -0
  40. package/dist/registry/tool-registry.d.ts +8 -0
  41. package/dist/replay/hash.d.ts +9 -0
  42. package/dist/replay/index.js +14 -4
  43. package/dist/replay/replay-provider-runner.d.ts +31 -1
  44. package/dist/security/index.js +25 -20
  45. package/dist/security/secret-vault.d.ts +2 -0
  46. package/dist/session-catalog/index.js +62 -8
  47. package/dist/session-catalog/project-server.js +109 -78
  48. package/dist/session-catalog/protocol.d.ts +11 -4
  49. package/dist/session-catalog/store.d.ts +2 -2
  50. package/dist/storage/index.js +224 -67
  51. package/dist/storage/memory-consolidator.d.ts +4 -2
  52. package/dist/storage/session-resume-validation.d.ts +24 -0
  53. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  54. package/dist/storage/session-store/fork-session.d.ts +13 -1
  55. package/dist/storage/session-store/load-cache.d.ts +11 -0
  56. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  57. package/dist/storage/session-store.d.ts +18 -0
  58. package/dist/tools/index.js +174 -74
  59. package/dist/types/config/mcp-features.d.ts +31 -1
  60. package/dist/types/config/root.d.ts +12 -0
  61. package/dist/types/config/tools.d.ts +22 -0
  62. package/dist/types/config/ui.d.ts +7 -4
  63. package/dist/types/default-config.d.ts +1 -0
  64. package/dist/types/index.js +24 -1
  65. package/dist/types/session.d.ts +9 -1
  66. package/dist/utils/index.d.ts +1 -0
  67. package/dist/utils/index.js +214 -76
  68. package/dist/utils/project-state-guard.d.ts +21 -0
  69. package/dist/utils/session-scoped-path.d.ts +17 -0
  70. package/dist/utils/todos-format.d.ts +20 -0
  71. package/instructions/leader-after-task.md +3 -4
  72. package/instructions/system-lite.md +10 -13
  73. package/instructions/system-pro.md +18 -25
  74. package/instructions/system.md +18 -23
  75. package/package.json +3 -3
  76. package/skills/wrongstack-kanban/SKILL.md +95 -124
@@ -2177,12 +2177,11 @@ function redactHqValueInternal(value, options, preserveRawContentKeys) {
2177
2177
  });
2178
2178
  return { value: result.value, redacted: result.redacted };
2179
2179
  }
2180
+ function redactHqEventPayload(type, payload, options = {}) {
2181
+ return redactHqValueInternal(payload, options, HQ_PROJECT_STATE_EVENT_TYPES.has(type));
2182
+ }
2180
2183
  function redactHqEvent(event, options = {}) {
2181
- const payload = redactHqValueInternal(
2182
- event.payload,
2183
- options,
2184
- HQ_PROJECT_STATE_EVENT_TYPES.has(event.type)
2185
- );
2184
+ const payload = redactHqEventPayload(event.type, event.payload, options);
2186
2185
  const nextEvent = {
2187
2186
  ...event,
2188
2187
  payload: payload.value
@@ -2334,7 +2333,9 @@ function queuedFrameCoalesceKey(frame) {
2334
2333
  if (frame.type !== "client.event" || !frame.event.type.endsWith(".snapshot")) {
2335
2334
  return void 0;
2336
2335
  }
2337
- return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? ""].join("|");
2336
+ const payload = frame.event.payload;
2337
+ const chunk = typeof payload?.chunkIndex === "number" && Number.isFinite(payload.chunkIndex) ? String(payload.chunkIndex) : "";
2338
+ return [frame.event.type, frame.event.sessionId ?? "", frame.event.runId ?? "", chunk].join("|");
2338
2339
  }
2339
2340
  function defaultSocketFactory(url) {
2340
2341
  const WebSocketCtor = globalThis.WebSocket;
@@ -2667,6 +2668,10 @@ var HqPublisher = class {
2667
2668
  if (bytes > this.maxQueuedBytes) {
2668
2669
  this.droppedFrames += 1;
2669
2670
  this.droppedBytes += bytes;
2671
+ process.emitWarning(
2672
+ `HQ telemetry frame of ${bytes} bytes exceeds the ${this.maxQueuedBytes}-byte offline queue cap and was dropped.`,
2673
+ { code: "WRONGSTACK_HQ_FRAME_TOO_LARGE" }
2674
+ );
2670
2675
  return;
2671
2676
  }
2672
2677
  if (coalesceKey !== void 0) {
@@ -4338,6 +4343,18 @@ function metadataReferencedByText(metadata, haystack) {
4338
4343
  }
4339
4344
 
4340
4345
  // src/utils/todos-format.ts
4346
+ function formatTodoForModel(todo) {
4347
+ const binding = todo.kanbanBoardId && todo.kanbanTaskId ? ` <kanban ${todo.kanbanBoardId}/${todo.kanbanTaskId}>` : "";
4348
+ const blocked = todo.blockedBy?.length ? ` [blocked by: ${todo.blockedBy.join("; ")}]` : "";
4349
+ return `- [${todo.status}]${blocked} ${todo.content} (${todo.id})${binding}`;
4350
+ }
4351
+ function formatTodosForModel(todos, emptyLine = "- No active todos remain.") {
4352
+ return todos.length ? todos.map(formatTodoForModel).join("\n") : emptyLine;
4353
+ }
4354
+ function hasKanbanBoundTodos(todos) {
4355
+ if (!Array.isArray(todos)) return false;
4356
+ return todos.some((todo) => Boolean(todo.kanbanBoardId && todo.kanbanTaskId));
4357
+ }
4341
4358
  function hasOpenTodos(todos) {
4342
4359
  if (!Array.isArray(todos) || todos.length === 0) return false;
4343
4360
  return todos.some((t2) => t2.status === "pending" || t2.status === "in_progress");
@@ -4620,6 +4637,12 @@ function getCalibrationState(calibrationKey = CALIBRATION_GLOBAL_KEY) {
4620
4637
  import * as path10 from "node:path";
4621
4638
 
4622
4639
  // src/core/conversation-state.ts
4640
+ function hasToolResultBlock(message) {
4641
+ return message !== void 0 && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_result");
4642
+ }
4643
+ function hasToolUseBlock(message) {
4644
+ return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
4645
+ }
4623
4646
  var ConversationState = class {
4624
4647
  ctx;
4625
4648
  listeners = /* @__PURE__ */ new Set();
@@ -4687,15 +4710,32 @@ var ConversationState = class {
4687
4710
  */
4688
4711
  overflowCount(arr) {
4689
4712
  let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
4690
- if (Context.MAX_MESSAGE_TOKENS <= 0) return drop;
4713
+ if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
4691
4714
  let total = 0;
4692
4715
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
4693
- if (total <= Context.MAX_MESSAGE_TOKENS) return drop;
4716
+ if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
4694
4717
  while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
4695
4718
  total -= arr[drop]?._estTokens ?? 0;
4696
4719
  drop++;
4697
4720
  }
4698
- return drop;
4721
+ return this.protocolSafeDropCount(arr, drop);
4722
+ }
4723
+ /**
4724
+ * Front eviction must not retain a `tool_result` after evicting the
4725
+ * immediately preceding assistant `tool_use`. Long tool-heavy sessions sit
4726
+ * at the retention cap, so an unsafe boundary would create a fresh orphan on
4727
+ * nearly every append and make the request-time repair discard protocol
4728
+ * history continuously.
4729
+ *
4730
+ * Move the boundary backward to retain the complete exchange for one more
4731
+ * eviction cycle. Moving it forward would also drop non-protocol text/images
4732
+ * that may share either message. The temporary one-message cap overshoot is
4733
+ * the minimum lossless representation; once enough newer messages exist, the
4734
+ * next eviction boundary naturally moves past both halves together.
4735
+ */
4736
+ protocolSafeDropCount(arr, drop) {
4737
+ if (drop <= 0 || drop >= arr.length) return drop;
4738
+ return hasToolResultBlock(arr[drop]) && hasToolUseBlock(arr[drop - 1]) ? drop - 1 : drop;
4699
4739
  }
4700
4740
  /**
4701
4741
  * Append a content block to the trailing user message's content array.
@@ -5103,6 +5143,28 @@ var Context = class _Context {
5103
5143
  return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
5104
5144
  }
5105
5145
  }
5146
+ _journalDropCount = 0;
5147
+ _journalDropWarnAt = 0;
5148
+ /** Throttled notice that a conversation event never reached the journal. */
5149
+ warnConversationJournalDrop(eventType) {
5150
+ this._journalDropCount++;
5151
+ const now = Date.now();
5152
+ if (now - this._journalDropWarnAt < 5e3) return;
5153
+ this._journalDropWarnAt = now;
5154
+ const dropped = this._journalDropCount;
5155
+ this._journalDropCount = 0;
5156
+ console.warn(
5157
+ JSON.stringify({
5158
+ level: "error",
5159
+ event: "session.conversation_journal_drop",
5160
+ sessionId: this.session?.id,
5161
+ eventType,
5162
+ droppedEvents: dropped,
5163
+ message: "Session writer is not draining; replay of this session will be incomplete.",
5164
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5165
+ })
5166
+ );
5167
+ }
5106
5168
  enqueueConversationJournal(event, writer) {
5107
5169
  const bytes = this.conversationJournalBytes(event);
5108
5170
  const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
@@ -5120,18 +5182,21 @@ var Context = class _Context {
5120
5182
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
5121
5183
  this._conversationJournalQueue.splice(index, 1);
5122
5184
  }
5123
- if (snapshotBytes <= _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
5124
- this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
5125
- this._conversationJournalBytes += snapshotBytes;
5126
- }
5185
+ this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
5186
+ this._conversationJournalBytes += snapshotBytes;
5127
5187
  } else {
5128
5188
  this._conversationJournalQueue.push({ event, bytes, writer });
5129
5189
  this._conversationJournalBytes += bytes;
5130
5190
  }
5131
5191
  while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
5132
- const dropped = this._conversationJournalQueue.shift();
5192
+ const index = this._conversationJournalQueue.findIndex(
5193
+ (queued) => queued.event.type !== "messages_replaced"
5194
+ );
5195
+ if (index === -1) break;
5196
+ const [dropped] = this._conversationJournalQueue.splice(index, 1);
5133
5197
  if (!dropped) break;
5134
5198
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
5199
+ this.warnConversationJournalDrop(dropped.event.type);
5135
5200
  }
5136
5201
  this.startConversationJournalDrain();
5137
5202
  }
@@ -7220,7 +7285,7 @@ ${text}` : text;
7220
7285
  if (a.ctx.agentId === "leader" && a.tools.get("todo") !== void 0 && hasOpenTodos(a.ctx.todos) && todoReconcileSteers < 2) {
7221
7286
  todoReconcileSteers++;
7222
7287
  queueLoopSteer(
7223
- "[todo-reconciliation] The live todo/Kanban list still has open work, but you tried to end the turn without reconciling it. Call the `todo` tool now with the complete current list. Mark work you actually finished as completed, put the one item you are actively working on in_progress, and leave the rest pending. If the current item is genuinely unfinished, continue doing the work before answering; do not merely repeat the previous final response or emit <nextsteps>."
7288
+ "[todo-reconciliation] The live todo/Kanban list still has open work, but you tried to end the turn without reconciling it. Call the `todo` tool now with the complete current list. Mark work you actually finished as completed, put the one item you are actively working on in_progress, and leave the rest pending. If the current item is genuinely unfinished, continue doing the work before answering; do not merely repeat the previous final response or emit <nextsteps>.\nCanonical live list:\n" + formatTodosForModel(a.ctx.todos) + (hasKanbanBoundTodos(a.ctx.todos) ? "\nEach <kanban board/task> binding must be resent verbatim as `kanbanBoardId`/`kanbanTaskId`; a row without it is not applied to its card." : "")
7224
7289
  );
7225
7290
  await a.extensions.runAfterIteration(a.ctx, i);
7226
7291
  continue;
@@ -7441,12 +7506,15 @@ function buildLiveNextStepsGateBlock(ctx) {
7441
7506
  const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
7442
7507
  const normalized = todo.content.replace(/\s+/g, " ").trim();
7443
7508
  const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
7444
- return `- [${todo.status}] ${content}`;
7509
+ return formatTodoForModel({ ...todo, content });
7445
7510
  });
7446
7511
  const omitted = openTodos.length - todoSnapshot.length;
7447
7512
  if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
7448
7513
  const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
7449
- "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state."
7514
+ "Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state.",
7515
+ ...hasKanbanBoundTodos(openTodos) ? [
7516
+ "Rows below carry a <kanban board/task> binding. Pass those exact ids back as `kanbanBoardId`/`kanbanTaskId` on every row you resend; a row that arrives without its binding is not applied to its card."
7517
+ ] : []
7450
7518
  ] : [];
7451
7519
  return {
7452
7520
  type: "text",
@@ -8162,6 +8230,7 @@ var Agent = class {
8162
8230
  /** Resolved loop-detector settings (see `tools.loopDetection`). */
8163
8231
  loopDetection;
8164
8232
  autonomousContinue;
8233
+ refreshSystemPrompt;
8165
8234
  tracer;
8166
8235
  extensions;
8167
8236
  _toolHandler;
@@ -8191,6 +8260,7 @@ var Agent = class {
8191
8260
  this.autoExtendLimit = init.autoExtendLimit ?? true;
8192
8261
  this.loopDetection = resolveLoopDetection(init.loopDetection);
8193
8262
  this.autonomousContinue = init.autonomousContinue ?? false;
8263
+ this.refreshSystemPrompt = init.refreshSystemPrompt ?? false;
8194
8264
  this.tracer = init.tracer;
8195
8265
  this.extensions = init.extensions ?? new ExtensionRegistry();
8196
8266
  this._logger = this.container.resolve(TOKENS.Logger).child({ sessionId: this.ctx.session.id });
@@ -8268,6 +8338,7 @@ var Agent = class {
8268
8338
  context: { phase: "concurrency-guard" }
8269
8339
  });
8270
8340
  }
8341
+ let newInputHash;
8271
8342
  const inputText = typeof userInput === "string" ? userInput : userInput?.prompt ?? "";
8272
8343
  if (inputText.length > 0) {
8273
8344
  const hash2 = createHash7("sha256").update(inputText).digest("hex");
@@ -8277,7 +8348,7 @@ var Agent = class {
8277
8348
  iterations: 0
8278
8349
  };
8279
8350
  }
8280
- this._lastInputHash = hash2;
8351
+ newInputHash = hash2;
8281
8352
  }
8282
8353
  this._runInProgress = true;
8283
8354
  const controller = new RunController({ parentSignal: opts.signal });
@@ -8294,16 +8365,33 @@ var Agent = class {
8294
8365
  });
8295
8366
  this.ctx.tools = this.tools.listForProvider();
8296
8367
  this.ctx.catalogTools = this.tools.list();
8297
- const span = this.tracer?.startSpan("agent.run", {
8298
- "agent.model": opts.model ?? this.ctx.model,
8299
- "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
8300
- });
8301
- const { blocks, text } = normalizeInput(userInput);
8302
- const inputPayload = { content: blocks, text, ctx: this.ctx };
8303
- await this.extensions.runBeforeRun(this.ctx, inputPayload);
8304
8368
  const runStartedAt = Date.now();
8305
8369
  const runStartedIso = new Date(runStartedAt).toISOString();
8370
+ let span;
8306
8371
  try {
8372
+ if (this.refreshSystemPrompt) {
8373
+ const builder = this.container.safeResolve(TOKENS.SystemPromptBuilder);
8374
+ if (builder) {
8375
+ const onlineAgents = Array.isArray(this.ctx.meta["promptOnlineAgents"]) ? this.ctx.meta["promptOnlineAgents"] : void 0;
8376
+ this.ctx.systemPrompt = await builder.build({
8377
+ cwd: this.ctx.cwd,
8378
+ projectRoot: this.ctx.projectRoot,
8379
+ tools: this.ctx.tools,
8380
+ catalogTools: this.ctx.catalogTools,
8381
+ provider: this.ctx.provider.id,
8382
+ model: opts.model ?? this.ctx.model,
8383
+ onlineAgents
8384
+ });
8385
+ }
8386
+ }
8387
+ span = this.tracer?.startSpan("agent.run", {
8388
+ "agent.model": opts.model ?? this.ctx.model,
8389
+ "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
8390
+ });
8391
+ const { blocks, text } = normalizeInput(userInput);
8392
+ const inputPayload = { content: blocks, text, ctx: this.ctx };
8393
+ await this.extensions.runBeforeRun(this.ctx, inputPayload);
8394
+ this._lastInputHash = newInputHash;
8307
8395
  this.events.emit("agent.run.started", {
8308
8396
  sessionId,
8309
8397
  ctx: this.ctx,
@@ -8771,12 +8859,97 @@ var FallbackProfileManager = class {
8771
8859
  return this.resolveRefs([ref], exclude);
8772
8860
  }
8773
8861
  /**
8774
- * Resolve every usable configured target as an uncapped last-resort chain.
8862
+ * Resolve every usable configured target as a bounded last-resort chain.
8775
8863
  * Normal smart defaults stay bounded; callers append this only after the
8776
- * preferred chain and only when automatic fallback is enabled.
8864
+ * preferred chain and only when automatic fallback is enabled. The cap
8865
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
8866
+ * from producing a degenerate chain of doomed requests during a systemic
8867
+ * outage — by this point the smart default, bridge, and default profile
8868
+ * have already failed.
8777
8869
  */
8778
8870
  resolveAllConfigured(exclude) {
8779
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
8871
+ return this.smartDefault(exclude, this.lastResortCap());
8872
+ }
8873
+ /**
8874
+ * Effective cap for the last-resort append. Reads the user-configurable
8875
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
8876
+ * otherwise falls back to the compiled-in default
8877
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
8878
+ */
8879
+ lastResortCap() {
8880
+ const configured = this.config.fallbackMaxLastResortCandidates;
8881
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
8882
+ return Math.floor(configured);
8883
+ }
8884
+ return MAX_LAST_RESORT_CANDIDATES;
8885
+ }
8886
+ /**
8887
+ * Build the complete fallback candidate chain shared by the agent-loop
8888
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
8889
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
8890
+ * gate so both consumers produce identical ordering and depth semantics.
8891
+ *
8892
+ * Layering (each step deduped against all prior):
8893
+ * 1. Bridge (emergency continuity route).
8894
+ * 2. Configured primary, when the live context drifted from it.
8895
+ * 3. The selected chain (explicit refs → named profile → smart default),
8896
+ * via {@link resolveEffective}.
8897
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
8898
+ * 5. Every other configured provider — last resort, ONLY when the chain was
8899
+ * auto-derived AND `effectiveFallbackAuto` is true.
8900
+ *
8901
+ * Returns the empty chain when `closedWorld` is true and no explicit
8902
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
8903
+ *
8904
+ * @internal caller-aware options (primary, closedWorld) are accepted because
8905
+ * the agent loop has context the manager does not own; the resolution
8906
+ * pipeline itself is identical for both callers.
8907
+ */
8908
+ resolveCandidates(current, opts = {}) {
8909
+ const configuredPrimary = opts.primary ?? {
8910
+ providerId: this.config.provider,
8911
+ model: this.config.model
8912
+ };
8913
+ const configFallbackAuto = this.config.fallbackAuto;
8914
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
8915
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
8916
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
8917
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
8918
+ const fromExplicitSource = explicitUsable || profileUsable;
8919
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
8920
+ fallbackModels: explicitRefs,
8921
+ fallbackProfile: opts.fallbackProfile,
8922
+ fallbackAuto: effectiveFallbackAuto,
8923
+ exclude: current
8924
+ });
8925
+ const candidates = [];
8926
+ if (opts.closedWorld) {
8927
+ candidates.push(...selectedChain);
8928
+ } else {
8929
+ candidates.push(...this.resolveBridge(current));
8930
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
8931
+ candidates.push({
8932
+ providerId: configuredPrimary.providerId,
8933
+ model: configuredPrimary.model,
8934
+ providerSwitched: configuredPrimary.providerId !== current.providerId
8935
+ });
8936
+ }
8937
+ candidates.push(...selectedChain);
8938
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
8939
+ candidates.push(...this.resolve("default", { exclude: current }));
8940
+ }
8941
+ if (!fromExplicitSource && effectiveFallbackAuto) {
8942
+ const cap = this.lastResortCap();
8943
+ if (cap > 0) {
8944
+ const usedKeys = new Set(
8945
+ candidates.map((c) => `${c.providerId}/${c.model}`)
8946
+ );
8947
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
8948
+ candidates.push(...lastResort);
8949
+ }
8950
+ }
8951
+ }
8952
+ return dedupeChain(candidates, current);
8780
8953
  }
8781
8954
  // ── Provider availability (read-only) ──────────────────────────────────
8782
8955
  checkProvider(providerId) {
@@ -8900,7 +9073,20 @@ var FallbackProfileManager = class {
8900
9073
  );
8901
9074
  }
8902
9075
  };
9076
+ var MAX_LAST_RESORT_CANDIDATES = 12;
8903
9077
  var FREEZER_EMPTY = Object.freeze([]);
9078
+ function dedupeChain(entries, current) {
9079
+ const seen = /* @__PURE__ */ new Set();
9080
+ const currentKey = `${current.providerId}/${current.model}`;
9081
+ return Object.freeze(
9082
+ entries.filter((entry) => {
9083
+ const key = `${entry.providerId}/${entry.model}`;
9084
+ if (key === currentKey || seen.has(key)) return false;
9085
+ seen.add(key);
9086
+ return true;
9087
+ })
9088
+ );
9089
+ }
8904
9090
 
8905
9091
  // src/core/fallback-model.ts
8906
9092
  function fallbackProfileChain(config, profileName) {
@@ -8949,56 +9135,12 @@ function sameTarget(a, b) {
8949
9135
  }
8950
9136
  function fallbackCandidates(config, current, opts = {}) {
8951
9137
  const mgr = opts.sharedManager ?? new FallbackProfileManager(config);
8952
- const configuredPrimary = opts.primary ?? primaryTarget(config);
8953
- const configFallbackAuto = config.fallbackAuto;
8954
- const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
8955
- const explicitRefs = opts.fallbackModels ?? config.fallbackModels;
8956
- const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && mgr.resolveRefs(explicitRefs, current).length > 0;
8957
- const profileUsable = opts.fallbackProfile !== void 0 && mgr.hasProfile(opts.fallbackProfile) && mgr.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
8958
- const fromExplicitSource = explicitUsable || profileUsable;
8959
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? mgr.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? mgr.resolve(opts.fallbackProfile, { exclude: current }) : Object.freeze([]) : mgr.resolveEffective({
8960
- fallbackModels: explicitRefs,
9138
+ return mgr.resolveCandidates(current, {
9139
+ fallbackModels: opts.fallbackModels,
8961
9140
  fallbackProfile: opts.fallbackProfile,
8962
- fallbackAuto: effectiveFallbackAuto,
8963
- exclude: current
9141
+ primary: opts.primary ?? primaryTarget(config),
9142
+ closedWorld: opts.closedWorld
8964
9143
  });
8965
- const candidates = [];
8966
- if (opts.closedWorld) {
8967
- candidates.push(...selectedChain);
8968
- const seen2 = /* @__PURE__ */ new Set();
8969
- return Object.freeze(
8970
- candidates.filter((entry) => {
8971
- const key = `${entry.providerId}/${entry.model}`;
8972
- if (key === `${current.providerId}/${current.model}` || seen2.has(key)) return false;
8973
- seen2.add(key);
8974
- return true;
8975
- })
8976
- );
8977
- }
8978
- candidates.push(...mgr.resolveBridge(current));
8979
- if (!sameTarget(configuredPrimary, current)) {
8980
- candidates.push({
8981
- providerId: configuredPrimary.providerId,
8982
- model: configuredPrimary.model,
8983
- providerSwitched: configuredPrimary.providerId !== current.providerId
8984
- });
8985
- }
8986
- candidates.push(...selectedChain);
8987
- if (!fromExplicitSource && opts.fallbackProfile !== "default") {
8988
- candidates.push(...mgr.resolve("default", { exclude: current }));
8989
- }
8990
- if (!fromExplicitSource && effectiveFallbackAuto) {
8991
- candidates.push(...mgr.resolveAllConfigured(current));
8992
- }
8993
- const seen = /* @__PURE__ */ new Set();
8994
- return Object.freeze(
8995
- candidates.filter((entry) => {
8996
- const key = `${entry.providerId}/${entry.model}`;
8997
- if (key === `${current.providerId}/${current.model}` || seen.has(key)) return false;
8998
- seen.add(key);
8999
- return true;
9000
- })
9001
- );
9002
9144
  }
9003
9145
  var primaryTarget = (cfg) => ({ providerId: cfg.provider, model: cfg.model });
9004
9146
  function maxContextOf(provider) {
@@ -10500,7 +10642,7 @@ async function buildEnvironment(ctx, env) {
10500
10642
  ]);
10501
10643
  const tier = env.tier;
10502
10644
  const lines = ["## Environment"];
10503
- if (tier === "minimal") {
10645
+ if (tier === "minimal" || tier === "aggressive") {
10504
10646
  lines.push(`- Git: ${git} | Date: ${today}`);
10505
10647
  } else {
10506
10648
  lines.push(`- Operating system: ${platform2}`);
@@ -10508,38 +10650,26 @@ async function buildEnvironment(ctx, env) {
10508
10650
  lines.push(`- Shell: ${shell}`);
10509
10651
  lines.push(`- Node.js: ${node}`);
10510
10652
  }
10511
- if (tier === "off" || tier === "medium" || tier === "aggressive") {
10653
+ if (tier === "off" || tier === "medium") {
10512
10654
  lines.push(`- Detected languages: ${langs}`);
10513
10655
  }
10514
10656
  lines.push(`- Git status: ${git}`);
10515
10657
  lines.push(`- Today's date: ${today}`);
10516
- if (tier === "aggressive") {
10517
- if (ctx.provider || ctx.model) {
10518
- lines.push(
10519
- `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
10520
- );
10521
- }
10522
- if (modelCapabilities) {
10523
- lines.push(
10524
- `- Context window: ${modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
10525
- );
10526
- }
10527
- }
10528
- if (tier !== "aggressive" && modelCapabilities) {
10658
+ if (modelCapabilities) {
10529
10659
  lines.push(
10530
10660
  `- Context window: ${modelCapabilities.maxContextTokens.toLocaleString()} tokens max`
10531
10661
  );
10532
10662
  }
10533
- if (tier !== "aggressive" && (ctx.provider || ctx.model)) {
10663
+ if (ctx.provider || ctx.model) {
10534
10664
  lines.push(
10535
10665
  `- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
10536
10666
  );
10537
10667
  }
10538
- if (tier !== "aggressive" && env.modeId && env.modeId !== "default") {
10668
+ if (env.modeId && env.modeId !== "default") {
10539
10669
  lines.push(`- Mode: ${env.modeId}`);
10540
10670
  }
10541
10671
  }
10542
- if (ctx.tools.some((tool) => tool.name === "bash") && effShell !== "posix" && tier !== "minimal") {
10672
+ if (ctx.tools.some((tool) => tool.name === "bash") && effShell !== "posix" && tier !== "minimal" && tier !== "aggressive") {
10543
10673
  const guide = shellGuidanceBlock(effShell, tier === "light" ? "short" : "full");
10544
10674
  if (guide) lines.push("", guide);
10545
10675
  }
@@ -10562,6 +10692,70 @@ async function buildEnvironment(ctx, env) {
10562
10692
  return text;
10563
10693
  }
10564
10694
 
10695
+ // src/core/system-prompt-glossary.ts
10696
+ function findDomainGlossaryEntries(memory) {
10697
+ return safeList(memory, { limit: 200 }).then((rows) => rows ?? []).then(
10698
+ (rows) => rows.filter(
10699
+ (r) => Array.isArray(r?.tags) && r.tags.includes("domain-term")
10700
+ )
10701
+ );
10702
+ }
10703
+ async function safeList(memory, opts) {
10704
+ try {
10705
+ const rows = await memory.list("project-memory", opts.limit);
10706
+ if (!Array.isArray(rows) || rows.length === 0) return null;
10707
+ return rows;
10708
+ } catch {
10709
+ return null;
10710
+ }
10711
+ }
10712
+ var GLOSSARY_HEADING = "# Project Jargon Dictionary (auto-mined from SAGE; update via SageDomainTermExtractor)";
10713
+ async function renderDomainGlossary(ctx, memory, options = {}) {
10714
+ if (!memory) return null;
10715
+ const maxEntries = options.maxEntries ?? 12;
10716
+ const maxEntryChars = options.maxEntryChars ?? 80;
10717
+ const entries = await findDomainGlossaryEntries(memory);
10718
+ if (entries.length === 0) return "";
10719
+ const sorted = [...entries].sort((a, b) => {
10720
+ const dc = (b.confidence ?? 0) - (a.confidence ?? 0);
10721
+ if (dc !== 0) return dc;
10722
+ return b.ts.localeCompare(a.ts);
10723
+ });
10724
+ const lines = [GLOSSARY_HEADING, ""];
10725
+ let emitted = 0;
10726
+ for (const entry of sorted) {
10727
+ if (emitted >= maxEntries) break;
10728
+ const { term, definition } = parseTermEntry(entry.text);
10729
+ if (!term) continue;
10730
+ const def = (definition || "").slice(0, maxEntryChars).trim();
10731
+ lines.push(def ? `- **${term}** \u2014 ${def}` : `- **${term}**`);
10732
+ emitted += 1;
10733
+ }
10734
+ if (emitted === 0) return "";
10735
+ lines.push("");
10736
+ lines.push(
10737
+ `_Source: <${ctx.projectRoot}>/.wrongstack/domain-terms.md (auto-generated; do not edit by hand)._`
10738
+ );
10739
+ lines.push(
10740
+ '_Lookup tag: `domain-term` (search: `memory_search({ query: "domain-term", scope: "project-memory" })`)._'
10741
+ );
10742
+ return lines.join("\n");
10743
+ }
10744
+ function parseTermEntry(text) {
10745
+ const trimmed = text.trim();
10746
+ const separators = [" \u2014 ", " \u2013 ", " - ", " -- "];
10747
+ for (const sep of separators) {
10748
+ const idx = trimmed.indexOf(sep);
10749
+ if (idx > 0) {
10750
+ return {
10751
+ term: trimmed.slice(0, idx).trim(),
10752
+ definition: trimmed.slice(idx + sep.length).trim()
10753
+ };
10754
+ }
10755
+ }
10756
+ return { term: trimmed, definition: "" };
10757
+ }
10758
+
10565
10759
  // src/skills/limits.ts
10566
10760
  var SKILL_LIMITS = {
10567
10761
  /**
@@ -10828,7 +11022,7 @@ function renderOnlineAgents(agents, tier, cache) {
10828
11022
  return { text: cache.text, cache };
10829
11023
  }
10830
11024
  const totalCount = agents.length;
10831
- if (tier === "minimal" || tier === "light") {
11025
+ if (tier === "minimal" || tier === "light" || tier === "aggressive") {
10832
11026
  const text2 = ` (${totalCount} agent${totalCount !== 1 ? "s" : ""} online)`;
10833
11027
  return { text: text2, cache: { hash: hash2, text: text2 } };
10834
11028
  }
@@ -10853,8 +11047,8 @@ ${agentList}`;
10853
11047
  async function buildMemoryAndSkills(mem) {
10854
11048
  const parts = [];
10855
11049
  let skillBodyCache = mem.skillBodyCache;
10856
- const memoryCount = mem.tier === "minimal" || mem.tier === "light" ? 3 : 5;
10857
- const compactMemory = mem.tier === "minimal";
11050
+ const memoryCount = mem.tier === "minimal" || mem.tier === "light" || mem.tier === "aggressive" ? 3 : 5;
11051
+ const compactMemory = mem.tier === "minimal" || mem.tier === "aggressive";
10858
11052
  if (mem.memoryStore && mem.injectMemory !== false) {
10859
11053
  try {
10860
11054
  if (mem.memoryStore.scoreRelevant) {
@@ -11035,7 +11229,7 @@ var DefaultSystemPromptBuilder = class {
11035
11229
  }
11036
11230
  /**
11037
11231
  * Returns the max tool description length for the current tier.
11038
- * Per the design doc: off=80, minimal=40, light=50, medium=60, aggressive=70.
11232
+ * The aggressive tier has the shortest descriptions; off keeps the most detail.
11039
11233
  */
11040
11234
  toolDescLimit() {
11041
11235
  switch (this.tier) {
@@ -11046,7 +11240,7 @@ var DefaultSystemPromptBuilder = class {
11046
11240
  case "medium":
11047
11241
  return 50;
11048
11242
  case "aggressive":
11049
- return 60;
11243
+ return 20;
11050
11244
  default:
11051
11245
  return 70;
11052
11246
  }
@@ -11158,6 +11352,12 @@ var DefaultSystemPromptBuilder = class {
11158
11352
  }
11159
11353
  }
11160
11354
  }
11355
+ if (this.opts.domainGlossary) {
11356
+ const glossary = await renderDomainGlossary(ctx, this.opts.domainGlossary);
11357
+ if (glossary) {
11358
+ volatile.push(tagBlock({ type: "text", text: glossary }, "glossary"));
11359
+ }
11360
+ }
11161
11361
  if (!ctx.subagent) {
11162
11362
  session.push(
11163
11363
  tagBlock(
@@ -11315,8 +11515,8 @@ ${hint.trim()}`);
11315
11515
  return Array.isArray(role) ? role.filter((r) => typeof r === "string") : [];
11316
11516
  })();
11317
11517
  const roleList = enumValues.length > 0 ? enumValues.join(", ") : "(no roster configured)";
11318
- if (this.tier === "minimal") {
11319
- } else if (this.tier === "light" || this.tier === "medium" || this.tier === "aggressive") {
11518
+ if (this.tier === "minimal" || this.tier === "aggressive") {
11519
+ } else if (this.tier === "light" || this.tier === "medium") {
11320
11520
  const delegation = section("tool.delegation.compact", {
11321
11521
  roleList
11322
11522
  });
@@ -11352,7 +11552,7 @@ ${hint.trim()}`);
11352
11552
  }
11353
11553
  }
11354
11554
  const hasGitTool = tools.some((t2) => t2.name === "git");
11355
- if (hasGitTool && this.tier !== "minimal" && this.tier !== "light") {
11555
+ if (hasGitTool && this.tier !== "minimal" && this.tier !== "light" && this.tier !== "aggressive") {
11356
11556
  const commitHygiene = section("tool.commit.hygiene");
11357
11557
  if (commitHygiene) lines.push(commitHygiene);
11358
11558
  }
@@ -11369,7 +11569,7 @@ ${hint.trim()}`);
11369
11569
  }
11370
11570
  const hasContextManager = tools.some((t2) => t2.name === "context_manager");
11371
11571
  if (hasContextManager) {
11372
- if (this.tier === "minimal" || this.tier === "light") {
11572
+ if (this.tier === "minimal" || this.tier === "light" || this.tier === "aggressive") {
11373
11573
  } else if (this.tier === "medium") {
11374
11574
  const contextManagement = section("tool.context.management.compact");
11375
11575
  if (contextManagement) lines.push(contextManagement);
@@ -11422,11 +11622,11 @@ ${hint.trim()}`);
11422
11622
  return result.text;
11423
11623
  }
11424
11624
  async buildMode() {
11425
- if (this.opts.modePrompt) return this.opts.modePrompt;
11426
- if (!this.opts.modeStore) return "";
11427
- const mode = await this.opts.modeStore.getActiveMode();
11428
- if (!mode?.prompt) return "";
11429
- return mode.prompt;
11625
+ if (this.opts.modeStore) {
11626
+ const mode = await this.opts.modeStore.getActiveMode();
11627
+ if (mode?.prompt) return mode.prompt;
11628
+ }
11629
+ return this.opts.modePrompt ?? "";
11430
11630
  }
11431
11631
  };
11432
11632
  export {
@@ -18,7 +18,7 @@ import { type InstructionTemplateContext } from './instruction-template.js';
18
18
  * `getContextBreakdown()` to attribute real token counts per category in the
19
19
  * `/context` display.
20
20
  */
21
- export type SystemBlockSource = 'identity' | 'tool-usage' | 'environment' | 'skills' | 'mode' | 'plan' | 'leader-after-task' | 'contributor' | 'ledger' | 'nextsteps';
21
+ export type SystemBlockSource = 'identity' | 'tool-usage' | 'environment' | 'skills' | 'mode' | 'plan' | 'leader-after-task' | 'contributor' | 'ledger' | 'glossary' | 'nextsteps';
22
22
  /**
23
23
  * Side-table mapping each system-prompt TextBlock to the section it came from.
24
24
  * Kept as a WeakMap rather than a field on TextBlock so the label never reaches