@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
@@ -9,6 +9,17 @@ export declare class SessionLoadCache {
9
9
  constructor(entries?: Map<string, LoadCacheEntry>);
10
10
  private bytes;
11
11
  clear(sessionId?: string): void;
12
+ /**
13
+ * A hit hands back fresh `messages` / `events` arrays over the cached
14
+ * contents.
15
+ *
16
+ * The entry outlives every caller, and callers treat what they get as their
17
+ * own: `resume()` passes `messages` straight into a live conversation, and
18
+ * anything walking `events` may splice it. Returning the cached arrays
19
+ * themselves let one caller's edit rewrite what the next one loads. The
20
+ * elements are still shared — copying them would defeat the cache — so
21
+ * entries remain read-only *contents* behind private containers.
22
+ */
12
23
  getFresh(id: string, stat: FileStatSnapshot, full: boolean): SessionData | null;
13
24
  set(id: string, stat: FileStatSnapshot, data: SessionData): void;
14
25
  private delete;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Prunable === is a transcript. This module held the only complete sidecar
3
+ * list in the codebase while the listing scans held shorter ones; sharing the
4
+ * predicate is what stops them disagreeing again.
5
+ */
1
6
  export declare function isPrunableSessionJsonl(name: string): boolean;
2
7
  export declare function pruneSessionFiles(storeDir: string, maxAgeDays: number, deleteSession: (id: string) => Promise<void>): Promise<number>;
3
8
  //# sourceMappingURL=prune-helpers.d.ts.map
@@ -57,6 +57,18 @@ export declare class DefaultSessionStore implements SessionStore {
57
57
  private ensureShardDir;
58
58
  create(meta: Omit<SessionMetadata, 'startedAt'>): Promise<SessionWriter>;
59
59
  fork(id: string, opts?: SessionForkOptions): Promise<ForkedSession>;
60
+ /**
61
+ * Implements {@link SessionForkHost.readRawEvents} — the parent stream a fork
62
+ * inherits, unmodified.
63
+ *
64
+ * Deliberately NOT `load()`: that loader empties superseded snapshot payloads
65
+ * in place and front-drops events past its retention budget, both of which
66
+ * are correct for reconstructing a conversation and wrong for copying a
67
+ * journal prefix into a child. Streaming with an accept-everything predicate
68
+ * keeps the scrubbing contract (`searchEvents` scrubs each line the same way
69
+ * `load()` does) without either transformation.
70
+ */
71
+ readRawEvents(id: string): Promise<SessionEvent[]>;
60
72
  /**
61
73
  * Capture the deterministic post-tool workspace identity through the store-owned CAS.
62
74
  */
@@ -167,6 +179,12 @@ export declare class DefaultSessionStore implements SessionStore {
167
179
  private listFromDirectoryScan;
168
180
  private collectShardKeys;
169
181
  private readOrBuildShardManifest;
182
+ /**
183
+ * Shard manifests are invalidated by other store processes via atomic
184
+ * delete/rebuild. Validate the in-memory projection against the persisted
185
+ * file so one long-lived process cannot retain another process's stale view.
186
+ */
187
+ private freshShardManifestCacheEntry;
170
188
  private collectSessionFilesInShard;
171
189
  private collectSessionFiles;
172
190
  /** Recursively collect session IDs from date-shard subdirectories.
@@ -614,17 +614,6 @@ var CouncilOrchestrator = class {
614
614
  fallbackProfileManager;
615
615
  seatCaller;
616
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();
628
617
  constructor(opts) {
629
618
  if (!opts.caller && !opts.seatCaller && !opts.judgeCaller) {
630
619
  throw new Error("CouncilOrchestrator: provide `caller`, `seatCaller`, or `judgeCaller`.");
@@ -644,8 +633,9 @@ var CouncilOrchestrator = class {
644
633
  /**
645
634
  * Resolve the effective profile for a question. String ids and the default
646
635
  * 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().
636
+ * normalized on each call. Callers are allowed to reuse and mutate an
637
+ * ad-hoc object between decisions, so object-identity caching would serve a
638
+ * stale snapshot.
649
639
  */
650
640
  resolveProfile(profile) {
651
641
  if (typeof profile === "string" || profile === void 0) {
@@ -655,15 +645,11 @@ var CouncilOrchestrator = class {
655
645
  defaultProfile: this.defaultProfile
656
646
  });
657
647
  }
658
- const cached = this.profileCache.get(profile);
659
- if (cached) return cached;
660
- const resolved = resolveCouncilProfile(profile, {
648
+ return resolveCouncilProfile(profile, {
661
649
  registry: this.profiles,
662
650
  personas: this.personas,
663
651
  defaultProfile: this.defaultProfile
664
652
  });
665
- this.profileCache.set(profile, resolved);
666
- return resolved;
667
653
  }
668
654
  async ask(question) {
669
655
  const startedAt = Date.now();
@@ -2164,12 +2150,97 @@ var FallbackProfileManager = class {
2164
2150
  return this.resolveRefs([ref], exclude);
2165
2151
  }
2166
2152
  /**
2167
- * Resolve every usable configured target as an uncapped last-resort chain.
2153
+ * Resolve every usable configured target as a bounded last-resort chain.
2168
2154
  * Normal smart defaults stay bounded; callers append this only after the
2169
- * preferred chain and only when automatic fallback is enabled.
2155
+ * preferred chain and only when automatic fallback is enabled. The cap
2156
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
2157
+ * from producing a degenerate chain of doomed requests during a systemic
2158
+ * outage — by this point the smart default, bridge, and default profile
2159
+ * have already failed.
2170
2160
  */
2171
2161
  resolveAllConfigured(exclude) {
2172
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
2162
+ return this.smartDefault(exclude, this.lastResortCap());
2163
+ }
2164
+ /**
2165
+ * Effective cap for the last-resort append. Reads the user-configurable
2166
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
2167
+ * otherwise falls back to the compiled-in default
2168
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
2169
+ */
2170
+ lastResortCap() {
2171
+ const configured = this.config.fallbackMaxLastResortCandidates;
2172
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
2173
+ return Math.floor(configured);
2174
+ }
2175
+ return MAX_LAST_RESORT_CANDIDATES;
2176
+ }
2177
+ /**
2178
+ * Build the complete fallback candidate chain shared by the agent-loop
2179
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
2180
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
2181
+ * gate so both consumers produce identical ordering and depth semantics.
2182
+ *
2183
+ * Layering (each step deduped against all prior):
2184
+ * 1. Bridge (emergency continuity route).
2185
+ * 2. Configured primary, when the live context drifted from it.
2186
+ * 3. The selected chain (explicit refs → named profile → smart default),
2187
+ * via {@link resolveEffective}.
2188
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
2189
+ * 5. Every other configured provider — last resort, ONLY when the chain was
2190
+ * auto-derived AND `effectiveFallbackAuto` is true.
2191
+ *
2192
+ * Returns the empty chain when `closedWorld` is true and no explicit
2193
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
2194
+ *
2195
+ * @internal caller-aware options (primary, closedWorld) are accepted because
2196
+ * the agent loop has context the manager does not own; the resolution
2197
+ * pipeline itself is identical for both callers.
2198
+ */
2199
+ resolveCandidates(current, opts = {}) {
2200
+ const configuredPrimary = opts.primary ?? {
2201
+ providerId: this.config.provider,
2202
+ model: this.config.model
2203
+ };
2204
+ const configFallbackAuto = this.config.fallbackAuto;
2205
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
2206
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
2207
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
2208
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
2209
+ const fromExplicitSource = explicitUsable || profileUsable;
2210
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2211
+ fallbackModels: explicitRefs,
2212
+ fallbackProfile: opts.fallbackProfile,
2213
+ fallbackAuto: effectiveFallbackAuto,
2214
+ exclude: current
2215
+ });
2216
+ const candidates = [];
2217
+ if (opts.closedWorld) {
2218
+ candidates.push(...selectedChain);
2219
+ } else {
2220
+ candidates.push(...this.resolveBridge(current));
2221
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
2222
+ candidates.push({
2223
+ providerId: configuredPrimary.providerId,
2224
+ model: configuredPrimary.model,
2225
+ providerSwitched: configuredPrimary.providerId !== current.providerId
2226
+ });
2227
+ }
2228
+ candidates.push(...selectedChain);
2229
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
2230
+ candidates.push(...this.resolve("default", { exclude: current }));
2231
+ }
2232
+ if (!fromExplicitSource && effectiveFallbackAuto) {
2233
+ const cap = this.lastResortCap();
2234
+ if (cap > 0) {
2235
+ const usedKeys = new Set(
2236
+ candidates.map((c) => `${c.providerId}/${c.model}`)
2237
+ );
2238
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
2239
+ candidates.push(...lastResort);
2240
+ }
2241
+ }
2242
+ }
2243
+ return dedupeChain(candidates, current);
2173
2244
  }
