@webpresso/agent-kit 3.1.25 → 3.1.28

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 (64) hide show
  1. package/catalog/AGENTS.md.tpl +6 -10
  2. package/catalog/agent/commands/verify.md +2 -2
  3. package/catalog/agent/rules/changeset-release.md +7 -1
  4. package/catalog/agent/rules/workflow-skills-routing.md +6 -3
  5. package/catalog/agent/skills/autopilot/SKILL.md +48 -11
  6. package/catalog/agent/skills/claude/SKILL.md +7 -0
  7. package/catalog/agent/skills/codex/SKILL.md +7 -0
  8. package/catalog/agent/skills/fix/SKILL.md +33 -8
  9. package/catalog/agent/skills/grok/SKILL.md +7 -0
  10. package/catalog/agent/skills/investigate/SKILL.md +12 -1
  11. package/catalog/agent/skills/pll/SKILL.md +53 -48
  12. package/catalog/agent/skills/team/SKILL.md +26 -9
  13. package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
  14. package/catalog/agent/skills/verify/SKILL.md +71 -51
  15. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
  16. package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
  17. package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
  18. package/dist/esm/ci/native-session-memory-cache.js +9 -1
  19. package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
  20. package/dist/esm/ci/vitest-ci-shards.js +6 -4
  21. package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
  22. package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
  23. package/dist/esm/cli/auto-update/installer.d.ts +4 -17
  24. package/dist/esm/cli/auto-update/installer.js +6 -57
  25. package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
  26. package/dist/esm/cli/auto-update/self-invocation.js +62 -0
  27. package/dist/esm/cli/commands/audit.js +1 -0
  28. package/dist/esm/cli/commands/dash/index.js +3 -3
  29. package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
  30. package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
  31. package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
  32. package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
  33. package/dist/esm/cli/commands/dash/plan-once.d.ts +122 -0
  34. package/dist/esm/cli/commands/dash/plan-once.js +224 -0
  35. package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
  36. package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
  37. package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
  38. package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
  39. package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
  40. package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
  41. package/dist/esm/cli/commands/dash/tui/runtime.js +72 -0
  42. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
  43. package/dist/esm/cli/commands/package-manager.js +2 -1
  44. package/dist/esm/cli/commands/quality-log-store.d.ts +6 -0
  45. package/dist/esm/cli/commands/quality-log-store.js +55 -15
  46. package/dist/esm/cli/commands/self-install-guard.d.ts +14 -16
  47. package/dist/esm/cli/commands/self-install-guard.js +17 -23
  48. package/dist/esm/hooks/guard-switch/index.js +8 -4
  49. package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
  50. package/dist/esm/hooks/precompact/index.js +5 -3
  51. package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
  52. package/dist/esm/hooks/sessionstart/index.js +61 -4
  53. package/dist/esm/hooks/shared/types.d.ts +11 -0
  54. package/dist/esm/hooks/shared/types.js +21 -0
  55. package/dist/esm/paths/state-root.js +36 -3
  56. package/dist/esm/session-memory/current-session.js +2 -0
  57. package/dist/esm/session-memory/native-runtime.js +5 -3
  58. package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
  59. package/dist/esm/session-memory/native-warm-markers.js +22 -0
  60. package/dist/esm/session-memory/sync/types.d.ts +4 -0
  61. package/dist/esm/session-memory/types.d.ts +3 -1
  62. package/dist/esm/session-memory/types.js +2 -0
  63. package/dist/esm/ultragoal/runtime.js +44 -2
  64. package/package.json +15 -12
@@ -30,13 +30,17 @@
30
30
  * chain unresolvable after repair; 12 lease held; 13 ownership lost.
31
31
  * 7. Release the lease owner-checked in a finally.
32
32
  *
