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 +56 -29
- package/package.json +3 -1
- package/src/adapters/claude-attach.js +1 -0
- package/src/adapters/handshake/antigravity.js +81 -0
- package/src/adapters/handshake/claude-code.js +150 -0
- package/src/adapters/handshake/codex.js +75 -0
- package/src/adapters/handshake/common.js +130 -0
- package/src/adapters/handshake/cursor.js +100 -0
- package/src/adapters/handshake/index.js +35 -0
- package/src/adapters/handshake/vscode-copilot.js +82 -0
- package/src/adapters/registry.js +22 -1
- package/src/api.js +2 -0
- package/src/context/delta.js +97 -0
- package/src/context/id.js +61 -0
- package/src/context/packet.js +210 -0
- package/src/context/select.js +65 -0
- package/src/index.js +65 -15
- package/src/projection/runs.js +2 -2
package/README.md
CHANGED
|
@@ -8,15 +8,15 @@ __ _______ ____ ___ ____ ___ _______ __
|
|
|
8
8
|
\_/ |_____|_| \_\___\____|___|_| |_|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Vericify is a local-first
|
|
11
|
+
Vericify is a local-first run intelligence hub for multi-agent workflows and AI agent state.
|
|
12
12
|
|
|
13
|
-
It gives a workspace
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
- `
|
|
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
|
|
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.
|
|
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
|
+
}
|