2174
2245
  // ── Provider availability (read-only) ──────────────────────────────────
2175
2246
  checkProvider(providerId) {
@@ -2293,7 +2364,20 @@ var FallbackProfileManager = class {
2293
2364
  );
2294
2365
  }
2295
2366
  };
2367
+ var MAX_LAST_RESORT_CANDIDATES = 12;
2296
2368
  var FREEZER_EMPTY = Object.freeze([]);
2369
+ function dedupeChain(entries, current) {
2370
+ const seen = /* @__PURE__ */ new Set();
2371
+ const currentKey = `${current.providerId}/${current.model}`;
2372
+ return Object.freeze(
2373
+ entries.filter((entry) => {
2374
+ const key = `${entry.providerId}/${entry.model}`;
2375
+ if (key === currentKey || seen.has(key)) return false;
2376
+ seen.add(key);
2377
+ return true;
2378
+ })
2379
+ );
2380
+ }
2297
2381
 
2298
2382
  // src/core/fallback-model.ts
2299
2383
  function fallbackProfileChain(config, profileName) {
@@ -2869,6 +2953,12 @@ function normalizeForComparison(text) {
2869
2953
  }
2870
2954
 
2871
2955
  // src/coordination/agents/project-agent-learning-structured.ts
2956
+ function directiveTrials(entry) {
2957
+ const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
2958
+ const applied = count(entry.applied);
2959
+ const wins = Math.min(applied, count(entry.wins));
2960
+ return { applied, wins, losses: applied - wins };
2961
+ }
2872
2962
  function parseLearnedEntryStamp(entry) {
2873
2963
  const structuredMatch = entry.match(
2874
2964
  /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
@@ -2987,6 +3077,10 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
2987
3077
  const category = parseLearnedCategory(attributes["category"]) ?? "fact";
2988
3078
  const capturedAt = attributes["capturedAt"] ?? "";
2989
3079
  const skill = attributes["skill"];
3080
+ const trials = directiveTrials({
3081
+ applied: Number(attributes["applied"]),
3082
+ wins: Number(attributes["wins"])
3083
+ });
2990
3084
  const start = (stamp.index ?? 0) + stamp[0].length;
2991
3085
  const end = stamps[index + 1]?.index ?? raw.length;
2992
3086
  const parsed = parseEntryBody(raw.slice(start, end));
@@ -2998,7 +3092,8 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
2998
3092
  why: parsed.why || WHY_BY_CATEGORY[category],
2999
3093
  how: parsed.how,
3000
3094
  capturedAt,
3001
- ...skill ? { skill } : {}
3095
+ ...skill ? { skill } : {},
3096
+ ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
3002
3097
  });
3003
3098
  }
