@wrongstack/core 0.302.2 → 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 (97) hide show
  1. package/README.md +1 -1
  2. package/dist/chronicle/index.js +117 -30
  3. package/dist/chronicle/project-server.js +12 -5
  4. package/dist/coordination/agents/index.js +4645 -3448
  5. package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
  6. package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
  7. package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
  8. package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
  9. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  10. package/dist/coordination/agents/project-agent-files.d.ts +12 -3
  11. package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
  12. package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
  13. package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
  14. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  15. package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
  16. package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
  17. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  18. package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
  19. package/dist/coordination/agents/role-skills.d.ts +11 -1
  20. package/dist/coordination/agents/types.d.ts +10 -2
  21. package/dist/coordination/director-prompts.d.ts +19 -6
  22. package/dist/coordination/director-tools.d.ts +2 -2
  23. package/dist/coordination/fleet.d.ts +0 -6
  24. package/dist/coordination/index.d.ts +1 -1
  25. package/dist/coordination/index.js +5746 -4326
  26. package/dist/coordination/mail-tools.d.ts +1 -1
  27. package/dist/core/agent-types.d.ts +4 -2
  28. package/dist/core/agent.d.ts +1 -0
  29. package/dist/core/context.d.ts +19 -0
  30. package/dist/core/conversation-state.d.ts +14 -0
  31. package/dist/core/fallback-profile-manager.d.ts +70 -2
  32. package/dist/core/index.js +317 -108
  33. package/dist/core/system-prompt-blocks.d.ts +1 -1
  34. package/dist/core/system-prompt-builder.d.ts +13 -1
  35. package/dist/core/system-prompt-glossary.d.ts +73 -0
  36. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  37. package/dist/defaults/index.js +1635 -1120
  38. package/dist/execution/council-orchestrator.d.ts +3 -13
  39. package/dist/execution/index.js +3136 -2706
  40. package/dist/execution/one-shot-llm.d.ts +5 -0
  41. package/dist/goal/index.js +7 -0
  42. package/dist/hq/index.js +17 -7
  43. package/dist/hq/protocol/kanban.d.ts +21 -0
  44. package/dist/hq/protocol.js +5 -1
  45. package/dist/hq/redaction.d.ts +14 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5502 -3483
  48. package/dist/infrastructure/index.js +247 -122
  49. package/dist/kernel/events/agent-events.d.ts +28 -0
  50. package/dist/plugin/index.js +111 -7
  51. package/dist/registry/index.js +11 -0
  52. package/dist/registry/tool-registry.d.ts +8 -0
  53. package/dist/replay/hash.d.ts +9 -0
  54. package/dist/replay/index.js +14 -4
  55. package/dist/replay/replay-provider-runner.d.ts +31 -1
  56. package/dist/security/index.js +92 -21
  57. package/dist/security/kanban-boundary.d.ts +5 -1
  58. package/dist/security/secret-vault.d.ts +2 -0
  59. package/dist/session-catalog/index.js +86 -10
  60. package/dist/session-catalog/project-server.js +100 -17
  61. package/dist/session-catalog/protocol.d.ts +20 -4
  62. package/dist/session-catalog/store.d.ts +19 -3
  63. package/dist/storage/index.js +305 -69
  64. package/dist/storage/memory-consolidator.d.ts +4 -2
  65. package/dist/storage/plan-store.d.ts +1 -1
  66. package/dist/storage/session-resume-validation.d.ts +24 -0
  67. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  68. package/dist/storage/session-store/fork-session.d.ts +13 -1
  69. package/dist/storage/session-store/load-cache.d.ts +11 -0
  70. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  71. package/dist/storage/session-store.d.ts +18 -0
  72. package/dist/tasking/index.js +5 -0
  73. package/dist/tools/index.js +1151 -831
  74. package/dist/types/config/mcp-features.d.ts +31 -1
  75. package/dist/types/config/root.d.ts +23 -1
  76. package/dist/types/config/skills-fleet-brain.d.ts +34 -0
  77. package/dist/types/config/tools.d.ts +22 -0
  78. package/dist/types/config/ui.d.ts +14 -0
  79. package/dist/types/config.d.ts +1 -0
  80. package/dist/types/default-config.d.ts +1 -0
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/multi-agent.d.ts +7 -0
  84. package/dist/types/session.d.ts +9 -1
  85. package/dist/types/task-graph.d.ts +2 -0
  86. package/dist/types/tool-executor.d.ts +2 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.js +217 -76
  89. package/dist/utils/project-state-guard.d.ts +21 -0
  90. package/dist/utils/session-scoped-path.d.ts +17 -0
  91. package/dist/utils/todos-format.d.ts +20 -0
  92. package/instructions/leader-after-task.md +3 -4
  93. package/instructions/system-lite.md +17 -14
  94. package/instructions/system-pro.md +29 -30
  95. package/instructions/system.md +29 -28
  96. package/package.json +3 -3
  97. package/skills/wrongstack-kanban/SKILL.md +95 -93
