vericify 0.1.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 ADDED
@@ -0,0 +1,389 @@
1
+ # Vericify
2
+
3
+ ```text
4
+ __ _______ ____ ___ ____ ___ _______ __
5
+ \ \ / / ____| _ \|_ _/ ___|_ _| ___\ \ / /
6
+ \ \ / /| _| | |_) || | | | || |_ \ V /
7
+ \ V / | |___| _ < | | |___ | || _| | |
8
+ \_/ |_____|_| \_\___\____|___|_| |_|
9
+ ```
10
+
11
+ Vericify is a local-first operations hub for agent work.
12
+
13
+ It gives a workspace a durable run model instead of making you reconstruct intent from chat scrollback, shell history, and raw logs.
14
+
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
+
17
+ ## Product Summary
18
+
19
+ Vericify is for people building with agents who want to answer questions like:
20
+
21
+ - What is moving right now?
22
+ - What is blocked?
23
+ - Which run diverged?
24
+ - Which earlier run looks like a recovery path?
25
+ - What should I publish or hand off?
26
+
27
+ The package installs one CLI:
28
+
29
+ ```bash
30
+ vericify
31
+ ```
32
+
33
+ Default behavior is simple:
34
+
35
+ - run it inside the repo you want to observe
36
+ - store native state in `.vericify/`
37
+ - read partner state from `agent-state/*` when present
38
+ - open the terminal hub with `vericify` or `vericify hub`
39
+
40
+ ## What Vericify Does
41
+
42
+ Vericify gives you:
43
+
44
+ - a terminal cockpit for run inspection
45
+ - a structured run model built from handoffs, posts, todos, events, and checkpoints
46
+ - a compare engine that explains divergence and recovery
47
+ - publishable run artifacts under `.vericify/published/`
48
+ - a local-first sync outbox under `.vericify/sync-outbox/`
49
+ - partner compatibility with [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm)
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ npm install -g vericify
55
+ ```
56
+
57
+ Node `18+` is required.
58
+
59
+ ## Five-Minute Start
60
+
61
+ ### 1) Start in any repo
62
+
63
+ ```bash
64
+ cd /path/to/repo
65
+ vericify adapters
66
+ vericify hub
67
+ ```
68
+
69
+ If Vericify already sees `.vericify/` or `agent-state/*`, the hub can immediately project runs from that workspace.
70
+
71
+ ### 2) If you already use [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm)
72
+
73
+ This is the easiest path.
74
+
75
+ If your workspace already contains `agent-state/*`, Vericify reads it automatically:
76
+
77
+ ```bash
78
+ cd /path/to/ace-workspace
79
+ vericify adapters
80
+ vericify hub
81
+ ```
82
+
83
+ No extra conversion step is required.
84
+
85
+ ### 3) If you are using another agent client and want to label it explicitly
86
+
87
+ Attach the adapter to the current workspace:
88
+
89
+ ```bash
90
+ vericify attach --adapter codex --label "Primary Codex session"
91
+ vericify adapters
92
+ ```
93
+
94
+ That creates or updates `.vericify/adapters.json` for the current repo and marks the adapter as attached.
95
+
96
+ ## Do I Need `--session-id`?
97
+
98
+ No. `--session-id` is optional.
99
+
100
+ Use no session ID when:
101
+
102
+ - one active session per tool is enough
103
+ - you only want to label the workspace relationship
104
+ - you want the lightest setup
105
+
106
+ Add a session ID when:
107
+
108
+ - you run multiple sessions from the same tool
109
+ - you want stable names in artifacts and dashboards
110
+ - you want to distinguish roles like `claude-main` and `claude-review`
111
+
112
+ Good session IDs look like:
113
+
114
+ - `claude-main`
115
+ - `codex-bugfix`
116
+ - `cursor-review-01`
117
+
118
+ ## What Attachment Looks Like
119
+
120
+ ### A) Attach without a session ID
121
+
122
+ This is valid:
123
+
124
+ ```bash
125
+ vericify attach --adapter codex --label "Primary Codex session"
126
+ ```
127
+
128
+ Simplified output:
129
+
130
+ ```json
131
+ {
132
+ "path": ".vericify/adapters.json",
133
+ "attachment": {
134
+ "adapter_id": "codex",
135
+ "capture_mode": "manual",
136
+ "label": "Primary Codex session"
137
+ }
138
+ }
139
+ ```
140
+
141
+ Notes:
142
+
143
+ - `capture_mode` defaults to `manual` if you do not set one
144
+ - this is enough to tell Vericify "track this tool in this workspace"
145
+
146
+ ### B) Attach with a session ID
147
+
148
+ Use this when you want a named session:
149
+
150
+ ```bash
151
+ vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment --label "Claude main"
152
+ ```
153
+
154
+ Simplified output:
155
+
156
+ ```json
157
+ {
158
+ "path": ".vericify/adapters.json",
159
+ "attachment": {
160
+ "adapter_id": "claude-code",
161
+ "capture_mode": "attachment",
162
+ "session_id": "claude-main",
163
+ "label": "Claude main"
164
+ }
165
+ }
166
+ ```
167
+
168
+ After that, verify status:
169
+
170
+ ```bash
171
+ vericify adapters
172
+ ```
173
+
174
+ Look for:
175
+
176
+ - `detection_status: "attached"`
177
+ - `session_id` when you supplied one
178
+ - `label_override` with your human-friendly name
179
+
180
+ ## Easy Use Cases
181
+
182
+ ### Use case 1: Observe an existing [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm) workspace
183
+
184
+ ```bash
185
+ cd /path/to/ace-workspace
186
+ vericify adapters
187
+ vericify hub
188
+ ```
189
+
190
+ Best when you already have `agent-state/*` and want a cockpit, history, and compare surface.
191
+
192
+ ### Use case 2: Tag a Codex or Claude Code workspace before richer capture exists
193
+
194
+ ```bash
195
+ vericify attach --adapter codex --label "Primary Codex"
196
+ vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment
197
+ vericify hub
198
+ ```
199
+
200
+ Best when you want durable workspace metadata now, even before vendor-specific live capture bridges are added.
201
+
202
+ ### Use case 3: Create a native run trail manually
203
+
204
+ ```bash
205
+ vericify handoff --id h1 --from capability-ops --to capability-build --title "Implement compare engine" --status open
206
+ vericify post --run-id handoff:h1 --agent-id capability-build --kind progress --summary "Builder started compare engine"
207
+ vericify snapshot
208
+ vericify hub
209
+ ```
210
+
211
+ Best when you want to model agent work directly in Vericify.
212
+
213
+ ### Use case 4: Compare two runs
214
+
215
+ ```bash
216
+ vericify compare --run-id handoff:run-a --compare-run-id workspace:current
217
+ ```
218
+
219
+ The comparison report includes:
220
+
221
+ - composite similarity
222
+ - latest checkpoint similarity
223
+ - actor overlap
224
+ - node overlap
225
+ - layer-level divergence cues
226
+ - recovery explanations
227
+ - recommended next actions
228
+
229
+ ### Use case 5: Publish or queue a run artifact
230
+
231
+ ```bash
232
+ vericify publish --run-id handoff:run-a --compare-run-id workspace:current --title "Run A artifact"
233
+ vericify sync --run-id handoff:run-a --compare-run-id workspace:current --endpoint https://sync.example.test
234
+ ```
235
+
236
+ This writes:
237
+
238
+ - `.vericify/published/<artifact-id>/run-artifact.json`
239
+ - `.vericify/published/<artifact-id>/SUMMARY.md`
240
+ - `.vericify/sync-outbox/<item>.json`
241
+
242
+ ## Product Spec
243
+
244
+ ### Core Objects
245
+
246
+ - `Run`: one coordinated attempt to complete an objective
247
+ - `Branch`: an alternate path inside a run
248
+ - `Lane`: one concurrent execution stream
249
+ - `Handoff`: transfer of responsibility
250
+ - `Checkpoint`: durable snapshot at a meaningful transition
251
+ - `Delta`: structural, semantic, or operational change between checkpoints
252
+
253
+ ### Storage Contract
254
+
255
+ Vericify writes native state here:
256
+
257
+ ```text
258
+ .vericify/
259
+ |-- adapters.json
260
+ |-- handoffs.json
261
+ |-- todo-state.json
262
+ |-- run-ledger.json
263
+ |-- status-events.ndjson
264
+ |-- process-posts.json
265
+ |-- published/
266
+ `-- sync-outbox/
267
+ ```
268
+
269
+ Vericify also reads partner state here when available:
270
+
271
+ ```text
272
+ agent-state/
273
+ `-- ...
274
+ ```
275
+
276
+ This is why it works naturally with [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm) workspaces.
277
+
278
+ ### Adapter Contract
279
+
280
+ Current adapter registry:
281
+
282
+ - `local-state`
283
+ - `ace`
284
+ - `codex`
285
+ - `claude-code`
286
+ - `cursor`
287
+ - `vscode-copilot-chat`
288
+ - `antigravity`
289
+
290
+ Current truth:
291
+
292
+ - `local-state` is fully implemented
293
+ - `ace` is the partner path for [ACE / ace-swarm](https://www.npmjs.com/package/ace-swarm) style workspaces
294
+ - peer adapters are currently attachment and detection contracts
295
+ - auto-detection is heuristic for several tools, so explicit attachment is the reliable path
296
+
297
+ ### Checkpoint Policy
298
+
299
+ Checkpoint triggers currently include:
300
+
301
+ - `handoff`
302
+ - `status_transition`
303
+ - `process_milestone`
304
+ - `ledger_update`
305
+ - `operator_save`
306
+ - `branch_fork`
307
+
308
+ Capture modes currently include:
309
+
310
+ - `semantic`
311
+ - `git`
312
+ - `hybrid`
313
+
314
+ Today the package is semantic-first. Git-backed provenance can be attached when available.
315
+
316
+ ## CLI Reference
317
+
318
+ ### High-signal commands
319
+
320
+ ```bash
321
+ vericify help
322
+ vericify adapters
323
+ vericify attach --adapter codex --label "Primary Codex session"
324
+ vericify attach --adapter claude-code --session-id claude-main --capture-mode attachment --label "Claude main"
325
+ vericify hub
326
+ vericify snapshot
327
+ vericify compare --run-id handoff:run-a --compare-run-id workspace:current
328
+ vericify publish --run-id handoff:run-a
329
+ vericify sync --run-id handoff:run-a --endpoint https://sync.example.test
330
+ ```
331
+
332
+ ### Native writer commands
333
+
334
+ ```bash
335
+ vericify handoff --id h1 --from capability-ops --to capability-build --title "Review" --status open
336
+ vericify todo --id todo-1 --title "Write store" --status in_progress
337
+ vericify ledger --tool vericify --category major_update --message "Writer landed"
338
+ vericify event --source-module capability-build --event-type STORE_WRITE --status started --payload-json '{"summary":"native write"}'
339
+ vericify post --run-id handoff:h1 --agent-id capability-build --kind progress --summary "Checkpoint emitted"
340
+ ```
341
+
342
+ ### Helpful defaults
343
+
344
+ - current working directory is the workspace unless you pass `--workspace-root`
345
+ - `vericify` with no command opens `hub`
346
+ - `--session-id` is optional
347
+ - `--compare-run-id` is optional for `publish` and `sync`
348
+
349
+ ## Terminal Hub Controls
350
+
351
+ Start the cockpit:
352
+
353
+ ```bash
354
+ vericify hub
355
+ ```
356
+
357
+ Keys:
358
+
359
+ - `q` quit
360
+ - `j` / `k` move between runs
361
+ - `Enter` inspect selected run
362
+ - `c` compare selected run
363
+ - `y` history view
364
+ - `h` back to hub
365
+ - `r` refresh
366
+ - `/` command palette
367
+ - `Ctrl+R` command history search
368
+
369
+ ## Programmatic API
370
+
371
+ The package also exports:
372
+
373
+ - `loadWorkspaceState`
374
+ - `projectWorkspaceState`
375
+ - `detectAdapters`
376
+ - `attachAdapter`
377
+ - `buildRunComparison`
378
+ - `publishRunArtifact`
379
+ - `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 ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "vericify",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Local-first run intelligence and operations hub for agent systems.",
7
+ "main": "./src/api.js",
8
+ "exports": {
9
+ ".": "./src/api.js",
10
+ "./api": "./src/api.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "bin": {
14
+ "vericify": "./src/index.js"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "adapters": "node src/index.js adapters",
22
+ "attach": "node src/index.js attach",
23
+ "hub": "node src/index.js hub",
24
+ "compare": "node src/index.js compare",
25
+ "publish": "node src/index.js publish",
26
+ "sync": "node src/index.js sync",
27
+ "handoff": "node src/index.js handoff",
28
+ "todo": "node src/index.js todo",
29
+ "ledger": "node src/index.js ledger",
30
+ "event": "node src/index.js event",
31
+ "post": "node src/index.js post",
32
+ "snapshot": "node src/index.js snapshot",
33
+ "help": "node src/index.js help",
34
+ "test": "node --test test/**/*.test.mjs"
35
+ },
36
+ "keywords": [
37
+ "agents",
38
+ "ai",
39
+ "observability",
40
+ "tui",
41
+ "runs",
42
+ "checkpoints",
43
+ "diff",
44
+ "codex",
45
+ "claude-code",
46
+ "cursor",
47
+ "copilot",
48
+ "ace-swarm"
49
+ ],
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "license": "UNLICENSED",
54
+ "engines": {
55
+ "node": ">=18"
56
+ }
57
+ }
@@ -0,0 +1,37 @@
1
+ import { listLocalStatePaths, loadLocalState } from "./local-state.js";
2
+ import {
3
+ attachWorkspaceAdapter,
4
+ detectWorkspaceAdapters,
5
+ listAdapterDefinitions,
6
+ readWorkspaceAdapterAttachments,
7
+ } from "./registry.js";
8
+
9
+ export const DEFAULT_ADAPTER = "local-state";
10
+
11
+ export function listDefaultWorkspacePaths(workspaceRoot) {
12
+ return listLocalStatePaths(workspaceRoot);
13
+ }
14
+
15
+ export function loadWorkspaceState(workspaceRoot) {
16
+ const state = loadLocalState(workspaceRoot);
17
+ return {
18
+ ...state,
19
+ adapterProfiles: detectWorkspaceAdapters(workspaceRoot, state.adapterAttachments),
20
+ };
21
+ }
22
+
23
+ export function listAvailableAdapters() {
24
+ return listAdapterDefinitions();
25
+ }
26
+
27
+ export function detectAdapters(workspaceRoot) {
28
+ return detectWorkspaceAdapters(workspaceRoot);
29
+ }
30
+
31
+ export function readAdapterAttachments(workspaceRoot) {
32
+ return readWorkspaceAdapterAttachments(workspaceRoot);
33
+ }
34
+
35
+ export function attachAdapter(workspaceRoot, input) {
36
+ return attachWorkspaceAdapter(workspaceRoot, input);
37
+ }
@@ -0,0 +1,86 @@
1
+ import { hashText, unique } from "../core/util.js";
2
+ import { readJson, readNdjson } from "../core/fs.js";
3
+ import { listStorePaths, resolveStoreLayout } from "../store/paths.js";
4
+ import { readAdapterAttachments } from "../store/adapter-attachments.js";
5
+
6
+ export function listLocalStatePaths(workspaceRoot) {
7
+ return listStorePaths(workspaceRoot);
8
+ }
9
+
10
+ function mergeByKey(preferredItems, partnerItems, getKey) {
11
+ const merged = new Map();
12
+ for (const item of partnerItems) merged.set(getKey(item), item);
13
+ for (const item of preferredItems) merged.set(getKey(item), item);
14
+ return [...merged.values()];
15
+ }
16
+
17
+ function mergeOrderedIds(preferred, partner) {
18
+ return unique([...preferred, ...partner]);
19
+ }
20
+
21
+ export function loadLocalState(workspaceRoot) {
22
+ const paths = resolveStoreLayout(workspaceRoot);
23
+ const adapterAttachmentState = readAdapterAttachments(workspaceRoot);
24
+ const vericifyState = {
25
+ handoffRegistry: readJson(paths.vericify.handoffRegistry, { handoffs: {} }),
26
+ todoState: readJson(paths.vericify.todoState, { nodes: {}, order: [] }),
27
+ runLedger: readJson(paths.vericify.runLedger, { entries: [] }),
28
+ processPosts: readJson(paths.vericify.processPosts, { posts: [] }),
29
+ statusEvents: readNdjson(paths.vericify.statusEvents),
30
+ };
31
+ const partnerState = {
32
+ handoffRegistry: readJson(paths.partner.handoffRegistry, { handoffs: {} }),
33
+ todoState: readJson(paths.partner.todoState, { nodes: {}, order: [] }),
34
+ runLedger: readJson(paths.partner.runLedger, { entries: [] }),
35
+ processPosts: readJson(paths.partner.processPosts, { posts: [] }),
36
+ statusEvents: readNdjson(paths.partner.statusEvents),
37
+ };
38
+
39
+ const handoffs = mergeByKey(
40
+ Object.values(vericifyState.handoffRegistry.handoffs ?? {}),
41
+ Object.values(partnerState.handoffRegistry.handoffs ?? {}),
42
+ (handoff) => handoff.handoff_id ?? hashText(JSON.stringify(handoff))
43
+ );
44
+ const todoNodes = mergeByKey(
45
+ Object.values(vericifyState.todoState.nodes ?? {}),
46
+ Object.values(partnerState.todoState.nodes ?? {}),
47
+ (node) => node.id ?? hashText(JSON.stringify(node))
48
+ );
49
+ const ledgerEntries = mergeByKey(
50
+ Array.isArray(vericifyState.runLedger.entries) ? vericifyState.runLedger.entries : [],
51
+ Array.isArray(partnerState.runLedger.entries) ? partnerState.runLedger.entries : [],
52
+ (entry) => entry.id ?? hashText(`${entry.timestamp_utc}:${entry.tool}:${entry.message}`)
53
+ );
54
+ const statusEvents = mergeByKey(
55
+ vericifyState.statusEvents,
56
+ partnerState.statusEvents,
57
+ (event) => event.event_id ?? hashText(`${event.timestamp}:${event.source_module}:${event.event_type}:${event.status}`)
58
+ ).sort((left, right) => String(left.timestamp).localeCompare(String(right.timestamp)));
59
+ const processPosts = mergeByKey(
60
+ Array.isArray(vericifyState.processPosts.posts) ? vericifyState.processPosts.posts : [],
61
+ Array.isArray(partnerState.processPosts.posts) ? partnerState.processPosts.posts : [],
62
+ (post) => post.process_post_id ?? hashText(`${post.timestamp}:${post.agent_id}:${post.summary}`)
63
+ ).sort((left, right) => String(left.timestamp).localeCompare(String(right.timestamp)));
64
+
65
+ return {
66
+ workspaceRoot,
67
+ handoffs,
68
+ todoNodes,
69
+ todoOrder: mergeOrderedIds(
70
+ Array.isArray(vericifyState.todoState.order) ? vericifyState.todoState.order : [],
71
+ Array.isArray(partnerState.todoState.order) ? partnerState.todoState.order : []
72
+ ),
73
+ ledgerEntries,
74
+ statusEvents,
75
+ processPosts,
76
+ adapterAttachments: Array.isArray(adapterAttachmentState.attachments) ? adapterAttachmentState.attachments : [],
77
+ sourceRefs: {
78
+ adapterAttachments: [paths.vericify.adapterAttachments],
79
+ handoffRegistry: [paths.vericify.handoffRegistry, paths.partner.handoffRegistry],
80
+ todoState: [paths.vericify.todoState, paths.partner.todoState],
81
+ runLedger: [paths.vericify.runLedger, paths.partner.runLedger],
82
+ statusEvents: [paths.vericify.statusEvents, paths.partner.statusEvents],
83
+ processPosts: [paths.vericify.processPosts, paths.partner.processPosts],
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,126 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { readAdapterAttachments, upsertAdapterAttachment } from "../store/adapter-attachments.js";
4
+
5
+ const ADAPTER_DEFINITIONS = [
6
+ {
7
+ adapter_id: "local-state",
8
+ label: "Local State",
9
+ category: "native",
10
+ description: "Reads Vericify-native .vericify/* storage plus partner agent-state/* when present.",
11
+ detection_paths: [".vericify", "agent-state"],
12
+ capture_modes: ["filesystem"],
13
+ notes: ["Always available as the baseline local-first adapter."],
14
+ },
15
+ {
16
+ adapter_id: "ace",
17
+ label: "ACE / ace-swarm",
18
+ category: "partner",
19
+ description: "Partner adapter for ace-swarm workspaces that already emit agent-state/* artifacts.",
20
+ detection_paths: ["agent-state", ".agents", "AGENTS.md"],
21
+ capture_modes: ["filesystem", "attachment"],
22
+ notes: ["Out-of-box partner path when an ace-swarm style workspace is present."],
23
+ },
24
+ {
25
+ adapter_id: "codex",
26
+ label: "Codex",
27
+ category: "peer",
28
+ description: "Peer adapter contract for Codex sessions and workspace-attached process traces.",
29
+ detection_paths: [".codex", "AGENTS.md"],
30
+ capture_modes: ["attachment", "filesystem"],
31
+ notes: ["Auto-detection is heuristic; explicit attachment is the reliable path."],
32
+ },
33
+ {
34
+ adapter_id: "claude-code",
35
+ label: "Claude Code",
36
+ category: "peer",
37
+ description: "Peer adapter contract for Claude Code sessions and workspace-attached traces.",
38
+ detection_paths: [".claude", "CLAUDE.md"],
39
+ capture_modes: ["attachment", "filesystem"],
40
+ notes: ["Use adapter attachment when workspace hints are absent."],
41
+ },
42
+ {
43
+ adapter_id: "cursor",
44
+ label: "Cursor",
45
+ category: "peer",
46
+ description: "Peer adapter contract for Cursor-driven agent sessions.",
47
+ detection_paths: [".cursor", ".cursor/rules", ".cursorrules"],
48
+ capture_modes: ["attachment", "filesystem"],
49
+ notes: ["Workspace rules are treated as detection hints, not proof of live capture."],
50
+ },
51
+ {
52
+ adapter_id: "vscode-copilot-chat",
53
+ label: "VS Code Copilot Chat",
54
+ category: "peer",
55
+ description: "Peer adapter contract for VS Code Copilot Chat sessions and workspace traces.",
56
+ detection_paths: [".vscode", ".github/copilot-instructions.md"],
57
+ capture_modes: ["attachment", "filesystem"],
58
+ notes: ["Detection relies on editor/workspace hints; attach explicitly for certainty."],
59
+ },
60
+ {
61
+ adapter_id: "antigravity",
62
+ label: "Antigravity",
63
+ category: "peer",
64
+ description: "Peer adapter contract for Antigravity sessions and workspace traces.",
65
+ detection_paths: [".antigravity", "antigravity.config.json"],
66
+ capture_modes: ["attachment", "filesystem"],
67
+ notes: ["Manual attachment is expected until a dedicated capture bridge ships."],
68
+ },
69
+ ];
70
+
71
+ function detectPaths(workspaceRoot, relPaths) {
72
+ return relPaths
73
+ .map((relPath) => resolve(workspaceRoot, relPath))
74
+ .filter((path) => existsSync(path));
75
+ }
76
+
77
+ function profileStatus(definition, attached, detectedPaths) {
78
+ if (definition.adapter_id === "local-state") return "ready";
79
+ if (attached) return "attached";
80
+ if (detectedPaths.length) return "detected";
81
+ return "manual";
82
+ }
83
+
84
+ export function listAdapterDefinitions() {
85
+ return ADAPTER_DEFINITIONS.map((definition) => ({ ...definition }));
86
+ }
87
+
88
+ export function readWorkspaceAdapterAttachments(workspaceRoot) {
89
+ return readAdapterAttachments(workspaceRoot);
90
+ }
91
+
92
+ export function attachWorkspaceAdapter(workspaceRoot, input) {
93
+ return upsertAdapterAttachment(workspaceRoot, input);
94
+ }
95
+
96
+ export function detectWorkspaceAdapters(workspaceRoot, attachments = undefined) {
97
+ const attachmentList = attachments ?? readAdapterAttachments(workspaceRoot).attachments ?? [];
98
+ const byId = new Map(attachmentList.map((attachment) => [attachment.adapter_id, attachment]));
99
+ return ADAPTER_DEFINITIONS.map((definition) => {
100
+ const attached = byId.get(definition.adapter_id);
101
+ const detectedPaths = detectPaths(workspaceRoot, definition.detection_paths);
102
+ const status = profileStatus(definition, attached, detectedPaths);
103
+ return {
104
+ adapter_id: definition.adapter_id,
105
+ label: definition.label,
106
+ category: definition.category,
107
+ description: definition.description,
108
+ capture_modes: [...definition.capture_modes],
109
+ detection_status: status,
110
+ attached: Boolean(attached),
111
+ ready: status === "ready" || status === "attached" || status === "detected",
112
+ detection_paths: definition.detection_paths.map((relPath) => resolve(workspaceRoot, relPath)),
113
+ detected_paths: detectedPaths,
114
+ session_id: attached?.session_id,
115
+ source_refs: detectedPaths,
116
+ notes: [
117
+ ...definition.notes,
118
+ ...(attached?.notes ? [String(attached.notes)] : []),
119
+ ],
120
+ attached_at: attached?.attached_at,
121
+ updated_at: attached?.updated_at,
122
+ capture_mode: attached?.capture_mode,
123
+ label_override: attached?.label,
124
+ };
125
+ });
126
+ }