@wrongstack/core 0.299.0 → 0.301.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 (165) 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 +10 -2
  13. package/dist/coordination/fleet-manager.d.ts +48 -3
  14. package/dist/coordination/ifleet-manager.d.ts +2 -0
  15. package/dist/coordination/index.js +6880 -6440
  16. package/dist/coordination/mail-tools.d.ts +3 -3
  17. package/dist/coordination/mailbox-project-server.js +3 -4
  18. package/dist/coordination/mailbox-types.d.ts +6 -0
  19. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  20. package/dist/core/agent-response.d.ts +5 -1
  21. package/dist/core/agent-tools.d.ts +3 -0
  22. package/dist/core/context.d.ts +20 -0
  23. package/dist/core/fallback-model.d.ts +48 -0
  24. package/dist/core/index.d.ts +11 -9
  25. package/dist/core/index.js +1119 -310
  26. package/dist/core/instruction-template.d.ts +83 -0
  27. package/dist/core/next-steps-slot.d.ts +88 -0
  28. package/dist/core/system-prompt-blocks.d.ts +10 -1
  29. package/dist/core/system-prompt-builder.d.ts +40 -4
  30. package/dist/core/system-prompt-memory-skills.d.ts +1 -0
  31. package/dist/core/system-prompt-skill-bodies.d.ts +3 -3
  32. package/dist/defaults/index.js +9805 -8767
  33. package/dist/design/index.js +11 -3
  34. package/dist/execution/auto-compaction-middleware.d.ts +17 -0
  35. package/dist/execution/autonomy-brain.d.ts +15 -2
  36. package/dist/execution/brain-runtime.d.ts +3 -1
  37. package/dist/execution/compaction-core.d.ts +41 -2
  38. package/dist/execution/council-brain.d.ts +47 -1
  39. package/dist/execution/council-orchestrator.d.ts +25 -6
  40. package/dist/execution/council-prompts.d.ts +12 -1
  41. package/dist/execution/index.d.ts +11 -10
  42. package/dist/execution/index.js +10507 -9189
  43. package/dist/execution/tool-executor.d.ts +4 -1
  44. package/dist/execution/topic-shift-advisor.d.ts +53 -0
  45. package/dist/extension/index.js +8 -2
  46. package/dist/extension/registry.d.ts +8 -1
  47. package/dist/fleet-notifier.d.ts +9 -2
  48. package/dist/goal/index.js +11 -3
  49. package/dist/hooks/index.js +50 -13
  50. package/dist/hooks/runner.d.ts +12 -1
  51. package/dist/hq/index.js +59 -21
  52. package/dist/hq/protocol/fleet.d.ts +20 -0
  53. package/dist/hq/protocol.js +10 -0
  54. package/dist/hq/publisher.d.ts +21 -3
  55. package/dist/index.d.ts +22 -20
  56. package/dist/index.js +12211 -8572
  57. package/dist/infrastructure/index.js +108 -21
  58. package/dist/kernel/events/brain-events.d.ts +9 -0
  59. package/dist/kernel/events/provider-events.d.ts +42 -1
  60. package/dist/kernel/events/session-events.d.ts +13 -0
  61. package/dist/kernel/events/tool-events.d.ts +3 -3
  62. package/dist/models/index.d.ts +1 -0
  63. package/dist/models/index.js +18 -1
  64. package/dist/models/provider-credentials.d.ts +54 -0
  65. package/dist/plugin/api.d.ts +6 -0
  66. package/dist/plugin/config.d.ts +55 -0
  67. package/dist/plugin/index.d.ts +2 -1
  68. package/dist/plugin/index.js +2179 -544
  69. package/dist/plugins/auto-review-plugin.d.ts +3 -0
  70. package/dist/plugins/cloud-config-sync-plugin.d.ts +22 -0
  71. package/dist/plugins/review-claim-registry.d.ts +23 -8
  72. package/dist/plugins/review-types.d.ts +2 -0
  73. package/dist/registry/index.js +109 -74
  74. package/dist/registry/tool-registry.d.ts +15 -0
  75. package/dist/security/capabilities.d.ts +2 -0
  76. package/dist/security/index.d.ts +2 -2
  77. package/dist/security/index.js +166 -44
  78. package/dist/security/permission-helpers.d.ts +23 -6
  79. package/dist/security/permission-policy.d.ts +16 -0
  80. package/dist/security/readonly-permission-policy.d.ts +20 -0
  81. package/dist/security/totp.d.ts +14 -0
  82. package/dist/session-registry-atomic-file.d.ts +32 -5
  83. package/dist/skills/frontmatter.d.ts +6 -0
  84. package/dist/skills/index.js +22 -5
  85. package/dist/storage/cloud-config-sync/sanitize.d.ts +54 -0
  86. package/dist/storage/cloud-config-sync.d.ts +87 -0
  87. package/dist/storage/director-state.d.ts +7 -0
  88. package/dist/storage/index.d.ts +1 -0
  89. package/dist/storage/index.js +887 -74
  90. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +898 -335
  93. package/dist/tools/one-shot-llm-tool.d.ts +1 -0
  94. package/dist/tools/plugin-manager.d.ts +27 -0
  95. package/dist/types/config/mcp-features.d.ts +18 -11
  96. package/dist/types/config/root.d.ts +8 -1
  97. package/dist/types/config/runtime.d.ts +20 -6
  98. package/dist/types/config/skills-fleet-brain.d.ts +12 -4
  99. package/dist/types/config/tools.d.ts +16 -0
  100. package/dist/types/context-window.d.ts +1 -0
  101. package/dist/types/council.d.ts +11 -0
  102. package/dist/types/hooks.d.ts +14 -0
  103. package/dist/types/index.d.ts +3 -3
  104. package/dist/types/index.js +1 -0
  105. package/dist/types/multi-agent.d.ts +12 -0
  106. package/dist/types/one-shot-llm.d.ts +25 -0
  107. package/dist/types/plugin.d.ts +28 -0
  108. package/dist/types/provider.d.ts +16 -0
  109. package/dist/types/runtime-capability-manifest.d.ts +168 -0
  110. package/dist/types/skill.d.ts +5 -0
  111. package/dist/types/system-prompt.d.ts +3 -1
  112. package/dist/types/tool-executor.d.ts +8 -1
  113. package/dist/utils/context-breakdown.d.ts +8 -2
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +141 -31
  116. package/dist/utils/regex-guard.d.ts +16 -2
  117. package/dist/utils/sage-output-block.d.ts +7 -10
  118. package/dist/utils/wstack-paths.d.ts +11 -3
  119. package/dist/worktree/index.js +4 -4
  120. package/instructions/agents/browser.md +1 -4
  121. package/instructions/agents/e2e.md +17 -15
  122. package/instructions/agents/ios.md +3 -3
  123. package/instructions/agents/search.md +1 -1
  124. package/instructions/autonomy/goal-preamble.md +4 -4
  125. package/instructions/coordination/director-preamble.md +2 -3
  126. package/instructions/coordination/subagent-baseline.md +3 -1
  127. package/instructions/leader-after-task.md +12 -0
  128. package/instructions/llm/chimera-review.md +1 -1
  129. package/instructions/modes/audit-lite.md +1 -1
  130. package/instructions/sections/tool/common-patterns.md +18 -2
  131. package/instructions/sections/tool/mailbox-compact.md +1 -1
  132. package/instructions/sections/tool/mailbox-full.md +1 -1
  133. package/instructions/system-lite.md +91 -3
  134. package/instructions/system-pro.md +296 -102
  135. package/instructions/system.md +248 -92
  136. package/package.json +3 -3
  137. package/skills/api-design/SKILL.md +3 -0
  138. package/skills/audit-log/SKILL.md +2 -0
  139. package/skills/auto-review/SKILL.md +6 -1
  140. package/skills/bug-hunter/SKILL.md +4 -1
  141. package/skills/chimera/SKILL.md +3 -0
  142. package/skills/data-governance/SKILL.md +3 -0
  143. package/skills/design-system/SKILL.md +5 -2
  144. package/skills/docker-deploy/SKILL.md +2 -0
  145. package/skills/git-flow/SKILL.md +2 -0
  146. package/skills/mailbox-bridge/SKILL.md +3 -0
  147. package/skills/mnemosyne/SKILL.md +2 -0
  148. package/skills/multi-agent/SKILL.md +4 -1
  149. package/skills/node-modern/SKILL.md +4 -1
  150. package/skills/observability/SKILL.md +3 -0
  151. package/skills/output-standards/SKILL.md +2 -0
  152. package/skills/plugin-author/SKILL.md +4 -1
  153. package/skills/prompt-engineering/SKILL.md +3 -1
  154. package/skills/react-modern/SKILL.md +6 -3
  155. package/skills/refactor-planner/SKILL.md +2 -0
  156. package/skills/research-web/SKILL.md +3 -0
  157. package/skills/sdd/SKILL.md +3 -1
  158. package/skills/security-scanner/SKILL.md +3 -0
  159. package/skills/skill-creator/SKILL.md +2 -0
  160. package/skills/tech-stack/SKILL.md +3 -0
  161. package/skills/testing/SKILL.md +4 -1
  162. package/skills/typescript-strict/SKILL.md +4 -1
  163. package/skills/wrongstack-kanban/SKILL.md +6 -3
  164. package/skills/wrongstack-mailbox/SKILL.md +4 -1
  165. package/skills/wrongstack-mailbox-mcp/SKILL.md +10 -8