@@ -139,6 +139,34 @@ export interface AgentEventMap {
139
139
  */
140
140
  transcriptPath?: string | undefined;
141
141
  };
142
+ /**
143
+ * A spawn resolved fewer skills than it selected. Emitted so a skill that
144
+ * silently failed to load — missing from the loader, gated by a capability
145
+ * the subagent lacks, or cut by the prompt budget — is observable instead of
146
+ * leaving the agent believing it received guidance it never got.
147
+ */
148
+ 'subagent.skills.dropped': {
149
+ sessionId?: string | undefined;
150
+ role?: string | undefined;
151
+ /** Skills whose body (and project addendum) reached the prompt. */
152
+ selected: string[];
153
+ /** skill → reason it was dropped. */
154
+ dropped: Record<string, string>;
155
+ };
156
+ /**
157
+ * A background learning-distillation pass finished for a roster role.
158
+ * Emitted by the fleet host's auto-optimize scheduler so surfaces can show
159
+ * that an agent's skills were refined without a user action.
160
+ */
161
+ 'agent.learning.optimized': {
162
+ sessionId?: string | undefined;
163
+ role: string;
164
+ trigger: string;
165
+ status: string;
166
+ /** Skill addenda refreshed by the pass. */
167
+ skills: string[];
168
+ error?: string | undefined;
169
+ };
142
170
  'subagent.task_started': {
143
171
  /** Parent/host session id. */
144
172
  sessionId?: string | undefined;
@@ -2443,12 +2443,97 @@ var FallbackProfileManager = class {
2443
2443
  return this.resolveRefs([ref], exclude);
2444
2444
  }
2445
2445
  /**
2446
- * Resolve every usable configured target as an uncapped last-resort chain.
2446
+ * Resolve every usable configured target as a bounded last-resort chain.
2447
2447
  * Normal smart defaults stay bounded; callers append this only after the
2448
- * preferred chain and only when automatic fallback is enabled.
2448
+ * preferred chain and only when automatic fallback is enabled. The cap
2449
+ * ({@link MAX_LAST_RESORT_CANDIDATES}) prevents a config with many providers
2450
+ * from producing a degenerate chain of doomed requests during a systemic
2451
+ * outage — by this point the smart default, bridge, and default profile
2452
+ * have already failed.
2449
2453
  */
2450
2454
  resolveAllConfigured(exclude) {
2451
- return this.smartDefault(exclude, Number.POSITIVE_INFINITY);
2455
+ return this.smartDefault(exclude, this.lastResortCap());
2456
+ }
2457
+ /**
2458
+ * Effective cap for the last-resort append. Reads the user-configurable
2459
+ * {@link Config.fallbackMaxLastResortCandidates} when set and valid;
2460
+ * otherwise falls back to the compiled-in default
2461
+ * {@link MAX_LAST_RESORT_CANDIDATES}.
2462
+ */
2463
+ lastResortCap() {
2464
+ const configured = this.config.fallbackMaxLastResortCandidates;
2465
+ if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
2466
+ return Math.floor(configured);
2467
+ }
2468
+ return MAX_LAST_RESORT_CANDIDATES;
2469
+ }
2470
+ /**
2471
+ * Build the complete fallback candidate chain shared by the agent-loop
2472
+ * extension and the one-shot orchestrator. Centralizes the bridge → primary →
2473
+ * selected → default-profile → all-configured ladder and the fromExplicitSource
2474
+ * gate so both consumers produce identical ordering and depth semantics.
2475
+ *
2476
+ * Layering (each step deduped against all prior):
2477
+ * 1. Bridge (emergency continuity route).
2478
+ * 2. Configured primary, when the live context drifted from it.
2479
+ * 3. The selected chain (explicit refs → named profile → smart default),
2480
+ * via {@link resolveEffective}.
2481
+ * 4. The "default" profile — extra depth, ONLY when the chain was auto-derived.
2482
+ * 5. Every other configured provider — last resort, ONLY when the chain was
2483
+ * auto-derived AND `effectiveFallbackAuto` is true.
2484
+ *
2485
+ * Returns the empty chain when `closedWorld` is true and no explicit
2486
+ * refs/profile resolved — a model allowlist never leaks to unlisted models.
2487
+ *
2488
+ * @internal caller-aware options (primary, closedWorld) are accepted because
2489
+ * the agent loop has context the manager does not own; the resolution
2490
+ * pipeline itself is identical for both callers.
2491
+ */
2492
+ resolveCandidates(current, opts = {}) {
2493
+ const configuredPrimary = opts.primary ?? {
2494
+ providerId: this.config.provider,
2495
+ model: this.config.model
2496
+ };
2497
+ const configFallbackAuto = this.config.fallbackAuto;
2498
+ const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
2499
+ const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
2500
+ const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
2501
+ const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
2502
+ const fromExplicitSource = explicitUsable || profileUsable;
2503
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
2504
+ fallbackModels: explicitRefs,
2505
+ fallbackProfile: opts.fallbackProfile,
2506
+ fallbackAuto: effectiveFallbackAuto,
2507
+ exclude: current
2508
+ });
2509
+ const candidates = [];
2510
+ if (opts.closedWorld) {
2511
+ candidates.push(...selectedChain);
2512
+ } else {
2513
+ candidates.push(...this.resolveBridge(current));
2514
+ if (!(configuredPrimary.providerId === current.providerId && configuredPrimary.model === current.model)) {
2515
+ candidates.push({
2516
+ providerId: configuredPrimary.providerId,
2517
+ model: configuredPrimary.model,
2518
+ providerSwitched: configuredPrimary.providerId !== current.providerId
2519
+ });
2520
+ }
2521
+ candidates.push(...selectedChain);
2522
+ if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
2523
+ candidates.push(...this.resolve("default", { exclude: current }));
2524
+ }
2525
+ if (!fromExplicitSource && effectiveFallbackAuto) {
2526
+ const cap = this.lastResortCap();
2527
+ if (cap > 0) {
2528
+ const usedKeys = new Set(
2529
+ candidates.map((c) => `${c.providerId}/${c.model}`)
2530
+ );
2531
+ const lastResort = this.smartDefault(current, Number.POSITIVE_INFINITY).filter((c) => !usedKeys.has(`${c.providerId}/${c.model}`)).slice(0, cap);
2532
+ candidates.push(...lastResort);
2533
+ }
2534
+ }
2535
+ }
2536
+ return dedupeChain(candidates, current);
2452
2537
  }
