@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.
- package/catalog/AGENTS.md.tpl +7 -11
- package/catalog/agent/commands/verify.md +2 -2
- package/catalog/agent/rules/changeset-release.md +4 -2
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/pre-implementation.md +2 -1
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/workflow-skills-routing.md +9 -4
- package/catalog/agent/skills/autopilot/SKILL.md +48 -11
- package/catalog/agent/skills/claude/SKILL.md +7 -0
- package/catalog/agent/skills/codex/SKILL.md +7 -0
- package/catalog/agent/skills/fix/SKILL.md +36 -8
- package/catalog/agent/skills/grok/SKILL.md +7 -0
- package/catalog/agent/skills/investigate/SKILL.md +12 -1
- package/catalog/agent/skills/pll/SKILL.md +53 -48
- package/catalog/agent/skills/team/SKILL.md +26 -9
- package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
- package/catalog/agent/skills/verify/SKILL.md +89 -51
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
- package/dist/esm/audit/blueprint-pr-coverage.js +68 -7
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +3 -0
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +93 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
- package/dist/esm/ci/native-session-memory-cache.js +9 -1
- package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
- package/dist/esm/ci/vitest-ci-shards.js +6 -4
- package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
- package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
- package/dist/esm/cli/auto-update/installer.d.ts +4 -17
- package/dist/esm/cli/auto-update/installer.js +6 -57
- package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
- package/dist/esm/cli/auto-update/self-invocation.js +62 -0
- package/dist/esm/cli/commands/audit.js +4 -0
- package/dist/esm/cli/commands/blueprint/mutations.js +35 -18
- package/dist/esm/cli/commands/blueprint/router.js +27 -2
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/dash/index.js +3 -3
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
- package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +142 -0
- package/dist/esm/cli/commands/dash/plan-once.js +261 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
- package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
- package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
- package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
- package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +311 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +1 -4
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +61 -23
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/package-manager.js +2 -1
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.js +5 -8
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/commands/self-install-guard.d.ts +14 -32
- package/dist/esm/cli/commands/self-install-guard.js +17 -63
- package/dist/esm/cli/direct-provider-launch.js +25 -1
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +56 -0
- package/dist/esm/hooks/guard-switch/index.js +8 -4
- package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
- package/dist/esm/hooks/precompact/index.js +5 -3
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +4 -4
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
- package/dist/esm/hooks/sessionstart/index.js +61 -4
- package/dist/esm/hooks/shared/types.d.ts +11 -0
- package/dist/esm/hooks/shared/types.js +21 -0
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +10 -0
- package/dist/esm/mcp/tools/audits.d.ts +1 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/paths/state-root.js +9 -0
- package/dist/esm/review/authority.js +17 -2
- package/dist/esm/review/availability.js +18 -5
- package/dist/esm/review/subject.d.ts +15 -0
- package/dist/esm/review/subject.js +105 -10
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/session-memory/current-session.js +2 -0
- package/dist/esm/session-memory/native-runtime.js +5 -3
- package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
- package/dist/esm/session-memory/native-warm-markers.js +22 -0
- package/dist/esm/session-memory/sync/types.d.ts +4 -0
- package/dist/esm/session-memory/types.d.ts +3 -1
- package/dist/esm/session-memory/types.js +2 -0
- package/dist/esm/ultragoal/runtime.js +44 -2
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/package.json +17 -14
package/dist/esm/hooks/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...(
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,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
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
21
|
-
* success-gated
|
|
22
|
-
* `cd <worktree> && git commit
|
|
23
|
-
* EVERY `git -C <dir>` flag on the
|
|
24
|
-
* cumulatively).
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
29
|
-
* success-gated
|
|
30
|
-
* `cd <worktree> && git commit
|
|
31
|
-
* EVERY `git -C <dir>` flag on the
|
|
32
|
-
* cumulatively).
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
//
|
|
638
|
-
//
|
|
639
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|