vericify 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vericify",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first run intelligence and operations hub for agent systems.",
@@ -1,4 +1,6 @@
1
+ import { hashText } from "../core/util.js";
1
2
  import { listLocalStatePaths, loadLocalState } from "./local-state.js";
3
+ import { loadPeerAdapterStates } from "./peer-capture.js";
2
4
  import {
3
5
  attachWorkspaceAdapter,
4
6
  detectWorkspaceAdapters,
@@ -8,15 +10,61 @@ import {
8
10
 
9
11
  export const DEFAULT_ADAPTER = "local-state";
10
12
 
13
+ function mergeByKey(preferred, secondary, getKey) {
14
+ const merged = new Map();
15
+ for (const item of secondary) merged.set(getKey(item), item);
16
+ for (const item of preferred) merged.set(getKey(item), item);
17
+ return [...merged.values()];
18
+ }
19
+
11
20
  export function listDefaultWorkspacePaths(workspaceRoot) {
12
21
  return listLocalStatePaths(workspaceRoot);
13
22
  }
14
23
 
15
24
  export function loadWorkspaceState(workspaceRoot) {
16
25
  const state = loadLocalState(workspaceRoot);
26
+ const adapterProfiles = detectWorkspaceAdapters(workspaceRoot, state.adapterAttachments);
27
+ const peer = loadPeerAdapterStates(workspaceRoot, adapterProfiles);
28
+
29
+ const handoffs = mergeByKey(
30
+ state.handoffs,
31
+ peer.handoffs,
32
+ (h) => h.handoff_id ?? hashText(JSON.stringify(h))
33
+ );
34
+ const todoNodes = mergeByKey(
35
+ state.todoNodes,
36
+ peer.todoNodes,
37
+ (n) => n.id ?? hashText(JSON.stringify(n))
38
+ );
39
+ const ledgerEntries = mergeByKey(
40
+ state.ledgerEntries,
41
+ peer.ledgerEntries,
42
+ (e) => e.id ?? hashText(`${e.timestamp_utc}:${e.tool}:${e.message}`)
43
+ );
44
+ const statusEvents = mergeByKey(
45
+ state.statusEvents,
46
+ peer.statusEvents,
47
+ (e) => e.event_id ?? hashText(`${e.timestamp}:${e.source_module}:${e.event_type}:${e.status}`)
48
+ ).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
49
+ const processPosts = mergeByKey(
50
+ state.processPosts,
51
+ peer.processPosts,
52
+ (p) => p.process_post_id ?? hashText(`${p.timestamp}:${p.agent_id}:${p.summary}`)
53
+ ).sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
54
+
17
55
  return {
18
56
  ...state,
19
- adapterProfiles: detectWorkspaceAdapters(workspaceRoot, state.adapterAttachments),
57
+ handoffs,
58
+ todoNodes,
59
+ todoOrder: [...new Set([...state.todoOrder, ...peer.todoOrder])],
60
+ ledgerEntries,
61
+ statusEvents,
62
+ processPosts,
63
+ adapterProfiles,
64
+ sourceRefs: {
65
+ ...state.sourceRefs,
66
+ peer: peer.sourceRefs,
67
+ },
20
68
  };
21
69
  }
22
70
 
@@ -0,0 +1,285 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { readJson, readText } from "../core/fs.js";
4
+ import { hashText } from "../core/util.js";
5
+
6
+ function safeStatMtime(path) {
7
+ try {
8
+ return statSync(path).mtime.toISOString();
9
+ } catch {
10
+ return new Date().toISOString();
11
+ }
12
+ }
13
+
14
+ function peerId(adapterId, suffix) {
15
+ return `peer:${adapterId}:${hashText(suffix).slice(0, 10)}`;
16
+ }
17
+
18
+ function latestMtime(paths) {
19
+ let latest = 0;
20
+ for (const p of paths) {
21
+ try {
22
+ const t = statSync(p).mtime.getTime();
23
+ if (t > latest) latest = t;
24
+ } catch {
25
+ // ignore unreachable paths
26
+ }
27
+ }
28
+ return latest ? new Date(latest).toISOString() : new Date().toISOString();
29
+ }
30
+
31
+ function makeLedgerEntry(adapterId, label, timestamp, detectedPaths) {
32
+ return {
33
+ id: peerId(adapterId, `ledger:${timestamp}:${adapterId}`),
34
+ timestamp_utc: timestamp,
35
+ tool: adapterId,
36
+ category: "peer_observation",
37
+ message: `${label} workspace detected`,
38
+ metadata: {
39
+ adapter_id: adapterId,
40
+ source_adapter: adapterId,
41
+ detected_paths: detectedPaths,
42
+ },
43
+ };
44
+ }
45
+
46
+ function makeContextEvent(adapterId, filePath, content) {
47
+ return {
48
+ event_id: peerId(adapterId, `context:${filePath}`),
49
+ timestamp: safeStatMtime(filePath),
50
+ source_module: adapterId,
51
+ event_type: "CONTEXT_FILE",
52
+ status: "in_progress",
53
+ source_adapter: adapterId,
54
+ payload: {
55
+ summary: content.trim().slice(0, 120).replace(/\n+/g, " ") || `${basename(filePath)} present`,
56
+ file: basename(filePath),
57
+ },
58
+ };
59
+ }
60
+
61
+ function captureContextFile(adapterId, filePath, sourceRefs) {
62
+ if (!existsSync(filePath)) return null;
63
+ sourceRefs.push(filePath);
64
+ return makeContextEvent(adapterId, filePath, readText(filePath, ""));
65
+ }
66
+
67
+ function scanJsonFiles(dir) {
68
+ try {
69
+ return readdirSync(dir)
70
+ .filter((name) => name.endsWith(".json"))
71
+ .map((name) => join(dir, name));
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ function passthroughJson(filePath) {
78
+ const empty = { handoffs: [], todoNodes: [], ledgerEntries: [], statusEvents: [], processPosts: [] };
79
+ try {
80
+ const data = readJson(filePath, null);
81
+ if (!data || typeof data !== "object") return empty;
82
+ return {
83
+ handoffs: data.handoffs && typeof data.handoffs === "object"
84
+ ? Object.values(data.handoffs).filter((v) => v?.handoff_id)
85
+ : [],
86
+ todoNodes: data.nodes && typeof data.nodes === "object"
87
+ ? Object.values(data.nodes).filter((v) => v?.id)
88
+ : [],
89
+ ledgerEntries: Array.isArray(data.entries) ? data.entries.filter((v) => v?.id) : [],
90
+ statusEvents: Array.isArray(data.events) ? data.events.filter((v) => v?.event_id) : [],
91
+ processPosts: Array.isArray(data.posts) ? data.posts.filter((v) => v?.process_post_id) : [],
92
+ };
93
+ } catch {
94
+ return empty;
95
+ }
96
+ }
97
+
98
+ function mergePassthrough(out, pt) {
99
+ out.handoffs.push(...pt.handoffs);
100
+ out.todoNodes.push(...pt.todoNodes);
101
+ out.ledgerEntries.push(...pt.ledgerEntries);
102
+ out.statusEvents.push(...pt.statusEvents);
103
+ out.processPosts.push(...pt.processPosts);
104
+ }
105
+
106
+ // ── Per-adapter capture ─────────────────────────────────────────────────────
107
+
108
+ function captureCodex(workspaceRoot, profile, out) {
109
+ const sourceRefs = [];
110
+ const events = [];
111
+ const ev = captureContextFile("codex", resolve(workspaceRoot, "AGENTS.md"), sourceRefs);
112
+ if (ev) events.push(ev);
113
+
114
+ const codexDir = resolve(workspaceRoot, ".codex");
115
+ for (const jf of scanJsonFiles(codexDir)) {
116
+ sourceRefs.push(jf);
117
+ mergePassthrough(out, passthroughJson(jf));
118
+ }
119
+
120
+ if (sourceRefs.length) {
121
+ out.ledgerEntries.push(makeLedgerEntry("codex", profile.label, latestMtime(sourceRefs), sourceRefs));
122
+ out.statusEvents.push(...events);
123
+ out.sourceRefs.push(...sourceRefs);
124
+ }
125
+ }
126
+
127
+ function captureClaudeCode(workspaceRoot, profile, out) {
128
+ const sourceRefs = [];
129
+ const events = [];
130
+ const ev = captureContextFile("claude-code", resolve(workspaceRoot, "CLAUDE.md"), sourceRefs);
131
+ if (ev) events.push(ev);
132
+
133
+ const claudeDir = resolve(workspaceRoot, ".claude");
134
+ const settingsFiles = [join(claudeDir, "settings.json"), join(claudeDir, "settings.local.json")];
135
+ for (const sf of settingsFiles) {
136
+ if (existsSync(sf)) sourceRefs.push(sf);
137
+ }
138
+ for (const jf of scanJsonFiles(claudeDir)) {
139
+ if (!sourceRefs.includes(jf)) sourceRefs.push(jf);
140
+ mergePassthrough(out, passthroughJson(jf));
141
+ }
142
+
143
+ if (sourceRefs.length) {
144
+ out.ledgerEntries.push(makeLedgerEntry("claude-code", profile.label, latestMtime(sourceRefs), sourceRefs));
145
+ out.statusEvents.push(...events);
146
+ out.sourceRefs.push(...sourceRefs);
147
+ }
148
+ }
149
+
150
+ function captureCursor(workspaceRoot, profile, out) {
151
+ const sourceRefs = [];
152
+ const events = [];
153
+ const ev = captureContextFile("cursor", resolve(workspaceRoot, ".cursorrules"), sourceRefs);
154
+ if (ev) events.push(ev);
155
+
156
+ const rulesDir = resolve(workspaceRoot, ".cursor/rules");
157
+ if (existsSync(rulesDir)) {
158
+ try {
159
+ const ruleFiles = readdirSync(rulesDir)
160
+ .filter((name) => /\.(md|txt|mdc)$/.test(name))
161
+ .map((name) => join(rulesDir, name));
162
+ for (const rf of ruleFiles) {
163
+ const rev = captureContextFile("cursor", rf, sourceRefs);
164
+ if (rev) events.push(rev);
165
+ }
166
+ } catch {
167
+ // ignore unreadable rules dir
168
+ }
169
+ }
170
+
171
+ if (sourceRefs.length) {
172
+ out.ledgerEntries.push(makeLedgerEntry("cursor", profile.label, latestMtime(sourceRefs), sourceRefs));
173
+ out.statusEvents.push(...events);
174
+ out.sourceRefs.push(...sourceRefs);
175
+ }
176
+ }
177
+
178
+ function captureVscodeCopilot(workspaceRoot, profile, out) {
179
+ const sourceRefs = [];
180
+ const events = [];
181
+ const ev = captureContextFile(
182
+ "vscode-copilot-chat",
183
+ resolve(workspaceRoot, ".github/copilot-instructions.md"),
184
+ sourceRefs
185
+ );
186
+ if (ev) events.push(ev);
187
+
188
+ const settings = resolve(workspaceRoot, ".vscode/settings.json");
189
+ if (existsSync(settings)) sourceRefs.push(settings);
190
+
191
+ if (sourceRefs.length) {
192
+ out.ledgerEntries.push(
193
+ makeLedgerEntry("vscode-copilot-chat", profile.label, latestMtime(sourceRefs), sourceRefs)
194
+ );
195
+ out.statusEvents.push(...events);
196
+ out.sourceRefs.push(...sourceRefs);
197
+ }
198
+ }
199
+
200
+ function captureAntigravity(workspaceRoot, profile, out) {
201
+ const sourceRefs = [];
202
+ const events = [];
203
+ const configJson = resolve(workspaceRoot, "antigravity.config.json");
204
+ if (existsSync(configJson)) {
205
+ sourceRefs.push(configJson);
206
+ mergePassthrough(out, passthroughJson(configJson));
207
+ }
208
+
209
+ const antigravityDir = resolve(workspaceRoot, ".antigravity");
210
+ for (const jf of scanJsonFiles(antigravityDir)) {
211
+ if (!sourceRefs.includes(jf)) sourceRefs.push(jf);
212
+ mergePassthrough(out, passthroughJson(jf));
213
+ }
214
+
215
+ if (sourceRefs.length) {
216
+ out.ledgerEntries.push(
217
+ makeLedgerEntry("antigravity", profile.label, latestMtime(sourceRefs), sourceRefs)
218
+ );
219
+ out.statusEvents.push(...events);
220
+ out.sourceRefs.push(...sourceRefs);
221
+ }
222
+ }
223
+
224
+ const CAPTURE_FNS = {
225
+ codex: captureCodex,
226
+ "claude-code": captureClaudeCode,
227
+ cursor: captureCursor,
228
+ "vscode-copilot-chat": captureVscodeCopilot,
229
+ antigravity: captureAntigravity,
230
+ };
231
+
232
+ // ── Public API ──────────────────────────────────────────────────────────────
233
+
234
+ export function capturePeerAdapterState(workspaceRoot, profile) {
235
+ const out = {
236
+ handoffs: [],
237
+ todoNodes: [],
238
+ todoOrder: [],
239
+ ledgerEntries: [],
240
+ statusEvents: [],
241
+ processPosts: [],
242
+ sourceRefs: [],
243
+ };
244
+
245
+ if (profile.detection_status !== "attached" && profile.detection_status !== "detected") {
246
+ return out;
247
+ }
248
+
249
+ const fn = CAPTURE_FNS[profile.adapter_id];
250
+ if (!fn) return out;
251
+
252
+ try {
253
+ fn(workspaceRoot, profile, out);
254
+ } catch {
255
+ // peer capture never throws
256
+ }
257
+
258
+ return out;
259
+ }
260
+
261
+ export function loadPeerAdapterStates(workspaceRoot, adapterProfiles) {
262
+ const merged = {
263
+ handoffs: [],
264
+ todoNodes: [],
265
+ todoOrder: [],
266
+ ledgerEntries: [],
267
+ statusEvents: [],
268
+ processPosts: [],
269
+ sourceRefs: [],
270
+ };
271
+
272
+ for (const profile of adapterProfiles) {
273
+ if (profile.category !== "peer") continue;
274
+ const captured = capturePeerAdapterState(workspaceRoot, profile);
275
+ merged.handoffs.push(...captured.handoffs);
276
+ merged.todoNodes.push(...captured.todoNodes);
277
+ merged.todoOrder.push(...captured.todoOrder);
278
+ merged.ledgerEntries.push(...captured.ledgerEntries);
279
+ merged.statusEvents.push(...captured.statusEvents);
280
+ merged.processPosts.push(...captured.processPosts);
281
+ merged.sourceRefs.push(...captured.sourceRefs);
282
+ }
283
+
284
+ return merged;
285
+ }
package/src/tui/app.js CHANGED
@@ -11,6 +11,7 @@ import { upsertTodoNode } from "../store/todo-state.js";
11
11
  import { formatTimestamp } from "../core/util.js";
12
12
  import { completeCommand, executeCommand } from "./commands.js";
13
13
  import {
14
+ buildAdaptersSections,
14
15
  buildCompareSections,
15
16
  buildHistorySections,
16
17
  buildHubMetrics,
@@ -165,7 +166,9 @@ export async function runHub({ workspaceRoot, feedFile, loadState, watchedPaths
165
166
  ? buildInspectSections(selected, { tier })
166
167
  : state.view === "history"
167
168
  ? buildHistorySections(state.projected, selected, { tier })
168
- : buildHubSections(state.projected, selected, { tier })),
169
+ : state.view === "adapters"
170
+ ? buildAdaptersSections(state.projected, { tier })
171
+ : buildHubSections(state.projected, selected, { tier })),
169
172
  ];
170
173
 
171
174
  line(boxTitle(` Vericify Hub | ${state.mode} | ${workspaceRoot} `, width));
@@ -175,7 +178,7 @@ export async function runHub({ workspaceRoot, feedFile, loadState, watchedPaths
175
178
  renderPanelGrid(sections, width, tier);
176
179
  const prompt = state.mode === "command"
177
180
  ? `/${state.commandBuffer}`
178
- : "[q quit] [j/k move] [Enter inspect] [c compare] [y history] [r refresh] [/ command] [h hub]";
181
+ : "[q quit] [j/k move] [Enter inspect] [c compare] [y history] [a adapters] [r refresh] [/ command] [h hub]";
179
182
  line(truncate(prompt, width));
180
183
  line(boxFooter(width));
181
184
  };
@@ -512,6 +515,13 @@ export async function runHub({ workspaceRoot, feedFile, loadState, watchedPaths
512
515
  recordInteractiveTransition(before, { source: "keyboard" });
513
516
  return;
514
517
  }
518
+ if (key.name === "a") {
519
+ const before = captureRuntimeState();
520
+ state.view = "adapters";
521
+ rerender();
522
+ recordInteractiveTransition(before, { source: "keyboard" });
523
+ return;
524
+ }
515
525
  if (key.name === "r") {
516
526
  const before = captureRuntimeState();
517
527
  refresh();
@@ -1,5 +1,5 @@
1
1
  export function completeCommand(input, runIds) {
2
- const commands = ["help", "quit", "refresh", "hub", "run", "inspect", "compare", "history", "select", "next", "prev", "focus", "post", "handoff", "todo", "ledger", "event"];
2
+ const commands = ["help", "quit", "refresh", "hub", "run", "inspect", "compare", "history", "adapters", "select", "next", "prev", "focus", "post", "handoff", "todo", "ledger", "event"];
3
3
  const parts = input.split(/\s+/);
4
4
  const token = parts.at(-1) ?? "";
5
5
  const options = parts.length > 1 ? [...runIds, ...commands] : [...commands, ...runIds];
@@ -107,6 +107,9 @@ export function executeCommand(command, state) {
107
107
  const compareTarget = positionals[0];
108
108
  return { patch: { view: "compare", compareTarget }, message: "Switched to compare view." };
109
109
  }
110
+ if (name === "adapters") {
111
+ return { patch: { view: "adapters" }, message: "Switched to adapters view." };
112
+ }
110
113
  if (name === "history") {
111
114
  return { patch: { view: "history" }, message: "Switched to history view." };
112
115
  }
package/src/tui/panels.js CHANGED
@@ -352,6 +352,79 @@ export function buildRunBoardSection(projected, selectedRun, options = {}) {
352
352
  };
353
353
  }
354
354
 
355
+ const PEER_ADAPTER_IDS = new Set(["codex", "claude-code", "cursor", "vscode-copilot-chat", "antigravity"]);
356
+ const STATUS_GLYPH = { ready: "✓", attached: "●", detected: "◉", manual: "○" };
357
+
358
+ function relPath(absPath, root) {
359
+ return absPath.startsWith(root + "/") ? absPath.slice(root.length + 1) : absPath;
360
+ }
361
+
362
+ function adapterStatusRow(profile, workspaceRoot) {
363
+ const glyph = STATUS_GLYPH[profile.detection_status] ?? "?";
364
+ const status = profile.detection_status.padEnd(8);
365
+ const label = (profile.label_override ?? profile.label).padEnd(22);
366
+ const cat = `(${profile.category})`.padEnd(10);
367
+ let detail = "";
368
+ if (profile.detection_status === "manual") {
369
+ detail = `→ vericify attach --adapter ${profile.adapter_id}`;
370
+ } else {
371
+ const paths = profile.detected_paths.slice(0, 3).map((p) => relPath(p, workspaceRoot));
372
+ detail = paths.join(" ");
373
+ if (profile.session_id) detail += ` session:${profile.session_id}`;
374
+ }
375
+ return `${glyph} ${status} ${label} ${cat} ${detail}`;
376
+ }
377
+
378
+ function buildPeerObservationRows(projected, tier) {
379
+ const limit = tier === "narrow" ? 4 : 6;
380
+ const items = (projected.runs ?? [])
381
+ .flatMap((run) => run.activity_items ?? [])
382
+ .filter((item) =>
383
+ item.label === "peer_observation" ||
384
+ (PEER_ADAPTER_IDS.has(item.actor_id) && item.source_kind === "status_event")
385
+ )
386
+ .sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)));
387
+ return top(items, limit).map((item) =>
388
+ `${formatTimestamp(item.timestamp)} [${item.actor_id}] ${item.summary}`
389
+ );
390
+ }
391
+
392
+ function buildAttachGuideRows(profiles) {
393
+ const manual = profiles.filter((p) => p.detection_status === "manual" && p.category === "peer");
394
+ if (!manual.length) return ["All peer adapters detected or attached."];
395
+ return [
396
+ "Activate with: vericify attach --adapter <id>",
397
+ ...manual.map((p) => ` ○ ${p.adapter_id.padEnd(22)} ${p.label}`),
398
+ ];
399
+ }
400
+
401
+ export function buildAdaptersSections(projected, options = {}) {
402
+ const { tier = "wide" } = options;
403
+ const profiles = projected.adapter_profiles ?? [];
404
+ const workspaceRoot = projected.workspace?.root_path ?? "";
405
+ const adapterRows = profiles.map((p) => adapterStatusRow(p, workspaceRoot));
406
+ const observationRows = buildPeerObservationRows(projected, tier);
407
+ const guideRows = buildAttachGuideRows(profiles);
408
+ return [
409
+ {
410
+ title: "Adapter Sources",
411
+ rows: adapterRows.length ? adapterRows : ["No adapters resolved."],
412
+ tone: "STATUS",
413
+ fullWidth: true,
414
+ },
415
+ {
416
+ title: "Peer Observations",
417
+ rows: observationRows.length ? observationRows : ["No peer capture data yet."],
418
+ tone: "TRACE",
419
+ },
420
+ {
421
+ title: "Attach Guide",
422
+ rows: guideRows,
423
+ tone: "WAIT",
424
+ },
425
+ ];
426
+ }
427
+
355
428
  export function buildHubSections(projected, selectedRun, options = {}) {
356
429
  const { tier = "wide" } = options;
357
430
  return [