2453
2538
  // ── Provider availability (read-only) ──────────────────────────────────
2454
2539
  checkProvider(providerId) {
@@ -2572,7 +2657,20 @@ var FallbackProfileManager = class {
2572
2657
  );
2573
2658
  }
2574
2659
  };
2660
+ var MAX_LAST_RESORT_CANDIDATES = 12;
2575
2661
  var FREEZER_EMPTY = Object.freeze([]);
2662
+ function dedupeChain(entries, current) {
2663
+ const seen = /* @__PURE__ */ new Set();
2664
+ const currentKey = `${current.providerId}/${current.model}`;
2665
+ return Object.freeze(
2666
+ entries.filter((entry) => {
2667
+ const key = `${entry.providerId}/${entry.model}`;
2668
+ if (key === currentKey || seen.has(key)) return false;
2669
+ seen.add(key);
2670
+ return true;
2671
+ })
2672
+ );
2673
+ }
2576
2674
 
2577
2675
  // src/plugins/auto-review-plugin.ts
2578
2676
  init_error();
@@ -6371,7 +6469,9 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
6371
6469
  const agentRange = dayFilter("day");
6372
6470
  const uniqueAgents = this.db.prepare(`SELECT COUNT(DISTINCT agent_id) n FROM agent_daily${agentRange.where}`).get(...agentRange.params).n;
6373
6471
  const requestRange = dayFilter("day");
6374
- const logicalRequests = this.db.prepare(`SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`).get(...requestRange.params).n;
6472
+ const logicalRequests = this.db.prepare(
6473
+ `SELECT COUNT(DISTINCT logical_request_id) n FROM logical_request_daily${requestRange.where}`
6474
+ ).get(...requestRange.params).n;
6375
6475
  const fileRange = dayFilter("day");
