vectorforce 0.0.0-stage → 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/.agents/mcp_config.json +17 -0
- package/.codex/config.toml +8 -0
- package/AGENTS.md +43 -0
- package/LICENSE +21 -0
- package/README.md +173 -2
- package/bin/vf.js +756 -0
- package/codex.json +14 -0
- package/commandcode.json +11 -0
- package/musecode.json +18 -0
- package/package.json +39 -4
- package/src/core/errors.js +45 -0
- package/src/core/hash.js +56 -0
- package/src/core/types.js +194 -0
- package/src/index.js +17 -0
- package/src/mcp/harness-config.js +498 -0
- package/src/mcp/mcp-server.js +507 -0
- package/src/operations/briefing-service.js +313 -0
- package/src/operations/checkpoint-service.js +120 -0
- package/src/operations/handoff-service.js +343 -0
- package/src/operations/project-service.js +132 -0
- package/src/operations/reasoning-service.js +356 -0
- package/src/policy/authorizer.js +147 -0
- package/src/providers/code-index-provider.js +285 -0
- package/src/providers/document-provider.js +105 -0
- package/src/providers/git-provider.js +98 -0
- package/src/providers/llm-provider.js +410 -0
- package/src/storage/sqlite-store.js +387 -0
- package/src/studio/server.js +456 -0
- package/studio/dist/assets/index-DTKNBk3V.css +2 -0
- package/studio/dist/assets/index-X-wHcJ6r.js +9 -0
- package/studio/dist/favicon.svg +1 -0
- package/studio/dist/icons.svg +24 -0
- package/studio/dist/index.html +17 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { ErrorCode, VectorForceError } from '../core/errors.js';
|
|
5
|
+
import { validateCheckpoint, validateReceipt, SCHEMA_VERSION_RECEIPT } from '../core/types.js';
|
|
6
|
+
import { generateUuid } from '../core/hash.js';
|
|
7
|
+
|
|
8
|
+
export class SqliteStore {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} [dbPath=':memory:'] Path to SQLite database file or ':memory:'
|
|
11
|
+
*/
|
|
12
|
+
constructor(dbPath = ':memory:') {
|
|
13
|
+
this.dbPath = dbPath;
|
|
14
|
+
if (dbPath !== ':memory:') {
|
|
15
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
this.db = new DatabaseSync(dbPath);
|
|
18
|
+
this._initSchema();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_initSchema() {
|
|
22
|
+
this.db.exec(`
|
|
23
|
+
PRAGMA journal_mode = WAL;
|
|
24
|
+
PRAGMA foreign_keys = ON;
|
|
25
|
+
PRAGMA synchronous = NORMAL;
|
|
26
|
+
PRAGMA user_version = 1;
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
29
|
+
project_id TEXT PRIMARY KEY,
|
|
30
|
+
canonical_path TEXT NOT NULL UNIQUE,
|
|
31
|
+
name TEXT NOT NULL,
|
|
32
|
+
created_at TEXT NOT NULL
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
36
|
+
task_id TEXT NOT NULL,
|
|
37
|
+
project_id TEXT NOT NULL,
|
|
38
|
+
title TEXT NOT NULL,
|
|
39
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'abandoned')),
|
|
40
|
+
created_at TEXT NOT NULL,
|
|
41
|
+
updated_at TEXT NOT NULL,
|
|
42
|
+
PRIMARY KEY (project_id, task_id),
|
|
43
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
47
|
+
checkpoint_id TEXT PRIMARY KEY,
|
|
48
|
+
project_id TEXT NOT NULL,
|
|
49
|
+
task_id TEXT NOT NULL,
|
|
50
|
+
parent_id TEXT,
|
|
51
|
+
timestamp TEXT NOT NULL,
|
|
52
|
+
author_json TEXT NOT NULL,
|
|
53
|
+
summary TEXT NOT NULL,
|
|
54
|
+
claims_json TEXT NOT NULL,
|
|
55
|
+
evidence_json TEXT NOT NULL,
|
|
56
|
+
metadata_json TEXT NOT NULL,
|
|
57
|
+
FOREIGN KEY (project_id, task_id) REFERENCES tasks(project_id, task_id) ON DELETE CASCADE,
|
|
58
|
+
FOREIGN KEY (parent_id) REFERENCES checkpoints(checkpoint_id)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_task ON checkpoints(project_id, task_id, timestamp);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS operations (
|
|
64
|
+
operation_id TEXT PRIMARY KEY,
|
|
65
|
+
project_id TEXT NOT NULL,
|
|
66
|
+
task_id TEXT NOT NULL,
|
|
67
|
+
request_hash TEXT NOT NULL,
|
|
68
|
+
operation_type TEXT NOT NULL,
|
|
69
|
+
status TEXT NOT NULL,
|
|
70
|
+
reference_id TEXT,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
warnings_json TEXT NOT NULL,
|
|
73
|
+
errors_json TEXT NOT NULL,
|
|
74
|
+
UNIQUE(project_id, task_id, request_hash)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE TABLE IF NOT EXISTS document_policies (
|
|
78
|
+
project_id TEXT PRIMARY KEY,
|
|
79
|
+
allowed_paths_json TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL,
|
|
81
|
+
FOREIGN KEY (project_id) REFERENCES projects(project_id) ON DELETE CASCADE
|
|
82
|
+
);
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
close() {
|
|
87
|
+
this.db.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Project & Task Operations ---
|
|
91
|
+
|
|
92
|
+
registerProject(projectId, canonicalPath, name) {
|
|
93
|
+
const existing = this.db.prepare('SELECT * FROM projects WHERE canonical_path = ?').get(canonicalPath);
|
|
94
|
+
if (existing) {
|
|
95
|
+
return existing;
|
|
96
|
+
}
|
|
97
|
+
const now = new Date().toISOString();
|
|
98
|
+
this.db.prepare(
|
|
99
|
+
'INSERT INTO projects (project_id, canonical_path, name, created_at) VALUES (?, ?, ?, ?)'
|
|
100
|
+
).run(projectId, canonicalPath, name, now);
|
|
101
|
+
return { project_id: projectId, canonical_path: canonicalPath, name, created_at: now };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getProjectByPath(canonicalPath) {
|
|
105
|
+
return this.db.prepare('SELECT * FROM projects WHERE canonical_path = ?').get(canonicalPath) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getProjectById(projectId) {
|
|
109
|
+
return this.db.prepare('SELECT * FROM projects WHERE project_id = ?').get(projectId) || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
createTask(projectId, taskId, title) {
|
|
113
|
+
const project = this.getProjectById(projectId);
|
|
114
|
+
if (!project) {
|
|
115
|
+
throw new VectorForceError(ErrorCode.PROJECT_NOT_INITIALIZED, `Project ${projectId} does not exist`);
|
|
116
|
+
}
|
|
117
|
+
const existing = this.db.prepare(
|
|
118
|
+
'SELECT * FROM tasks WHERE project_id = ? AND task_id = ?'
|
|
119
|
+
).get(projectId, taskId);
|
|
120
|
+
if (existing) {
|
|
121
|
+
return existing;
|
|
122
|
+
}
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
this.db.prepare(
|
|
125
|
+
'INSERT INTO tasks (task_id, project_id, title, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
126
|
+
).run(taskId, projectId, title, 'active', now, now);
|
|
127
|
+
return { task_id: taskId, project_id: projectId, title, status: 'active', created_at: now, updated_at: now };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getTask(projectId, taskId) {
|
|
131
|
+
return this.db.prepare(
|
|
132
|
+
'SELECT * FROM tasks WHERE project_id = ? AND task_id = ?'
|
|
133
|
+
).get(projectId, taskId) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
listTasks(projectId, status = null) {
|
|
137
|
+
if (status && status !== 'all') {
|
|
138
|
+
return this.db.prepare(
|
|
139
|
+
'SELECT * FROM tasks WHERE project_id = ? AND status = ? ORDER BY updated_at DESC'
|
|
140
|
+
).all(projectId, status);
|
|
141
|
+
}
|
|
142
|
+
return this.db.prepare(
|
|
143
|
+
'SELECT * FROM tasks WHERE project_id = ? ORDER BY updated_at DESC'
|
|
144
|
+
).all(projectId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
updateTaskStatus(projectId, taskId, status) {
|
|
148
|
+
const validStatuses = ['active', 'completed', 'abandoned'];
|
|
149
|
+
if (!validStatuses.includes(status)) {
|
|
150
|
+
throw new VectorForceError(ErrorCode.TASK_NOT_FOUND, `Invalid task status: ${status}. Must be one of: ${validStatuses.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
const result = this.db.prepare(
|
|
154
|
+
'UPDATE tasks SET status = ?, updated_at = ? WHERE project_id = ? AND task_id = ?'
|
|
155
|
+
).run(status, now, projectId, taskId);
|
|
156
|
+
if (result.changes === 0) {
|
|
157
|
+
throw new VectorForceError(ErrorCode.TASK_NOT_FOUND, `Task ${taskId} not found in project ${projectId}`);
|
|
158
|
+
}
|
|
159
|
+
return this.getTask(projectId, taskId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- Policy Operations ---
|
|
163
|
+
|
|
164
|
+
setDocumentPolicy(projectId, allowedPaths) {
|
|
165
|
+
const now = new Date().toISOString();
|
|
166
|
+
const sortedUnique = Array.from(new Set(allowedPaths)).sort();
|
|
167
|
+
this.db.prepare(`
|
|
168
|
+
INSERT INTO document_policies (project_id, allowed_paths_json, updated_at)
|
|
169
|
+
VALUES (?, ?, ?)
|
|
170
|
+
ON CONFLICT(project_id) DO UPDATE SET
|
|
171
|
+
allowed_paths_json = excluded.allowed_paths_json,
|
|
172
|
+
updated_at = excluded.updated_at
|
|
173
|
+
`).run(projectId, JSON.stringify(sortedUnique), now);
|
|
174
|
+
return sortedUnique;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getDocumentPolicy(projectId) {
|
|
178
|
+
const row = this.db.prepare('SELECT allowed_paths_json FROM document_policies WHERE project_id = ?').get(projectId);
|
|
179
|
+
return row ? JSON.parse(row.allowed_paths_json) : [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Checkpoint Operations ---
|
|
183
|
+
|
|
184
|
+
getCheckpoint(checkpointId) {
|
|
185
|
+
const row = this.db.prepare('SELECT * FROM checkpoints WHERE checkpoint_id = ?').get(checkpointId);
|
|
186
|
+
if (!row) return null;
|
|
187
|
+
return {
|
|
188
|
+
schema_version: 'vectorforce/checkpoint/v1',
|
|
189
|
+
checkpoint_id: row.checkpoint_id,
|
|
190
|
+
project_id: row.project_id,
|
|
191
|
+
task_id: row.task_id,
|
|
192
|
+
parent_id: row.parent_id,
|
|
193
|
+
timestamp: row.timestamp,
|
|
194
|
+
author: JSON.parse(row.author_json),
|
|
195
|
+
summary: row.summary,
|
|
196
|
+
claims: JSON.parse(row.claims_json),
|
|
197
|
+
evidence: JSON.parse(row.evidence_json),
|
|
198
|
+
metadata: JSON.parse(row.metadata_json)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Retrieve checkpoints for a project, optionally filtered by task, since timestamp, or limit.
|
|
204
|
+
* @param {string} projectId
|
|
205
|
+
* @param {object} [options]
|
|
206
|
+
* @param {string} [options.taskId]
|
|
207
|
+
* @param {string} [options.since]
|
|
208
|
+
* @param {number} [options.limit]
|
|
209
|
+
* @returns {Array<object>}
|
|
210
|
+
*/
|
|
211
|
+
getCheckpoints(projectId, options = {}) {
|
|
212
|
+
let sql = 'SELECT * FROM checkpoints WHERE project_id = ?';
|
|
213
|
+
const params = [projectId];
|
|
214
|
+
if (options.taskId) {
|
|
215
|
+
sql += ' AND task_id = ?';
|
|
216
|
+
params.push(options.taskId);
|
|
217
|
+
}
|
|
218
|
+
if (options.since) {
|
|
219
|
+
sql += ' AND timestamp >= ?';
|
|
220
|
+
params.push(options.since);
|
|
221
|
+
}
|
|
222
|
+
sql += ' ORDER BY timestamp ASC';
|
|
223
|
+
if (options.limit) {
|
|
224
|
+
sql += ' LIMIT ?';
|
|
225
|
+
params.push(options.limit);
|
|
226
|
+
}
|
|
227
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
228
|
+
return rows.map(row => ({
|
|
229
|
+
schema_version: 'vectorforce/checkpoint/v1',
|
|
230
|
+
checkpoint_id: row.checkpoint_id,
|
|
231
|
+
project_id: row.project_id,
|
|
232
|
+
task_id: row.task_id,
|
|
233
|
+
parent_id: row.parent_id,
|
|
234
|
+
timestamp: row.timestamp,
|
|
235
|
+
author: JSON.parse(row.author_json),
|
|
236
|
+
summary: row.summary,
|
|
237
|
+
claims: JSON.parse(row.claims_json),
|
|
238
|
+
evidence: JSON.parse(row.evidence_json),
|
|
239
|
+
metadata: JSON.parse(row.metadata_json)
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Find leaf checkpoints (checkpoints that are not parents of any other checkpoint in the task).
|
|
245
|
+
* @param {string} projectId
|
|
246
|
+
* @param {string} taskId
|
|
247
|
+
* @returns {Array<object>}
|
|
248
|
+
*/
|
|
249
|
+
getLeafCheckpoints(projectId, taskId) {
|
|
250
|
+
const rows = this.db.prepare(`
|
|
251
|
+
SELECT c.* FROM checkpoints c
|
|
252
|
+
WHERE c.project_id = ? AND c.task_id = ?
|
|
253
|
+
AND NOT EXISTS (
|
|
254
|
+
SELECT 1 FROM checkpoints child
|
|
255
|
+
WHERE child.parent_id = c.checkpoint_id
|
|
256
|
+
)
|
|
257
|
+
ORDER BY c.timestamp DESC
|
|
258
|
+
`).all(projectId, taskId);
|
|
259
|
+
|
|
260
|
+
return rows.map(row => ({
|
|
261
|
+
schema_version: 'vectorforce/checkpoint/v1',
|
|
262
|
+
checkpoint_id: row.checkpoint_id,
|
|
263
|
+
project_id: row.project_id,
|
|
264
|
+
task_id: row.task_id,
|
|
265
|
+
parent_id: row.parent_id,
|
|
266
|
+
timestamp: row.timestamp,
|
|
267
|
+
author: JSON.parse(row.author_json),
|
|
268
|
+
summary: row.summary,
|
|
269
|
+
claims: JSON.parse(row.claims_json),
|
|
270
|
+
evidence: JSON.parse(row.evidence_json),
|
|
271
|
+
metadata: JSON.parse(row.metadata_json)
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Record a checkpoint atomically with an operation receipt.
|
|
277
|
+
* If a matching request_hash exists for (project_id, task_id), returns idempotent replay receipt.
|
|
278
|
+
*/
|
|
279
|
+
saveCheckpointAtomic(checkpoint, requestHash, operationId = generateUuid(), operationType = 'checkpoint') {
|
|
280
|
+
validateCheckpoint(checkpoint);
|
|
281
|
+
|
|
282
|
+
// Check for idempotent replay
|
|
283
|
+
const existingOp = this.db.prepare(
|
|
284
|
+
'SELECT * FROM operations WHERE project_id = ? AND task_id = ? AND request_hash = ?'
|
|
285
|
+
).get(checkpoint.project_id, checkpoint.task_id, requestHash);
|
|
286
|
+
|
|
287
|
+
if (existingOp) {
|
|
288
|
+
return {
|
|
289
|
+
schema_version: SCHEMA_VERSION_RECEIPT,
|
|
290
|
+
operation_id: existingOp.operation_id,
|
|
291
|
+
operation_type: existingOp.operation_type,
|
|
292
|
+
status: 'idempotent_replay',
|
|
293
|
+
reference_id: existingOp.reference_id,
|
|
294
|
+
timestamp: existingOp.created_at,
|
|
295
|
+
request_hash: existingOp.request_hash,
|
|
296
|
+
warnings: JSON.parse(existingOp.warnings_json),
|
|
297
|
+
errors: JSON.parse(existingOp.errors_json),
|
|
298
|
+
briefing_path: null
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate parent checkpoint
|
|
303
|
+
if (checkpoint.parent_id !== null) {
|
|
304
|
+
const parent = this.db.prepare('SELECT * FROM checkpoints WHERE checkpoint_id = ?').get(checkpoint.parent_id);
|
|
305
|
+
if (!parent) {
|
|
306
|
+
throw new VectorForceError(
|
|
307
|
+
ErrorCode.PARENT_CHECKPOINT_NOT_FOUND,
|
|
308
|
+
`Parent checkpoint ${checkpoint.parent_id} does not exist`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (parent.project_id !== checkpoint.project_id || parent.task_id !== checkpoint.task_id) {
|
|
312
|
+
throw new VectorForceError(
|
|
313
|
+
ErrorCode.CROSS_TASK_PARENT_FORBIDDEN,
|
|
314
|
+
`Parent checkpoint belongs to task ${parent.task_id}, not ${checkpoint.task_id}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Atomic transaction
|
|
320
|
+
const now = new Date().toISOString();
|
|
321
|
+
this.db.exec('BEGIN IMMEDIATE;');
|
|
322
|
+
try {
|
|
323
|
+
this.db.prepare(`
|
|
324
|
+
INSERT INTO checkpoints (
|
|
325
|
+
checkpoint_id, project_id, task_id, parent_id, timestamp,
|
|
326
|
+
author_json, summary, claims_json, evidence_json, metadata_json
|
|
327
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
328
|
+
`).run(
|
|
329
|
+
checkpoint.checkpoint_id,
|
|
330
|
+
checkpoint.project_id,
|
|
331
|
+
checkpoint.task_id,
|
|
332
|
+
checkpoint.parent_id,
|
|
333
|
+
checkpoint.timestamp,
|
|
334
|
+
JSON.stringify(checkpoint.author),
|
|
335
|
+
checkpoint.summary,
|
|
336
|
+
JSON.stringify(checkpoint.claims),
|
|
337
|
+
JSON.stringify(checkpoint.evidence),
|
|
338
|
+
JSON.stringify(checkpoint.metadata || {})
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Update task updated_at
|
|
342
|
+
this.db.prepare(`
|
|
343
|
+
UPDATE tasks SET updated_at = ? WHERE project_id = ? AND task_id = ?
|
|
344
|
+
`).run(now, checkpoint.project_id, checkpoint.task_id);
|
|
345
|
+
|
|
346
|
+
// Record operation
|
|
347
|
+
this.db.prepare(`
|
|
348
|
+
INSERT INTO operations (
|
|
349
|
+
operation_id, project_id, task_id, request_hash, operation_type,
|
|
350
|
+
status, reference_id, created_at, warnings_json, errors_json
|
|
351
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
352
|
+
`).run(
|
|
353
|
+
operationId,
|
|
354
|
+
checkpoint.project_id,
|
|
355
|
+
checkpoint.task_id,
|
|
356
|
+
requestHash,
|
|
357
|
+
operationType,
|
|
358
|
+
'committed',
|
|
359
|
+
checkpoint.checkpoint_id,
|
|
360
|
+
now,
|
|
361
|
+
JSON.stringify([]),
|
|
362
|
+
JSON.stringify([])
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
this.db.exec('COMMIT;');
|
|
366
|
+
} catch (err) {
|
|
367
|
+
this.db.exec('ROLLBACK;');
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const receipt = {
|
|
372
|
+
schema_version: SCHEMA_VERSION_RECEIPT,
|
|
373
|
+
operation_id: operationId,
|
|
374
|
+
operation_type: operationType,
|
|
375
|
+
status: 'committed',
|
|
376
|
+
reference_id: checkpoint.checkpoint_id,
|
|
377
|
+
timestamp: now,
|
|
378
|
+
request_hash: requestHash,
|
|
379
|
+
warnings: [],
|
|
380
|
+
errors: [],
|
|
381
|
+
briefing_path: null
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
validateReceipt(receipt);
|
|
385
|
+
return receipt;
|
|
386
|
+
}
|
|
387
|
+
}
|