vericify 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,7 @@ Node `18+` is required.
62
62
 
63
63
  ```bash
64
64
  cd /path/to/repo
65
+ vericify context
65
66
  vericify adapters
66
67
  vericify hub
67
68
  ```
@@ -76,6 +77,7 @@ If your workspace already contains `agent-state/*`, Vericify reads it automatica
76
77
 
77
78
  ```bash
78
79
  cd /path/to/ace-workspace
80
+ vericify context
79
81
  vericify adapters
80
82
  vericify hub
81
83
  ```
@@ -93,6 +95,30 @@ vericify adapters
93
95
 
94
96
  That creates or updates `.vericify/adapters.json` for the current repo and marks the adapter as attached.
95
97
 
98
+ For Claude Code, `vericify attach --adapter claude-code` also writes compact-bootstrap guidance into `CLAUDE.md` and, when `.claude/` exists, installs the Vericify hook block into `.claude/settings.json`.
99
+
100
+ ## Compact Bootstrap And Resume
101
+
102
+ Use the compact packet when you want session bootstrap or resume without rereading the full projected state:
103
+
104
+ ```bash
105
+ vericify context
106
+ vericify delta --since vcx_...
107
+ vericify snapshot --format compact
108
+ ```
109
+
110
+ `vericify context` returns one minified JSON packet with:
111
+
112
+ - the selected run
113
+ - status
114
+ - current focus
115
+ - blockers
116
+ - a strict `live_signal` subset
117
+ - the last meaningful events
118
+ - a fresh opaque continuity id
119
+
120
+ Use `--pretty` if you want the compact packet formatted for humans.
121
+
96
122
  ## Do I Need `--session-id`?
97
123
 
98
124
  No. `--session-id` is optional.
@@ -194,10 +220,11 @@ Best when you already have `agent-state/*` and want a cockpit, history, and comp
194
220
  ```bash
195
221
  vericify attach --adapter codex --label "Primary Codex"
196
222
  vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment
223
+ vericify context
197
224
  vericify hub
198
225
  ```
199
226
 
200
- Best when you want durable workspace metadata now, even before vendor-specific live capture bridges are added.
227
+ Best when you want durable workspace metadata now. Claude Code attach also writes the startup guidance for `vericify context`, the `vericify delta --since=<id>` resume path, and the `PreCompact` reinjection hook when `.claude/` is available.
201
228
 
202
229
  ### Use case 3: Create a native run trail manually
203
230
 
@@ -319,11 +346,14 @@ Today the package is semantic-first. Git-backed provenance can be attached when
319
346
 