6376
6476
  const uniqueFiles = this.db.prepare(`SELECT COUNT(DISTINCT path_key) n FROM file_seen_daily${fileRange.where}`).get(...fileRange.params).n;
6377
6477
  const costRange = dayFilter("day");
@@ -6680,10 +6780,13 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
6680
6780
  ON CONFLICT(day, family) DO UPDATE SET count = count + 1, failure_count = failure_count + excluded.failure_count`
6681
6781
  ).run(day, family, failed);
6682
6782
  if (failed) bump("failures = failures + 1");
6683
- if (event.outcome === "cancelled" || event.outcome === "abandoned") bump("cancellations = cancellations + 1");
6783
+ if (event.outcome === "cancelled" || event.outcome === "abandoned")
6784
+ bump("cancellations = cancellations + 1");
6684
6785
  if (family === "agent") bump("agent_events = agent_events + 1");
6685
6786
  if (event.correlation.logicalRequestId) {
6686
- this.db.prepare("INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)").run(day, event.correlation.logicalRequestId);
6787
+ this.db.prepare(
6788
+ "INSERT OR IGNORE INTO logical_request_daily (day, logical_request_id) VALUES (?, ?)"
6789
+ ).run(day, event.correlation.logicalRequestId);
6687
6790
  }
6688
6791
  if (event.scope.agentId) {
6689
6792
  this.db.prepare("INSERT OR IGNORE INTO agent_daily (day, agent_id) VALUES (?, ?)").run(day, event.scope.agentId);
@@ -6703,7 +6806,8 @@ var ChronicleMetricsStore = class _ChronicleMetricsStore {
6703
6806
  durationCount
6704
6807
  );
6705
6808
  } else if (type === "process.started") bump("processes = processes + 1");
6706
- else if (type === "process.completed" && event.outcome === "failure") bump("failed_processes = failed_processes + 1");
6809
+ else if (type === "process.completed" && event.outcome === "failure")
6810
+ bump("failed_processes = failed_processes + 1");
6707
6811
  if (event.resource?.kind === "file" || type.startsWith("file.")) {
6708
6812
  bump("file_events_all = file_events_all + 1");
6709
6813
  if (event.resource?.path) {
@@ -759,6 +759,17 @@ var ToolRegistry = class _ToolRegistry {
759
759
  this._providerListSnapshotVersion = this._version;
760
760
  return arr;
761
761
  }
762
+ /**
763
+ * Whether an enabled catalog tool is part of the direct provider surface.
764
+ *
765
+ * This is intentionally different from `get()`/`list()`: a tool can remain
766
+ * executable through a lazy gateway while its schema is omitted from the
767
+ * next provider request. Disabled and unknown tools always return false.
768
+ */
769
+ isExposedToProvider(name) {
770
+ if (this._disabled.has(name) || !this.tools.has(name)) return false;
771
+ return this._providerToolNames?.has(name) ?? true;
772
+ }
762
773
  /**
763
774
  * Group tools by their `category` field. Tools without a category
764
775
  * are placed under the key `""` (empty string). Returns a Map of
@@ -149,6 +149,14 @@ export declare class ToolRegistry {
149
149
  exposeToProvider(names: string | readonly string[]): void;
150
150
  /** Tools serialized into provider requests and described by the system prompt. */
151
151
  listForProvider(): Tool[];
152
+ /**
153
+ * Whether an enabled catalog tool is part of the direct provider surface.
154
+ *
155
+ * This is intentionally different from `get()`/`list()`: a tool can remain
156
+ * executable through a lazy gateway while its schema is omitted from the
157
+ * next provider request. Disabled and unknown tools always return false.
158
+ */
159
+ isExposedToProvider(name: string): boolean;
152
160
  /**
153
161
  * Group tools by their `category` field. Tools without a category
154
162
  * are placed under the key `""` (empty string). Returns a Map of
@@ -17,6 +17,15 @@ import type { Request } from '../types/provider.js';
17
17
  * sampling knobs (`temperature`, `topP`, `stopSequences`,
18
18
  * `toolChoice`). Anything else on the `Request` (metadata,
19
19
  * future extensions) is ignored so replay stays forward-compat.
20
+ * - The same rule applies *inside* each message. `Request.messages`
21
+ * is the live `ctx.messages` array, whose entries carry local
22
+ * bookkeeping the provider adapters explicitly drop: `ts` (wall
23
+ * clock at the moment the turn was created), `_estTokens` (a
24
+ * mutation-time cache) and `origin`. Hashing those made every
25
+ * hash a function of when the run happened, so a recorded
26
+ * response could never be found again and `mode: 'replay'` threw
27
+ * on the first call it was asked to serve. See
28
+ * {@link semanticMessage}.
20
29
  * - We serialize to JSON. The `ContentBlock` and `Message` shapes
21
30
  * are pure data; this works as long as no `undefined` values
22
31
  * sneak in (those get dropped by `JSON.stringify`, which is
@@ -15,11 +15,15 @@ function sortKeys(value) {
15
15
  }
16
16
  return value;
17
17
  }
18
+ function semanticMessage(message) {
19
+ const { ts: _ts, _estTokens: _estimate, origin: _origin, ...semantic } = message;
20
+ return semantic;
21
+ }
18
22
  function hashRequest(request) {
19
23
  const payload = {
20
24
  model: request.model,
21
25
  system: request.system,
22
- messages: request.messages,
26
+ messages: request.messages.map(semanticMessage),
23
27
  tools: request.tools,
24
28
  maxTokens: request.maxTokens,
25
29
  temperature: request.temperature,
@@ -40,16 +44,22 @@ var ReplayProviderRunner = class {
40
44
  }
41
45
  inner;
42
46
  opts;
47
+ /** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
48
+ resolveSessionId() {
49
+ const { sessionId } = this.opts;
50
+ return typeof sessionId === "function" ? sessionId() : sessionId;
51
+ }
43
52
  async run(runOpts) {
44
53
  const hash = hashRequest(runOpts.request);
45
- const cached = await this.opts.log.lookup(this.opts.sessionId, hash);
54
+ const sessionId = this.resolveSessionId();
55
+ const cached = await this.opts.log.lookup(sessionId, hash);
46
56
  if (this.opts.mode === "replay") {
47
57
  if (!cached) {
48
58
  this.opts.logger?.warn?.(
49
59
  `replay: no recorded response for hash ${hash} (model ${runOpts.request.model})`
50
60
  );
51
61
  throw new Error(
52
- `ReplayProviderRunner: no recorded response for hash ${hash} in session ${this.opts.sessionId}. Either the request changed since recording, or this session has no replay log.`
62
+ `ReplayProviderRunner: no recorded response for hash ${hash} in session ${sessionId}. Either the request changed since recording, or this session has no replay log.`
53
63
  );
54
64
  }
55
65
  this.opts.logger?.debug?.(
@@ -65,7 +75,7 @@ var ReplayProviderRunner = class {
65
75
  }
66
76
  const response = await this.inner.run(runOpts);
67
77
  await this.opts.log.record({
68
- sessionId: this.opts.sessionId,
78
+ sessionId,
69
79
  request: runOpts.request,
70
80
  response
71
81
  });
@@ -26,11 +26,39 @@ import type { Response } from '../types/provider.js';
26
26
  * a particular request is seen, it's recorded; the next time
27
27
  * the same request is made, the recorded response is served.
28
28
  * This is the cheapest path to a deterministic test harness.
29
+ *
30
+ * ## What a hit requires
31
+ *
32
+ * The lookup key is `hashRequest`, which covers everything the provider is
33
+ * actually sent — including `system`. The agent loop appends volatile blocks
34
+ * to the system prompt on every request (the continuity ledger, the live
35
+ * next-steps gate, memory evidence; see `agent-response.ts`), and those blocks
36
+ * describe the state of *this* run. A re-run whose ledger or retrieved memory
37
+ * differs therefore produces a different request, and correctly misses: the
38
+ * recorded response answered a different prompt.
39
+ *
40
+ * So `mode: 'replay'` reproduces a run faithfully only when the loop is driven
41
+ * back through the same states — which is what a test harness does, and what a
42
+ * fresh interactive session does not. In an interactive session, prefer
43
+ * `'record'` (build the log) and use `'replay'` from a harness that pins the
44
+ * volatile inputs. `hashRequest` itself is stable across time: local message
45
+ * bookkeeping (`ts`, `_estTokens`, `origin`) is stripped before hashing.
29
46
  */
30
47
  export type ReplayMode = 'record' | 'replay' | 'auto';
31
48
  export interface ReplayProviderRunnerOptions {
32
49
  log: ReplayLogStore;
33
- sessionId: string;
50
+ /**
51
+ * Which session's log to read and write.
52
+ *
53
+ * Accepts a resolver because recording is wired before the session exists:
54
+ * the boot path binds this runner while the session writer is still being
55
+ * created, and the user may then swap sessions (`/resume`, new session)
56
+ * without the runner being rebuilt. A frozen string there meant the log was
57
+ * filed under a synthetic id with no session behind it. A resolver keeps the
58
+ * log next to the transcript it belongs to, for the session that is actually
59
+ * live at the time of the call.
60
+ */
61
+ sessionId: string | (() => string);
34
62
  mode: ReplayMode;
35
63
  /**
36
64
  * Optional logger — receives a debug line on every replay hit
@@ -48,6 +76,8 @@ export declare class ReplayProviderRunner implements ProviderRunner {
48
76
  private readonly inner;
49
77
  private readonly opts;
50
78
  constructor(inner: ProviderRunner, opts: ReplayProviderRunnerOptions);
79
+ /** Resolve the target session for this call. See {@link ReplayProviderRunnerOptions.sessionId}. */
80
+ private resolveSessionId;
51
81
  run(runOpts: RunProviderOptions): Promise<Response>;
52
82
  }
53
83
  //# sourceMappingURL=replay-provider-runner.d.ts.map
@@ -1298,11 +1298,13 @@ function windowsAccountName() {
1298
1298
  import { realpath } from "node:fs/promises";
1299
1299
  import * as path2 from "node:path";
1300
1300
  import {
1301
+ evaluateContractGraphReadiness,
1301
1302
  evaluateKanbanBoundaryOpaque,
1302
1303
  evaluateKanbanBoundaryPath,
1303
1304
  readBoard,
1304
1305
  resolveKanbanBoundaryLayers
1305
1306
  } from "@wrongstack/kanban";
1307
+ var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo"]);
1306
1308
  var PATH_KEYS2 = /* @__PURE__ */ new Set([
1307
1309
  "path",
1308
1310
  "paths",
@@ -1327,12 +1329,62 @@ var TOOL_PATH_KEYS = {
1327
1329
  // filesystem target in language_info. Keep ambiguous names tool-specific.
1328
1330
  language_info: ["target"]
1329
1331
  };
1330
- async function evaluateToolKanbanBoundary(tool, input, ctx) {
1332
+ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
1333
+ if (tool.name === "kanban") return { decision: "allow" };
1331
1334
  const identity = resolveKanbanIdentity(ctx);
1332
- if (!identity.boardId) return { decision: "allow" };
1335
+ const governanceRequired = options.requireGovernance && isGovernedMutation(tool, input);
1336
+ if (!identity.boardId) {
1337
+ return governanceRequired ? {
1338
+ decision: "block",
1339
+ reason: "Kanban governance is mandatory before product mutation. Create a managed card, add its required details and executable acceptance criteria, then call kanban start_task to bind it to this run."
1340
+ } : { decision: "allow" };
1341
+ }
1333
1342
  const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
1334
- if (!board) return { decision: "allow" };
1343
+ if (!board) {
1344
+ return governanceRequired ? {
1345
+ decision: "block",
1346
+ reason: `Active Kanban board not found: ${identity.boardId}. Recreate or select a valid managed card before mutation.`,
1347
+ boardId: identity.boardId,
1348
+ ...identity.taskId ? { taskId: identity.taskId } : {}
1349
+ } : { decision: "allow" };
1350
+ }
1335
1351
  const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
1352
+ if (governanceRequired && board.lifecycle?.mode === "managed") {
1353
+ if (!identity.taskId) {
1354
+ return {
1355
+ decision: "block",
1356
+ reason: "Kanban governance requires an active task, not only a board. Complete the card details and call kanban start_task before mutation.",
1357
+ boardId: board.id
1358
+ };
1359
+ }
1360
+ if (!task) {
1361
+ return {
1362
+ decision: "block",
1363
+ reason: `Active Kanban task not found: ${identity.taskId}. Call kanban start_task with a valid card.`,
1364
+ boardId: board.id,
1365
+ taskId: identity.taskId
1366
+ };
1367
+ }
1368
+ const readiness = evaluateContractGraphReadiness(board, task.id);
1369
+ if (!readiness.ready) {
1370
+ return {
1371
+ decision: "block",
1372
+ reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
1373
+ boardId: board.id,
1374
+ taskId: task.id
1375
+ };
1376
+ }
1377
+ if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
1378
+ const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
1379
+ const assignmentStatus = task.assignment?.status ?? "missing";
1380
+ return {
1381
+ decision: "block",
1382
+ 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.`,
1383
+ boardId: board.id,
1384
+ taskId: task.id
1385
+ };
1386
+ }
1387
+ }
1336
1388
  if (identity.leaseId && task?.assignment) {
1337
1389
  const caps2 = tool.capabilities ?? [];
1338
1390
  const isWrite = caps2.includes("fs.write") || caps2.includes("fs.write.outside-project") || caps2.some((c) => c.startsWith("shell."));
@@ -1368,6 +1420,19 @@ async function evaluateToolKanbanBoundary(tool, input, ctx) {
1368
1420
  ...task ? { taskId: task.id } : {}
1369
1421
  };
1370
1422
  }
