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 +1 -1
- package/src/adapters/index.js +49 -1
- package/src/adapters/peer-capture.js +285 -0
- package/src/tui/app.js +12 -2
- package/src/tui/commands.js +4 -1
- package/src/tui/panels.js +73 -0
package/package.json
CHANGED
package/src/adapters/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
:
|
|
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();
|
package/src/tui/commands.js
CHANGED
|
@@ -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 [
|