vericify 1.2.0 → 1.3.1

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
@@ -8,15 +8,15 @@ __ _______ ____ ___ ____ ___ _______ __
8
8
  \_/ |_____|_| \_\___\____|___|_| |_|
9
9
  ```
10
10
 
11
- Vericify is a local-first operations hub for agent work.
11
+ Vericify is a local-first run intelligence hub for multi-agent workflows and AI agent state.
12
12
 
13
- It gives a workspace a durable run model instead of making you reconstruct intent from chat scrollback, shell history, and raw logs.
13
+ It gives a workspace durable run records, checkpointed handoffs, and a four-layer compare engine instead of making you reconstruct intent from chat scrollback, shell history, and raw logs.
14
14
 
15
15
  It works on its own, and it also plugs neatly into [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm) workspaces that already emit `agent-state/*`.
16
16
 
17
17
  ## Product Summary
18
18
 
19
- Vericify is for people building with agents who want to answer questions like:
19
+ Vericify is for engineers building with AI agents who want run-level observability and clear answers to questions like:
20
20
 
21
21
  - What is moving right now?
22
22
  - What is blocked?
@@ -41,12 +41,12 @@ Default behavior is simple:
41
41
 
42
42
  Vericify gives you:
43
43
 
44
- - a terminal cockpit for run inspection
44
+ - a terminal cockpit with four views — Hub, Inspect, Compare, History — for run inspection and agent observability
45
45
  - a structured run model built from handoffs, posts, todos, events, and checkpoints
46
- - a compare engine that explains divergence and recovery
46
+ - a compare engine that measures divergence and recovery across four layers: exact diff, structural delta, semantic similarity (MinHash), and operational timing
47
47
  - publishable run artifacts under `.vericify/published/`
48
48
  - a local-first sync outbox under `.vericify/sync-outbox/`
49
- - partner compatibility with [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm)
49
+ - adapter contracts for Claude Code, Codex, Cursor, VS Code Copilot, Antigravity, and [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm) workspaces
50
50
 
51
51
  ## Install
52
52
 
@@ -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. This is the primary path for workflow continuity across context resets:
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.
@@ -189,15 +215,16 @@ vericify hub
189
215
 
190
216
  Best when you already have `agent-state/*` and want a cockpit, history, and compare surface.
191
217
 
192
- ### Use case 2: Tag a Codex or Claude Code workspace before richer capture exists
218
+ ### Use case 2: Tag a Codex, Claude Code, Cursor, VS Code Copilot, or Antigravity workspace before richer capture exists
193
219
 
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
 
@@ -216,14 +243,13 @@ Best when you want to model agent work directly in Vericify.
216
243
  vericify compare --run-id handoff:run-a --compare-run-id workspace:current
217
244
  ```
218
245
 
219
- The comparison report includes:
246
+ The comparison runs four layers simultaneously — exact diff, structural graph delta, semantic MinHash similarity, and operational timing analysis — and returns:
220
247
 
221
- - composite similarity
248
+ - composite similarity score
222
249
  - latest checkpoint similarity
223
- - actor overlap
224
- - node overlap
250
+ - actor and node overlap
225
251
  - layer-level divergence cues
226
- - recovery explanations
252
+ - recovery paths from prior runs
227
253
  - recommended next actions
228
254
 
229
255
  ### Use case 5: Publish or queue a run artifact
@@ -248,7 +274,7 @@ This writes:
248
274
  - `Lane`: one concurrent execution stream
249
275
  - `Handoff`: transfer of responsibility
250
276
  - `Checkpoint`: durable snapshot at a meaningful transition
251
- - `Delta`: structural, semantic, or operational change between checkpoints
277
+ - `Delta`: change between checkpoints, measured across four layers: exact diff, structural graph, semantic MinHash, and operational timing
252
278
 
253
279
  ### Storage Contract
254
280
 
@@ -319,11 +345,14 @@ Today the package is semantic-first. Git-backed provenance can be attached when
319
345
 
320
346
  ```bash
321
347
  vericify help
348
+ vericify context
349
+ vericify delta --since vcx_...
322
350
  vericify adapters
323
351
  vericify attach --adapter codex --label "Primary Codex session"
324
352
  vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment --label "Claude main"
325
353
  vericify hub
326
354
  vericify snapshot
355
+ vericify snapshot --format compact
327
356
  vericify compare --run-id handoff:run-a --compare-run-id workspace:current
328
357
  vericify publish --run-id handoff:run-a
329
358
  vericify sync --run-id handoff:run-a --endpoint https://sync.example.test
@@ -354,36 +383,34 @@ Start the cockpit:
354
383
  vericify hub
355
384
  ```
356
385
 
386
+ The cockpit has four views. Each answers a different operational question.
387
+
357
388
  Keys:
358
389
 
359
390
  - `q` quit
360
391
  - `j` / `k` move between runs
361
- - `Enter` inspect selected run
362
- - `c` compare selected run
363
- - `y` history view
364
- - `h` back to hub
392
+ - `Enter` inspect selected run — lane activity, blockers, checkpoint timeline
393
+ - `c` compare selected run — four-layer diff across exact, structural, semantic, and operational dimensions
394
+ - `y` history view — prior runs indexed, recovery patterns ranked by similarity to the current state
395
+ - `a` adapters view — attached tools and detection status
396
+ - `h` back to hub — all active runs, what is moving, what is blocked
365
397
  - `r` refresh
366
398
  - `/` command palette
367
399
  - `Ctrl+R` command history search
368
400
 
369
401
  ## Programmatic API
370
402
 
371
- The package also exports:
403
+ The package exports functions for workspace state projection, run comparison, and artifact publishing:
372
404
 
373
405
  - `loadWorkspaceState`
374
406
  - `projectWorkspaceState`
407
+ - `buildCompactPacket`
408
+ - `buildCompactPacketDetails`
409
+ - `buildCompactDelta`
375
410
  - `detectAdapters`
411
+ - `listAvailableAdapters`
412
+ - `listDefaultWorkspacePaths`
376
413
  - `attachAdapter`
377
414
  - `buildRunComparison`
378
415
  - `publishRunArtifact`
379
416
  - `enqueueSyncOutboxItem`
380
-
381
- ## Honest Status
382
-
383
- Vericify is intentionally honest about what is implemented now.
384
-
385
- - peer client adapters are not full live capture bridges yet
386
- - hosted sync is an outbox contract today, not a running SaaS backend
387
- - git-backed diffing exists as optional provenance, not the whole product
388
-
389
- That is deliberate: the package is meant to be useful now, without pretending unfinished infrastructure already exists.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vericify",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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
+ }