1423
+ function isGovernedMutation(tool, input) {
1424
+ if (!tool.mutating || GOVERNANCE_CONTROL_TOOLS.has(tool.name)) return false;
1425
+ if (tool.capabilities?.includes("tool.meta")) return false;
1426
+ if (tool.name === "git") {
1427
+ const command = input["command"];
1428
+ if (command === "status" || command === "log" || command === "diff") return false;
1429
+ if (command === "worktree" && input["worktreeAction"] === "list") return false;
1430
+ }
1431
+ const capabilities = tool.capabilities ?? [];
1432
+ return capabilities.some(
1433
+ (capability) => capability === "fs.write" || capability === "fs.write.outside-project" || capability === "package.install" || capability === "tool.mutate.any" || capability.startsWith("shell.") || capability === "net.outbound"
1434
+ );
1435
+ }
1371
1436
  function resolveKanbanIdentity(ctx) {
1372
1437
  const metaKanban = ctx.meta["kanban"];
1373
1438
  const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
@@ -1685,6 +1750,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
1685
1750
  const obj = input;
1686
1751
  if (subjectKey) {
1687
1752
  const value = obj[subjectKey];
1753
+ if (Array.isArray(value)) {
1754
+ return escapeGlobSubject(JSON.stringify(value));
1755
+ }
1688
1756
  if (typeof value === "string") {
1689
1757
  if (isPathSubjectKey(subjectKey)) {
1690
1758
  const normalized = normalizePathSubject(value);
@@ -3541,23 +3609,21 @@ function unwrapDataKey(buf, keyFile) {
3541
3609
  });
3542
3610
  }
3543
3611
  }
3544
- function checkKeyFilePermissions(keyFile, opts) {
3545
- if (process.platform === "win32") return;
3612
+ function keyFileNeedsHardening(keyFile, opts) {
3613
+ if (process.platform === "win32") return false;
3546
3614
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
3547
3615
  try {
3548
3616
  const stat = fs2.statSync(keyFile);
3549
3617
  const actualMode = stat.mode & 511;
3550
3618
  if (actualMode !== KEY_FILE_MODE) {
3551
- void restrictFilePermissions(keyFile, {
3552
- label: "secret-vault",
3553
- warn
3554
- }).catch(() => void 0);
3555
3619
  warn(
3556
3620
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
3557
3621
  );
3622
+ return true;
3558
3623
  }
3559
3624
  } catch {
3560
3625
  }
3626
+ return false;
3561
3627
  }
3562
3628
  function writeKeyFileAtomicSync(keyFile, content) {
3563
3629
  const tmp = `${keyFile}.${randomBytes2(4).toString("hex")}.tmp`;
@@ -3623,6 +3689,14 @@ var DefaultSecretVault = class {
3623
3689
  }).catch(() => void 0);
3624
3690
  this.pendingHardening.push(p);
3625
3691
  }
3692
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
3693
+ checkKeyFilePermissions() {
3694
+ if (keyFileNeedsHardening(this.keyFile, {
3695
+ warn: (msg) => this.logWarn(msg)
3696
+ })) {
3697
+ this.scheduleKeyHardening();
3698
+ }
3699
+ }
3626
3700
  /** Flush all pending key-file hardening promises. */
3627
3701
  flushHardening() {
3628
3702
  if (this.pendingHardening.length === 0) return Promise.resolve();
@@ -3710,7 +3784,7 @@ var DefaultSecretVault = class {
3710
3784
  writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
3711
3785
  }
3712
3786
  this.scheduleKeyHardening();
3713
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3787
+ this.checkKeyFilePermissions();
3714
3788
  this.key = newKey;
3715
3789
  this._keyVersion = newVersion;
3716
3790
  return { oldVersion, newVersion };
@@ -3726,12 +3800,9 @@ var DefaultSecretVault = class {
3726
3800
  const passphrase = getVaultPassphrase();
3727
3801
  if (!passphrase || !this.key) return;
3728
3802
  try {
3729
- writeKeyFileAtomicSync(
3730
- this.keyFile,
3731
- wrapDataKey(this.key, this._keyVersion, passphrase)
3732
- );
3803
+ writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
3733
3804
  this.scheduleKeyHardening();
3734
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3805
+ this.checkKeyFilePermissions();
3735
3806
  } catch {
3736
3807
  }
3737
3808
  }
@@ -3743,13 +3814,13 @@ var DefaultSecretVault = class {
3743
3814
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
3744
3815
  this.key = key2;
3745
3816
  this._keyVersion = version;
3746
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3817
+ this.checkKeyFilePermissions();
3747
3818
  return this.key;
3748
3819
  }
3749
3820
  if (buf.length === KEY_BYTES) {
3750
3821
  this.key = buf;
3751
3822
  this._keyVersion = 1;
3752
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3823
+ this.checkKeyFilePermissions();
3753
3824
  this.migrateToWrappedIfPassphrase();
3754
3825
  return this.key;
3755
3826
  }
@@ -3773,7 +3844,7 @@ var DefaultSecretVault = class {
3773
3844
  }
3774
3845
  this.key = Buffer.from(key2);
3775
3846
  this._keyVersion = version;
3776
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3847
+ this.checkKeyFilePermissions();
3777
3848
  this.migrateToWrappedIfPassphrase();
3778
3849
  return this.key;
3779
3850
  }
@@ -3800,13 +3871,13 @@ var DefaultSecretVault = class {
3800
3871
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
3801
3872
  this.key = winnerKey;
3802
3873
  this._keyVersion = version;
3803
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3874
+ this.checkKeyFilePermissions();
3804
3875
  return this.key;
3805
3876
  }
3806
3877
  if (buf.length === KEY_BYTES) {
3807
3878
  this.key = buf;
3808
3879
  this._keyVersion = 1;
3809
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3880
+ this.checkKeyFilePermissions();
3810
3881
  return this.key;
3811
3882
  }
3812
3883
  if (buf.length === VERSIONED_KEY_FILE_SIZE) {
@@ -3822,7 +3893,7 @@ var DefaultSecretVault = class {
3822
3893
  const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
3823
3894
  this.key = Buffer.from(winnerKey);
3824
3895
  this._keyVersion = version;
3825
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
3896
+ this.checkKeyFilePermissions();
3826
3897
  return this.key;
3827
3898
  }
3828
3899
  throw new ConfigError({
@@ -5,6 +5,10 @@ export interface ToolKanbanBoundaryEvaluation extends KanbanBoundaryEvaluation {
5
5
  boardId?: string | undefined;
6
6
  taskId?: string | undefined;
7
7
  }
8
+ export interface ToolKanbanGovernanceOptions {
9
+ /** Require every product mutation to run inside a ready, running Kanban card. */
10
+ requireGovernance?: boolean | undefined;
11
+ }
8
12
  /** Resolve the live board/task policy and gate one tool invocation. */
9
- export declare function evaluateToolKanbanBoundary(tool: Tool, input: Record<string, unknown>, ctx: Context): Promise<ToolKanbanBoundaryEvaluation>;
13
+ export declare function evaluateToolKanbanBoundary(tool: Tool, input: Record<string, unknown>, ctx: Context, options?: ToolKanbanGovernanceOptions): Promise<ToolKanbanBoundaryEvaluation>;
10
14
  //# sourceMappingURL=kanban-boundary.d.ts.map
@@ -46,6 +46,8 @@ export declare class DefaultSecretVault implements RotatableSecretVault {
46
46
  * can never become an unhandled one.
47
47
  */
48
48
  private scheduleKeyHardening;
49
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
50
+ private checkKeyFilePermissions;
49
51
  /** Flush all pending key-file hardening promises. */
50
52
  flushHardening(): Promise<void>;
51
53
  /** Current key version. Starts at 1; incremented by rotateKey(). */