3004
3099
  if (structured.length === 0) {
@@ -3128,7 +3223,8 @@ function loadProjectAgentLearningPolicy(role, projectRoot) {
3128
3223
  enabled: parsed.enabled !== false,
3129
3224
  lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
3130
3225
  ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
3131
- ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {}
3226
+ ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
3227
+ ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
3132
3228
  };
3133
3229
  } catch {
3134
3230
  return { ...DEFAULT_LEARNING_POLICY };
@@ -3538,6 +3634,8 @@ The file below stores **learning data for this project's "${role}" agent** \u201
3538
3634
  - **Self-contained** \u2014 understandable without the surrounding session context.
3539
3635
  - **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.
3540
3636
 
3637
+ **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.
3638
+
3541
3639
  **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.
3542
3640
 
3543
3641
  **Bad** (session log \u2014 rejected at capture time):
@@ -3822,8 +3920,16 @@ var TOOLS = {
3822
3920
  "test",
3823
3921
  "mailbox"
3824
3922
  ],
3825
- /** Version control. */
3826
- vcs: ["read", "grep", "glob", "git", "diff"],
3923
+ /**
3924
+ * Version control.
3925
+ *
3926
+ * `mailbox` is in every preset on purpose: a subagent that hits a wall must
3927
+ * be able to say so. This was the one preset without it, which left the `git`
3928
+ * and `release` roles able to fail but not to ask — the two roles whose work
3929
+ * most often needs a decision from the leader (force-push, tag collision,
3930
+ * dirty tree) and least often has a safe default.
3931
+ */
3932
+ vcs: ["read", "grep", "glob", "git", "diff", "mailbox"],
3827
3933
  /** Dependency management + CVE audit. */
3828
3934
  deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
3829
3935
  /** Documentation authoring. */
@@ -6992,7 +7098,9 @@ ${content}`);
6992
7098
  }
6993
7099
  if (typeof config.maxConcurrent === "number") {
6994
7100
  if (!Number.isFinite(config.maxConcurrent) || config.maxConcurrent < 0) {
6995
- issues.push(`maxConcurrent must be a non-negative number (got ${config.maxConcurrent})`);
7101
+ issues.push(
7102
+ `maxConcurrent must be a non-negative number (got ${config.maxConcurrent})`
7103
+ );
6996
7104
  } else if (config.maxConcurrent === 0) {
6997
7105
  warnings.push("maxConcurrent is 0 \u2014 subagent concurrency effectively disabled");
6998
7106
  } else {
@@ -7004,12 +7112,28 @@ ${content}`);
7004
7112
  const v = fleetBudget[key];
