@webpresso/agent-kit 3.1.26 → 3.1.30

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 (150) hide show
  1. package/catalog/AGENTS.md.tpl +7 -11
  2. package/catalog/agent/commands/verify.md +2 -2
  3. package/catalog/agent/rules/changeset-release.md +4 -2
  4. package/catalog/agent/rules/ci-cost-local-first.md +139 -0
  5. package/catalog/agent/rules/ci-test-perf.md +5 -1
  6. package/catalog/agent/rules/cmd-execution.md +9 -0
  7. package/catalog/agent/rules/pre-implementation.md +2 -1
  8. package/catalog/agent/rules/test-scan-perf.md +80 -0
  9. package/catalog/agent/rules/workflow-skills-routing.md +9 -4
  10. package/catalog/agent/skills/autopilot/SKILL.md +48 -11
  11. package/catalog/agent/skills/claude/SKILL.md +7 -0
  12. package/catalog/agent/skills/codex/SKILL.md +7 -0
  13. package/catalog/agent/skills/fix/SKILL.md +36 -8
  14. package/catalog/agent/skills/grok/SKILL.md +7 -0
  15. package/catalog/agent/skills/investigate/SKILL.md +12 -1
  16. package/catalog/agent/skills/pll/SKILL.md +53 -48
  17. package/catalog/agent/skills/team/SKILL.md +26 -9
  18. package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
  19. package/catalog/agent/skills/verify/SKILL.md +89 -51
  20. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
  21. package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
  22. package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
  23. package/dist/esm/audit/registry.d.ts +4 -4
  24. package/dist/esm/audit/registry.js +3 -0
  25. package/dist/esm/audit/roadmap-links.js +6 -0
  26. package/dist/esm/audit/test-scan-perf.d.ts +18 -0
  27. package/dist/esm/audit/test-scan-perf.js +275 -0
  28. package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
  29. package/dist/esm/blueprint/lifecycle/engine.js +53 -6
  30. package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
  31. package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
  32. package/dist/esm/blueprint/markdown/helpers.js +6 -2
  33. package/dist/esm/ci/act-replay.js +110 -7
  34. package/dist/esm/ci/act-runner.d.ts +4 -0
  35. package/dist/esm/ci/act-runner.js +85 -26
  36. package/dist/esm/ci/act-worktree-git.d.ts +11 -0
  37. package/dist/esm/ci/act-worktree-git.js +219 -0
  38. package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
  39. package/dist/esm/ci/native-session-memory-cache.js +9 -1
  40. package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
  41. package/dist/esm/ci/vitest-ci-shards.js +6 -4
  42. package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
  43. package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
  44. package/dist/esm/cli/auto-update/installer.d.ts +4 -17
  45. package/dist/esm/cli/auto-update/installer.js +6 -57
  46. package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
  47. package/dist/esm/cli/auto-update/self-invocation.js +62 -0
  48. package/dist/esm/cli/commands/audit.js +4 -0
  49. package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
  50. package/dist/esm/cli/commands/blueprint/router.js +27 -2
  51. package/dist/esm/cli/commands/ci.d.ts +8 -1
  52. package/dist/esm/cli/commands/ci.js +80 -21
  53. package/dist/esm/cli/commands/dash/index.js +3 -3
  54. package/dist/esm/cli/commands/dash/keymap.js +1 -0
  55. package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
  56. package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
  57. package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
  58. package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
  59. package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
  60. package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
  61. package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
  62. package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
  63. package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
  64. package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
  65. package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
  66. package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
  67. package/dist/esm/cli/commands/dash/plan-once.d.ts +142 -0
  68. package/dist/esm/cli/commands/dash/plan-once.js +261 -0
  69. package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
  70. package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
  71. package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
  72. package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
  73. package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
  74. package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
  75. package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
  76. package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
  77. package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
  78. package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
  79. package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
  80. package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
  81. package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
  82. package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
  83. package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
  84. package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
  85. package/dist/esm/cli/commands/dash/tui/runtime.js +311 -4
  86. package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
  87. package/dist/esm/cli/commands/dash/tui/state.js +257 -4
  88. package/dist/esm/cli/commands/format.js +0 -2
  89. package/dist/esm/cli/commands/init/config.d.ts +0 -1
  90. package/dist/esm/cli/commands/init/config.js +1 -4
  91. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
  92. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
  93. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
  94. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
  95. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
  96. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
  97. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
  98. package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
  99. package/dist/esm/cli/commands/logs.d.ts +15 -0
  100. package/dist/esm/cli/commands/logs.js +192 -3
  101. package/dist/esm/cli/commands/package-manager.js +2 -1
  102. package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
  103. package/dist/esm/cli/commands/quality-log-store.js +187 -51
  104. package/dist/esm/cli/commands/review.js +5 -8
  105. package/dist/esm/cli/commands/secrets.d.ts +1 -0
  106. package/dist/esm/cli/commands/secrets.js +4 -1
  107. package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
  108. package/dist/esm/cli/commands/self-install-guard.js +17 -63
  109. package/dist/esm/cli/direct-provider-launch.js +25 -1
  110. package/dist/esm/hooks/doctor.d.ts +15 -0
  111. package/dist/esm/hooks/doctor.js +56 -0
  112. package/dist/esm/hooks/guard-switch/index.js +8 -4
  113. package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
  114. package/dist/esm/hooks/precompact/index.js +5 -3
  115. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
  116. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
  117. package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
  118. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
  119. package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
  120. package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
  121. package/dist/esm/hooks/sessionstart/index.js +61 -4
  122. package/dist/esm/hooks/shared/types.d.ts +11 -0
  123. package/dist/esm/hooks/shared/types.js +21 -0
  124. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  125. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  126. package/dist/esm/mcp/tools/audit.js +10 -0
  127. package/dist/esm/mcp/tools/audits.d.ts +1 -0
  128. package/dist/esm/mcp/tools/ci-act.js +9 -11
  129. package/dist/esm/paths/state-root.js +9 -0
  130. package/dist/esm/review/authority.js +17 -2
  131. package/dist/esm/review/availability.js +18 -5
  132. package/dist/esm/review/subject.d.ts +15 -0
  133. package/dist/esm/review/subject.js +105 -10
  134. package/dist/esm/runtime/executor.d.ts +1 -0
  135. package/dist/esm/runtime/executor.js +2 -1
  136. package/dist/esm/secret-gate/runner.d.ts +1 -0
  137. package/dist/esm/secret-gate/runner.js +4 -1
  138. package/dist/esm/session-memory/current-session.js +2 -0
  139. package/dist/esm/session-memory/native-runtime.js +5 -3
  140. package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
  141. package/dist/esm/session-memory/native-warm-markers.js +22 -0
  142. package/dist/esm/session-memory/sync/types.d.ts +4 -0
  143. package/dist/esm/session-memory/types.d.ts +3 -1
  144. package/dist/esm/session-memory/types.js +2 -0
  145. package/dist/esm/ultragoal/runtime.js +44 -2
  146. package/dist/esm/worktrees/current.d.ts +1 -0
  147. package/dist/esm/worktrees/current.js +1 -0
  148. package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
  149. package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
  150. package/package.json +17 -14