320
347
  ```bash
321
348
  vericify help
349
+ vericify context
350
+ vericify delta --since vcx_...
322
351
  vericify adapters
323
352
  vericify attach --adapter codex --label "Primary Codex session"
324
353
  vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment --label "Claude main"
325
354
  vericify hub
326
355
  vericify snapshot
356
+ vericify snapshot --format compact
327
357
  vericify compare --run-id handoff:run-a --compare-run-id workspace:current
328
358
  vericify publish --run-id handoff:run-a
329
359
  vericify sync --run-id handoff:run-a --endpoint https://sync.example.test
@@ -372,6 +402,8 @@ The package also exports:
372
402
 
373
403
  - `loadWorkspaceState`
374
404
  - `projectWorkspaceState`
405
+ - `buildCompactPacket`
406
+ - `buildCompactDelta`
375
407
  - `detectAdapters`
376
408
  - `attachAdapter`
377
409
  - `buildRunComparison`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vericify",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first run intelligence and operations hub for agent systems.",
@@ -20,6 +20,8 @@
20
20
  "scripts": {
21
21
  "adapters": "node src/index.js adapters",
22
22
  "attach": "node src/index.js attach",
23
+ "context": "node src/index.js context",
24
+ "delta": "node src/index.js delta",
23
25
  "hub": "node src/index.js hub",
24
26
  "compare": "node src/index.js compare",
25
27
  "publish:artifact": "node src/index.js publish",
@@ -0,0 +1 @@
1
+ export { attachClaudeWorkspace } from "./handshake/claude-code.js";
@@ -0,0 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { readJson } from "../../core/fs.js";
4
+ import { createHandshakeResult, emitHandshakeStartedEvent, mergeJsonFile, writeInstructionBlock } from "./common.js";
5
+
6
+ function antigravityInstructionBlock() {
7
+ return `## Vericify Process Posts
8
+
9
+ When working in this workspace, emit native Vericify process posts at these moments:
10
+
11
+ - **Blocker:** \`vericify post --run-id workspace:current --agent-id antigravity --kind blocker --summary "<what is blocking>"\`
12
+ - **Handoff:** \`vericify post --run-id workspace:current --agent-id antigravity --kind handoff_note --summary "<handoff context>"\`
13
+ - **Completion:** \`vericify post --run-id workspace:current --agent-id antigravity --kind completion --summary "<what changed>"\`
14
+ - **Intentional silence:** \`vericify post --run-id workspace:current --agent-id antigravity --kind stale_ack --summary "<reason>"\`
15
+
16
+ You may also write structured state to \`.antigravity/vericify-state.json\` using the passthrough schema.`;
17
+ }
18
+
19
+ export function antigravityHandshake(workspaceRoot, options = {}) {
20
+ const result = createHandshakeResult();
21
+ const skippedReasons = [];
22
+ const antigravityDir = resolve(workspaceRoot, ".antigravity");
23
+ const statePath = resolve(antigravityDir, "vericify-state.json");
24
+ const instructionPath = resolve(antigravityDir, "vericify-instructions.md");
25
+ const configPath = resolve(workspaceRoot, "antigravity.config.json");
26
+
27
+ if (existsSync(antigravityDir)) {
28
+ try {
29
+ const stateResult = mergeJsonFile(statePath, (existing) => ({
30
+ ...(existing && typeof existing === "object" ? existing : {}),
31
+ _vericify_passthrough: true,
32
+ handoffs: existing?.handoffs && typeof existing.handoffs === "object" ? existing.handoffs : {},
33
+ nodes: existing?.nodes && typeof existing.nodes === "object" ? existing.nodes : {},
34
+ entries: Array.isArray(existing?.entries) ? existing.entries : [],
35
+ events: Array.isArray(existing?.events) ? existing.events : [],
36
+ posts: Array.isArray(existing?.posts) ? existing.posts : [],
37
+ }));
38
+ result.passthrough_written = Boolean(stateResult.written);
39
+ result.passthrough_path = statePath;
40
+ if (stateResult.skipped) skippedReasons.push(stateResult.reason);
41
+ } catch (error) {
42
+ skippedReasons.push(`.antigravity/vericify-state.json write failed: ${error instanceof Error ? error.message : String(error)}`);
43
+ }
44
+
45
+ try {
46
+ const instructionResult = writeInstructionBlock(instructionPath, "## Vericify Process Posts", antigravityInstructionBlock(), {
47
+ createIfAbsent: true,
48
+ createDirs: true,
49
+ });
50
+ result.instruction_written = Boolean(instructionResult.written);
51
+ result.instruction_path = instructionPath;
52
+ if (instructionResult.skipped) skippedReasons.push(instructionResult.reason);
53
+ } catch (error) {
54
+ skippedReasons.push(`antigravity instructions write failed: ${error instanceof Error ? error.message : String(error)}`);
55
+ }
56
+
57
+ if (existsSync(configPath)) {
58
+ const config = readJson(configPath, null);
59
+ const hasVericifyConfig = Boolean(config && typeof config === "object" && "vericify" in config);
60
+ if (hasVericifyConfig) {
61
+ skippedReasons.push("antigravity.config.json already contains vericify keys; left unchanged");
62
+ }
63
+ }
64
+ } else {
65
+ skippedReasons.push(".antigravity directory is missing; passthrough config and instructions skipped");
66
+ }
67
+
68
+ const eventResult = emitHandshakeStartedEvent(workspaceRoot, {
69
+ adapterId: "antigravity",
70
+ sessionId: options.sessionId ?? null,
71
+ hooksWritten: false,
72
+ instructionWritten: result.instruction_written,
73
+ passthroughWritten: result.passthrough_written,
74
+ });
75
+
76
+ return {
77
+ ...result,
78
+ event_emitted: eventResult.event_emitted,
79
+ skipped_reasons: [...skippedReasons, ...eventResult.skipped_reasons].filter(Boolean),
80
+ };
81
+ }
@@ -0,0 +1,150 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { readJson, readText } from "../../core/fs.js";
4
+ import { createHandshakeResult, emitHandshakeStartedEvent } from "./common.js";
5
+
6
+ const CLAUDE_DIR = ".claude";
7
+ const CLAUDE_SETTINGS_FILE = "settings.json";
8
+ const VERICIFY_MARKER = "<!-- vericify-instruction -->";
9
+ const VERICIFY_END_MARKER = "<!-- /vericify-instruction -->";
10
+ const VERICIFY_CONTEXT_COMMAND = "vericify context";
11
+ const VERICIFY_CONTEXT_RESUME_COMMAND = "vericify delta --since=<id>";
12
+ const VERICIFY_PRECOMPACT_COMMAND = "vericify context --run-id workspace:current";
13
+
14
+ function markerBlock() {
15
+ return `${VERICIFY_MARKER}
16
+ ## Vericify Bootstrap
17
+
18
+ At session start, run \`${VERICIFY_CONTEXT_COMMAND}\` and use that packet as the default bootstrap surface.
19
+
20
+ Between sessions, prefer \`${VERICIFY_CONTEXT_RESUME_COMMAND}\` over rereading raw ACE files.
21
+
22
+ When Claude compaction fires, re-inject \`${VERICIFY_PRECOMPACT_COMMAND}\`.
23
+
24
+ ## Vericify Process Posts
25
+
26
+ When working in this workspace, emit native Vericify process posts at these moments:
27
+
28
+ - **Blocker:** \`vericify post --run-id workspace:current --agent-id claude-code --kind blocker --summary "<what is blocking and what is needed>"\`
29
+ - **Handoff initiated or acknowledged:** \`vericify post --run-id workspace:current --agent-id claude-code --kind handoff_note --summary "<what was handed off and what is expected next>"\`
30
+ - **Meaningful work completed:** \`vericify post --run-id workspace:current --agent-id claude-code --kind completion --summary "<what changed and what is now true>"\`
31
+ - **Extended silence is intentional:** \`vericify post --run-id workspace:current --agent-id claude-code --kind stale_ack --summary "<why silence is expected and when to re-check>"\`
32
+
33
+ These posts are how Vericify tracks run narrative. Without them the hub cannot distinguish active silence from stale work.
34
+ ${VERICIFY_END_MARKER}`;
35
+ }
36
+
37
+ function upsertClaudeInstruction(workspaceRoot) {
38
+ const path = resolve(workspaceRoot, "CLAUDE.md");
39
+ const existing = readText(path, "");
40
+ const nextBlock = markerBlock();
41
+ const blockPattern = new RegExp(`${VERICIFY_MARKER}[\\s\\S]*?${VERICIFY_END_MARKER}`);
42
+ const prefix = existing.trimEnd();
43
+ const next = blockPattern.test(existing)
44
+ ? existing.replace(blockPattern, nextBlock)
45
+ : prefix
46
+ ? `${prefix}\n\n${nextBlock}\n`
47
+ : `${nextBlock}\n`;
48
+ if (next !== existing) {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, next);
51
+ return { path, written: true };
52
+ }
53
+ return { path, written: false };
54
+ }
55
+
56
+ function upsertClaudeSettings(workspaceRoot, commands) {
57
+ const claudeDir = resolve(workspaceRoot, CLAUDE_DIR);
58
+ const settingsPath = join(claudeDir, CLAUDE_SETTINGS_FILE);
59
+ if (!existsSync(claudeDir)) {
60
+ return {
61
+ path: settingsPath,
62
+ written: false,
63
+ skipped_reason: `${CLAUDE_DIR} directory is missing; Claude hook installation skipped`,
64
+ };
65
+ }
66
+
67
+ const currentSettings = readJson(settingsPath, {});
68
+ const nextSettings = {
69
+ ...currentSettings,
70
+ hooks: {
71
+ ...(currentSettings.hooks && typeof currentSettings.hooks === "object" ? currentSettings.hooks : {}),
72
+ },
73
+ };
74
+ for (const [hookName, command] of Object.entries(commands)) {
75
+ const currentGroups = Array.isArray(nextSettings.hooks[hookName])
76
+ ? nextSettings.hooks[hookName].map((group) => ({
77
+ ...group,
78
+ hooks: Array.isArray(group.hooks) ? group.hooks.map((hook) => ({ ...hook })) : [],
79
+ }))
80
+ : [];
81
+ const nextGroups = currentGroups.map((group) => ({
82
+ ...group,
83
+ hooks: Array.isArray(group.hooks) ? group.hooks.filter((hook) => !hook?._vericify) : [],
84
+ }));
85
+ const targetIndex = nextGroups.findIndex((group) => String(group?.matcher ?? "") === "");
86
+ const targetGroup = targetIndex >= 0 ? nextGroups[targetIndex] : { matcher: "", hooks: [] };
87
+ targetGroup.hooks = Array.isArray(targetGroup.hooks) ? targetGroup.hooks : [];
88
+ targetGroup.hooks.push({
89
+ _vericify: true,
90
+ type: "command",
91
+ command,
92
+ });
93
+ if (targetIndex >= 0) {
94
+ nextGroups[targetIndex] = targetGroup;
95
+ } else {
96
+ nextGroups.push(targetGroup);
97
+ }
98
+ nextSettings.hooks[hookName] = nextGroups.filter((group) => Array.isArray(group.hooks) && group.hooks.length > 0);
99
+ }
100
+
101
+ const nextRaw = `${JSON.stringify(nextSettings, null, 2)}\n`;
102
+ const currentRaw = readText(settingsPath, "");
103
+ if (nextRaw !== currentRaw) {
104
+ mkdirSync(dirname(settingsPath), { recursive: true });
105
+ writeFileSync(settingsPath, nextRaw);
106
+ return { path: settingsPath, written: true, settings: nextSettings };
107
+ }
108
+ return { path: settingsPath, written: false, settings: nextSettings };
109
+ }
110
+
111
+ export function claudeCodeHandshake(workspaceRoot, options = {}) {
112
+ const result = createHandshakeResult();
113
+ const skippedReasons = [];
114
+
115
+ try {
116
+ const settingsResult = upsertClaudeSettings(workspaceRoot, {
117
+ PostToolUse: "vericify event --source-module claude-code --event-type TOOL_COMPLETED --status done",
118
+ Stop: "vericify event --source-module claude-code --event-type SESSION_STOPPED --status done",
119
+ PreCompact: VERICIFY_PRECOMPACT_COMMAND,
120
+ });
121
+ result.hooks_written = Boolean(settingsResult.written);
122
+ result.hooks_path = settingsResult.path;
123
+ if (settingsResult.skipped_reason) skippedReasons.push(settingsResult.skipped_reason);
124
+ } catch (error) {
125
+ skippedReasons.push(`Claude settings write failed: ${error instanceof Error ? error.message : String(error)}`);
126
+ }
127
+
128
+ try {
129
+ const instructionResult = upsertClaudeInstruction(workspaceRoot);
130
+ result.instruction_written = Boolean(instructionResult.written);
131
+ result.instruction_path = instructionResult.path;
132
+ } catch (error) {
133
+ skippedReasons.push(`CLAUDE.md write failed: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+
136
+ const eventResult = emitHandshakeStartedEvent(workspaceRoot, {
137
+ adapterId: "claude-code",
138
+ sessionId: options.sessionId ?? null,
139
+ hooksWritten: result.hooks_written,
140
+ instructionWritten: result.instruction_written,
141
+ });
142
+
143
+ return {
144
+ ...result,
145
+ event_emitted: eventResult.event_emitted,
146
+ skipped_reasons: [...skippedReasons, ...eventResult.skipped_reasons],
147
+ };
148
+ }
149
+
150
+ export { claudeCodeHandshake as attachClaudeWorkspace };
@@ -0,0 +1,75 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createHandshakeResult, emitHandshakeStartedEvent, mergeJsonFile, writeInstructionBlock } from "./common.js";
4
+
5
+ const VERICIFY_MARKER = "<!-- vericify-instruction -->";
6
+
7
+ function codexInstructionBlock() {
8
+ return `${VERICIFY_MARKER}
9
+ ## Vericify Process Posts
10
+
11
+ When working in this workspace, emit native Vericify process posts at these moments:
12
+
13
+ - **Blocker:** \`vericify post --run-id workspace:current --agent-id codex --kind blocker --summary "<what is blocking>"\`
14
+ - **Handoff:** \`vericify post --run-id workspace:current --agent-id codex --kind handoff_note --summary "<handoff context>"\`
15
+ - **Completion:** \`vericify post --run-id workspace:current --agent-id codex --kind completion --summary "<what changed>"\`
16
+ - **Intentional silence:** \`vericify post --run-id workspace:current --agent-id codex --kind stale_ack --summary "<reason>"\`
17
+
18
+ You may also write structured state directly to \`.codex/vericify-state.json\` using the passthrough schema (handoffs, nodes, entries, events, posts).
19
+ <!-- /vericify-instruction -->`;
20
+ }
21
+
22
+ export function codexHandshake(workspaceRoot, options = {}) {
23
+ const result = createHandshakeResult();
24
+ const skippedReasons = [];
25
+ let instructionResult = { skipped: false, reason: "" };
26
+ const codexDir = resolve(workspaceRoot, ".codex");
27
+ const statePath = resolve(codexDir, "vericify-state.json");
28
+ const agentsPath = resolve(workspaceRoot, "AGENTS.md");
29
+
30
+ if (existsSync(codexDir)) {
31
+ try {
32
+ const mergeResult = mergeJsonFile(statePath, (existing) => ({
33
+ ...(existing && typeof existing === "object" ? existing : {}),
34
+ _vericify_passthrough: true,
35
+ handoffs: existing?.handoffs && typeof existing.handoffs === "object" ? existing.handoffs : {},
36
+ nodes: existing?.nodes && typeof existing.nodes === "object" ? existing.nodes : {},
37
+ entries: Array.isArray(existing?.entries) ? existing.entries : [],
38
+ events: Array.isArray(existing?.events) ? existing.events : [],
39
+ posts: Array.isArray(existing?.posts) ? existing.posts : [],
40
+ }));
41
+ result.passthrough_written = Boolean(mergeResult.written);
42
+ result.passthrough_path = statePath;
43
+ } catch (error) {
44
+ skippedReasons.push(`.codex/vericify-state.json write failed: ${error instanceof Error ? error.message : String(error)}`);
45
+ }
46
+ } else {
47
+ skippedReasons.push(".codex directory is missing; passthrough config skipped");
48
+ }
49
+
50
+ try {
51
+ instructionResult = writeInstructionBlock(agentsPath, VERICIFY_MARKER, codexInstructionBlock(), {
52
+ createIfAbsent: true,
53
+ createDirs: true,
54
+ });
55
+ result.instruction_written = Boolean(instructionResult.written);
56
+ result.instruction_path = agentsPath;
57
+ if (instructionResult.skipped) skippedReasons.push(instructionResult.reason);
58
+ } catch (error) {
59
+ skippedReasons.push(`AGENTS.md write failed: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+
62
+ const eventResult = emitHandshakeStartedEvent(workspaceRoot, {
63
+ adapterId: "codex",
64
+ sessionId: options.sessionId ?? null,
65
+ hooksWritten: false,
66
+ instructionWritten: result.instruction_written,
67
+ passthroughWritten: result.passthrough_written,
68
+ });
69
+
70
+ return {
71
+ ...result,
72
+ event_emitted: eventResult.event_emitted,
73
+ skipped_reasons: [...skippedReasons, ...(instructionResult.skipped ? [instructionResult.reason] : []), ...eventResult.skipped_reasons].filter(Boolean),
74
+ };
75
+ }
@@ -0,0 +1,130 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { readJson, readText } from "../../core/fs.js";
4
+ import { isoNow } from "../../core/util.js";
5
+ import { appendStatusEvent } from "../../store/status-events.js";
6
+
7
+ export function createHandshakeResult() {
8
+ return {
9
+ hooks_written: false,
10
+ hooks_path: null,
11
+ mcp_written: false,
12
+ mcp_path: null,
13
+ instruction_written: false,
14
+ instruction_path: null,
15
+ passthrough_written: false,
16
+ passthrough_path: null,
17
+ event_emitted: false,
18
+ skipped_reasons: [],
19
+ };
20
+ }
21
+
22
+ function ensureParentDir(filePath) {
23
+ mkdirSync(dirname(filePath), { recursive: true });
24
+ }
25
+
26
+ export function mergeJsonFile(filePath, mergeFn, { createIfAbsent = true } = {}) {
27
+ if (!existsSync(filePath) && !createIfAbsent) {
28
+ return {
29
+ written: false,
30
+ skipped: true,
31
+ reason: `${filePath} is missing; skipped.`,
32
+ value: null,
33
+ };
34
+ }
35
+
36
+ const currentRaw = readText(filePath, "");
37
+ const existing = currentRaw.trim() ? readJson(filePath, null) : null;
38
+ const next = mergeFn(existing);
39
+ const nextRaw = `${JSON.stringify(next, null, 2)}\n`;
40
+ if (nextRaw === currentRaw) {
41
+ return {
42
+ written: false,
43
+ skipped: false,
44
+ reason: "Already up to date.",
45
+ value: next,
46
+ };
47
+ }
48
+
49
+ ensureParentDir(filePath);
50
+ writeFileSync(filePath, nextRaw);
51
+ return {
52
+ written: true,
53
+ skipped: false,
54
+ reason: "",
55
+ value: next,
56
+ };
57
+ }
58
+
59
+ export function writeInstructionBlock(filePath, markerComment, block, { createIfAbsent = true, createDirs = true } = {}) {
60
+ const currentRaw = readText(filePath, "");
61
+ if (!currentRaw && !createIfAbsent) {
62
+ return {
63
+ written: false,
64
+ skipped: true,
65
+ reason: `${filePath} is missing; skipped.`,
66
+ };
67
+ }
68
+
69
+ if (currentRaw.includes(markerComment)) {
70
+ return {
71
+ written: false,
72
+ skipped: false,
73
+ reason: "Instruction block already present.",
74
+ };
75
+ }
76
+
77
+ if (createDirs) {
78
+ ensureParentDir(filePath);
79
+ }
80
+
81
+ const body = String(block ?? "").trimEnd();
82
+ const next = currentRaw.trimEnd()
83
+ ? `${currentRaw.trimEnd()}\n\n${body}\n`
84
+ : `${body}\n`;
85
+ if (next === currentRaw) {
86
+ return {
87
+ written: false,
88
+ skipped: false,
89
+ reason: "Already up to date.",
90
+ };
91
+ }
92
+
93
+ writeFileSync(filePath, next);
94
+ return {
95
+ written: true,
96
+ skipped: false,
97
+ reason: "",
98
+ };
99
+ }
100
+
101
+ export function emitHandshakeStartedEvent(workspaceRoot, { adapterId, sessionId, hooksWritten, instructionWritten, mcpWritten = false, passthroughWritten = false }) {
102
+ try {
103
+ appendStatusEvent(workspaceRoot, {
104
+ source_module: adapterId,
105
+ event_type: "SESSION_STARTED",
106
+ status: "started",
107
+ payload: {
108
+ summary: `${adapterId} adapter attached.`,
109
+ adapter_id: adapterId,
110
+ session_id: sessionId ?? null,
111
+ hooks_written: Boolean(hooksWritten),
112
+ instruction_written: Boolean(instructionWritten),
113
+ workspace_root: workspaceRoot,
114
+ mcp_written: Boolean(mcpWritten),
115
+ passthrough_written: Boolean(passthroughWritten),
116
+ },
117
+ });
118
+ return {
119
+ event_emitted: true,
120
+ skipped_reasons: [],
121
+ timestamp: isoNow(),
122
+ };
123
+ } catch (error) {
124
+ return {
125
+ event_emitted: false,
126
+ skipped_reasons: [`Failed to emit SESSION_STARTED: ${error instanceof Error ? error.message : String(error)}`],
127
+ timestamp: isoNow(),
128
+ };
129
+ }
130
+ }
@@ -0,0 +1,100 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { createHandshakeResult, emitHandshakeStartedEvent, mergeJsonFile, writeInstructionBlock } from "./common.js";
4
+
5
+ const VERICIFY_MARKER = "<!-- vericify-instruction -->";
6
+
7
+ function cursorInstructionBlock() {
8
+ return `---
9
+ description: Vericify process post instructions
10
+ alwaysApply: true
11
+ ---
12
+
13
+ ## Vericify Process Posts
14
+
15
+ When working in this workspace, emit native Vericify process posts at these moments:
16
+
17
+ - **Blocker:** \`vericify post --run-id workspace:current --agent-id cursor --kind blocker --summary "<what is blocking>"\`
18
+ - **Handoff:** \`vericify post --run-id workspace:current --agent-id cursor --kind handoff_note --summary "<handoff context>"\`
19
+ - **Completion:** \`vericify post --run-id workspace:current --agent-id cursor --kind completion --summary "<what changed>"\`
20
+ - **Intentional silence:** \`vericify post --run-id workspace:current --agent-id cursor --kind stale_ack --summary "<reason>"\``;
21
+ }
22
+
23
+ export function cursorHandshake(workspaceRoot, options = {}) {
24
+ const result = createHandshakeResult();
25
+ const skippedReasons = [];
26
+ const cursorDir = resolve(workspaceRoot, ".cursor");
27
+ const cursorMcpPath = resolve(cursorDir, "mcp.json");
28
+ const rulesDir = resolve(cursorDir, "rules");
29
+ const fallbackRulesPath = resolve(workspaceRoot, ".cursorrules");
30
+ const mcpPlaceholder = {
31
+ _vericify: true,
32
+ mcpServers: {
33
+ vericify: {
34
+ command: "vericify-mcp",
35
+ args: ["--workspace-root", "."],
36
+ _note: "vericify-mcp not yet installed. Run: npm install -g vericify-mcp",
37
+ },
38
+ },
39
+ };
40
+
41
+ if (existsSync(cursorDir)) {
42
+ try {
43
+ const mcpResult = mergeJsonFile(cursorMcpPath, (existing) => {
44
+ const current = existing && typeof existing === "object" ? existing : {};
45
+ return {
46
+ ...current,
47
+ _vericify: true,
48
+ mcpServers: {
49
+ ...(current.mcpServers && typeof current.mcpServers === "object" ? current.mcpServers : {}),
50
+ vericify: current.mcpServers?.vericify ?? mcpPlaceholder.mcpServers.vericify,
51
+ },
52
+ };
53
+ });
54
+ result.mcp_written = Boolean(mcpResult.written);
55
+ result.mcp_path = cursorMcpPath;
56
+ if (mcpResult.skipped) skippedReasons.push(mcpResult.reason);
57
+ } catch (error) {
58
+ skippedReasons.push(`.cursor/mcp.json write failed: ${error instanceof Error ? error.message : String(error)}`);
59
+ }
60
+ } else {
61
+ skippedReasons.push(".cursor directory is missing; MCP placeholder skipped");
62
+ }
63
+
64
+ let instructionTarget = null;
65
+ if (existsSync(rulesDir)) {
66
+ instructionTarget = resolve(rulesDir, "vericify.mdc");
67
+ } else if (existsSync(fallbackRulesPath)) {
68
+ instructionTarget = fallbackRulesPath;
69
+ }
70
+
71
+ if (instructionTarget) {
72
+ try {
73
+ const instructionResult = writeInstructionBlock(instructionTarget, VERICIFY_MARKER, cursorInstructionBlock(), {
74
+ createIfAbsent: true,
75
+ createDirs: true,
76
+ });
77
+ result.instruction_written = Boolean(instructionResult.written);
78
+ result.instruction_path = instructionTarget;
79
+ if (instructionResult.skipped) skippedReasons.push(instructionResult.reason);
80
+ } catch (error) {
81
+ skippedReasons.push(`Cursor instruction write failed: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ } else {
84
+ skippedReasons.push("No .cursor/rules/ or .cursorrules file exists; instruction block skipped");
85
+ }
86
+
87
+ const eventResult = emitHandshakeStartedEvent(workspaceRoot, {
88
+ adapterId: "cursor",
89
+ sessionId: options.sessionId ?? null,
90
+ hooksWritten: false,
91
+ instructionWritten: result.instruction_written,
92
+ mcpWritten: result.mcp_written,
93
+ });
94
+
95
+ return {
96
+ ...result,
97
+ event_emitted: eventResult.event_emitted,
98
+ skipped_reasons: [...skippedReasons, ...eventResult.skipped_reasons].filter(Boolean),
99
+ };
100
+ }
@@ -0,0 +1,35 @@
1
+ import { createHandshakeResult } from "./common.js";
2
+ import { antigravityHandshake } from "./antigravity.js";
3
+ import { claudeCodeHandshake } from "./claude-code.js";
4
+ import { codexHandshake } from "./codex.js";
5
+ import { cursorHandshake } from "./cursor.js";
6
+ import { vscodeCopilotHandshake } from "./vscode-copilot.js";
7
+
8
+ const HANDSHAKE_FNS = {
9
+ "claude-code": claudeCodeHandshake,
10
+ codex: codexHandshake,
11
+ cursor: cursorHandshake,
12
+ "vscode-copilot-chat": vscodeCopilotHandshake,
13
+ antigravity: antigravityHandshake,
14
+ };
15
+
16
+ export function runHandshake(workspaceRoot, options = {}) {
17
+ const adapterId = String(options.adapterId ?? "").trim();
18
+ const fn = HANDSHAKE_FNS[adapterId];
19
+ if (!fn) {
20
+ return {
21
+ skipped: true,
22
+ reason: "No handshake defined for this adapter.",
23
+ ...createHandshakeResult(),
24
+ };
25
+ }
26
+
27
+ try {
28
+ return fn(workspaceRoot, options);
29
+ } catch (error) {
30
+ return {
31
+ ...createHandshakeResult(),
32
+ skipped_reasons: [`Handshake failed: ${error instanceof Error ? error.message : String(error)}`],
33
+ };
34
+ }
35
+ }