33
- * Post-swap import rule: the child `vp update -g @webpresso/agent-kit` may
34
- * delete this process's on-disk package tree (old installId) before probe/
35
- * repair run. Post-child helpers MUST be static imports loaded at process
36
- * start never `await import("#cli/…")` after the child exits, or Node
37
- * re-resolves against a removed path (`Cannot find module …/global-vp.js`).
38
- * Probe/repair logic itself reads the vite-plus store SSOT + PATH, not this
39
- * process's dying package root.
33
+ * Survival layers (in-repo SOTA bar):
34
+ * 1. Prefer a **stable** non-versioned launcher when re-execing the guard
35
+ * (`#cli/auto-update/self-invocation.js`) primary defense.
36
+ * 2. Self-package helpers (probe, repair, tombstone) MUST be static imports
37
+ * loaded at process start never package-local dynamic import, or Node
38
+ * re-resolves against a removed installId path after `vp update -g`.
39
+ * 3. Argument builders depend on cycle-free leaves (self-invocation,
40
+ * guard-tombstone), never on this command module solely for argv.
41
+ *
42
+ * Probe/repair reads the vite-plus store SSOT + PATH, not this process's
43
+ * dying package root.
40
44
  *
41
45
  * Custody boundary (stated exactly, mirrored from install-lock.ts): a stale,
42
46
  * OS-suspended holder whose child completes a mutation AFTER the successor
@@ -46,10 +50,14 @@
46
50
  */
47
51
  import { spawn } from "node:child_process";
48
52
  import { buildVpGlobalInstallCommand, resolveBundledVpCommand, } from "#cli/auto-update/detect-pm.js";
53
+ import { writeGuardTombstone } from "#cli/auto-update/guard-tombstone.js";
49
54
  import { INSTALL_LOCK_HEARTBEAT_MS, claimInstallLock, refreshInstallLock, releaseInstallLock, resolveInstallLockPath, } from "#cli/auto-update/install-lock.js";
55
+ import { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher, } from "#cli/auto-update/self-invocation.js";
50
56
  import { probeGlobalWpResolvable } from "#cli/commands/init/package-root.js";
51
57
  import { resolveGlobalCapableVpCommand } from "#cli/global-vp.js";
52
58
  import { resolveEffectiveRunnerRoot } from "#tool-runtime";