@@ -1790,6 +1790,61 @@ export function checkProjectMcpPins(cwd = process.cwd(), deps = {}) {
1790
1790
  : `${parsed.definitions.length} exact-pinned vp dlx server(s) validated`,
1791
1791
  };
1792
1792
  }
1793
+ /**
1794
+ * `claude-mcp-duplicate-registration` — advisory. A machine can register the
1795
+ * webpresso MCP server twice for the same Claude Code session: a manual
1796
+ * `claude mcp add webpresso` entry in `~/.claude.json` (user scope or a
1797
+ * project scope) plus the agent-kit marketplace plugin's bundled server
1798
+ * (plugin cache under `~/.claude/plugins/`). Both spawn `wp mcp`, so affected
1799
+ * sessions run two server processes and carry a duplicated wp_* tool list.
1800
+ * Degrades to ok on missing/unreadable config — loud only on real duplication.
1801
+ */
1802
+ export function checkClaudeMcpDuplicateRegistration(options = {}) {
1803
+ const name = "claude MCP duplicate registration";
1804
+ const home = options.home ?? homedir();
1805
+ const exists = options.exists ?? existsSync;
1806
+ const readFile = options.readFile ?? ((candidate) => readFileSync(candidate, "utf8"));
1807
+ const pluginCacheDir = join(home, ".claude", "plugins", "cache", "webpresso", "agent-kit");
1808
+ if (!exists(pluginCacheDir)) {
1809
+ return { name, ok: true, detail: "skipped (agent-kit Claude plugin not installed)" };
1810
+ }
1811
+ const claudeJsonPath = join(home, ".claude.json");
1812
+ if (!exists(claudeJsonPath)) {
1813
+ return { name, ok: true, detail: "skipped (no ~/.claude.json)" };
1814
+ }
1815
+ let parsed;
1816
+ try {
1817
+ parsed = JSON.parse(readFile(claudeJsonPath));
1818
+ }
1819
+ catch {
1820
+ return { name, ok: true, detail: "skipped (~/.claude.json unreadable)" };
1821
+ }
1822
+ if (parsed === null || typeof parsed !== "object") {
1823
+ return { name, ok: true, detail: "skipped (~/.claude.json is not an object)" };
1824
+ }
1825
+ const config = parsed;
1826
+ const manualSources = [];
1827
+ if (config.mcpServers && "webpresso" in config.mcpServers) {
1828
+ manualSources.push("user scope");
1829
+ }
1830
+ for (const [projectDir, project] of Object.entries(config.projects ?? {})) {
1831
+ if (project?.mcpServers && "webpresso" in project.mcpServers) {
1832
+ manualSources.push(`project scope ${projectDir}`);
1833
+ }
1834
+ }
1835
+ if (manualSources.length === 0) {
1836
+ return { name, ok: true, detail: "plugin-scoped registration only" };
1837
+ }
1838
+ return {
1839
+ name,
1840
+ ok: false,
1841
+ advisory: true,
1842
+ detail: `the agent-kit Claude plugin bundles the webpresso MCP server, but ~/.claude.json also ` +
1843
+ `registers "webpresso" manually (${manualSources.join(", ")}). Affected sessions run two ` +
1844
+ `wp mcp processes with a duplicated wp_* tool list. Remove the manual entry: ` +
1845
+ `claude mcp remove webpresso (run inside the project directory for project-scope entries).`,
1846
+ };
1847
+ }
1793
1848
  const QUOTED_TOML_STRING = /"((?:[^"\\]|\\.)*)"/gu;
1794
1849
  const DANGLING_SYMLINK_SCAN_DEPTH = 3;