7005
7113
  if (v === void 0) continue;
7006
7114
  if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
7007
- issues.push(`fleet.budget.${key} must be a non-negative number (got ${JSON.stringify(v)})`);
7115
+ issues.push(
7116
+ `fleet.budget.${key} must be a non-negative number (got ${JSON.stringify(v)})`
7117
+ );
7008
7118
  } else {
7009
7119
  ok.push(`fleet.budget.${key} ${v}`);
7010
7120
  }
7011
7121
  }
7012
7122
  }
7123
+ const lastResortCap = config.fallbackMaxLastResortCandidates;
7124
+ if (lastResortCap !== void 0) {
7125
+ if (typeof lastResortCap !== "number" || !Number.isFinite(lastResortCap) || lastResortCap < 0) {
7126
+ issues.push(
7127
+ `fallbackMaxLastResortCandidates must be a non-negative number (got ${String(lastResortCap)})`
7128
+ );
7129
+ } else if (Math.floor(lastResortCap) === 0) {
7130
+ warnings.push(
7131
+ `fallbackMaxLastResortCandidates is ${lastResortCap} \u2014 floors to 0, last-resort auto-discovery append is disabled`
7132
+ );
7133
+ } else {
7134
+ ok.push(`fallbackMaxLastResortCandidates ${Math.floor(lastResortCap)}`);
7135
+ }
7136
+ }
7013
7137
  for (const [key, entry] of Object.entries(matrix)) {
7014
7138
  const eProvider = entry.provider ?? config.provider;
7015
7139
  const eModel = entry.model;
@@ -8281,19 +8405,23 @@ function badge(state) {
8281
8405
  var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
8282
8406
 
8283
8407
  // src/utils/tool-name.ts
8408
+ import { createHash as createHash2 } from "node:crypto";
8284
8409
  var WIRE_TOOL_NAME_MAX_LENGTH = 128;
8285
- function sanitizeWireToolName(name) {
8286
- const replaced = name.replace(/[^a-zA-Z0-9_-]/g, "_");
8287
- const clamped = replaced.slice(0, WIRE_TOOL_NAME_MAX_LENGTH);
8288
- return clamped.length > 0 ? clamped : "tool";
8410
+ var MCP_SERVER_SEGMENT_MAX_LENGTH = 48;
8411
+ var MCP_IDENTITY_HASH_LENGTH = 10;
8412
+ function collisionSafeMcpSegment(value, maxLength) {
8413
+ const replaced = value.replace(/[^a-zA-Z0-9_-]/g, "_") || "tool";
8414
+ if (replaced === value && replaced.length <= maxLength) return replaced;
8415
+ const suffix = `_${createHash2("sha256").update(value, "utf8").digest("hex").slice(0, MCP_IDENTITY_HASH_LENGTH)}`;
8416
+ return `${replaced.slice(0, Math.max(1, maxLength - suffix.length))}${suffix}`;
8289
8417
  }
8290
8418
  function mcpServerToolPrefix(serverName) {
8291
- return `mcp__${sanitizeWireToolName(serverName)}__`;
8419
+ return `mcp__${collisionSafeMcpSegment(serverName, MCP_SERVER_SEGMENT_MAX_LENGTH)}__`;
8292
8420
  }
8293
8421
  function mcpQualifiedToolName(serverName, toolName) {
8294
- return sanitizeWireToolName(
8295
- `${mcpServerToolPrefix(serverName)}${sanitizeWireToolName(toolName)}`
8296
- );
8422
+ const prefix = mcpServerToolPrefix(serverName);
8423
+ const toolSegment = collisionSafeMcpSegment(toolName, WIRE_TOOL_NAME_MAX_LENGTH - prefix.length);
8424
+ return `${prefix}${toolSegment}`;
8297
8425
  }
8298
8426
 
8299
8427
  // src/tools/mcp-use.ts
@@ -8565,52 +8693,24 @@ var OneShotOrchestrator = class {
8565
8693
  }
8566
8694
  /**
8567
8695
  * Build the fallback model chain from input + config + current target.
8696
+ * Delegates to {@link FallbackProfileManager.resolveCandidates} — the
8697
+ * shared constructor used by both the agent loop and the one-shot path,
8698
+ * so both produce identical ordering, depth, and fromExplicitSource
8699
+ * semantics.
8700
+ *
8568
8701
  * The injected {@link OneShotOrchestratorOptions.fallbackProfileManager}
8569
8702
  * is the only allowed manager — OneShot never owns a private snapshot
8570
8703
  * so a live `ConfigStore` change reaches every call without rebuilding
8571
8704
  * the manager.
8572
8705
  */
8573
8706
  resolveFallbackChain(input, config, target) {
8574
- const mgr = this.opts.fallbackProfileManager;
8575
- let fromExplicitSource = false;
8576
- let selected = mgr.resolveEffective({
8577
- fallbackAuto: false,
8578
- exclude: target
8707
+ return this.opts.fallbackProfileManager.resolveCandidates(target, {
8708
+ fallbackModels: input.fallbackModels && input.fallbackModels.length > 0 ? input.fallbackModels : config.fallbackModels,
8709
+ // The one-shot already tried `target` as the primary before entering
8710
+ // the chain. Passing it as `primary` suppresses the primary-insertion
8711
+ // step in resolveCandidates (primary === current → not pushed).
8712
+ primary: target
8579
8713
  });
8580
- if (input.fallbackModels && input.fallbackModels.length > 0) {
8581
- const resolved = mgr.resolveEffective({
8582
- fallbackModels: input.fallbackModels,
8583
- fallbackAuto: false,
8584
- exclude: target
8585
- });
8586
- selected = resolved;
8587
- fromExplicitSource = resolved.length > 0;
8588
- } else if (config.fallbackModels && config.fallbackModels.length > 0) {
8589
- const resolved = mgr.resolveEffective({
8590
- fallbackModels: config.fallbackModels,
8591
- fallbackAuto: false,
8592
- exclude: target
8593
- });
8594
- selected = resolved;
8595
- fromExplicitSource = resolved.length > 0;
8596
- } else if (config.fallbackAuto !== false) {
8597
- selected = mgr.resolveEffective({
8598
- fallbackAuto: true,
8599
- exclude: target
8600
- });
8601
- }
8602
- if (config.fallbackAuto === false) return selected;
8603
- if (fromExplicitSource) return selected;
8604
- const combined = [...selected, ...mgr.resolveAllConfigured(target)];
8605
- const seen = /* @__PURE__ */ new Set();
8606
- return Object.freeze(
8607
- combined.filter((entry) => {
8608
- const key = `${entry.providerId}/${entry.model}`;
8609
- if (seen.has(key)) return false;
8610
- seen.add(key);
8611
- return true;
8612
- })
8613
- );
8614
8714
  }
8615
8715
  /** Attempt a provider call while preserving the actual failure for callers. */
8616
8716
  async tryCall(provider, request, signal, providerId, model) {
@@ -281,10 +281,40 @@ export interface SageConfig {
281
281
  * many days. Undefined/0 disables purge (default).
282
282
  */
283
283
  purgeDeletedAfterDays?: number | undefined;
284
+ /**
285
+ * How deep periodic hygiene verifies anchors.
286
+ * - `existence` (default): file/path still exists (`fs.access` only).
287
+ * - `content`: also content-hash / symbol / command resolution.
288
+ * - `git`: content checks plus git blob hash comparison when available.
289
+ */
290
+ verifyDepth?: 'existence' | 'content' | 'git' | undefined;
284
291
  } | undefined;
285
292
  embeddings?: {
286
- /** Optional future semantic layer. Disabled by default and never required. */
293
+ /**
294
+ * When true, retrieval re-ranks lexical candidates with the offline
295
+ * hashing embedding provider (hybrid). Default: false. Fail-open:
296
+ * lexical results are always available if embeddings fail.
297
+ */
287
298
  enabled?: boolean | undefined;
288
299
  } | undefined;
300
+ /**
301
+ * Opt-in automatic capture of durable tool outcomes into SAGE.
302
+ * Off by default — operators enable per-project when they want
303
+ * command success notes recorded without an explicit `remember` call.
304
+ */
305
+ capture?: {
306
+ /** Persist successful command/tool outcomes as `tool_outcome` memories. Default: false. */
307
+ toolOutcomes?: boolean | undefined;
308
+ /** Persist recurring error signatures as `error_pattern` memories. Default: false. */
309
+ errorPatterns?: boolean | undefined;
310
+ } | undefined;
311
+ /**
312
+ * Optional automated triage scheduling. When true, `setupSage` schedules a
313
+ * daily job (1h after boot, then every 24h): hygiene + bounded triage that
314
+ * files review proposals (never auto-deletes). Default: false.
315
+ */
316
+ triage?: {
317
+ dailyDryRun?: boolean | undefined;
318
+ } | undefined;
289
319
  }
290
320
  //# sourceMappingURL=mcp-features.d.ts.map
@@ -144,6 +144,18 @@ export interface Config {
144
144
  primaryProbeInterval?: number | undefined;
145
145
  stickyFallbackTurns?: number | undefined;
146
146
  } | undefined;
147
+ /**
148
+ * Maximum number of candidates appended by the last-resort fallback sweep
149
+ * (`resolveAllConfigured`). When the smart default, bridge, and named/default
150
+ * profiles have all failed, every other configured provider is appended —
151
+ * this cap bounds the chain so a config with many providers does not produce
152
+ * a degenerate sequence of doomed requests during a systemic outage.
153
+ *
154
+ * Set to a higher value for rich multi-provider configs that want maximum
155
+ * diversity; set to 0 to disable the last-resort append entirely. The
156
+ * compiled-in default is exposed as `MAX_LAST_RESORT_CANDIDATES` (12).
157
+ */
158
+ fallbackMaxLastResortCandidates?: number | undefined;
147
159
  /**
148
160
  * Lifecycle command/HTTP hooks, keyed by event. Commands receive HookInput
149
161
  * JSON on stdin; HTTP hooks receive the same object as a POST body. A typed
@@ -39,6 +39,28 @@ export interface ToolsConfig {
39
39
  * `/settings` ("Filesystem access").
40
40
  */
41
41
  restrictToProjectRoot?: boolean | undefined;
42
+ /**
43
+ * Require a ready, running **managed** Kanban card before any product
44
+ * mutation. Default: **false** — Kanban records work, it does not permit it,
45
+ * and that is the shipped contract the system prompt and the `kanban` tool
46
+ * description both state.
47
+ *
48
+ * Turning this on inverts that for this installation: a mutating tool
49
+ * (`write`, `edit`, `exec`, …) is refused until the run is bound to a card
50
+ * that is on a managed board, passes `evaluateContractGraphReadiness`, and
51
+ * sits in Running with a live assignment. Control tools (`kanban`, `todo`,
52
+ * `plan`, `task`) are always exempt so the agent can always record evidence
53
+ * or start the next card.
54
+ *
55
+ * Non-managed boards — session mirrors, SDD mirrors, plain imports — are
56
+ * skipped rather than blocked: they structurally cannot carry a lifecycle,
57
+ * so demanding one would deadlock every mutation with no reachable remedy.
58
+ * They still fall through to their path-scoped `boundary` policy.
59
+ *
60
+ * Independent of the two checks that are ALWAYS on regardless of this flag:
61
+ * the dispatch lease fence and the board/task filesystem `boundary`.
62
+ */
63
+ kanbanGovernance?: boolean | undefined;
42
64
  /**
43
65
  * Per-command policy for the `exec` tool's allowlist. The tool ships a
44
66
  * curated default allowlist of dev/build commands; this extends or trims it.
@@ -14,6 +14,7 @@ export declare const DEFAULT_TOOLS_CONFIG: Readonly<{
14
14
  disabledTools: readonly string[];
15
15
  autoExtendLimit: true;
16
16
  restrictToProjectRoot: true;
17
+ kanbanGovernance: false;
17
18
  loopDetection: Readonly<{
18
19
  mode: 'steer-then-cut' | 'cut' | 'off';
19
20
  steerThreshold: number;
@@ -734,6 +734,9 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
734
734
  disabledTools: Object.freeze([]),
735
735
  autoExtendLimit: true,
736
736
  restrictToProjectRoot: true,
737
+ // Off by default: the board is a record of the work, not a permit for it.
738
+ // See ToolsConfig.kanbanGovernance for what turning it on costs and gates.
739
+ kanbanGovernance: false,
737
740
  loopDetection: Object.freeze({
738
741
  mode: "steer-then-cut",
739
742
  steerThreshold: 3,
@@ -8,7 +8,15 @@ export interface SessionMetadata {
8
8
  provider?: string | undefined;
9
9
  startedAt: string;
10
10
  endedAt?: string | undefined;
11
- /** Set when a session is closed with open tool calls — used to restore pending state on resume. */
11
+ /**
12
+ * Tool calls the previous run had issued but not resolved when it closed,
13
+ * as recorded on `session_end`.
14
+ *
15
+ * Diagnostic only. Resume does NOT restore pending state from this: it
16
+ * derives the same fact from the replayed conversation
17
+ * ({@link SessionData.pendingToolUseCount}), which also covers the crash
18
+ * case where no `session_end` was ever written. Nothing re-executes these.
19
+ */
12
20
  pendingToolUses?: string[] | undefined;
13
21
  /** Parent journal metadata when this session was created by fork(). */
14
22
  forkedFrom?: {
@@ -31,6 +31,7 @@ export { isSafePathSegment, MAX_PATH_SEGMENT_LENGTH, resolveContainedPath, } fro
31
31
  export { getPerfProfile, indexParallelBatchSize, isFrugalPerf, type PerfProfile, SageCachePragmas, sqliteCachePragmas, tuiStreamFlushMs, useDaemonPerfDefaults, } from './perf-profile.js';
32
32
  export { isPidAlive } from './pid.js';
33
33
  export * from './project-identity.js';
34
+ export { activateProjectStateGuard, type ProjectStateGuard, type ProjectStateGuardOptions, startProjectStateGuard, } from './project-state-guard.js';
34
35
  export { type ProjectWatchEvent, type ProjectWatchSubscription, watchProjectTree, } from './project-watch.js';
35
36
  export { capSubject, type CompileFail, type CompileResult, compileUserRegex, MAX_SUBJECT_LEN, } from './regex-guard.js';
36
37
  export * from './safe-json.js';