@wrongstack/core 0.300.0 → 0.302.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 (146) hide show
  1. package/dist/chronicle/index.d.ts +2 -0
  2. package/dist/chronicle/index.js +297 -10
  3. package/dist/chronicle/project-server.js +253 -15
  4. package/dist/chronicle/sqlite-compaction.d.ts +20 -0
  5. package/dist/chronicle/sqlite-journal.d.ts +57 -0
  6. package/dist/coordination/agents/capability-manifest.d.ts +7 -0
  7. package/dist/coordination/agents/index.d.ts +3 -2
  8. package/dist/coordination/agents/index.js +818 -570
  9. package/dist/coordination/agents/project-agent-identity.d.ts +6 -6
  10. package/dist/coordination/agents/role-skills.d.ts +1 -1
  11. package/dist/coordination/agents/types.d.ts +6 -0
  12. package/dist/coordination/director.d.ts +2 -2
  13. package/dist/coordination/index.js +6787 -6447
  14. package/dist/coordination/mail-tools.d.ts +3 -3
  15. package/dist/coordination/mailbox-project-server.js +3 -4
  16. package/dist/coordination/mailbox-types.d.ts +6 -0
  17. package/dist/core/agent-response.d.ts +5 -1
  18. package/dist/core/agent-tools.d.ts +3 -0
  19. package/dist/core/context.d.ts +20 -0
  20. package/dist/core/index.d.ts +8 -7
  21. package/dist/core/index.js +893 -284
  22. package/dist/core/instruction-template.d.ts +6 -3
  23. package/dist/core/next-steps-slot.d.ts +88 -0
  24. package/dist/core/system-prompt-builder.d.ts +5 -3
  25. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  26. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  27. package/dist/defaults/index.js +9797 -8898
  28. package/dist/design/index.js +11 -3
  29. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  30. package/dist/execution/autonomy-brain.d.ts +8 -2
  31. package/dist/execution/brain-runtime.d.ts +3 -1
  32. package/dist/execution/compaction-core.d.ts +41 -2
  33. package/dist/execution/council-brain.d.ts +37 -2
  34. package/dist/execution/council-orchestrator.d.ts +2 -2
  35. package/dist/execution/index.d.ts +11 -10
  36. package/dist/execution/index.js +5179 -4078
  37. package/dist/execution/tool-executor.d.ts +4 -1
  38. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  39. package/dist/extension/index.js +8 -2
  40. package/dist/extension/registry.d.ts +8 -1
  41. package/dist/goal/index.js +11 -3
  42. package/dist/hooks/index.js +42 -9
  43. package/dist/hooks/runner.d.ts +12 -1
  44. package/dist/hq/index.js +41 -17
  45. package/dist/hq/publisher.d.ts +21 -3
  46. package/dist/index.d.ts +21 -20
  47. package/dist/index.js +10901 -8067
  48. package/dist/infrastructure/index.js +108 -21
  49. package/dist/kernel/events/session-events.d.ts +13 -0
  50. package/dist/kernel/events/tool-events.d.ts +3 -3
  51. package/dist/models/index.d.ts +1 -0
  52. package/dist/models/index.js +17 -0
  53. package/dist/models/provider-credentials.d.ts +54 -0
  54. package/dist/plugin/index.d.ts +1 -0
  55. package/dist/plugin/index.js +2024 -502
  56. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  57. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  58. package/dist/plugins/review-claim-registry.d.ts +23 -8
  59. package/dist/plugins/review-types.d.ts +2 -0
  60. package/dist/registry/index.js +109 -74
  61. package/dist/registry/tool-registry.d.ts +15 -0
  62. package/dist/security/capabilities.d.ts +2 -0
  63. package/dist/security/index.d.ts +1 -1
  64. package/dist/security/index.js +9 -2
  65. package/dist/security/readonly-permission-policy.d.ts +20 -0
  66. package/dist/session-registry-atomic-file.d.ts +32 -5
  67. package/dist/session-registry-types.d.ts +17 -0
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/skills/frontmatter.d.ts +6 -0
  70. package/dist/skills/index.js +22 -5
  71. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  72. package/dist/storage/cloud-config-sync.d.ts +87 -0
  73. package/dist/storage/index.d.ts +2 -1
  74. package/dist/storage/index.js +854 -66
  75. package/dist/tools/index.d.ts +1 -1
  76. package/dist/tools/index.js +511 -234
  77. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  78. package/dist/tools/plugin-manager.d.ts +27 -0
  79. package/dist/types/config/mcp-features.d.ts +18 -11
  80. package/dist/types/config/root.d.ts +8 -1
  81. package/dist/types/config/runtime.d.ts +20 -6
  82. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  83. package/dist/types/config/tools.d.ts +16 -0
  84. package/dist/types/context-window.d.ts +1 -0
  85. package/dist/types/hooks.d.ts +14 -0
  86. package/dist/types/index.d.ts +2 -2
  87. package/dist/types/index.js +1 -0
  88. package/dist/types/multi-agent.d.ts +2 -0
  89. package/dist/types/one-shot-llm.d.ts +16 -0
  90. package/dist/types/provider.d.ts +16 -0
  91. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  92. package/dist/types/skill.d.ts +5 -0
  93. package/dist/types/system-prompt.d.ts +3 -1
  94. package/dist/types/tool-executor.d.ts +8 -1
  95. package/dist/utils/context-breakdown.d.ts +8 -2
  96. package/dist/utils/index.d.ts +1 -1
  97. package/dist/utils/index.js +141 -31
  98. package/dist/utils/regex-guard.d.ts +16 -2
  99. package/dist/utils/sage-output-block.d.ts +7 -10
  100. package/dist/utils/wstack-paths.d.ts +11 -3
  101. package/instructions/agents/browser.md +1 -4
  102. package/instructions/agents/e2e.md +17 -15
  103. package/instructions/agents/ios.md +3 -3
  104. package/instructions/agents/search.md +1 -1
  105. package/instructions/autonomy/goal-preamble.md +4 -4
  106. package/instructions/coordination/director-preamble.md +2 -3
  107. package/instructions/coordination/subagent-baseline.md +3 -1
  108. package/instructions/leader-after-task.md +12 -0
  109. package/instructions/llm/chimera-review.md +1 -1
  110. package/instructions/modes/audit-lite.md +1 -1
  111. package/instructions/sections/tool/common-patterns.md +18 -2
  112. package/instructions/sections/tool/mailbox-compact.md +1 -1
  113. package/instructions/sections/tool/mailbox-full.md +1 -1
  114. package/instructions/system-lite.md +10 -0
  115. package/instructions/system-pro.md +23 -14
  116. package/instructions/system.md +22 -13
  117. package/package.json +3 -3
  118. package/skills/api-design/SKILL.md +3 -0
  119. package/skills/audit-log/SKILL.md +2 -0
  120. package/skills/auto-review/SKILL.md +6 -1
  121. package/skills/bug-hunter/SKILL.md +4 -1
  122. package/skills/chimera/SKILL.md +3 -0
  123. package/skills/data-governance/SKILL.md +3 -0
  124. package/skills/design-system/SKILL.md +5 -2
  125. package/skills/docker-deploy/SKILL.md +2 -0
  126. package/skills/git-flow/SKILL.md +2 -0
  127. package/skills/mailbox-bridge/SKILL.md +3 -0
  128. package/skills/mnemosyne/SKILL.md +2 -0
  129. package/skills/multi-agent/SKILL.md +4 -1
  130. package/skills/node-modern/SKILL.md +4 -1
  131. package/skills/observability/SKILL.md +3 -0
  132. package/skills/output-standards/SKILL.md +2 -0
  133. package/skills/plugin-author/SKILL.md +4 -1
  134. package/skills/prompt-engineering/SKILL.md +3 -1
  135. package/skills/react-modern/SKILL.md +6 -3
  136. package/skills/refactor-planner/SKILL.md +2 -0
  137. package/skills/research-web/SKILL.md +3 -0
  138. package/skills/sdd/SKILL.md +3 -1
  139. package/skills/security-scanner/SKILL.md +3 -0
  140. package/skills/skill-creator/SKILL.md +2 -0
  141. package/skills/tech-stack/SKILL.md +3 -0
  142. package/skills/testing/SKILL.md +4 -1
  143. package/skills/typescript-strict/SKILL.md +4 -1
  144. package/skills/wrongstack-kanban/SKILL.md +6 -3
  145. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  146. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -15,6 +15,8 @@ export interface AutoReviewConfig {
15
15
  * omitted), and the remaining entries form the fallback chain.