1795
1850
  function collectDanglingWorktreePaths(raw, worktreesRoot, exists, alreadyReported) {
@@ -1994,6 +2049,7 @@ export async function runHooksDoctor(opts = {}) {
1994
2049
  checks.push(checkCodexConfigKeys());
1995
2050
  }
1996
2051
  checks.push(checkProjectMcpPins(opts.cwd));
2052
+ checks.push(checkClaudeMcpDuplicateRegistration());
1997
2053
  checks.push({ advisory: true, ...checkDanglingHostPaths() });
1998
2054
  checks.push({ advisory: true, ...checkRootLauncherContract() });
1999
2055
  checks.push({ advisory: true, ...checkNativePluginRuntime() });
@@ -6,9 +6,10 @@ import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
6
6
  import { runHook } from "#hooks/shared/hook-bootstrap";
7
7
  import { resolveHookProjectDir } from "#hooks/shared/project-dir.js";
8
8
  import { createManagedHookExecutionContext, reportSessionMemoryHookDegradation, sessionMemoryHookStoreOptions, } from "#hooks/shared/session-memory-deadline.js";
9
+ import { resolveHookSessionId } from "#hooks/shared/types.js";
9
10
  import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
10
11
  import { buildContinuityEvent, buildPromptContinuityEvents } from "#session-memory/hook-capture.js";
11
- import { repoHashFromRoot } from "#session-memory/repo-hash.js";
12
+ import { computeRepoHash, repoHashFromRoot } from "#session-memory/repo-hash.js";
12
13
  import { SessionMemorySessionStore, } from "#session-memory/session.js";
13
14
  import { classifyPromptRoutingIntent, } from "#hooks/prompt-routing";
14
15
  import { setActivePromptRouting, setGuardEnabled } from "./state.js";
@@ -67,13 +68,15 @@ function capturePromptContinuity(input, projectDir, env, deps, routing) {
67
68
  const store = deps.createStore?.(dbPath, deps.storeOptions) ??
68
69
  new SessionMemorySessionStore(dbPath, deps.storeOptions);
69
70
  try {
70
- const repoHash = deps.repoHash?.(projectDir) ?? repoHashFromRoot(projectDir);
71
+ // Match readCurrentSessionId hashing (computeRepoHash), not path-only.
72
+ const repoHash = deps.repoHash?.(projectDir) ?? computeRepoHash(projectDir);
71
73
  const timestamp = (deps.now?.() ?? new Date()).toISOString();
74
+ const sessionId = resolveHookSessionId(input, env);
72
75
  for (const event of events) {
73
76
  store.captureEvent({
74
77
  repoHash,
75
78
  agentId: input.agent_id ?? input.agent_type ?? "UserPromptSubmit",
76
- ...(input.session_id === undefined ? {} : { sessionId: input.session_id }),
79
+ ...(sessionId === undefined ? {} : { sessionId }),
77
80
  event: {
78
81
  ...event,
79
82
  ts: timestamp,
@@ -124,13 +127,14 @@ export function processGuardSwitchInput(inputValue, cwd, env = process.env, deps
124
127
  reason: "🛡️ Guard enabled — pretool validators active",
125
128
  };
126
129
  }
130
+ const routingSessionId = resolveHookSessionId(input, env);
127
131
  const routing = normalized.length === 0
128
132
  ? null
129
133
  : {
130
134
  intent: deps.classifyPromptRoutingIntent?.(normalized) ??
131
135
  classifyPromptRoutingIntent(normalized),
132
136
  updatedAt: (deps.now?.() ?? new Date()).toISOString(),
133
- ...(input.session_id === undefined ? {} : { sessionId: input.session_id }),
137
+ ...(routingSessionId === undefined ? {} : { sessionId: routingSessionId }),
134
138
  ...(input.turn_id === undefined ? {} : { turnId: input.turn_id }),
135
139
  };
136
140
  if (routing !== null) {
@@ -5,11 +5,11 @@ import { tmpdir } from "node:os";
5
5
  import { runHook } from "#hooks/shared/hook-bootstrap";
6
6
  import { resolveHookProjectDir } from "#hooks/shared/project-dir.js";
7
7
  import { createManagedHookExecutionContext, reportSessionMemoryHookDegradation, sessionMemoryHookStoreOptions, } from "#hooks/shared/session-memory-deadline.js";
8
- import { getCommand, getContent, getFilePath, isBashInput, isFileEditInput, isFileReadInput, isFileWriteInput, } from "#hooks/shared/types";
8
+ import { getCommand, getContent, getFilePath, isBashInput, isFileEditInput, isFileReadInput, isFileWriteInput, resolveHookSessionId, } from "#hooks/shared/types";
9
9
  import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
10
10
  import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
11
11
  import { buildContinuityEvent } from "#session-memory/hook-capture.js";
12
- import { repoHashFromRoot } from "#session-memory/repo-hash.js";
12
+ import { computeRepoHash, repoHashFromRoot } from "#session-memory/repo-hash.js";
13
13
  import { SessionMemorySessionStore, } from "#session-memory/session.js";
14
14
  export const LINTABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json", ".css"];
15
15
  export const DEFAULT_MAX_CAPTURE_BYTES = 2048;
@@ -116,7 +116,8 @@ export function capturePostToolUse(input, projectDir, env = process.env, deps =
116
116
  const store = deps.createStore?.(dbPath, deps.storeOptions) ??
117
117
  new SessionMemorySessionStore(dbPath, deps.storeOptions);
118
118
  try {
119
- const repoHash = deps.repoHash?.(projectDir) ?? repoHashFromRoot(projectDir);
119
+ // Match readCurrentSessionId hashing (computeRepoHash), not path-only.
120
+ const repoHash = deps.repoHash?.(projectDir) ?? computeRepoHash(projectDir);
120
121
  const event = buildContinuityEvent({
121
122
  eventType,
122
123
  toolName: input.tool_name ?? "PostToolUse",
@@ -131,10 +132,11 @@ export function capturePostToolUse(input, projectDir, env = process.env, deps =
131
132
  },
132
133
  maxContentBytes: DEFAULT_MAX_CAPTURE_BYTES,
133
134
  });
135
+ const sessionId = resolveHookSessionId(input, env);
134
136
  store.captureEvent({
135
137
  repoHash,
136
138
  agentId: input.tool_name ?? "default",
137
- ...(input.session_id === undefined ? {} : { sessionId: input.session_id }),
139
+ ...(sessionId === undefined ? {} : { sessionId }),
138
140
  event: { ...event, ts: (deps.now?.() ?? new Date()).toISOString() },
139
141
  });
140
142
  return true;
@@ -6,9 +6,10 @@ import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
6
6
  import { runHook } from "#hooks/shared/hook-bootstrap";
7
7
  import { resolveHookProjectDir } from "#hooks/shared/project-dir.js";
8
8
  import { createManagedHookExecutionContext, reportSessionMemoryHookDegradation, sessionMemoryHookStoreOptions, } from "#hooks/shared/session-memory-deadline.js";
9
+ import { resolveHookSessionId } from "#hooks/shared/types.js";
9
10
  import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
10
11
  import { buildContinuityEvent } from "#session-memory/hook-capture.js";
11
- import { repoHashFromRoot } from "#session-memory/repo-hash.js";
12
+ import { computeRepoHash, repoHashFromRoot } from "#session-memory/repo-hash.js";
12
13
  import { SessionMemorySessionStore, } from "#session-memory/session.js";
13
14
  export const DEFAULT_MAX_SNAPSHOT_BYTES = 32 * 1024;
14
15
  export const DEFAULT_MAX_EVENT_BYTES = 2 * 1024;
@@ -61,14 +62,15 @@ export function buildOutput(inputValue, cwd, env, deps = {}) {
61
62
  try {
62
63
  const input = normalizeInput(inputValue);
63
64
  const projectDir = resolveHookProjectDir(input, cwd, env);
64
- const repoHash = deps.repoHash?.(projectDir) ?? repoHashFromRoot(projectDir);
65
+ // Match readCurrentSessionId hashing (computeRepoHash), not path-only.
66
+ const repoHash = deps.repoHash?.(projectDir) ?? computeRepoHash(projectDir);
65
67
  const dbPath = deps.dbPath ?? resolveSessionMemoryDbPath(projectDir, env);
66
68
  mkdirSync(dirname(dbPath), { recursive: true });
67
69
  const store = deps.createStore?.(dbPath, deps.storeOptions) ??
68
70
  new SessionMemorySessionStore(dbPath, deps.storeOptions);
69
71
  try {
70
72
  const agentId = input.agent_id ?? input.agent_type ?? "default";
71
- const sessionId = input.session_id;
73
+ const sessionId = resolveHookSessionId(input, env);
72
74
  const timestamp = (deps.now?.() ?? new Date()).toISOString();
73
75
  const boundary = buildContinuityEvent({
74
76
  eventType: "compaction_boundary",
@@ -371,7 +371,7 @@ function findWpAuditRedirect(command) {
371
371
  for (const variant of getCommandVariants(command)) {
372
372
  const kind = WP_AUDIT_RE.exec(variant)?.[1];
373
373
  if (kind === "guardrails") {
374
- return `"${variant}" denied — use the MCP batch audit tool: mcp__webpresso__wp_audits(preset="guardrails"). Returns aggregate structured, summary-first results.`;
374
+ return `"${variant}" denied — use the webpresso MCP batch audit tool: wp_audits(preset="guardrails"). Returns aggregate structured, summary-first results.`;
375
375
  }
376
376
  if (!kind || !AUDIT_KIND_SET.has(kind))
377
377
  continue;
@@ -381,11 +381,11 @@ function findWpAuditRedirect(command) {
381
381
  knownKinds.push(kind);
382
382
  }
383
383
  if (knownKinds.length > 1) {
384
- return `Multiple wp audit commands denied — use the MCP batch audit tool: mcp__webpresso__wp_audits(kinds=[${knownKinds.map((kind) => `"${kind}"`).join(", ")}]). Returns aggregate structured, summary-first results.`;
384
+ return `Multiple wp audit commands denied — use the webpresso MCP batch audit tool: wp_audits(kinds=[${knownKinds.map((kind) => `"${kind}"`).join(", ")}]). Returns aggregate structured, summary-first results.`;
385
385
  }
386
386
  const kind = knownKinds[0];
387
387
  if (kind && firstKnownVariant) {
388
- return `"${firstKnownVariant}" denied — use the MCP audit tool: mcp__webpresso__wp_audit(kind="${kind}"). Returns structured, summary-first results.`;
388
+ return `"${firstKnownVariant}" denied — use the webpresso MCP audit tool: wp_audit(kind="${kind}"). Returns structured, summary-first results.`;
389
389
  }
390
390
  return undefined;
391
391
  }
@@ -402,7 +402,7 @@ function findScriptRouteRedirect(command, routes) {
402
402
  process.stderr.write(`[forbidden-commands] guard.scriptRoutes["${script}"] -> "${kind}" is not a known audit kind; ignoring\n`);
403
403
  continue;
404
404
  }
405
- return `"${variant}" denied — this repo routes \`${script}\` to an audit: mcp__webpresso__wp_audit(kind="${kind}").`;
405
+ return `"${variant}" denied — this repo routes \`${script}\` to an audit: use the webpresso MCP tool wp_audit(kind="${kind}").`;
406
406
  }
407
407
  return undefined;
408
408
  }
@@ -1,6 +1,5 @@
1
1
  import type { CommandCategory } from "./forbidden-commands.js";
2
2
  export interface MCPRedirectConfig {
3
- serverName?: string;
4
3
  toolPrefix?: string;
5
4
  }
6
5
  export interface MCPRedirectContext {
@@ -1,5 +1,4 @@
1
1
  import { isMcpReady } from "#hooks/shared/mcp-sentinel";
2
- const DEFAULT_SERVER_NAME = "webpresso";
3
2
  const DEFAULT_TOOL_PREFIX = "wp_";
4
3
  let cachedDefaultMcpReady;
5
4
  export function resetMcpReadyCache() {
@@ -22,7 +21,6 @@ export function resolveMcpReady(value) {
22
21
  }
23
22
  export function resolveMcpConfig(mcp) {
24
23
  return {
25
- serverName: mcp?.serverName?.trim() || DEFAULT_SERVER_NAME,
26
24
  toolPrefix: mcp?.toolPrefix?.trim() || DEFAULT_TOOL_PREFIX,
27
25
  };
28
26
  }
@@ -50,10 +48,15 @@ export function buildRedirectMessage(ctx) {
50
48
  if (!resolveMcpReady(ctx.mcpReady)) {
51
49
  return `"${ctx.command}" denied — MCP not ready. Use: ${fallbackHint}`;
52
50
  }
53
- const { serverName, toolPrefix } = resolveMcpConfig(ctx.mcp);
54
- const toolMatchers = toolSuffixesForCategory(ctx.category).map((suffix) => `mcp__${serverName}__${toolPrefix}${suffix}(...)`);
51
+ // Suffix-only tool names: hosts register the webpresso MCP server under
52
+ // different prefixes (manual `mcp__webpresso__*` vs plugin-scoped
53
+ // `mcp__plugin_agent-kit_webpresso__*`), so a hardcoded full name resolves in
54
+ // some sessions and dangles in others. The `wp_<suffix>` identity is stable
55
+ // across every registration shape.
56
+ const { toolPrefix } = resolveMcpConfig(ctx.mcp);
57
+ const toolMatchers = toolSuffixesForCategory(ctx.category).map((suffix) => `${toolPrefix}${suffix}(...)`);
55
58
  return [
56
- `"${ctx.command}" denied — use wp MCP tool:`,
59
+ `"${ctx.command}" denied — use the webpresso MCP tool:`,
57
60
  ...toolMatchers.map((matcher) => ` ${matcher}`),
58
61
  "Returns structured, summary-first results. Raw output is clipped; overflow may include a log path for deeper investigation.",
59
62
  `Fallback if MCP unavailable: ${fallbackHint}`,
@@ -17,13 +17,15 @@ import type { ToolInput, ValidationResult } from "#hooks/shared/types";
17
17
  * effective cwd elsewhere (e.g. CI) → ALLOW (rule scoped to local ~/repos primaries)
18
18
  * ```
19
19
  *
20
- * The "effective cwd" is the tool's ambient `input.cwd`, lowered only by a
21
- * success-gated cwd chain that directly governs the forbidden git op, e.g.
22
- * `cd <worktree> && git commit` or `(cd <worktree> && git commit)`, then by
23
- * EVERY `git -C <dir>` flag on the matched invocation (git applies them
24
- * cumulatively). Unsupported cwd-changing control flow, option-bearing `cd`,
25
- * `cd -`, or unresolvable `cd`/`-C` targets fail closed because they could hide
26
- * a primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
20
+ * The "effective cwd" is the tool's ambient `input.cwd`, lowered by a
21
+ * success-gated `&&` chain that governs the forbidden git op, e.g.
22
+ * `cd <worktree> && git commit`, `cd <worktree> && ./bin/wp … && git commit`,
23
+ * or `(cd <worktree> && git commit)`, then by EVERY `git -C <dir>` flag on the
24
+ * matched invocation (git applies them cumulatively). Intermediate non-cwd
25
+ * commands after a proven `cd` leave the chain cwd unchanged. Unsupported
26
+ * control flow (`;` / `|` / braces), option-bearing `cd`, `cd -`, or
27
+ * unresolvable `cd`/`-C` targets fail closed because they could hide a
28
+ * primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
27
29
  * command with the tool cwd already set to the worktree.
28
30
  *
29
31
  * Known best-effort limits (acceptable for an agent guard with a documented
@@ -25,13 +25,15 @@ const VALIDATOR_NAME = "worktree-discipline";
25
25
  * effective cwd elsewhere (e.g. CI) → ALLOW (rule scoped to local ~/repos primaries)
26
26
  * ```
27
27
  *
28
- * The "effective cwd" is the tool's ambient `input.cwd`, lowered only by a
29
- * success-gated cwd chain that directly governs the forbidden git op, e.g.
30
- * `cd <worktree> && git commit` or `(cd <worktree> && git commit)`, then by
31
- * EVERY `git -C <dir>` flag on the matched invocation (git applies them
32
- * cumulatively). Unsupported cwd-changing control flow, option-bearing `cd`,
33
- * `cd -`, or unresolvable `cd`/`-C` targets fail closed because they could hide
34
- * a primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
28
+ * The "effective cwd" is the tool's ambient `input.cwd`, lowered by a
29
+ * success-gated `&&` chain that governs the forbidden git op, e.g.
30
+ * `cd <worktree> && git commit`, `cd <worktree> && ./bin/wp … && git commit`,
31
+ * or `(cd <worktree> && git commit)`, then by EVERY `git -C <dir>` flag on the
32
+ * matched invocation (git applies them cumulatively). Intermediate non-cwd
33
+ * commands after a proven `cd` leave the chain cwd unchanged. Unsupported
34
+ * control flow (`;` / `|` / braces), option-bearing `cd`, `cd -`, or
35
+ * unresolvable `cd`/`-C` targets fail closed because they could hide a
36
+ * primary-checkout mutation. Prefer `git -C <worktree> ...` or running the
35
37
  * command with the tool cwd already set to the worktree.
36
38
  *
37
39
  * Known best-effort limits (acceptable for an agent guard with a documented
@@ -634,12 +636,13 @@ const DIR_TOKEN = String.raw `(?:"[^"]+"|'[^']+'|[^\s;&|(){}]+)`;
634
636
  const QUOTED_ARG = String.raw `(?:"[^"]+"|'[^']+'|\S+)`;
635
637
  const CD_TARGET = String.raw `(?:--\s+)?(${DIR_TOKEN}|-)`;
636
638
  const CD_LEADER = String.raw `(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd)\s+`;
637
- // A success-gated cwd chain that directly governs the forbidden git op. We only
638
- // trust cwd changes in this shape; `;`, `||`, skipped/failed `cd`, quoted spoof
639
- // text, and other unsupported shell control flow fail closed instead.
639
+ // Pure cd/pushd segment that updates the success-gated cwd (optional leading env /
640
+ // command|builtin wrapper). Trailing whitespace is allowed; nothing else.
641
+ const PURE_CD_SEGMENT = new RegExp(String.raw `^\s*${CD_LEADER}${CD_TARGET}\s*$`, "u");
642
+ // Legacy pure trailing-cd chain (also used for subshell-local `(cd X && git …)`).
640
643
  const CD_CHAIN = new RegExp(String.raw `(?:^|[;&|(){}]\s*)?((?:\s*${CD_LEADER}${CD_TARGET}\s*&&)+)\s*$`, "u");
641
644
  const CD_IN_CHAIN = new RegExp(String.raw `(?:^|&&)\s*${CD_LEADER}${CD_TARGET}\s*(?=&&)`, "gu");
642
- const CWD_WORD = new RegExp(String.raw `(?:^|[;{}()&|]\s*|\s+)(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd)\b`, "gu");
645
+ const CWD_WORD = new RegExp(String.raw `(?:^|[;{}()&|]\s*|\s+)(?:[A-Za-z_]\w*=\S*\s+)*(?:command\s+|builtin\s+)?(?:cd|pushd|popd)\b`, "gu");
643
646
  const DASH_C = new RegExp(String.raw `-C\s+(${DIR_TOKEN})`, "gu");
644
647
  function isInsideSingleOrDoubleQuotes(command, index) {
645
648
  let quote = null;
@@ -667,6 +670,27 @@ function hasUnquotedCwdCommand(command) {
667
670
  }
668
671
  return false;
669
672
  }
673
+ /** True when unquoted `;` `|` `{` `}` appear (breaks a pure `&&` agent chain). */
674
+ function hasUnquotedUnsafeAndChainControl(command) {
675
+ for (let i = 0; i < command.length; i += 1) {
676
+ const ch = command[i];
677
+ if (ch !== ";" && ch !== "|" && ch !== "{" && ch !== "}" && ch !== "\n")
678
+ continue;
679
+ if (!isInsideSingleOrDoubleQuotes(command, i))
680
+ return true;
681
+ }
682
+ return false;
683
+ }
684
+ function hasUnquotedParen(command) {
685
+ for (let i = 0; i < command.length; i += 1) {
686
+ const ch = command[i];
687
+ if (ch !== "(" && ch !== ")")
688
+ continue;
689
+ if (!isInsideSingleOrDoubleQuotes(command, i))
690
+ return true;
691
+ }
692
+ return false;
693
+ }
670
694
  /** Apply one dir token to `cwd`, or signal ambiguity if it cannot be statically resolved. */
671
695
  function applyDir(token, cwd) {
672
696
  if (!token)
@@ -683,11 +707,85 @@ function isTrustedCwdChainStart(commandPrefix, chainStart) {
683
707
  const before = commandPrefix.slice(0, chainStart).trimEnd();
684
708
  return before.endsWith("&&") || before.endsWith("(") || before.endsWith("{");
685
709
  }
686
- function applySuccessGatedCwdChain(commandPrefix, baseCwd) {
710
+ /**
711
+ * Split a pure `&&` command prefix into segments. Returns null when the prefix
712
+ * uses `;` / `|` / braces / newlines we refuse to model.
713
+ */
714
+ function splitSuccessGatedAndSegments(commandPrefix) {
715
+ if (!commandPrefix.trim())
716
+ return [];
717
+ if (hasUnquotedUnsafeAndChainControl(commandPrefix))
718
+ return null;
719
+ const segments = [];
720
+ let current = "";
721
+ let quote = null;
722
+ let escaped = false;
723
+ for (let i = 0; i < commandPrefix.length; i += 1) {
724
+ const ch = commandPrefix[i] ?? "";
725
+ if (escaped) {
726
+ current += ch;
727
+ escaped = false;
728
+ continue;
729
+ }
730
+ if (quote !== "'" && ch === "\\") {
731
+ escaped = true;
732
+ current += ch;
733
+ continue;
734
+ }
735
+ if ((ch === '"' || ch === "'") && (quote === null || quote === ch)) {
736
+ quote = quote === ch ? null : ch;
737
+ current += ch;
738
+ continue;
739
+ }
740
+ if (!quote && ch === "&" && commandPrefix[i + 1] === "&") {
741
+ if (current.trim())
742
+ segments.push(current);
743
+ current = "";
744
+ i += 1;
745
+ continue;
746
+ }
747
+ current += ch;
748
+ }
749
+ if (quote || escaped)
750
+ return null;
751
+ if (current.trim())
752
+ segments.push(current);
753
+ return segments;
754
+ }
755
+ /**
756
+ * Walk pure `&&` segments: resolvable cd/pushd updates cwd; other segments must
757
+ * not smuggle unquoted cwd changes. Enables
758
+ * `cd <managed-worktree> && ./bin/wp … && git commit`.
759
+ */
760
+ function applyAndSegmentCwdChain(commandPrefix, baseCwd) {
761
+ const segments = splitSuccessGatedAndSegments(commandPrefix);
762
+ if (segments === null)
763
+ return { ambiguous: true };
764
+ if (segments.length === 0)
765
+ return { ambiguous: false, cwd: baseCwd };
766
+ let cwd = baseCwd;
767
+ for (const segment of segments) {
768
+ const pureCd = PURE_CD_SEGMENT.exec(segment);
769
+ if (pureCd) {
770
+ const next = applyDir(pureCd[1], cwd);
771
+ if (next === "ambiguous")
772
+ return { ambiguous: true };
773
+ cwd = next.cwd;
774
+ continue;
775
+ }
776
+ if (hasUnquotedCwdCommand(segment))
777
+ return { ambiguous: true };
778
+ }
779
+ return { ambiguous: false, cwd };
780
+ }
781
+ /**
782
+ * Legacy trailing pure-cd chain (and subshell-local `(cd X && git …)` forms).
783
+ * Kept for paren-scoped ops; intermediate non-cd commands are handled by
784
+ * {@link applyAndSegmentCwdChain} when no unquoted parens are present.
785
+ */
786
+ function applyLegacyTrailingCdChain(commandPrefix, baseCwd) {
687
787
  const chain = CD_CHAIN.exec(commandPrefix);
688
788
  const chainStart = chain?.index ?? commandPrefix.length;
689
- // If there is any other unquoted cwd-changing syntax before this simple chain,
690
- // we cannot prove the chain governs the op. Fail closed.
691
789
  if (hasUnquotedCwdCommand(commandPrefix.slice(0, chainStart)))
692
790
  return { ambiguous: true };
693
791
  if (!chain)
@@ -703,6 +801,20 @@ function applySuccessGatedCwdChain(commandPrefix, baseCwd) {
703
801
  }
704
802
  return { ambiguous: false, cwd };
705
803
  }
804
+ /**
805
+ * Success-gated cwd chain for a forbidden git op's command prefix.
806
+ *
807
+ * Prefer the `&&`-segment walk when the prefix has no unquoted parentheses so
808
+ * agents can run tools between a managed `cd` and a mutation. When parentheses
809
+ * appear (subshell-local ops), fall back to the legacy trailing-cd chain so
810
+ * non-persistent `(cd …) && git …` stays fail-closed.
811
+ */
812
+ function applySuccessGatedCwdChain(commandPrefix, baseCwd) {
813
+ if (!hasUnquotedParen(commandPrefix)) {
814
+ return applyAndSegmentCwdChain(commandPrefix, baseCwd);
815
+ }
816
+ return applyLegacyTrailingCdChain(commandPrefix, baseCwd);
817
+ }
706
818
  /**
707
819
  * The directory the git op actually runs in: `baseCwd` lowered by the
708
820
  * success-gated cwd chain that directly governs this op, then through EVERY
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { Database } from "#db/sqlite.js";
3
+ import { SessionMemorySessionStore } from "#session-memory/session.js";
3
4
  export declare const MAX_BYTES: number;
4
5
  export declare const TRUNCATION_NOTICE = "\n\n[truncated: file exceeded 200KB limit]";
5
6
  export declare const RESUME_MAX_EVENT_BYTES: number;
@@ -10,10 +11,19 @@ type StartInput = Record<string, unknown>;
10
11
  type EnvLike = Record<string, string | undefined>;
11
12
  export interface SessionStartDeps {
12
13
  readonly createDatabase?: (dbPath: string) => Pick<Database, "prepare" | "close">;
14
+ readonly createStore?: (dbPath: string) => Pick<SessionMemorySessionStore, "captureEvent" | "close">;
13
15
  readonly dbPath?: string;
14
16
  readonly repoHash?: (projectDir: string) => string;
17
+ readonly now?: () => Date;
18
+ readonly onStorageError?: (error: unknown) => void;
15
19
  }
16
20
  export declare function resolveSessionMemoryDbPath(projectDir: string, env: EnvLike): string;
21
+ /**
22
+ * Seed session-memory with a real host session id so `wp session-info` works
23
+ * under Grok (and similar hosts) before any Edit/Write tool capture. Fail-open:
24
+ * storage errors never break SessionStart stdout.
25
+ */
26
+ export declare function seedSessionIdentityCapture(input: StartInput, projectDir: string, env: EnvLike, deps?: SessionStartDeps): void;
17
27
  /**
18
28
  * Pure function: given a parsed input payload, a working directory, and
19
29
  * environment variables, produce the JSON string that the hook should write
@@ -18,20 +18,23 @@
18
18
  * Always emits valid JSON; `additionalContext` is empty when there is no
19
19
  * `.agent/routing.md`, continuity, or update banner to surface.
20
20
  */
21
- import { readFileSync, statSync } from "node:fs";
21
+ import { mkdirSync, readFileSync, statSync } from "node:fs";
22
22
  import { tmpdir } from "node:os";
23
23
  import { performance } from "node:perf_hooks";
24
- import { join } from "node:path";
24
+ import { dirname, join } from "node:path";
25
25
  import { renderSessionStartInstructionContext } from "#hooks/shared/instruction-surfaces";
26
26
  import { writeHookOutput } from "#hooks/shared/hook-bootstrap";
27
27
  import { rememberHookStdin } from "#hooks/logs/store.js";
28
28
  import { resolveHookProjectDir } from "#hooks/shared/project-dir.js";
29
+ import { resolveHookSessionId } from "#hooks/shared/types.js";
29
30
  import { getSurfacePath, NotInGitRepoError } from "#paths/state-root.js";
30
31
  import { WP_AGENT_LAUNCHED_VIA_WP_ENV, WP_AGENT_LAUNCHED_VIA_WP_VALUE, } from "#runtime/agent-launch-marker.js";
31
32
  import { Database } from "#db/sqlite.js";
32
33
  import { readUpdateBanner } from "./update-banner.js";
33
34
  import { isDirectEntrypoint } from "#hooks/shared/direct-entrypoint";
34
- import { repoHashFromRoot } from "#session-memory/repo-hash.js";
35
+ import { buildContinuityEvent } from "#session-memory/hook-capture.js";
36
+ import { computeRepoHash, repoHashFromRoot } from "#session-memory/repo-hash.js";
37
+ import { SessionMemorySessionStore } from "#session-memory/session.js";
35
38
  export const MAX_BYTES = 200 * 1024;
36
39
  export const TRUNCATION_NOTICE = "\n\n[truncated: file exceeded 200KB limit]";
37
40
  export const RESUME_MAX_EVENT_BYTES = 2 * 1024;
@@ -223,7 +226,8 @@ function buildResumeContext(input, projectDir, env, deps) {
223
226
  const rows = readRecentContinuityEvents({
224
227
  db,
225
228
  repoHash,
226
- sessionId: optionalString(input["session_id"]),
229
+ // Prefer stdin session id; fall back to GROK_SESSION_ID for Grok hooks.
230
+ sessionId: resolveHookSessionId(input, env),
227
231
  minPriority: parsePositiveInt(env.WP_SESSIONSTART_RESUME_MIN_PRIORITY, RESUME_MIN_PRIORITY),
228
232
  limit: parsePositiveInt(env.WP_SESSIONSTART_RESUME_MAX_EVENTS, 16),
229
233
  });
@@ -267,6 +271,56 @@ function buildDirectLaunchWarning(input, env) {
267
271
  "</wp_launch_warning>",
268
272
  ].join("\n");
269
273
  }
274
+ /**
275
+ * Seed session-memory with a real host session id so `wp session-info` works
276
+ * under Grok (and similar hosts) before any Edit/Write tool capture. Fail-open:
277
+ * storage errors never break SessionStart stdout.
278
+ */
279
+ export function seedSessionIdentityCapture(input, projectDir, env, deps = {}) {
280
+ const sessionId = resolveHookSessionId(input, env);
281
+ if (!sessionId)
282
+ return;
283
+ try {
284
+ const dbPath = deps.dbPath ?? resolveSessionMemoryDbPath(projectDir, env);
285
+ mkdirSync(dirname(dbPath), { recursive: true });
286
+ const store = deps.createStore?.(dbPath) ?? new SessionMemorySessionStore(dbPath);
287
+ try {
288
+ // Match `readCurrentSessionId` (computeRepoHash), not path-only hashing.
289
+ const repoHash = deps.repoHash?.(projectDir) ?? computeRepoHash(projectDir);
290
+ const event = buildContinuityEvent({
291
+ eventType: "session_identity",
292
+ toolName: "SessionStart",
293
+ content: `Session identity: ${sessionId}`,
294
+ summary: "Session identity for PR disclosure",
295
+ // Below RESUME_MIN_PRIORITY (50): kept for `wp session-info`, not resume inject.
296
+ priority: 40,
297
+ metadata: {
298
+ source: "sessionstart-hook",
299
+ hookEventName: "SessionStart",
300
+ sessionStartSource: sessionStartSource(input),
301
+ },
302
+ });
303
+ store.captureEvent({
304
+ repoHash,
305
+ agentId: "SessionStart",
306
+ sessionId,
307
+ event: {
308
+ ...event,
309
+ ts: (deps.now?.() ?? new Date()).toISOString(),
310
+ },
311
+ });
312
+ }
313
+ finally {
314
+ store.close();
315
+ }
316
+ }
317
+ catch (error) {
318
+ deps.onStorageError?.(error);
319
+ if (env.WP_SESSIONSTART_DEBUG === "1") {
320
+ process.stderr.write(`wp-sessionstart-identity: ${error.message}\n`);
321
+ }
322
+ }
323
+ }
270
324
  /**
271
325
  * Pure function: given a parsed input payload, a working directory, and
272
326
  * environment variables, produce the JSON string that the hook should write
@@ -277,6 +331,9 @@ function buildDirectLaunchWarning(input, env) {
277
331
  */
278
332
  export function buildOutput(input, cwd, env, deps = {}) {
279
333
  const projectDir = resolveHookProjectDir(input, cwd, env);
334
+ // Capture before resume read so this SessionStart can seed disclosure even
335
+ // when no prior events exist (Grok option-3 path).
336
+ seedSessionIdentityCapture(input, projectDir, env, deps);
280
337
  const target = join(projectDir, ".agent", "routing.md");
281
338
  let routingMd = null;
282
339
  try {
@@ -29,6 +29,16 @@ export type UpdatedInputEnvelope = {
29
29
  */
30
30
  export declare function normalizeHookToolInput(raw: Record<string, unknown>): ToolInput;
31
31
  export declare function parseToolInput(json: string): ToolInput;
32
+ type EnvLike = Record<string, string | undefined>;
33
+ /**
34
+ * Resolve a real host session id for session-memory capture / PR disclosure.
35
+ * Prefer stdin session fields (snake_case Claude/Codex or camelCase Grok), then
36
+ * Grok hook env `GROK_SESSION_ID` (documented for every Grok hook process).
37
+ */
38
+ export declare function resolveHookSessionId(input: {
39
+ readonly session_id?: unknown;
40
+ readonly sessionId?: unknown;
41
+ }, env?: EnvLike): string | undefined;
32
42
  export declare function isBashInput(input: ToolInput): boolean;
33
43
  export declare function isFileEditInput(input: ToolInput): boolean;
34
44
  export declare function isFileWriteInput(input: ToolInput): boolean;
@@ -72,3 +82,4 @@ export type DenyEnvelope = ClaudeCodexDenyEnvelope | GrokDualDenyEnvelope;
72
82
  export declare function buildDenyEnvelope(options: {
73
83
  readonly reason: string;
74
84
  }, env?: NodeJS.ProcessEnv): DenyEnvelope;
85
+ export {};
@@ -55,6 +55,27 @@ export function parseToolInput(json) {
55
55
  }
56
56
  return normalizeHookToolInput(parsed);
57
57
  }
58
+ /**
59
+ * Resolve a real host session id for session-memory capture / PR disclosure.
60
+ * Prefer stdin session fields (snake_case Claude/Codex or camelCase Grok), then
61
+ * Grok hook env `GROK_SESSION_ID` (documented for every Grok hook process).
62
+ */
63
+ export function resolveHookSessionId(input, env = process.env) {
64
+ const pick = (value) => {
65
+ if (typeof value !== "string")
66
+ return undefined;
67
+ const trimmed = value.trim();
68
+ return trimmed.length > 0 ? trimmed : undefined;
69
+ };
70
+ // Prefer snake_case when both present (Claude/Codex contract).
71
+ const fromInput = pick(input.session_id) ?? pick(input.sessionId);
72
+ if (fromInput)
73
+ return fromInput;
74
+ const fromEnv = env.GROK_SESSION_ID?.trim();
75
+ if (fromEnv)
76
+ return fromEnv;
77
+ return undefined;
78
+ }
58
79
  export function isBashInput(input) {
59
80
  return "command" in (input.tool_input || {});
60
81
  }