@@ -67,7 +67,10 @@ export declare class ToolExecutor {
67
67
  * Also installs the governed bridge because confirmed tools are re-run
68
68
  * through this method after a pending confirmation.
69
69
  */
70
- executeTool(tool: Tool, use: ToolUseBlock, ctx: Context, budget: number): Promise<{
70
+ executeTool(tool: Tool, use: ToolUseBlock, ctx: Context, budget: number, preToolContext?: {
71
+ text: string;
72
+ contextAs: 'inline' | 'separate';
73
+ }): Promise<{
71
74
  block: ToolResultBlock;
72
75
  bytes: number;
73
76
  }>;
@@ -0,0 +1,53 @@
1
+ import type { Context } from '../core/context.js';
2
+ import { type Message, type Provider } from '../types/index.js';
3
+ export interface TopicShiftAdvice {
4
+ suggestNewContext: boolean;
5
+ confidence: number;
6
+ reason: string;
7
+ nextTopic?: string | undefined;
8
+ source: 'explicit' | 'model' | 'cache' | 'local';
9
+ }
10
+ export interface TopicShiftAdvisorInput {
11
+ prompt: string;
12
+ messages: readonly Message[];
13
+ provider?: Provider | undefined;
14
+ model?: string | undefined;
15
+ contextTokens?: number | undefined;
16
+ maxContext?: number | undefined;
17
+ signal?: AbortSignal | undefined;
18
+ /** Notifies prompt surfaces only when the remote ambiguity check actually runs. */
19
+ onModelCheck?: ((busy: boolean) => void) | undefined;
20
+ }
21
+ export interface TopicShiftAdvisorOptions {
22
+ capacity?: number | undefined;
23
+ ttlMs?: number | undefined;
24
+ timeoutMs?: number | undefined;
25
+ now?: (() => number) | undefined;
26
+ }
27
+ /**
28
+ * Best-effort, bounded topic-boundary advisor for prompt-submit surfaces.
29
+ *
30
+ * Most prompts finish in the local gate. A separate provider call happens
31
+ * only after history is substantial AND the new prompt has little lexical
32
+ * continuity with recent user goals. Results are held in a small TTL/LRU so
33
+ * edit/retry or duplicate submits never pay twice.
34
+ */
35
+ export declare class TopicShiftAdvisor {
36
+ private readonly capacity;
37
+ private readonly ttlMs;
38
+ private readonly timeoutMs;
39
+ private readonly now;
40
+ private readonly cache;
41
+ constructor(options?: TopicShiftAdvisorOptions);
42
+ advise(input: TopicShiftAdvisorInput): Promise<TopicShiftAdvice>;
43
+ private classify;
44
+ private get;
45
+ private set;
46
+ }
47
+ /**
48
+ * Start a clean model context without deleting the session journal or visible
49
+ * chat. Durable memory, provider/model selection, settings and project state
50
+ * remain owned by their existing services.
51
+ */
52
+ export declare function startFreshTopicContext(ctx: Context): Promise<void>;
53
+ //# sourceMappingURL=topic-shift-advisor.d.ts.map
@@ -258,9 +258,15 @@ var ExtensionRegistry = class {
258
258
  * Build a composed provider runner. Extensions with `wrapProviderRunner`
259
259
  * form a middleware-style chain: the innermost extension wraps the
260
260
  * default runner, each subsequent wrapper wraps the previous.
261
+ *
262
+ * Utility calls may exclude agent-loop-only middleware (notably
263
+ * `fallback-model`) while retaining request-safety wrappers such as the
264
+ * prompt firewall. One-shot orchestration owns its provider and fallback
265
+ * route, so an agent-loop fallback wrapper cannot safely run against it.
261
266
  */
262
- wrapProviderRunner(inner) {
263
- const wrappers = this.extensions.filter((e) => e.wrapProviderRunner).map((e) => ({ name: e.name, wrap: expectDefined(e.wrapProviderRunner) }));
267
+ wrapProviderRunner(inner, options) {
268
+ const excluded = new Set(options?.exclude);
269
+ const wrappers = this.extensions.filter((e) => e.wrapProviderRunner && !excluded.has(e.name)).map((e) => ({ name: e.name, wrap: expectDefined(e.wrapProviderRunner) }));
264
270
  if (wrappers.length === 0) return inner;
265
271
  let composed = inner;
266
272
  for (let i = wrappers.length - 1; i >= 0; i--) {
@@ -78,8 +78,15 @@ export declare class ExtensionRegistry {
78
78
  * Build a composed provider runner. Extensions with `wrapProviderRunner`
79
79
  * form a middleware-style chain: the innermost extension wraps the
80
80
  * default runner, each subsequent wrapper wraps the previous.
81
+ *
82
+ * Utility calls may exclude agent-loop-only middleware (notably
83
+ * `fallback-model`) while retaining request-safety wrappers such as the
84
+ * prompt firewall. One-shot orchestration owns its provider and fallback
85
+ * route, so an agent-loop fallback wrapper cannot safely run against it.
81
86
  */
82
- wrapProviderRunner(inner: ProviderRunnerFn): ProviderRunnerFn;
87
+ wrapProviderRunner(inner: ProviderRunnerFn, options?: {
88
+ exclude?: readonly string[] | undefined;
89
+ }): ProviderRunnerFn;
83
90
  runBeforeToolExecution(...args: Parameters<BeforeToolExecutionHook>): Promise<Parameters<BeforeToolExecutionHook>[1]>;
84
91
  runAfterToolExecution(...args: Parameters<AfterToolExecutionHook>): Promise<void>;
85
92
  }
@@ -5,8 +5,13 @@ export interface FleetNotifierOptions {
5
5
  projectRoot: string;
6
6
  /** This process's pid, so a WebUI never pings itself. */
7
7
  selfPid?: number | undefined;
8
- /** Injectable POST for tests. Defaults to a timed `fetch`. */
9
- post?: ((url: string) => Promise<void>) | undefined;
8
+ /**
9
+ * Injectable POST for tests. Defaults to a timed `fetch`.
10
+ *
11
+ * The second parameter carries the target's API token (H3). It is optional
12
+ * so an existing `(url) => …` stub stays assignable.
13
+ */
14
+ post?: ((url: string, token?: string | undefined) => Promise<void>) | undefined;
10
15
  }
11
16
  export declare class FleetNotifier {
12
17
  private readonly baseDir;
@@ -21,6 +26,8 @@ export declare class FleetNotifier {
21
26
  notify(): void;
22
27
  /** Resolve same-project WebUI ping URLs (cached briefly). Exposed for tests. */
23
28
  endpoints(): Promise<string[]>;
29
+ /** Ping targets with their tokens (cached briefly). */
30
+ private targets;
24
31
  dispose(): void;
25
32
  /** Re-scope notifications after an in-process project switch. */
26
33
  setProjectRoot(projectRoot: string): void;
@@ -2765,6 +2765,9 @@ function canonicalProjectRoot(absRoot) {
2765
2765
  const commonDirFile = path4.join(gitDir, "commondir");
2766
2766
  if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
2767
2767
  const commonDir = path4.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
2768
+ const worktreesDir = path4.dirname(gitDir);
2769
+ if (path4.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2770
+ if (path4.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2768
2771
  if (path4.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2769
2772
  return path4.dirname(commonDir);
2770
2773
  } catch {
@@ -2787,7 +2790,7 @@ function safeProfileName(name) {
2787
2790
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2788
2791
  return safe || "default";
2789
2792
  }
2790
- function activeProfileName(globalRoot) {
2793
+ function bootstrapProfileName(globalRoot) {
2791
2794
  try {
2792
2795
  const parsed = JSON.parse(fs.readFileSync(path4.join(globalRoot, "config.json"), "utf8"));
2793
2796
  return safeProfileName(
@@ -2805,7 +2808,7 @@ function wstackGlobalRoot() {
2805
2808
  function resolveWstackPaths(opts) {
2806
2809
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2807
2810
  const homeDir = opts.userHome ?? os.homedir();
2808
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
2811
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2809
2812
  const profileDir = path4.join(globalRoot, "profiles", profileName);
2810
2813
  const hash = projectHash(opts.projectRoot);
2811
2814
  const slug = projectSlug(opts.projectRoot);
@@ -2875,7 +2878,12 @@ function resolveWstackPaths(opts) {
2875
2878
  projectRequirementIntakes: path4.join(projectDir, "requirement-intakes"),
2876
2879
  syncConfig: path4.join(profileDir, "sync.json"),
2877
2880
  configHistoryDir: path4.join(globalRoot, "config-history"),
2878
- projectStatus: (projectHash2) => path4.join(globalRoot, "projects", projectHash2, "status.json")
2881
+ projectStatus: (statusProjectSlug) => {
2882
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
2883
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
2884
+ }
2885
+ return path4.join(globalRoot, "projects", statusProjectSlug, "status.json");
2886
+ }
2879
2887
  };
2880
2888
  }
2881
2889
 
@@ -776,20 +776,36 @@ function hookMatcherMatches(matcher, toolName) {
776
776
 
777
777
  // src/hooks/runner.ts
778
778
  var DEFAULT_TIMEOUT_MS3 = 5e3;
779
+ var DEFAULT_BACKGROUND_TIMEOUT_MS = 6e4;
779
780
  var MAX_TIMEOUT_MS = 10 * 6e4;
781
+ function hookTimeoutMs(entry) {
782
+ return entry.timeoutMs ?? (entry.background ? DEFAULT_BACKGROUND_TIMEOUT_MS : DEFAULT_TIMEOUT_MS3);
783
+ }
784
+ function contextFromOutcome(outcome) {
785
+ return {
786
+ ...outcome.additionalContext ? { additionalContext: outcome.additionalContext } : {},
787
+ ...outcome.contextAs ? { contextAs: outcome.contextAs } : {}
788
+ };
789
+ }
780
790
  function normalizePreToolOutcome(outcome) {
781
- if ("action" in outcome) return outcome;
791
+ const context = contextFromOutcome(outcome);
792
+ if ("action" in outcome) return { ...outcome, ...context };
782
793
  if (outcome.decision === "block") {
783
- return { action: "deny", reason: outcome.reason?.trim() || "blocked by PreToolUse hook" };
794
+ return {
795
+ action: "deny",
796
+ reason: outcome.reason?.trim() || "blocked by PreToolUse hook",
797
+ ...context
798
+ };
784
799
  }
785
800
  if (outcome.modifiedInput && typeof outcome.modifiedInput === "object") {
786
801
  return {
787
802
  action: "mutate",
788
803
  input: outcome.modifiedInput,
789
- ...outcome.reason ? { reason: outcome.reason } : {}
804
+ ...outcome.reason ? { reason: outcome.reason } : {},
805
+ ...context
790
806
  };
791
807
  }
792
- return { action: "allow" };
808
+ return { action: "allow", ...context };
793
809
  }
794
810
  function malformedOutcome(outcome) {
795
811
  if (!("action" in outcome)) return void 0;
@@ -819,11 +835,26 @@ var HookRunner = class {
819
835
  has(event) {
820
836
  return this.registry.has(event);
821
837
  }
822
- async preToolUse(toolName, toolInput, env) {
838
+ async preToolUse(toolName, toolInput, env, toolMeta) {
823
839
  const entries = this.matching("PreToolUse", toolName);
824
840
  if (entries.length === 0) return {};
841
+ const declared = {
842
+ ...toolMeta?.capabilities ? { toolCapabilities: toolMeta.capabilities } : {},
843
+ ...toolMeta?.mutating === void 0 ? {} : { toolMutating: toolMeta.mutating }
844
+ };
825
845
  let current = toolInput;
826
846
  let mutated = false;
847
+ const contexts = [];
848
+ let contextAs;
849
+ const collectContext = (outcome) => {
850
+ if (outcome.additionalContext) contexts.push(outcome.additionalContext);
851
+ if (outcome.contextAs === "separate") contextAs = "separate";
852
+ else if (outcome.contextAs === "inline" && contextAs === void 0) contextAs = "inline";
853
+ };
854
+ const resultContext = () => ({
855
+ ...contexts.length > 0 ? { additionalContext: contexts.join("\n") } : {},
856
+ ...contextAs ? { contextAs } : {}
857
+ });
827
858
  const mutators = entries.filter((entry) => entry.stage === "mutate");
828
859
  const validators = entries.filter((entry) => entry.stage === "validate");
829
860
  for (const entry of mutators) {
@@ -831,13 +862,15 @@ var HookRunner = class {
831
862
  event: "PreToolUse",
832
863
  toolName,
833
864
  toolInput: current,
865
+ ...declared,
834
866
  ...this.base(env)
835
867
  };
836
868
  const outcome = await this.invoke(entry, payload, env);
837
869
  if (!outcome) continue;
838
870
  const normalized = normalizePreToolOutcome(outcome);
871
+ collectContext(normalized);
839
872
  if (normalized.action === "deny") {
840
- return { block: true, reason: normalized.reason };
873
+ return { block: true, reason: normalized.reason, ...resultContext() };
841
874
  }
842
875
  if (normalized.action === "mutate") {
843
876
  current = normalized.input;
@@ -849,21 +882,25 @@ var HookRunner = class {
849
882
  event: "PreToolUse",
850
883
  toolName,
851
884
  toolInput: current,
885
+ ...declared,
852
886
  ...this.base(env)
853
887
  };
854
888
  const outcome = await this.invoke(entry, payload, env);
855
889
  if (!outcome) continue;
856
890
  const normalized = normalizePreToolOutcome(outcome);
891
+ collectContext(normalized);
857
892
  if (normalized.action === "deny") {
858
- return { block: true, reason: normalized.reason };
893
+ return { block: true, reason: normalized.reason, ...resultContext() };
859
894
  }
860
895
  if (normalized.action === "mutate") {
861
896
  const reason = `PreToolUse validator "${entry.name}" attempted to mutate tool arguments`;
862
897
  this.opts.logger?.warn?.(reason);
863
- if (entry.failurePolicy === "closed") return { block: true, reason };
898
+ if (entry.failurePolicy === "closed") {
899
+ return { block: true, reason, ...resultContext() };
900
+ }
864
901
  }
865
902
  }
866
- return mutated ? { input: current } : {};
903
+ return { ...mutated ? { input: current } : {}, ...resultContext() };
867
904
  }
868
905
  async postToolUse(toolName, toolInput, result, env) {
869
906
  const payload = {
@@ -961,12 +998,12 @@ var HookRunner = class {
961
998
  result = await this.invokeInProcess(entry, payload, env);
962
999
  } else {
963
1000
  result = entry.kind === "shell" ? await runShellHookDetailed(
964
- { command: entry.command, timeoutMs: entry.timeoutMs },
1001
+ { command: entry.command, timeoutMs: hookTimeoutMs(entry) },
965
1002
  payload,
966
1003
  this.opts.logger,
967
1004
  { signal: env.signal }
968
1005
  ) : await runHttpHookDetailed(
969
- { url: entry.url, headers: entry.headers, timeoutMs: entry.timeoutMs },
1006
+ { url: entry.url, headers: entry.headers, timeoutMs: hookTimeoutMs(entry) },
970
1007
  payload,
971
1008
  this.opts.logger,
972
1009
  { signal: env.signal }
@@ -983,12 +1020,12 @@ var HookRunner = class {
983
1020
  }
984
1021
  if (!result.failure) return result.outcome;
985
1022
  this.opts.logger?.warn?.(
986
- `${payload.event} hook "${entry.name}" failed (${result.failure.kind}): ${result.failure.message}`
1023
+ `${payload.event} ${entry.background ? "background " : ""}hook "${entry.name}" failed (${result.failure.kind}): ${result.failure.message}`
987
1024
  );
988
1025
  return this.failureOutcome(entry, payload, result.failure);
989
1026
  }
990
1027
  async invokeInProcess(entry, payload, env) {
991
- const timeoutMs = Math.max(1, Math.min(entry.timeoutMs ?? DEFAULT_TIMEOUT_MS3, MAX_TIMEOUT_MS));
1028
+ const timeoutMs = Math.max(1, Math.min(hookTimeoutMs(entry), MAX_TIMEOUT_MS));
992
1029
  const controller = new AbortController();
993
1030
  let timedOut = false;
994
1031
  const onParentAbort = () => controller.abort(env.signal?.reason);
@@ -22,6 +22,8 @@ export interface PreToolUseResult {
22
22
  reason?: string | undefined;
23
23
  /** Present only when a hook replaced the tool input. */
24
24
  input?: Record<string, unknown>;
25
+ additionalContext?: string | undefined;
26
+ contextAs?: 'inline' | 'separate' | undefined;
25
27
  }
26
28
  export interface PromptResult {
27
29
  block?: boolean | undefined;
@@ -46,7 +48,16 @@ export declare class HookRunner {
46
48
  constructor(opts: HookRunnerOptions);
47
49
  /** Cheap guard so callers can skip building payloads when nothing listens. */
48
50
  has(event: HookEvent): boolean;
49
- preToolUse(toolName: string, toolInput: Record<string, unknown>, env: HookRunEnv): Promise<PreToolUseResult>;
51
+ preToolUse(toolName: string, toolInput: Record<string, unknown>, env: HookRunEnv,
52
+ /**
53
+ * What the tool declares about itself. Optional so existing callers (and
54
+ * tests) keep working, but the executor always passes it — a policy hook
55
+ * that keys on capability instead of tool name needs it to be there.
56
+ */
57
+ toolMeta?: {
58
+ capabilities?: readonly string[] | undefined;
59
+ mutating?: boolean | undefined;
60
+ }): Promise<PreToolUseResult>;
50
61
  postToolUse(toolName: string, toolInput: unknown, result: {
51
62
  content: string;
52
63
  isError: boolean;
package/dist/hq/index.js CHANGED
@@ -492,6 +492,9 @@ function isHqSubagentSummary(x) {
492
492
  const v = x;
493
493
  return typeof v.subagentId === "string" && typeof v.status === "string" && HQ_FLEET_SUBAGENT_STATUS.has(v.status);
494
494
  }
495
+ function isOptionalFiniteNumber(x) {
496
+ return x === void 0 || typeof x === "number" && Number.isFinite(x);
497
+ }
495
498
  function isHqFleetSnapshotPayload(x) {
496
499
  if (typeof x !== "object" || x === null) return false;
497
500
  const v = x;
@@ -501,6 +504,13 @@ function isHqFleetSnapshotPayload(x) {
501
504
  for (const s of v.subagents) {
502
505
  if (!isHqSubagentSummary(s)) return false;
503
506
  }
507
+ if (!isOptionalFiniteNumber(v.maxConcurrent)) return false;
508
+ if (!isOptionalFiniteNumber(v.maxSpawns)) return false;
509
+ if (!isOptionalFiniteNumber(v.usedSpawns)) return false;
510
+ if (!isOptionalFiniteNumber(v.remainingSpawns)) return false;
511
+ if (!isOptionalFiniteNumber(v.checkpointMaxSpawns)) return false;
512
+ if (v.effectiveSource !== void 0 && typeof v.effectiveSource !== "string") return false;
513
+ if (v.ceilingMismatch !== void 0 && typeof v.ceilingMismatch !== "boolean") return false;
504
514
  return true;
505
515
  }
506
516
  function isHqFleetEventPayload(x) {
@@ -1295,6 +1305,7 @@ function createMailboxEventPayload(input) {
1295
1305
  var TRANSCRIPT_EVENT_TYPES = /* @__PURE__ */ new Set(["session.transcript", "agent.message"]);
1296
1306
  var OPEN_STATE = 1;
1297
1307
  var CONNECT_WARN_AFTER_FAILURES = 5;
1308
+ var DEFAULT_CONNECT_WARN_COOLDOWN_MS = 5 * 6e4;
1298
1309
  var DEFAULT_RECONNECT_BASE_MS = 1e3;
1299
1310
  var DEFAULT_RECONNECT_MAX_MS = 3e4;
1300
1311
  var DEFAULT_DISCOVERY_POLL_MS = 5e3;
@@ -1306,6 +1317,7 @@ var DEFAULT_MAX_QUEUED_BYTES = Math.min(
1306
1317
  var DEFAULT_COMMAND_POLL_INTERVAL_MS = 500;
1307
1318
  var DEFAULT_COMMAND_POLL_LIMIT = 25;
1308
1319
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 25e3;
1320
+ var lastConnectWarningAtMs = 0;
1309
1321
  function queuedFrameCoalesceKey(frame) {
1310
1322
  if (frame.type !== "client.event" || !frame.event.type.endsWith(".snapshot")) {
1311
1323
  return void 0;
@@ -1357,6 +1369,8 @@ var HqPublisher = class {
1357
1369
  this.reconnect = options.reconnect ?? true;
1358
1370
  this.reconnectBaseMs = options.reconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
1359
1371
  this.reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
1372
+ this.connectWarnAfterFailures = options.connectWarnAfterFailures ?? CONNECT_WARN_AFTER_FAILURES;
1373
+ this.connectWarnCooldownMs = options.connectWarnCooldownMs ?? DEFAULT_CONNECT_WARN_COOLDOWN_MS;
1360
1374
  this.maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
1361
1375
  this.maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
1362
1376
  this.resolvedRedactionPolicy = resolveHqRedactionPolicy(options.redactionPolicy);
@@ -1370,6 +1384,8 @@ var HqPublisher = class {
1370
1384
  reconnect;
1371
1385
  reconnectBaseMs;
1372
1386
  reconnectMaxMs;
1387
+ connectWarnAfterFailures;
1388
+ connectWarnCooldownMs;
1373
1389
  maxQueuedMessages;
1374
1390
  maxQueuedBytes;
1375
1391
  resolvedRedactionPolicy;
@@ -1781,9 +1797,8 @@ var HqPublisher = class {
1781
1797
  if (this.stopped || !this.reconnect || this.reconnectTimer !== null) return;
1782
1798
  const delay = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** this.reconnectAttempt);
1783
1799
  this.reconnectAttempt += 1;
1784
- if (!this.connectWarningEmitted && this.reconnectAttempt >= CONNECT_WARN_AFTER_FAILURES) {
1785
- this.connectWarningEmitted = true;
1786
- this.emitConnectWarning();
1800
+ if (!this.connectWarningEmitted && this.reconnectAttempt >= this.connectWarnAfterFailures) {
1801
+ if (this.emitConnectWarning()) this.connectWarningEmitted = true;
1787
1802
  }
1788
1803
  this.reconnectTimer = setTimeout(() => {
1789
1804
  this.reconnectTimer = null;
@@ -1792,11 +1807,16 @@ var HqPublisher = class {
1792
1807
  this.reconnectTimer.unref?.();
1793
1808
  }
1794
1809
  emitConnectWarning() {
1810
+ const nowMs = Date.now();
1811
+ if (nowMs - lastConnectWarningAtMs < this.connectWarnCooldownMs) {
1812
+ return false;
1813
+ }
1814
+ lastConnectWarningAtMs = nowMs;
1795
1815
  const attempt = this.lastAttempt;
1796
1816
  const message = `WrongStack HQ publisher: ${this.reconnectAttempt} consecutive connection failures${attempt !== null ? ` to ${attempt.url} (client token ${attempt.hadToken ? "present" : "absent"})` : ""}. Either the HQ server is unreachable or it rejected the token (401). If HQ runs in client-token mode, verify WRONGSTACK_HQ_TOKEN / auth.json. Retries continue with backoff.`;
1797
1817
  if (this.logger) {
1798
1818
  this.logger.warn(message, { event: "hq.publisher.connect_failed" });
1799
- return;
1819
+ return true;
1800
1820
  }
1801
1821
  const warn = this.options.warn ?? ((msg) => console.warn(
1802
1822
  JSON.stringify({
@@ -1810,6 +1830,7 @@ var HqPublisher = class {
1810
1830
  warn(message);
1811
1831
  } catch {
1812
1832
  }
1833
+ return true;
1813
1834
  }
1814
1835
  /**
1815
1836
  * Dormant re-check while no HQ endpoint is discoverable. Uses a FIXED
@@ -2061,6 +2082,20 @@ function windowsAccountName() {
2061
2082
  return username;
2062
2083
  }
2063
2084
 
2085
+ // src/utils/pid.ts
2086
+ function isPidAlive(pid) {
2087
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2088
+ if (pid === process.pid) return true;
2089
+ try {
2090
+ process.kill(pid, 0);
2091
+ return true;
2092
+ } catch (err) {
2093
+ const code = err.code;
2094
+ if (code === "EPERM") return true;
2095
+ return false;
2096
+ }
2097
+ }
2098
+
2064
2099
  // src/utils/wstack-paths.ts
2065
2100
  import { createHash } from "node:crypto";
2066
2101
  import * as fs2 from "node:fs";
@@ -2078,6 +2113,9 @@ function canonicalProjectRoot(absRoot) {
2078
2113
  const commonDirFile = path2.join(gitDir, "commondir");
2079
2114
  if (!fs2.statSync(commonDirFile).isFile()) return checkoutRoot;
2080
2115
  const commonDir = path2.resolve(gitDir, fs2.readFileSync(commonDirFile, "utf8").trim());
2116
+ const worktreesDir = path2.dirname(gitDir);
2117
+ if (path2.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2118
+ if (path2.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2081
2119
  if (path2.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2082
2120
  return path2.dirname(commonDir);
2083
2121
  } catch {
@@ -2100,7 +2138,7 @@ function safeProfileName(name) {
2100
2138
  const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2101
2139
  return safe || "default";
2102
2140
  }
2103
- function activeProfileName(globalRoot) {
2141
+ function bootstrapProfileName(globalRoot) {
2104
2142
  try {
2105
2143
  const parsed = JSON.parse(fs2.readFileSync(path2.join(globalRoot, "config.json"), "utf8"));
2106
2144
  return safeProfileName(
@@ -2118,7 +2156,7 @@ function wstackGlobalRoot() {
2118
2156
  function resolveWstackPaths(opts) {
2119
2157
  const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2120
2158
  const homeDir = opts.userHome ?? os.homedir();
2121
- const profileName = safeProfileName(opts.profileName ?? activeProfileName(globalRoot));
2159
+ const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2122
2160
  const profileDir = path2.join(globalRoot, "profiles", profileName);
2123
2161
  const hash = projectHash(opts.projectRoot);
2124
2162
  const slug = projectSlug(opts.projectRoot);
@@ -2188,7 +2226,12 @@ function resolveWstackPaths(opts) {
2188
2226
  projectRequirementIntakes: path2.join(projectDir, "requirement-intakes"),
2189
2227
  syncConfig: path2.join(profileDir, "sync.json"),
2190
2228
  configHistoryDir: path2.join(globalRoot, "config-history"),
2191
- projectStatus: (projectHash2) => path2.join(globalRoot, "projects", projectHash2, "status.json")
2229
+ projectStatus: (statusProjectSlug) => {
2230
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
2231
+ throw new Error(`Invalid project slug: ${statusProjectSlug}`);
2232
+ }
2233
+ return path2.join(globalRoot, "projects", statusProjectSlug, "status.json");
2234
+ }
2192
2235
  };
2193
2236
  }
2194
2237
 
@@ -2319,22 +2362,13 @@ async function writeHqRuntimeFile(dataDir, file) {
2319
2362
  `, { mode: SECRET_FILE_MODE });
2320
2363
  await restrictFilePermissions(target, { label: "hq-runtime" });
2321
2364
  }
2322
- function isProcessAlive(pid) {
2323
- if (!Number.isInteger(pid) || pid <= 0) return false;
2324
- try {
2325
- process.kill(pid, 0);
2326
- return true;
2327
- } catch {
2328
- return false;
2329
- }
2330
- }
2331
2365
  function readHqRuntimeFileSync(dataDir) {
2332
2366
  try {
2333
2367
  const parsed = JSON.parse(
2334
2368
  syncFs.readFileSync(hqRuntimeFilePath(dataDir), "utf8")
2335
2369
  );
2336
2370
  if (typeof parsed.url !== "string" || parsed.url.trim().length === 0) return void 0;
2337
- if (typeof parsed.pid === "number" && !isProcessAlive(parsed.pid)) return void 0;
2371
+ if (typeof parsed.pid === "number" && !isPidAlive(parsed.pid)) return void 0;
2338
2372
  return {
2339
2373
  url: parsed.url,
2340
2374
  updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
@@ -2574,9 +2608,6 @@ function watchHqAuthFile(dataDir, onChange, opts = {}) {
2574
2608
  }
2575
2609
  }
2576
2610
  });
2577
- watcher.on("error", (err) => {
2578
- opts.warn?.(`HQ auth watcher error: ${err.message}`);
2579
- });
2580
2611
  return {
2581
2612
  close: () => {
2582
2613
  closed = true;
@@ -3313,7 +3344,14 @@ function startFleetTelemetryBridge(opts) {
3313
3344
  completedTasks: stats.completed,
3314
3345
  failedTasks: stats.stopped,
3315
3346
  ...stats.totalCostUsd !== void 0 ? { totalCostUsd: stats.totalCostUsd } : {},
3316
- subagents
3347
+ subagents,
3348
+ ...stats.maxConcurrent !== void 0 ? { maxConcurrent: stats.maxConcurrent } : {},
3349
+ ...stats.maxSpawns !== void 0 ? { maxSpawns: stats.maxSpawns } : {},
3350
+ ...stats.usedSpawns !== void 0 ? { usedSpawns: stats.usedSpawns } : {},
3351
+ ...stats.remainingSpawns !== void 0 ? { remainingSpawns: stats.remainingSpawns } : {},
3352
+ ...stats.effectiveSource !== void 0 ? { effectiveSource: stats.effectiveSource } : {},
3353
+ ...stats.checkpointMaxSpawns !== void 0 ? { checkpointMaxSpawns: stats.checkpointMaxSpawns } : {},
3354
+ ...stats.ceilingMismatch ? { ceilingMismatch: true } : {}
3317
3355
  };
3318
3356
  }
3319
3357
  const off = events.on("coordinator.stats", (stats) => {
@@ -7,6 +7,19 @@ export interface HqFleetSnapshotPayload {
7
7
  failedTasks: number;
8
8
  totalCostUsd?: number;
9
9
  subagents: readonly HqSubagentSummary[];
10
+ /** Concurrent-subagent ceiling (maxConcurrent). */
11
+ maxConcurrent?: number | undefined;
12
+ /** Lifetime spawn ceiling (maxSpawns). */
13
+ maxSpawns?: number | undefined;
14
+ /** Cumulative spawns consumed this director run. */
15
+ usedSpawns?: number | undefined;
16
+ /** maxSpawns − usedSpawns (clamped ≥ 0). */
17
+ remainingSpawns?: number | undefined;
18
+ /** Winning config source labels, e.g. "maxConcurrent=profile, maxSpawns=env". */
19
+ effectiveSource?: string | undefined;
20
+ /** Historical checkpoint maxSpawns when it disagreed with the live ceiling. */
21
+ checkpointMaxSpawns?: number | undefined;
22
+ ceilingMismatch?: boolean | undefined;
10
23
  }
11
24
  export interface HqFleetEventPayload {
12
25
  runId: string;
@@ -62,6 +75,13 @@ export interface HqFleetSummary {
62
75
  failedTasks: number;
63
76
  totalCostUsd?: number;
64
77
  lastActivityAt: string;
78
+ maxConcurrent?: number | undefined;
79
+ maxSpawns?: number | undefined;
80
+ usedSpawns?: number | undefined;
81
+ remainingSpawns?: number | undefined;
82
+ effectiveSource?: string | undefined;
83
+ checkpointMaxSpawns?: number | undefined;
84
+ ceilingMismatch?: boolean | undefined;
65
85
  }
66
86
  export interface HqQueuedCommand {
67
87
  commandId: string;
@@ -492,6 +492,9 @@ function isHqSubagentSummary(x) {
492
492
  const v = x;
493
493
  return typeof v.subagentId === "string" && typeof v.status === "string" && HQ_FLEET_SUBAGENT_STATUS.has(v.status);
494
494
  }
495
+ function isOptionalFiniteNumber(x) {
496
+ return x === void 0 || typeof x === "number" && Number.isFinite(x);
497
+ }
495
498
  function isHqFleetSnapshotPayload(x) {
496
499
  if (typeof x !== "object" || x === null) return false;
497
500
  const v = x;
@@ -501,6 +504,13 @@ function isHqFleetSnapshotPayload(x) {
501
504
  for (const s of v.subagents) {
502
505
  if (!isHqSubagentSummary(s)) return false;
503
506
  }
507
+ if (!isOptionalFiniteNumber(v.maxConcurrent)) return false;
508
+ if (!isOptionalFiniteNumber(v.maxSpawns)) return false;
509
+ if (!isOptionalFiniteNumber(v.usedSpawns)) return false;
510
+ if (!isOptionalFiniteNumber(v.remainingSpawns)) return false;
511
+ if (!isOptionalFiniteNumber(v.checkpointMaxSpawns)) return false;
512
+ if (v.effectiveSource !== void 0 && typeof v.effectiveSource !== "string") return false;
513
+ if (v.ceilingMismatch !== void 0 && typeof v.ceilingMismatch !== "boolean") return false;
504
514
  return true;
505
515
  }
506
516
  function isHqFleetEventPayload(x) {
@@ -32,6 +32,20 @@ export interface HqPublisherOptions {
32
32
  reconnect?: boolean;
33
33
  reconnectBaseMs?: number;
34
34
  reconnectMaxMs?: number;
35
+ /**
36
+ * Consecutive connect failures (never reaching `open`) before the one-time
37
+ * diagnostic warning fires. Defaults to `CONNECT_WARN_AFTER_FAILURES` (5).
38
+ */
39
+ connectWarnAfterFailures?: number;
40
+ /**
41
+ * Minimum gap between connect-failure warnings ACROSS all HqPublisher
42
+ * instances in this process. When a fleet of agents fails against the same
43
+ * dead/rejecting HQ at once, only the first instance to cross the threshold
44
+ * warns; the rest stay silent until the window elapses (each instance still
45
+ * warns at most once). Defaults to 5 minutes. Set to 0 to disable the
46
+ * process-wide gate entirely.
47
+ */
48
+ connectWarnCooldownMs?: number;
35
49
  maxQueuedMessages?: number;
36
50
  /** Hard byte cap on the outbound queue (prevents RAM blow-up when HQ is
37
51
  * offline). Older frames are dropped oldest-first when exceeded. The default
@@ -66,11 +80,13 @@ export interface HqPublisherOptions {
66
80
  */
67
81
  heartbeatIntervalMs?: number;
68
82
  /**
69
- * Diagnostic sink for the one-time consecutive-connect-failure warning.
70
- * The publisher is otherwise deliberately silent (dormant queueing), which
83
+ * Diagnostic sink for the consecutive-connect-failure warning. The
84
+ * publisher is otherwise deliberately silent (dormant queueing), which
71
85
  * made auth failures invisible: a token the server rejects — e.g. a wrong
72
86
  * `WRONGSTACK_HQ_TOKEN` against a remote HQ — looked exactly like "HQ not
73
- * running". Defaults to a single structured `console.warn` line.
87
+ * running". Emission is one-shot per instance and rate-limited process-wide
88
+ * via {@link HqPublisherOptions.connectWarnCooldownMs}. Defaults to a
89
+ * single structured `console.warn` line.
74
90
  */
75
91
  warn?: (message: string) => void;
76
92
  /** Logger for structured events. When provided, the `warn` callback is derived from it. */
@@ -99,6 +115,8 @@ export declare class HqPublisher {
99
115
  private readonly reconnect;
100
116
  private readonly reconnectBaseMs;
101
117
  private readonly reconnectMaxMs;
118
+ private readonly connectWarnAfterFailures;
119
+ private readonly connectWarnCooldownMs;
102
120
  private readonly maxQueuedMessages;
103
121
  private readonly maxQueuedBytes;
104
122
  private readonly resolvedRedactionPolicy;