16
16
  */
17
17
  fallbackProfile?: string | undefined;
18
+ /** How each review chooses its starting model from the resolved profile pool. */
19
+ modelSelection?: 'round-robin' | 'random' | undefined;
18
20
  /** Debounce window in ms — wait for quiet before firing review (default 15000). */
19
21
  debounceMs?: number | undefined;
20
22
  /** Max files per review batch (default 15). */
@@ -40,6 +42,7 @@ export interface ResolvedAutoReviewConfig {
40
42
  provider: string;
41
43
  model: string;
42
44
  fallbackModels: string[];
45
+ modelSelection: 'round-robin' | 'random';
43
46
  debounceMs: number;
44
47
  maxFilesPerBatch: number;
45
48
  maxConcurrentReviews: number;
@@ -0,0 +1,22 @@
1
+ import type { ConfigStore } from '../types/config.js';
2
+ import type { Plugin } from '../types/plugin.js';
3
+ import type { SecretVault } from '../types/secret-vault.js';
4
+ import type { WstackPaths } from '../utils/wstack-paths.js';
5
+ interface CloudConfigSyncPluginOptions {
6
+ paths?: WstackPaths | undefined;
7
+ configStore?: ConfigStore | undefined;
8
+ vault?: SecretVault | undefined;
9
+ appVersion?: string | undefined;
10
+ }
11
+ /**
12
+ * CloudConfigSyncPlugin — background config synchronization with my.wrongstack.com.
13
+ *
14
+ * Registers `/cloudsync` and, while enabled, runs a pull-then-push pass every
15
+ * `cloudSync.intervalSeconds` (default 300). Distinct from `wstack-sync` (the legacy
16
+ * GitHub category sync): this speaks the portal's namespaced, schema-validated,
17
+ * secret-free sync API v1 and never uploads credential material — the sanitizer in
18
+ * `storage/cloud-config-sync/sanitize.ts` is the client half of the server's contract.
19
+ */
20
+ export declare function createCloudConfigSyncPlugin(opts?: CloudConfigSyncPluginOptions): Plugin;
21
+ export {};
22
+ //# sourceMappingURL=cloud-config-sync-plugin.d.ts.map
@@ -1,24 +1,39 @@
1
1
  import type { EventBus } from '../kernel/events.js';
2
2
  import type { PluginAPI } from '../types/plugin.js';
3
3
  import type { ReviewContextBundle } from './review-types.js';
4
+ export interface ClaimStoreOptions {
5
+ /**
6
+ * Override the shared claim-ledger directory. Defaults to `<cwd>/.wrongstack`
7
+ * (git-ignored, shared by every session on the same working tree). Tests
8
+ * inject a temp dir; two EventBuses sharing one dir dedupe like two hosts.
9
+ */
10
+ storeDir?: string | undefined;
11
+ /** Claim lifetime in ms before it is treated as stale (default 30 min). */
12
+ ttlMs?: number | undefined;
13
+ /** Ledger compaction threshold in lines (default 10,000). Test override. */
14
+ maxLedgerLines?: number | undefined;
15
+ }
16
+ export declare function breakStaleLock(lockPath: string): Promise<boolean>;
4
17
  /**
5
18
  * Record review events emitted outside the automatic plugin paths, such as
6
19
  * `/review`, so the post-session pass also avoids repeating those files.
7
20
  */
8
- export declare function recordStartedReview(events: EventBus, payload: unknown): void;
21
+ export declare function recordStartedReview(events: EventBus, payload: unknown, opts?: ClaimStoreOptions | undefined): Promise<void>;
9
22
  /**
10
23
  * Release the exact claims associated with a completed review bundle. Bundle
11
24
  * identity prevents an older completion from removing newer in-flight claims.
12
25
  */
13
- export declare function recordCompletedReview(events: EventBus, payload: unknown): void;
26
+ export declare function recordCompletedReview(events: EventBus, payload: unknown): Promise<void>;
14
27
  /**
15
28
  * Atomically claim review inputs and emit only files whose exact content does
16
- * not already have a review in progress. The returned bundle contains only
17
- * claimed files and may be a strict subset of the input.
29
+ * not already have a review in progress across every session sharing the
30
+ * working tree. The returned bundle contains only claimed files and may be a
31
+ * strict subset of the input.
18
32
  *
19
- * Claims are installed immediately before the synchronous event emission, so
20
- * concurrent mid/post-session handlers cannot enqueue the same content twice.
21
- * A synchronous emit failure rolls the claims back.
33
+ * Claims are installed in the shared ledger (under its exclusive lock) before
34
+ * the event emission, so concurrent mid/post-session handlers in this
35
+ * process or another cannot enqueue the same content twice. A synchronous
36
+ * emit failure rolls the claims back.
22
37
  */
23
- export declare function emitReviewIfChanged(api: Pick<PluginAPI, 'events' | 'emitCustom'>, bundle: ReviewContextBundle): ReviewContextBundle | null;
38
+ export declare function emitReviewIfChanged(api: Pick<PluginAPI, 'events' | 'emitCustom'>, bundle: ReviewContextBundle, opts?: ClaimStoreOptions | undefined): Promise<ReviewContextBundle | null>;
24
39
  //# sourceMappingURL=review-claim-registry.d.ts.map
@@ -45,6 +45,8 @@ export interface ReviewContextBundle {
45
45
  * Absent for ordinary Chimera and manual review triggers.
46
46
  */
47
47
  reviewFallbackModels?: string[] | undefined;
48
+ /** Selection policy for the resolved auto-review profile pool. */
49
+ reviewModelSelection?: 'round-robin' | 'random' | undefined;
48
50
  /**
49
51
  * Cascade severity threshold from the auto-review plugin config.
50
52
  * When the review subagent finds findings at or above this level, a
@@ -269,80 +269,6 @@ function formatContext(ctx) {
269
269
  return parts.length > 0 ? `[${parts.join(" ")}]` : "";
270
270
  }
271
271
 
272
- // src/utils/tool-description-mode.ts
273
- var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
274
- function normalizeToolDescriptionMode(value) {
275
- if (typeof value !== "string") return void 0;
276
- const raw = value.trim().toLowerCase();
277
- if (raw === "extend" || raw === "extended" || raw === "full") return "extend";
278
- if (raw === "simple" || raw === "short" || raw === "brief") return "simple";
279
- return void 0;
280
- }
281
- function simplifyToolDescription(text, opts = {}) {
282
- const maxSentences = Math.max(1, opts.maxSentences ?? 2);
283
- const maxChars = Math.max(40, opts.maxChars ?? 180);
284
- const normalized = text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
285
- if (normalized.length <= maxChars) return normalized;
286
- const sentences = normalized.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) ?? [normalized];
287
- const selected = [];
288
- for (const sentence of sentences) {
289
- selected.push(sentence.trim());
290
- const candidate = selected.join(" ");
291
- if (selected.length >= maxSentences || candidate.length >= maxChars) break;
292
- }
293
- const summary = selected.join(" ").trim() || normalized;
294
- if (summary.length <= maxChars) return summary;
295
- const hardLimit = maxChars - 4;
296
- const boundary = findWordBoundary(summary, hardLimit);
297
- return `${summary.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd()} ...`;
298
- }
299
- function applyToolDescriptionModeToTool(tool, mode) {
300
- const existingOriginal = getOriginalDescription(tool);
301
- if (mode === "extend" && !existingOriginal) return tool;
302
- const original = existingOriginal ?? {
303
- description: tool.description,
304
- usageHint: tool.usageHint
305
- };
306
- const next = mode === "simple" ? withDescription(tool, {
307
- description: simplifyToolDescription(original.description),
308
- usageHint: original.usageHint === void 0 ? void 0 : simplifyToolDescription(original.usageHint)
309
- }) : withDescription(tool, original);
310
- return attachOriginalDescription(next, original);
311
- }
312
- function getOriginalDescription(tool) {
313
- return tool[ORIGINAL_TOOL_DESCRIPTION];
314
- }
315
- function attachOriginalDescription(tool, original) {
316
- Object.defineProperty(tool, ORIGINAL_TOOL_DESCRIPTION, {
317
- configurable: true,
318
- enumerable: false,
319
- value: original,
320
- writable: true
321
- });
322
- return tool;
323
- }
324
- function withDescription(tool, next) {
325
- const copy = {
326
- ...tool,
327
- description: next.description,
328
- usageHint: next.usageHint
329
- };
330
- if (next.usageHint === void 0) {
331
- delete copy.usageHint;
332
- }
333
- return copy;
334
- }
335
- function findWordBoundary(text, limit) {
336
- const semantic = Math.max(
337
- text.lastIndexOf(". ", limit),
338
- text.lastIndexOf("; ", limit),
339
- text.lastIndexOf(", ", limit)
340
- );
341
- if (semantic > 40) return semantic + 1;
342
- const space = text.lastIndexOf(" ", limit);
343
- return space > 40 ? space : limit;
344
- }
345
-
346
272
  // src/utils/tool-wire-compact.ts
347
273
  var TOOL_DESCRIPTION_MAX_CHARS = 400;
348
274
  var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
@@ -462,6 +388,80 @@ function estimateToolDefTokens(tool) {
462
388
  return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
463
389
  }
464
390
 
391
+ // src/utils/tool-description-mode.ts
392
+ var ORIGINAL_TOOL_DESCRIPTION = /* @__PURE__ */ Symbol.for("wrongstack.tool.originalDescription");
393
+ function normalizeToolDescriptionMode(value) {
394
+ if (typeof value !== "string") return void 0;
395
+ const raw = value.trim().toLowerCase();
396
+ if (raw === "extend" || raw === "extended" || raw === "full") return "extend";
397
+ if (raw === "simple" || raw === "short" || raw === "brief") return "simple";
398
+ return void 0;
399
+ }
400
+ function simplifyToolDescription(text, opts = {}) {
401
+ const maxSentences = Math.max(1, opts.maxSentences ?? 2);
402
+ const maxChars = Math.max(40, opts.maxChars ?? 180);
403
+ const normalized = text.replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
404
+ if (normalized.length <= maxChars) return normalized;
405
+ const sentences = normalized.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g) ?? [normalized];
406
+ const selected = [];
407
+ for (const sentence of sentences) {
408
+ selected.push(sentence.trim());
409
+ const candidate = selected.join(" ");
410
+ if (selected.length >= maxSentences || candidate.length >= maxChars) break;
411
+ }
412
+ const summary = selected.join(" ").trim() || normalized;
413
+ if (summary.length <= maxChars) return summary;
414
+ const hardLimit = maxChars - 4;
415
+ const boundary = findWordBoundary(summary, hardLimit);
416
+ return `${summary.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd()} ...`;
417
+ }
418
+ function applyToolDescriptionModeToTool(tool, mode) {
419
+ const existingOriginal = getOriginalDescription(tool);
420
+ if (mode === "extend" && !existingOriginal) return tool;
421
+ const original = existingOriginal ?? {
422
+ description: tool.description,
423
+ usageHint: tool.usageHint
424
+ };
425
+ const next = mode === "simple" ? withDescription(tool, {
426
+ description: simplifyToolDescription(original.description),
427
+ usageHint: original.usageHint === void 0 ? void 0 : simplifyToolDescription(original.usageHint)
428
+ }) : withDescription(tool, original);
429
+ return attachOriginalDescription(next, original);
430
+ }
431
+ function getOriginalDescription(tool) {
432
+ return tool[ORIGINAL_TOOL_DESCRIPTION];
433
+ }
434
+ function attachOriginalDescription(tool, original) {
435
+ Object.defineProperty(tool, ORIGINAL_TOOL_DESCRIPTION, {
436
+ configurable: true,
437
+ enumerable: false,
438
+ value: original,
439
+ writable: true
440
+ });
441
+ return tool;
442
+ }
443
+ function withDescription(tool, next) {
444
+ const copy = {
445
+ ...tool,
446
+ description: next.description,
447
+ usageHint: next.usageHint
448
+ };
449
+ if (next.usageHint === void 0) {
450
+ delete copy.usageHint;
451
+ }
452
+ return copy;
453
+ }
454
+ function findWordBoundary(text, limit) {
455
+ const semantic = Math.max(
456
+ text.lastIndexOf(". ", limit),
457
+ text.lastIndexOf("; ", limit),
458
+ text.lastIndexOf(", ", limit)
459
+ );
460
+ if (semantic > 40) return semantic + 1;
461
+ const space = text.lastIndexOf(" ", limit);
462
+ return space > 40 ? space : limit;
463
+ }
464
+
465
465
  // src/registry/tool-registry.ts
466
466
  var ToolRegistry = class _ToolRegistry {
467
467
  tools = /* @__PURE__ */ new Map();
@@ -477,6 +477,15 @@ var ToolRegistry = class _ToolRegistry {
477
477
  /** Cached `list()` result, frozen after build. Invalidated on _version change. */
478
478
  _listSnapshot;
479
479
  _listSnapshotVersion = -1;
480
+ /**
481
+ * Optional direct provider surface. `undefined` preserves the legacy
482
+ * behaviour (every enabled catalog tool is direct); when set, `list()` still
483
+ * exposes the complete executable catalog while `listForProvider()` returns
484
+ * only these names. This keeps lazy tools discoverable by tool_search/tool_use.
485
+ */
486
+ _providerToolNames;
487
+ _providerListSnapshot;
488
+ _providerListSnapshotVersion = -1;
480
489
  /** Pre-compute tool definition token estimate once at registration time. */
481
490
  _stampDefTokens(tool) {
482
491
  if (tool._estDefTokens === void 0) {
@@ -726,6 +735,30 @@ var ToolRegistry = class _ToolRegistry {
726
735
  this._listSnapshotVersion = this._version;
727
736
  return arr;
728
737
  }
738
+ /** Replace the direct provider surface. Pass `undefined` to expose the catalog. */
739
+ setProviderToolNames(names) {
740
+ this._providerToolNames = names ? new Set(names) : void 0;
741
+ this._version++;
742
+ }
743
+ /** Add registered or future tool names to the direct provider surface. */
744
+ exposeToProvider(names) {
745
+ if (!this._providerToolNames) return;
746
+ for (const name of typeof names === "string" ? [names] : names) {
747
+ this._providerToolNames.add(name);
748
+ }
749
+ this._version++;
750
+ }
751
+ /** Tools serialized into provider requests and described by the system prompt. */
752
+ listForProvider() {
753
+ if (!this._providerToolNames) return this.list();
754
+ if (this._providerListSnapshot && this._version === this._providerListSnapshotVersion) {
755
+ return this._providerListSnapshot;
756
+ }
757
+ const arr = Array.from(this.tools.entries()).filter(([name]) => !this._disabled.has(name) && this._providerToolNames?.has(name) === true).map(([, entry]) => entry.tool);
758
+ this._providerListSnapshot = arr;
759
+ this._providerListSnapshotVersion = this._version;
760
+ return arr;
761
+ }
729
762
  /**
730
763
  * Group tools by their `category` field. Tools without a category
731
764
  * are placed under the key `""` (empty string). Returns a Map of
@@ -752,6 +785,7 @@ var ToolRegistry = class _ToolRegistry {
752
785
  this.tools.clear();
753
786
  this.descriptionModes.clear();
754
787
  this._disabled.clear();
788
+ this._providerToolNames = void 0;
755
789
  this._version++;
756
790
  }
757
791
  /**
@@ -770,6 +804,7 @@ var ToolRegistry = class _ToolRegistry {
770
804
  for (const name of this._disabled) {
771
805
  copy._disabled.add(name);
772
806
  }
807
+ copy._providerToolNames = this._providerToolNames ? new Set(this._providerToolNames) : void 0;
773
808
  copy._version = this._version;
774
809
  return copy;
775
810
  }
@@ -36,6 +36,15 @@ export declare class ToolRegistry {
36
36
  /** Cached `list()` result, frozen after build. Invalidated on _version change. */
37
37
  private _listSnapshot;
38
38
  private _listSnapshotVersion;
39
+ /**
40
+ * Optional direct provider surface. `undefined` preserves the legacy
41
+ * behaviour (every enabled catalog tool is direct); when set, `list()` still
42
+ * exposes the complete executable catalog while `listForProvider()` returns
43
+ * only these names. This keeps lazy tools discoverable by tool_search/tool_use.
44
+ */
45
+ private _providerToolNames;
46
+ private _providerListSnapshot;
47
+ private _providerListSnapshotVersion;
39
48
  /** Pre-compute tool definition token estimate once at registration time. */
40
49
  private _stampDefTokens;
41
50
  /** Apply the description mode transform and stamp token estimates. */
@@ -134,6 +143,12 @@ export declare class ToolRegistry {
134
143
  owner: string;
135
144
  }[];
136
145
  list(): Tool[];
146
+ /** Replace the direct provider surface. Pass `undefined` to expose the catalog. */
147
+ setProviderToolNames(names: readonly string[] | undefined): void;
148
+ /** Add registered or future tool names to the direct provider surface. */
149
+ exposeToProvider(names: string | readonly string[]): void;
150
+ /** Tools serialized into provider requests and described by the system prompt. */
151
+ listForProvider(): Tool[];
137
152
  /**
138
153
  * Group tools by their `category` field. Tools without a category
139
154
  * are placed under the key `""` (empty string). Returns a Map of
@@ -45,6 +45,8 @@ export declare const ToolCapabilities: {
45
45
  readonly NET_OUTBOUND: 'net.outbound';
46
46
  /** Can mutate in-memory session todos only. */
47
47
  readonly SESSION_TODO: 'session.todo';
48
+ /** Can park after-task `<nextsteps>` suggestions for the current turn. */
49
+ readonly SESSION_NEXTSTEPS: 'session.nextsteps';
48
50
  /** Can mutate in-memory session mode only. */
49
51
  readonly SESSION_MODE: 'session.mode';
50
52
  /** Can inspect registered tool metadata. */
@@ -8,7 +8,7 @@ export { restrictDirPermissions, restrictFilePermissions, SECRET_DIR_MODE, SECRE
8
8
  export { evaluateToolKanbanBoundary, type ToolKanbanBoundaryEvaluation, } from './kanban-boundary.js';
9
9
  export { AutoApprovePermissionPolicy, alwaysAllowUnavailableReason, DefaultPermissionPolicy, type PermissionPolicyOptions, } from './permission-policy.js';
10
10
  export { TRUST_POLICY_JSON_SCHEMA, TRUST_POLICY_LIMITS, TRUST_POLICY_SCHEMA_VERSION, type TrustPolicyDiagnostic, type TrustPolicyDiagnosticCode, type TrustPolicyValidationResult, validateTrustPolicy, } from './permission-policy-schema.js';
11
- export { ReadOnlyPermissionPolicy } from './readonly-permission-policy.js';
11
+ export { ReadOnlyPermissionPolicy, toolMutates } from './readonly-permission-policy.js';
12
12
  export { DefaultSecretScrubber } from './secret-scrubber.js';
13
13
  export { base32Decode, base32Encode, buildOtpAuthUri, generateRecoveryCodes, generateTotpSecret, generateTotp, hashRecoveryCode, verifyRecoveryCode, verifyTotp, verifyTotpCounter, } from './totp.js';
14
14
  export { DefaultSecretVault, migratePlaintextSecrets, rewriteConfigEncrypted, rotateConfigKeys, type SecretVaultOptions, } from './secret-vault.js';
@@ -47,6 +47,8 @@ var ToolCapabilities = {
47
47
  NET_OUTBOUND: "net.outbound",
48
48
  /** Can mutate in-memory session todos only. */
49
49
  SESSION_TODO: "session.todo",
50
+ /** Can park after-task `<nextsteps>` suggestions for the current turn. */
51
+ SESSION_NEXTSTEPS: "session.nextsteps",
50
52
  /** Can mutate in-memory session mode only. */
51
53
  SESSION_MODE: "session.mode",
52
54
  /** Can inspect registered tool metadata. */
@@ -3166,9 +3168,11 @@ var MUTATION_CAPABILITIES = /* @__PURE__ */ new Set([
3166
3168
  ToolCapabilities.PACKAGE_INSTALL,
3167
3169
  ToolCapabilities.SUBAGENT_SPAWN
3168
3170
  ]);
3169
- function hasMutationCapability(tool) {
3171
+ function toolMutates(tool) {
3172
+ if (tool.mutating === true) return true;
3170
3173
  return (tool.capabilities ?? []).some((c) => MUTATION_CAPABILITIES.has(c));
3171
3174
  }
3175
+ var hasMutationCapability = toolMutates;
3172
3176
  function isAllowedReportFile(input, projectRoot) {
3173
3177
  if (!input || typeof input !== "object") return false;
3174
3178
  const record = input;
@@ -3201,10 +3205,12 @@ var ReadOnlyPermissionPolicy = class {
3201
3205
  if (isAllowedReportFile(input, this.projectRoot)) {
3202
3206
  return this.inner.evaluate(tool, input, ctx);
3203
3207
  }
3208
+ const matched = (tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c));
3209
+ const why = matched.length > 0 ? `requires mutation capabilities (${matched.join(", ")})` : "is declared mutating";
3204
3210
  return {
3205
3211
  permission: "deny",
3206
3212
  source: "readonly_mode",
3207
- reason: `Session is in read-only mode: tool "${tool.name}" requires mutation capabilities (${(tool.capabilities ?? []).filter((c) => MUTATION_CAPABILITIES.has(c)).join(", ")}) which are not allowed. Only .md report files under .temp_files/ may be written.`
3213
+ reason: `Session is in read-only mode: tool "${tool.name}" ${why}, which is not allowed. Only .md report files under .temp_files/ may be written.`
3208
3214
  };
3209
3215
  }
3210
3216
  async trust(rule) {
@@ -4227,6 +4233,7 @@ export {
4227
4233
  sanitizeApiError,
4228
4234
  scrubErrorDetail,
4229
4235
  scrubErrorText,
4236
+ toolMutates,
4230
4237
  validateDirectoryPolicy,
4231
4238
  validateTrustPolicy,
4232
4239
  verifyRecoveryCode,
@@ -31,6 +31,26 @@
31
31
  import type { Context } from '../core/context.js';
32
32
  import type { PermissionDecision, PermissionPolicy, PermissionTrace } from '../types/permission.js';
33
33
  import type { Tool } from '../types/tool.js';
34
+ /**
35
+ * Does this tool change something?
36
+ *
37
+ * `capabilities` is the precise answer and stays authoritative — but it is
38
+ * OPT-IN metadata, and a repo-wide count found 154 tools declaring
39
+ * `mutating: true` of which 121 carried no mutation capability at all (28 of
40
+ * those at `permission: 'auto'`, including tools that write API keys and one
41
+ * that sends messages outbound). Read-only mode consulted only `capabilities`,
42
+ * so every one of them ran unblocked and unprompted, while
43
+ * `permission-policy.ts` was already answering the same question with
44
+ * `tool.mutating || …`.
45
+ *
46
+ * Trusting `mutating` here closes all 121 at once and makes the two policies
47
+ * agree. It fails in the safe direction: a tool that declares itself mutating
48
+ * is blocked in read-only mode even if its capability list is incomplete.
49
+ * Fixing the capability labels is still worth doing — it is what the finer
50
+ * grained gates key on — but it is no longer what stands between read-only
51
+ * mode and a write.
52
+ */
53
+ export declare function toolMutates(tool: Tool): boolean;
34
54
  /**
35
55
  * Read-only permission policy — session-scoped toggle via ctx.meta['readOnly'].
36
56
  *
@@ -1,12 +1,39 @@
1
1
  import type { SessionRegistryEntry } from './session-registry-types.js';
2
2
  /** Also the scan interval for {@link pruneStaleTempFiles} — see its caller. */
3
3
  export declare const STALE_TMP_MS = 60000;
4
+ /** `host:pid` owner stamp written into the advisory lock on acquisition. */
5
+ export declare function lockOwnerStamp(): string;
4
6
  /**
5
- * Break a contended lock if it is stale: the recorded owner pid is no longer
6
- * alive, or the lock is older than {@link STALE_LOCK_MS}. Returns true when the
7
- * lock was removed (caller should retry acquisition). Best-effort and
8
- * race-tolerant a fresh lock (age ~0, live owner) is never broken, so the
9
- * common concurrent case self-heals on the next heartbeat.
7
+ * Unlink `lockPath` only if it still carries OUR exact host:pid stamp. A
8
+ * stale-lock breaker may have stolen the lock and re-stamped it with another
9
+ * host:pid; the resumed holder must not remove the new owner's lock. An EMPTY
10
+ * read is never ours to remove either it may be the new owner's lock in its
11
+ * open-to-stamp gap (that lock is mtime-bounded instead).
12
+ *
13
+ * Residual race: between the owner read and the unlink, a foreign-host lease
14
+ * break could replace the lock; unlinking would then remove the new owner's
15
+ * lock. Exploiting it requires a >STALE_LOCK_MS hold AND a rename+stamp
16
+ * landing inside that microsecond window, so it is accepted and bounded by
17
+ * the same foreign-lease logic that enabled it.
18
+ */
19
+ export declare function maybeUnlinkOwnedLock(lockPath: string): Promise<void>;
20
+ /**
21
+ * Break a contended lock if it is stale: a same-host owner pid that is no
22
+ * longer alive, or a foreign-host/ownerless lock older than
23
+ * {@link STALE_LOCK_MS}. A live same-host owner's lock is only broken past
24
+ * {@link SAME_HOST_STALE_MS} — a live-but-wedged holder must not degrade
25
+ * cross-session writes forever, but mtime alone must not false-positive while
26
+ * the holder is mid-critical-section. Returns true when the lock was removed
27
+ * (caller should retry acquisition).
28
+ *
29
+ * Removal is an atomic RENAME to a unique tombstone, never unlink: with
30
+ * unlink, two waiters can both read a stale lock and one's unlink can remove
31
+ * the OTHER's fresh lock, letting both into the critical section (POSIX).
32
+ * Rename wins for exactly one waiter; losers get ENOENT and simply retry.
33
+ * (A rename landing after the winner re-created the lock could still move the
34
+ * winner's fresh lock — a pre-existing microsecond TOCTOU; the caller's
35
+ * stamp-guarded cleanup prevents the resumed holder from unlinking the new
36
+ * owner's lock.)
10
37
  */
11
38
  export declare function breakStaleLock(lockPath: string): Promise<boolean>;
12
39
  export declare function writeAtomicFile(filePath: string, registry: Record<string, SessionRegistryEntry>): Promise<void>;
@@ -109,6 +109,21 @@ export interface AgentEntry {
109
109
  lastActivityAt: string;
110
110
  }
111
111
  export type SessionLiveStatus = 'active' | 'idle' | 'closing' | 'stale' | 'lost';
112
+ export interface SessionWebUIEndpointHint {
113
+ role: 'standalone' | 'parent-shell' | 'session-child';
114
+ surface: 'webui' | 'simpleui' | string;
115
+ host: string;
116
+ httpPort: number;
117
+ url: string;
118
+ /** Redundant owner PID used to cross-check against WebUI instance records. */
119
+ pid: number;
120
+ parentPid?: number | undefined;
121
+ parentShellId?: string | undefined;
122
+ runtimeId?: string | undefined;
123
+ attachable?: boolean | undefined;
124
+ protocolVersion?: number | undefined;
125
+ capabilities?: string[] | undefined;
126
+ }
112
127
  export interface SessionRegistryEntry {
113
128
  sessionId: string;
114
129
  projectSlug: string;
@@ -132,5 +147,7 @@ export interface SessionRegistryEntry {
132
147
  /** Count of tracked agents */
133
148
  agentCount: number;
134
149
  agents: AgentEntry[];
150
+ /** Optional WebUI endpoint hint. Session ownership remains keyed by `sessionId` + `pid`. */
151
+ webuiEndpoint?: SessionWebUIEndpointHint | undefined;
135
152
  }
136
153
  //# sourceMappingURL=session-registry-types.d.ts.map
@@ -13,7 +13,7 @@
13
13
  * @module session-registry
14
14
  */
15
15
  import type { AgentEntry, SessionRegistryEntry } from './session-registry-types.js';
16
- export type { AgentActivityTotals, AgentEntry, AgentLiveStatus, AgentRecentMail, AgentRecentTool, AgentTodoItem, SessionLiveStatus, SessionRegistryEntry, } from './session-registry-types.js';
16
+ export type { AgentActivityTotals, AgentEntry, AgentLiveStatus, AgentRecentMail, AgentRecentTool, AgentTodoItem, SessionLiveStatus, SessionRegistryEntry, SessionWebUIEndpointHint, } from './session-registry-types.js';
17
17
  export declare class SessionRegistry {
18
18
  private readonly filePath;
19
19
  private heartbeatTimer;
@@ -28,6 +28,12 @@ export interface ParsedSkillFrontmatter {
28
28
  * consult this field, so it neither grants nor restricts any tool.
29
29
  */
30
30
  allowedTools?: string[] | undefined;
31
+ /** WrongStack stable runtime capabilities required before this skill may load. */
32
+ requiredCapabilities?: string[] | undefined;
33
+ /** Exact registered tool names required before this skill may render or load. */
34
+ requiredTools?: string[] | undefined;
35
+ /** Capabilities that improve the workflow but have documented fallbacks. */
36
+ optionalCapabilities?: string[] | undefined;
31
37
  }
32
38
  /**
33
39
  * Parse the YAML frontmatter block from a raw SKILL.md file. Returns `{}` when
@@ -40,7 +40,14 @@ function securityScoreToTier(score) {
40
40
  }
41
41
 
42
42
  // src/skills/frontmatter.ts
43
- var SCALAR_KEYS = /* @__PURE__ */ new Set(["name", "description", "trigger", "version", "license", "compatibility"]);
43
+ var SCALAR_KEYS = /* @__PURE__ */ new Set([
44
+ "name",
45
+ "description",
46
+ "trigger",
47
+ "version",
48
+ "license",
49
+ "compatibility"
50
+ ]);
44
51
  function parseSkillFrontmatter(raw) {
45
52
  const text = normalizeLineEndings(raw);
46
53
  if (!text.startsWith("---")) return {};
@@ -99,8 +106,9 @@ function parseFrontmatterBlock(block) {
99
106
  out[normalizeKey(key)] = collected.join("\n").trim();
100
107
  continue;
101
108
  }
102
- if (key === "allowed-tools" || key === "allowedTools") {
103
- out.allowedTools = rest.split(/[\s,]+/).filter(Boolean);
109
+ if (key === "allowed-tools" || key === "allowedTools" || key === "required-capabilities" || key === "requiredCapabilities" || key === "required-tools" || key === "requiredTools" || key === "optional-capabilities" || key === "optionalCapabilities") {
110
+ const values = rest.replace(/^\[/, "").replace(/\]$/, "").split(/[\s,]+/).map(unquote).filter(Boolean);
111
+ out[normalizeKey(key)] = values;
104
112
  i++;
105
113
  continue;
106
114
  }
@@ -114,7 +122,11 @@ function parseFrontmatterBlock(block) {
114
122
  return out;
115
123
  }
116
124
  function normalizeKey(key) {
117
- return key === "allowed-tools" ? "allowedTools" : key;
125
+ if (key === "allowed-tools") return "allowedTools";
126
+ if (key === "required-capabilities") return "requiredCapabilities";
127
+ if (key === "required-tools") return "requiredTools";
128
+ if (key === "optional-capabilities") return "optionalCapabilities";
129
+ return key;
118
130
  }
119
131
  function unquote(s) {
120
132
  if (s.length >= 2 && (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'"))) {
@@ -980,7 +992,12 @@ async function openInEditor(filePath, env = process.env) {
980
992
  const child = spawn(parts[0], [...parts.slice(1), filePath], {
981
993
  stdio: "ignore",
982
994
  detached: true,
983
- shell
995
+ shell,
996
+ // `shell` routes through cmd.exe on win32, which flashes a console window
997
+ // before the editor appears. A GUI editor is unaffected by the flag; the
998
+ // shell wrapper is what it suppresses. Repo convention — see
999
+ // `core/tests/architecture/spawn-convention.test.ts`.
1000
+ windowsHide: true
984
1001
  });
985
1002
  child.unref();
986
1003
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Contract-aware partitioning and sanitization for my.wrongstack.com config sync.
3
+ *
4
+ * The portal's namespace registry owns a fixed set of `config.json` top-level keys and
5
+ * validates each payload against a strict, recursively-specified schema (server repo:
6
+ * `docs/CLIENT_SYNC_CONTRACT.md`). This module is the client half of that contract:
7
+ *
8
+ * - `buildNamespacePayloads` projects the local config onto the contract field tree, so a
9
+ * payload can never carry a field the server does not know (which would 422) — and, more
10
+ * importantly, can never carry credential material: secret-bearing paths simply are not
11
+ * in the tree.
12
+ * - `applyNamespacePayload` merges a pulled server payload back at contract-field
13
+ * granularity, so locally-kept fields the contract does not own (e.g. `tools.exec`,
14
+ * `mcpServers.*.env`, every `providers.*.apiKey`) are never clobbered by a pull.
15
+ *
16
+ * Stripping is therefore structural (projection), with `stripSecretMaterial` as a final
17
+ * defense-in-depth pass mirroring the server's own scan.
18
+ */
19
+ /** `true` = the whole value is contract-owned; an object = recurse per key; `'*'` = record wildcard. */
20
+ export type ContractNode = true | {
21
+ [key: string]: ContractNode;
22
+ };
23
+ /** Namespace → the contract tree over the `config.json` top-level keys it owns. */
24
+ export declare const CLOUD_SYNC_CONTRACT: Readonly<Record<string, ContractNode>>;
25
+ export declare const CLOUD_SYNC_NAMESPACES: string[];
26
+ /** Server-side schema versions, sent in every push envelope. */
27
+ export declare const NAMESPACE_SCHEMA_VERSIONS: Readonly<Record<string, number>>;
28
+ /**
29
+ * Top-level keys that must never leave the machine, per the contract. Everything not in a
30
+ * contract tree stays local implicitly; this list exists for documentation and tests.
31
+ */
32
+ export declare const LOCAL_ONLY_TOP_LEVEL: readonly ['version', 'configScope', 'hq', 'cloudSync', 'sync', 'models', 'apiKey', 'baseUrl'];
33
+ /**
34
+ * Defense-in-depth sweep run over every outgoing payload after projection: drops any
35
+ * secret-named key and any string value that is (or contains) an encrypted blob. Returns
36
+ * `undefined` when nothing survives.
37
+ */
38
+ export declare function stripSecretMaterial(value: unknown): unknown;
39
+ /**
40
+ * Builds the per-namespace payloads to push. Sections absent locally are omitted (every
41
+ * top-level contract key is optional server-side); a namespace with nothing to say is
42
+ * omitted entirely.
43
+ */
44
+ export declare function buildNamespacePayloads(config: Record<string, unknown>): Record<string, Record<string, unknown>>;
45
+ /**
46
+ * Applies a pulled namespace snapshot to the local config object, returning a new object.
47
+ *
48
+ * Only keys present in the payload are touched (an omitted optional section means "the
49
+ * server doesn't know", never "deleted"), and inside each section only contract-owned
50
+ * paths are replaced — `providers.*.apiKey`, `mcpServers.*.env`, `tools.exec`, and any
51
+ * field newer than the contract stay exactly as they are locally.
52
+ */
53
+ export declare function applyNamespacePayload(config: Record<string, unknown>, namespace: string, payload: Record<string, unknown>): Record<string, unknown>;
54
+ //# sourceMappingURL=sanitize.d.ts.map