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 +33 -1
- 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
|
@@ -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,
|
|
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.
|
|
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
|
+
}
|