59
+ /** Re-export leaf helpers for public ergonomics / existing imports. */
60
+ export { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher };
53
61
  export function registerSelfInstallGuardCommand(cli) {
54
62
  cli
55
63
  .command("self-install-guard [...args]", "Serialize and verify a global agent-kit self-install (internal)")
@@ -145,18 +153,6 @@ function extractChildCommand(rawArgs) {
145
153
  return [];
146
154
  return rawArgs.slice(separator + 1);
147
155
  }
148
- /**
149
- * How wp re-invokes itself: the bun-compiled binary IS process.execPath (its
150
- * argv[1] is a virtual /$bunfs path), while the source/dev lane runs
151
- * `<runtime> <script>`.
152
- */
153
- export function buildSelfInvocationCommand(args, options = {}) {
154
- const execPath = options.execPath ?? process.execPath;
155
- const argv1 = options.argv1 ?? process.argv[1] ?? "";
156
- if (argv1.startsWith("/$bunfs"))
157
- return [execPath, ...args];
158
- return [execPath, argv1, ...args];
159
- }
160
156
  function defaultSpawnChild(command) {
161
157
  const [executable, ...args] = command;
162
158
  // detached: own process group, so lost-ownership kills reap descendants.
@@ -211,8 +207,6 @@ async function defaultRunRepair() {
211
207
  return child.exited;
212
208
  }
213
209
  function writeObservabilityTombstone() {
214
- // Lazy import keeps the guard's cold path minimal; best-effort by contract.
215
- void import("#cli/auto-update/installer.js")
216
- .then(({ writeGuardTombstone }) => writeGuardTombstone())
217
- .catch(() => undefined);
210
+ // Static import only leaf module; see survival layers in the file header.
211
+ writeGuardTombstone();
218
212
  }
@@ -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",
@@ -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
  }
@@ -1,7 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
- import { existsSync, mkdirSync, realpathSync } from "node:fs";
3
+ import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { readFile, realpath, stat } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
5
6
  import { dirname, join } from "node:path";
6
7
  import envPaths from "env-paths";
7
8
  import lockfile from "proper-lockfile";
@@ -67,18 +68,50 @@ function normalizeCwdForCache(cwd) {
67
68
  function sha256Hex(value) {
68
69
  return createHash("sha256").update(value).digest("hex");
69
70
  }
71
+ /**
72
+ * Probe whether a directory can hold state files.
73
+ * Used so quality CLIs (wp audit/test/…) keep working in sandboxes that
74
+ * deny writes under Application Support (e.g. Codex outside-voice).
75
+ *
76
+ * Any probe failure means the root is not usable (permission, ENOSPC, bad
77
+ * path, …). That is intentionally broader than `createCliLogSink`, which only
78
+ * remaps EPERM/EACCES/EROFS so unexpected IO errors stay loud at the log layer.
79
+ * A failed preferred env-paths root falls back once and is memoized for the
80
+ * process; set `WP_STATE_ROOT` to pin an explicit location instead.
81
+ */
82
+ function canWriteStateRoot(root) {
83
+ try {
84
+ mkdirSync(root, { recursive: true });
85
+ const probe = join(root, `.wp-write-probe-${process.pid}`);
86
+ writeFileSync(probe, "ok");
87
+ rmSync(probe, { force: true });
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
70
94
  export function getStateRoot() {
71
95
  if (cachedStateRoot !== null)
72
96
  return cachedStateRoot;
73
97
  const override = process.env.WP_STATE_ROOT;
74
98
  if (override && override.trim().length > 0) {
99
+ // Explicit override is authoritative — fail later if unwritable rather than
100
+ // silently relocating the user's chosen root.
75
101
  cachedStateRoot = override;
76
102
  return override;
77
103
  }
78
104
  const paths = envPaths("webpresso", { suffix: "" });
79
105
  const root = paths.data;
80
- cachedStateRoot = root;
81
- return root;
106
+ if (canWriteStateRoot(root)) {
107
+ cachedStateRoot = root;
108
+ return root;
109
+ }
110
+ const fallback = join(tmpdir(), "webpresso-state");
111
+ mkdirSync(fallback, { recursive: true });
112
+ process.stderr.write(`wp: state root not writable (${root}); using ${fallback}\n`);
113
+ cachedStateRoot = fallback;
114
+ return fallback;
82
115
  }
83
116
  export function getRepoKey() {
84
117
  if (cachedRepoKey !== null)
@@ -23,6 +23,8 @@ const LIVE_SESSION_ENV_KEYS = [
23
23
  "CLAUDE_SESSION_ID",
24
24
  "CLAUDE_CODE_SESSION_ID",
25
25
  "OPENCODE_SESSION_ID",
26
+ // Grok Build: hook processes always get this; tool children may in future builds.
27
+ "GROK_SESSION_ID",
26
28
  "RUNTIME_SESSION_ID",
27
29
  ];
28
30
  export function readLiveSessionIdFromEnv(env) {
@@ -9,6 +9,7 @@ import envPaths from "env-paths";
9
9
  import lockfile from "proper-lockfile";
10
10
  import { resolveAgentKitPackageRoot } from "#cli/commands/init/package-root.js";
11
11
  import { SESSION_MEMORY_NATIVE_ADDON_FILENAME, resolveSessionMemoryNativeTarget, } from "./native-targets.js";
12
+ import { NATIVE_WARM_LOG_CORRUPT_REBUILD, NATIVE_WARM_LOG_HIT, NATIVE_WARM_LOG_REBUILD, } from "./native-warm-markers.js";
12
13
  const requireFromHere = createRequire(import.meta.url);
13
14
  const NATIVE_WORKSPACE_DIRNAME = join("native", "session-memory-engine");
14
15
  const BUILD_PACKAGE = "session-memory-napi";
@@ -240,12 +241,12 @@ function ensureNativeModuleBuilt(options = {}) {
240
241
  const nodePath = compiledNodePath();
241
242
  const force = options.force === true;
242
243
  if (!force && !shouldRebuildNativeSessionMemoryNode(nodePath, workspaceRoot)) {
243
- console.log(`[native-session-memory] warm=hit path=${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
244
+ console.log(`[native-session-memory] ${NATIVE_WARM_LOG_HIT}${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
244
245
  return nodePath;
245
246
  }
246
247
  return withNativeBuildLock(sessionMemoryNativeCacheRoot(), () => {
247
248
  if (!force && !shouldRebuildNativeSessionMemoryNode(nodePath, workspaceRoot)) {
248
- console.log(`[native-session-memory] warm=hit path=${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
249
+ console.log(`[native-session-memory] ${NATIVE_WARM_LOG_HIT}${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
249
250
  return nodePath;
250
251
  }
251
252
  if (force && existsSync(nodePath)) {
@@ -256,7 +257,8 @@ function ensureNativeModuleBuilt(options = {}) {
256
257
  // Best-effort: cargo rebuild overwrites via copyFileSync below.
257
258
  }
258
259
  }
259
- console.log(`[native-session-memory] warm=${force ? "corrupt-rebuild" : "rebuild"} path=${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
260
+ const warmMarker = force ? NATIVE_WARM_LOG_CORRUPT_REBUILD : NATIVE_WARM_LOG_REBUILD;
261
+ console.log(`[native-session-memory] ${warmMarker}${nodePath} trust=${trustCachedNativeNode() ? "1" : "0"}`);
260
262
  const targetDir = cargoTargetDir();
261
263
  mkdirSync(targetDir, { recursive: true });
262
264
  try {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Grep-stable warm-path log markers for the session-memory native warm step.
3
+ *
4
+ * Emitted by `native-runtime` and classified by the Test job E1 summary greps
5
+ * in `.github/workflows/ci.agent-kit.yml`. YAML cannot import TS — contract
6
+ * tests in `src/ci/native-session-memory-cache.test.ts` pin that the workflow
7
+ * greps these exact substrings.
8
+ *
9
+ * Rename only with a coordinated workflow + contract-test update.
10
+ */
11
+ /** Exact-hit warm log marker (`grep -q` target + runtime emit prefix). */
12
+ export declare const NATIVE_WARM_LOG_HIT = "warm=hit path=";
13
+ /** Source/mtime rebuild warm log marker. */
14
+ export declare const NATIVE_WARM_LOG_REBUILD = "warm=rebuild path=";
15
+ /** Forced corrupt-node rebuild warm log marker. */
16
+ export declare const NATIVE_WARM_LOG_CORRUPT_REBUILD = "warm=corrupt-rebuild path=";
17
+ /** All E1-classified warm markers (corrupt-rebuild → hit → rebuild order matches workflow). */
18
+ export declare const NATIVE_WARM_LOG_MARKERS: readonly ["warm=corrupt-rebuild path=", "warm=hit path=", "warm=rebuild path="];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Grep-stable warm-path log markers for the session-memory native warm step.
3
+ *
4
+ * Emitted by `native-runtime` and classified by the Test job E1 summary greps
5
+ * in `.github/workflows/ci.agent-kit.yml`. YAML cannot import TS — contract
6
+ * tests in `src/ci/native-session-memory-cache.test.ts` pin that the workflow
7
+ * greps these exact substrings.
8
+ *
9
+ * Rename only with a coordinated workflow + contract-test update.
10
+ */
11
+ /** Exact-hit warm log marker (`grep -q` target + runtime emit prefix). */
12
+ export const NATIVE_WARM_LOG_HIT = "warm=hit path=";
13
+ /** Source/mtime rebuild warm log marker. */
14
+ export const NATIVE_WARM_LOG_REBUILD = "warm=rebuild path=";
15
+ /** Forced corrupt-node rebuild warm log marker. */
16
+ export const NATIVE_WARM_LOG_CORRUPT_REBUILD = "warm=corrupt-rebuild path=";
17
+ /** All E1-classified warm markers (corrupt-rebuild → hit → rebuild order matches workflow). */
18
+ export const NATIVE_WARM_LOG_MARKERS = [
19
+ NATIVE_WARM_LOG_CORRUPT_REBUILD,
20
+ NATIVE_WARM_LOG_HIT,
21
+ NATIVE_WARM_LOG_REBUILD,
22
+ ];
@@ -48,6 +48,7 @@ export declare const shareContinuityEventV1: z.ZodObject<{
48
48
  helper_waiting: "helper_waiting";
49
49
  rejected_approach: "rejected_approach";
50
50
  rule_snapshot: "rule_snapshot";
51
+ session_identity: "session_identity";
51
52
  tool_command: "tool_command";
52
53
  tool_edit: "tool_edit";
53
54
  tool_read: "tool_read";
@@ -81,6 +82,7 @@ export declare const shareAgentRunDeltaV1: z.ZodObject<{
81
82
  helper_waiting: "helper_waiting";
82
83
  rejected_approach: "rejected_approach";
83
84
  rule_snapshot: "rule_snapshot";
85
+ session_identity: "session_identity";
84
86
  tool_command: "tool_command";
85
87
  tool_edit: "tool_edit";
86
88
  tool_read: "tool_read";
@@ -119,6 +121,7 @@ export declare const localAgentRunDeltaEnvelopeV1: z.ZodObject<{
119
121
  helper_waiting: "helper_waiting";
120
122
  rejected_approach: "rejected_approach";
121
123
  rule_snapshot: "rule_snapshot";
124
+ session_identity: "session_identity";
122
125
  tool_command: "tool_command";
123
126
  tool_edit: "tool_edit";
124
127
  tool_read: "tool_read";
@@ -155,6 +158,7 @@ export declare const sessionShareEnvelopeV1: z.ZodObject<{
155
158
  helper_waiting: "helper_waiting";
156
159
  rejected_approach: "rejected_approach";
157
160
  rule_snapshot: "rule_snapshot";
161
+ session_identity: "session_identity";
158
162
  tool_command: "tool_command";
159
163
  tool_edit: "tool_edit";
160
164
  tool_read: "tool_read";
@@ -23,7 +23,9 @@ export interface SessionMemorySearchResult extends IndexedSessionMemoryChunk {
23
23
  score: number;
24
24
  tier: "porter" | "trigram" | "levenshtein";
25
25
  }
26
- export declare const SESSION_CONTINUITY_EVENT_TYPES: readonly ["user_prompt", "decision", "constraint", "tool_read", "tool_edit", "tool_command", "failure", "rejected_approach", "assistant_turn_summary", "compaction_boundary", "rule_snapshot", "helper_selected", "helper_started", "helper_waiting", "helper_completed", "helper_failed", "helper_returned"];
26
+ export declare const SESSION_CONTINUITY_EVENT_TYPES: readonly ["user_prompt", "decision", "constraint", "tool_read", "tool_edit", "tool_command", "failure", "rejected_approach", "assistant_turn_summary", "compaction_boundary", "rule_snapshot",
27
+ /** SessionStart seed so `wp session-info` can resolve a real host session id. */
28
+ "session_identity", "helper_selected", "helper_started", "helper_waiting", "helper_completed", "helper_failed", "helper_returned"];
27
29
  export type SessionContinuityEventType = (typeof SESSION_CONTINUITY_EVENT_TYPES)[number];
28
30
  /**
29
31
  * Optional dashboard identity carried in continuity-event metadata. It keeps
@@ -10,6 +10,8 @@ export const SESSION_CONTINUITY_EVENT_TYPES = [
10
10
  "assistant_turn_summary",
11
11
  "compaction_boundary",
12
12
  "rule_snapshot",
13
+ /** SessionStart seed so `wp session-info` can resolve a real host session id. */
14
+ "session_identity",
13
15
  "helper_selected",
14
16
  "helper_started",
15
17
  "helper_waiting",
@@ -2,8 +2,9 @@ import { spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { runQaCommand } from "#cli/commands/qa.js";
5
- import { validateTrackedReviewProvenance } from "#lifecycle/review-provenance.js";
5
+ import { collectV2ReviewProvenance, validateTrackedReviewProvenance, } from "#lifecycle/review-provenance.js";
6
6
  import { evaluateRequiredChecks } from "#mcp/tools/_shared/required-checks.js";
7
+ import { createDeliverySubjectAtRef, createPlanSubjectAtRef } from "#review/subject.js";
7
8
  import { scanBlueprintDirectory } from "#service/scanner.js";
8
9
  import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
9
10
  import matter from "gray-matter";
@@ -105,6 +106,11 @@ async function collectPrProbe(cwd) {
105
106
  function hasRequiredGreenChecks(checks) {
106
107
  return evaluateRequiredChecks(checks).green;
107
108
  }
109
+ /**
110
+ * Validation needs two distinct reviewers bound to the current ultragoal source
111
+ * snapshot. Prefer legacy frontmatter+reviews.md; fall back to current v2 plan +
112
+ * delivery authorities (distinct lanes) when their subjects still match HEAD.
113
+ */
108
114
  function collectValidationEvidence(cwd, blueprintSlugs, sourceSnapshot) {
109
115
  const entries = scanBlueprintDirectory({
110
116
  baseDir: resolveBlueprintRoot(cwd),
@@ -115,7 +121,43 @@ function collectValidationEvidence(cwd, blueprintSlugs, sourceSnapshot) {
115
121
  if (!entry)
116
122
  return undefined;
117
123
  const approvals = matter(readFileSync(entry.path, "utf8")).data.approvals;
118
- return validateTrackedReviewProvenance(entry.path, approvals, { sourceSnapshot });
124
+ const legacy = validateTrackedReviewProvenance(entry.path, approvals, { sourceSnapshot });
125
+ if (legacy.valid)
126
+ return legacy;
127
+ const stableSlug = slug.includes("/") ? (slug.split("/").pop() ?? slug) : slug;
128
+ let deliveryDigest;
129
+ let planDigest;
130
+ try {
131
+ deliveryDigest = createDeliverySubjectAtRef(cwd, "HEAD", stableSlug).digest;
132
+ planDigest = createPlanSubjectAtRef(cwd, "HEAD", stableSlug).digest;
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ const bind = (records) => records.map((record) => ({
138
+ reviewer: record.reviewer,
139
+ artifact: record.artifact,
140
+ sourceSnapshot,
141
+ targetHash: record.targetHash,
142
+ }));
143
+ const delivery = bind(collectV2ReviewProvenance(entry.path, "delivery", "HEAD").filter((record) => record.targetHash === deliveryDigest));
144
+ const plan = bind(collectV2ReviewProvenance(entry.path, "plan", "HEAD").filter((record) => record.targetHash === planDigest));
145
+ // Prefer delivery as code-review lane; plan as verify/architecture lane.
146
+ const byReviewer = new Map();
147
+ for (const record of [...plan, ...delivery]) {
148
+ byReviewer.set(record.reviewer, record);
149
+ }
150
+ const v2Records = [...byReviewer.values()];
151
+ const distinctReviewers = [...new Set(v2Records.map((record) => record.reviewer))];
152
+ const distinctArtifacts = [...new Set(v2Records.map((record) => record.artifact))];
153
+ return {
154
+ distinctArtifacts,
155
+ distinctReviewers,
156
+ records: v2Records,
157
+ valid: v2Records.length >= 2 &&
158
+ distinctReviewers.length === v2Records.length &&
159
+ distinctArtifacts.length === v2Records.length,
160
+ };
119
161
  });
120
162
  if (evaluations.some((evaluation) => evaluation?.valid !== true))
121
163
  return undefined;