vectorforce 0.0.0-stage → 0.2.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.
Files changed (37) hide show
  1. package/.agents/agents/AGENTS.md +12 -0
  2. package/.agents/agents/vectorforce-architect.md +35 -0
  3. package/.agents/agents/vectorforce-auditor.md +25 -0
  4. package/.agents/agents/vectorforce-samples-builder.md +30 -0
  5. package/.agents/mcp_config.json +11 -0
  6. package/.codex/config.toml +5 -0
  7. package/AGENTS.md +115 -0
  8. package/LICENSE +21 -0
  9. package/README.md +198 -2
  10. package/bin/vf.js +1159 -0
  11. package/codex.json +14 -0
  12. package/commandcode.json +11 -0
  13. package/musecode.json +14 -0
  14. package/package.json +40 -4
  15. package/src/core/errors.js +45 -0
  16. package/src/core/hash.js +56 -0
  17. package/src/core/types.js +194 -0
  18. package/src/index.js +17 -0
  19. package/src/mcp/harness-config.js +546 -0
  20. package/src/mcp/mcp-server.js +507 -0
  21. package/src/operations/briefing-service.js +313 -0
  22. package/src/operations/checkpoint-service.js +120 -0
  23. package/src/operations/handoff-service.js +343 -0
  24. package/src/operations/project-service.js +132 -0
  25. package/src/operations/reasoning-service.js +356 -0
  26. package/src/policy/authorizer.js +147 -0
  27. package/src/providers/code-index-provider.js +285 -0
  28. package/src/providers/document-provider.js +105 -0
  29. package/src/providers/git-provider.js +98 -0
  30. package/src/providers/llm-provider.js +425 -0
  31. package/src/storage/sqlite-store.js +387 -0
  32. package/src/studio/server.js +764 -0
  33. package/studio/dist/assets/index-C0_eVpV1.js +9 -0
  34. package/studio/dist/assets/index-CxTK-2ty.css +2 -0
  35. package/studio/dist/favicon.svg +1 -0
  36. package/studio/dist/icons.svg +24 -0
  37. package/studio/dist/index.html +17 -0
@@ -0,0 +1,343 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs';
3
+ import { join, resolve, dirname } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { sha256 } from '../core/hash.js';
6
+ import { ErrorCode, VectorForceError } from '../core/errors.js';
7
+ import { Authorizer } from '../policy/authorizer.js';
8
+ import {
9
+ SCHEMA_VERSION_HANDOFF,
10
+ SCHEMA_VERSION_MANIFEST,
11
+ SCHEMA_VERSION_RECEIPT,
12
+ validateHandoff,
13
+ validateManifest
14
+ } from '../core/types.js';
15
+
16
+ export class HandoffService {
17
+ /**
18
+ * @param {import('../storage/sqlite-store.js').SqliteStore} store
19
+ * @param {import('./project-service.js').ProjectService} projectService
20
+ * @param {import('./checkpoint-service.js').CheckpointService} [checkpointService]
21
+ */
22
+ constructor(store, projectService, checkpointService = null) {
23
+ this.store = store;
24
+ this.projectService = projectService;
25
+ this.checkpointService = checkpointService;
26
+ }
27
+
28
+ /**
29
+ * Export a selective, boundary-safe handoff package.
30
+ * @param {object} params
31
+ * @param {string} params.workspacePath
32
+ * @param {string} params.taskId
33
+ * @param {string} [params.checkpointId]
34
+ * @param {string} params.recipient Target recipient (developer name or team)
35
+ * @param {string} [params.destinationHarness='any'] Destination harness
36
+ * @param {boolean} [params.preview=false] If true, returns preview without writing
37
+ * @param {string} [params.outPath] Optional custom destination path
38
+ * @returns {{ preview?: boolean, receipt?: object, package_id: string, handoff: object, manifest: object, bundlePath?: string }}
39
+ */
40
+ exportHandoff(params) {
41
+ if (!params.recipient || typeof params.recipient !== 'string' || !params.recipient.trim()) {
42
+ throw new VectorForceError(ErrorCode.HANDOFF_RECIPIENT_REQUIRED, 'Target recipient is required for handoff export');
43
+ }
44
+
45
+ const destination = params.destinationHarness || 'any';
46
+ if (destination !== 'any') {
47
+ Authorizer.authorizeDestination(destination);
48
+ }
49
+
50
+ const { task, project } = this.projectService.resolveTask(params.workspacePath, params.taskId);
51
+
52
+ // Resolve checkpoint to export
53
+ let checkpoint;
54
+ if (params.checkpointId) {
55
+ checkpoint = this.store.getCheckpoint(params.checkpointId);
56
+ if (!checkpoint || checkpoint.project_id !== project.project_id || checkpoint.task_id !== task.task_id) {
57
+ throw new VectorForceError(
58
+ ErrorCode.SCHEMA_VALIDATION_FAILED,
59
+ `Checkpoint '${params.checkpointId}' not found for task '${task.task_id}'`
60
+ );
61
+ }
62
+ } else {
63
+ const leaves = this.store.getLeafCheckpoints(project.project_id, task.task_id);
64
+ if (leaves.length === 0) {
65
+ throw new VectorForceError(
66
+ ErrorCode.TASK_NOT_FOUND,
67
+ `Task '${task.task_id}' has no checkpoints recorded yet.`
68
+ );
69
+ }
70
+ if (leaves.length > 1) {
71
+ throw new VectorForceError(
72
+ ErrorCode.DIVERGENT_HEADS,
73
+ `Task has divergent heads. Explicit checkpoint ID required.`
74
+ );
75
+ }
76
+ checkpoint = leaves[0];
77
+ }
78
+
79
+ // Boundary Isolation: Strip internal IDs, private ancestor history, and unallowlisted data
80
+ const canonicalClaims = {
81
+ goals: [...checkpoint.claims.goals],
82
+ constraints: [...checkpoint.claims.constraints],
83
+ decisions: checkpoint.claims.decisions.map(d => ({ ...d })),
84
+ next_steps: [...checkpoint.claims.next_steps],
85
+ open_questions: checkpoint.claims.open_questions ? [...checkpoint.claims.open_questions] : []
86
+ };
87
+
88
+ const canonicalEvidence = {
89
+ git: checkpoint.evidence.git ? { ...checkpoint.evidence.git } : null,
90
+ documents: checkpoint.evidence.documents ? checkpoint.evidence.documents.map(d => ({ ...d })) : [],
91
+ gaps: checkpoint.evidence.gaps ? checkpoint.evidence.gaps.map(g => ({ ...g })) : []
92
+ };
93
+
94
+ const packageSeed = `${project.project_id}:${task.task_id}:${checkpoint.checkpoint_id}:${params.recipient.trim()}:${destination}`;
95
+ const packageId = `hand-${sha256(packageSeed).slice(0, 16)}`;
96
+
97
+ const handoff = {
98
+ schema_version: SCHEMA_VERSION_HANDOFF,
99
+ package_id: packageId,
100
+ created_at: new Date().toISOString(),
101
+ origin: {
102
+ project_name: project.name,
103
+ task_id: task.task_id,
104
+ task_title: task.title,
105
+ checkpoint_id: checkpoint.checkpoint_id
106
+ },
107
+ disclosure: {
108
+ recipient: params.recipient.trim(),
109
+ destination_harness: destination
110
+ },
111
+ checkpoint: {
112
+ summary: checkpoint.summary,
113
+ claims: canonicalClaims,
114
+ evidence: canonicalEvidence
115
+ }
116
+ };
117
+
118
+ validateHandoff(handoff);
119
+
120
+ const handoffJsonStr = JSON.stringify(handoff, null, 2);
121
+ const handoffHash = sha256(handoffJsonStr);
122
+ const handoffBytes = Buffer.byteLength(handoffJsonStr, 'utf8');
123
+
124
+ const manifest = {
125
+ schema_version: SCHEMA_VERSION_MANIFEST,
126
+ package_id: packageId,
127
+ created_at: handoff.created_at,
128
+ files: [
129
+ {
130
+ path: 'handoff.json',
131
+ sha256: handoffHash,
132
+ size_bytes: handoffBytes
133
+ }
134
+ ]
135
+ };
136
+
137
+ validateManifest(manifest);
138
+
139
+ if (params.preview) {
140
+ return {
141
+ preview: true,
142
+ package_id: packageId,
143
+ handoff,
144
+ manifest
145
+ };
146
+ }
147
+
148
+ // Write bundle
149
+ let targetPath;
150
+ if (params.outPath) {
151
+ targetPath = resolve(params.outPath);
152
+ // Path traversal check
153
+ if (params.outPath.includes('..')) {
154
+ throw new VectorForceError(ErrorCode.PATH_TRAVERSAL_DETECTED, `Path traversal in outPath: ${params.outPath}`);
155
+ }
156
+ } else {
157
+ const handoffsDir = join(homedir(), '.vectorforce', 'handoffs');
158
+ mkdirSync(handoffsDir, { recursive: true, mode: 0o700 });
159
+ targetPath = join(handoffsDir, `${packageId}.json`);
160
+ }
161
+
162
+ const bundle = {
163
+ schema_version: 'vectorforce/handoff-bundle/v1',
164
+ package_id: packageId,
165
+ created_at: handoff.created_at,
166
+ manifest,
167
+ handoff
168
+ };
169
+
170
+ mkdirSync(dirname(targetPath), { recursive: true });
171
+ writeFileSync(targetPath, JSON.stringify(bundle, null, 2), 'utf8');
172
+
173
+ const receipt = {
174
+ schema_version: SCHEMA_VERSION_RECEIPT,
175
+ operation_id: randomUUID(),
176
+ operation_type: 'export',
177
+ status: 'committed',
178
+ reference_id: packageId,
179
+ timestamp: new Date().toISOString(),
180
+ request_hash: sha256(JSON.stringify({ packageId, targetPath, recipient: params.recipient })),
181
+ warnings: [],
182
+ errors: [],
183
+ briefing_path: targetPath
184
+ };
185
+
186
+ return {
187
+ package_id: packageId,
188
+ bundlePath: targetPath,
189
+ receipt,
190
+ handoff,
191
+ manifest
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Safe import of a handoff package into a workspace with explicit context reconciliation.
197
+ * @param {object} params
198
+ * @param {string} params.workspacePath Current workspace path
199
+ * @param {string} params.bundlePath Path to handoff package JSON or bundle
200
+ * @param {string} [params.targetTaskId] Optional target task ID
201
+ * @returns {{ receipt: object, importedCheckpointId: string, task: object }}
202
+ */
203
+ importHandoff(params) {
204
+ if (!params.bundlePath || typeof params.bundlePath !== 'string') {
205
+ throw new VectorForceError(ErrorCode.HANDOFF_VALIDATION_FAILED, 'bundlePath is required');
206
+ }
207
+
208
+ const rawPath = resolve(params.bundlePath);
209
+ if (!existsSync(rawPath)) {
210
+ throw new VectorForceError(ErrorCode.HANDOFF_VALIDATION_FAILED, `Handoff bundle file not found: ${rawPath}`);
211
+ }
212
+
213
+ const stat = statSync(rawPath);
214
+ if (stat.size > 2 * 1024 * 1024) { // 2 MB safety cap
215
+ throw new VectorForceError(ErrorCode.DOCUMENT_TOO_LARGE, `Handoff bundle exceeds size limit (max 2MB)`);
216
+ }
217
+
218
+ let rawContent;
219
+ try {
220
+ rawContent = readFileSync(rawPath, 'utf8');
221
+ } catch (e) {
222
+ throw new VectorForceError(ErrorCode.HANDOFF_VALIDATION_FAILED, `Failed to read bundle: ${e.message}`);
223
+ }
224
+
225
+ let parsed;
226
+ try {
227
+ parsed = JSON.parse(rawContent);
228
+ } catch {
229
+ throw new VectorForceError(ErrorCode.HANDOFF_VALIDATION_FAILED, 'Invalid JSON in handoff bundle');
230
+ }
231
+
232
+ let handoff;
233
+ let manifest;
234
+
235
+ if (parsed.schema_version === 'vectorforce/handoff-bundle/v1') {
236
+ handoff = parsed.handoff;
237
+ manifest = parsed.manifest;
238
+ } else if (parsed.schema_version === SCHEMA_VERSION_HANDOFF) {
239
+ handoff = parsed;
240
+ } else {
241
+ throw new VectorForceError(ErrorCode.HANDOFF_VALIDATION_FAILED, 'Unrecognized bundle schema version');
242
+ }
243
+
244
+ // Schema validation
245
+ validateHandoff(handoff);
246
+
247
+ if (manifest) {
248
+ validateManifest(manifest);
249
+ // Validate integrity against manifest
250
+ const handoffFileEntry = manifest.files.find(f => f.path === 'handoff.json');
251
+ if (handoffFileEntry) {
252
+ const actualHandoffHash = sha256(JSON.stringify(handoff, null, 2));
253
+ if (handoffFileEntry.sha256 !== actualHandoffHash) {
254
+ throw new VectorForceError(
255
+ ErrorCode.HANDOFF_INTEGRITY_FAILED,
256
+ `Handoff integrity verification failed: hash mismatch against manifest`
257
+ );
258
+ }
259
+ }
260
+ }
261
+
262
+ // Resolve or initialize destination project
263
+ const project = this.projectService.getProjectForWorkspace(params.workspacePath);
264
+
265
+ // Determine target task
266
+ const targetTaskId = params.targetTaskId || handoff.origin.task_id;
267
+ let task = this.store.getTask(project.project_id, targetTaskId);
268
+ if (!task) {
269
+ task = this.store.createTask(
270
+ project.project_id,
271
+ targetTaskId,
272
+ `${handoff.origin.task_title || targetTaskId} [Imported]`
273
+ );
274
+ }
275
+
276
+ // Context Reconciliation (ADR-006):
277
+ // Check for existing checkpoints for this task
278
+ const existingHeads = this.store.getLeafCheckpoints(project.project_id, task.task_id);
279
+ let parentId = null;
280
+ let summaryPrefix = `[Handoff from ${handoff.origin.project_name}]`;
281
+
282
+ if (existingHeads.length > 0) {
283
+ parentId = existingHeads[0].checkpoint_id;
284
+ summaryPrefix = `[Reconciled Handoff from ${handoff.origin.project_name}]`;
285
+ }
286
+
287
+ // Attribution prefix on imported decisions
288
+ const attributedDecisions = handoff.checkpoint.claims.decisions.map(d => ({
289
+ id: d.id,
290
+ title: d.title,
291
+ rationale: `${d.rationale} (Imported from ${handoff.origin.project_name}:${handoff.origin.checkpoint_id.slice(0, 8)})`
292
+ }));
293
+
294
+ const checkpointRecord = {
295
+ schema_version: 'vectorforce/checkpoint/v1',
296
+ checkpoint_id: randomUUID(),
297
+ project_id: project.project_id,
298
+ task_id: task.task_id,
299
+ parent_id: parentId,
300
+ timestamp: new Date().toISOString(),
301
+ author: {
302
+ name: process.env.USER || 'developer',
303
+ harness: 'import'
304
+ },
305
+ summary: `${summaryPrefix} ${handoff.checkpoint.summary}`,
306
+ claims: {
307
+ goals: [...handoff.checkpoint.claims.goals],
308
+ constraints: [...handoff.checkpoint.claims.constraints],
309
+ decisions: attributedDecisions,
310
+ next_steps: [...handoff.checkpoint.claims.next_steps],
311
+ open_questions: handoff.checkpoint.claims.open_questions || []
312
+ },
313
+ evidence: {
314
+ git: handoff.checkpoint.evidence.git || null,
315
+ documents: handoff.checkpoint.evidence.documents || [],
316
+ gaps: [
317
+ ...(handoff.checkpoint.evidence.gaps || []),
318
+ {
319
+ type: 'import',
320
+ target: handoff.package_id,
321
+ reason: `Imported from ${handoff.origin.project_name}`
322
+ }
323
+ ]
324
+ },
325
+ metadata: {
326
+ imported_from_package: handoff.package_id,
327
+ imported_origin_project: handoff.origin.project_name,
328
+ imported_origin_task: handoff.origin.task_id
329
+ }
330
+ };
331
+
332
+ const operationId = randomUUID();
333
+ const requestHash = sha256(JSON.stringify({ rawPath, targetTaskId, parentId }));
334
+
335
+ const receipt = this.store.saveCheckpointAtomic(checkpointRecord, requestHash, operationId, 'import');
336
+
337
+ return {
338
+ receipt,
339
+ importedCheckpointId: receipt.reference_id,
340
+ task
341
+ };
342
+ }
343
+ }
@@ -0,0 +1,132 @@
1
+ import { Authorizer } from '../policy/authorizer.js';
2
+ import { sha256 } from '../core/hash.js';
3
+ import { basename, dirname } from 'node:path';
4
+ import { ErrorCode, VectorForceError } from '../core/errors.js';
5
+
6
+ export class ProjectService {
7
+ /**
8
+ * @param {import('../storage/sqlite-store.js').SqliteStore} store
9
+ */
10
+ constructor(store) {
11
+ this.store = store;
12
+ }
13
+
14
+ /**
15
+ * Initialize or retrieve a project for a workspace directory.
16
+ * @param {string} workspacePath
17
+ * @param {string} [customName]
18
+ * @returns {{ project_id: string, canonical_path: string, name: string, created_at: string }}
19
+ */
20
+ initProject(workspacePath, customName = null) {
21
+ const canonical = Authorizer.canonicalizeWorkspace(workspacePath);
22
+ const existing = this.store.getProjectByPath(canonical);
23
+ if (existing) {
24
+ return existing;
25
+ }
26
+ const idHash = sha256(canonical).slice(0, 16);
27
+ const projectId = `proj-${idHash}`;
28
+ const name = customName || basename(canonical);
29
+ return this.store.registerProject(projectId, canonical, name);
30
+ }
31
+
32
+ /**
33
+ * Get project for a workspace path, walking up to root if in a subdirectory.
34
+ * @param {string} workspacePath
35
+ */
36
+ getProjectForWorkspace(workspacePath) {
37
+ if (workspacePath) {
38
+ let canonical = Authorizer.canonicalizeWorkspace(workspacePath);
39
+ while (true) {
40
+ const project = this.store.getProjectByPath(canonical);
41
+ if (project) {
42
+ return project;
43
+ }
44
+ const parent = dirname(canonical);
45
+ if (parent === canonical) break;
46
+ canonical = parent;
47
+ }
48
+ }
49
+
50
+ // Resilience fallback: if workspacePath is root or doesn't match, check if there's an initialized project in the database
51
+ const allProjects = this.store.db.prepare('SELECT * FROM projects').all();
52
+ if (allProjects.length === 1) {
53
+ return allProjects[0];
54
+ }
55
+
56
+ throw new VectorForceError(
57
+ ErrorCode.PROJECT_NOT_INITIALIZED,
58
+ `Workspace ${workspacePath} has not been initialized. Run 'vf init' first.`
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Create a new task within a project.
64
+ * @param {string} workspacePath
65
+ * @param {string} taskId Kebab-case slug
66
+ * @param {string} title
67
+ */
68
+ createTask(workspacePath, taskId, title) {
69
+ const project = this.getProjectForWorkspace(workspacePath);
70
+ return this.store.createTask(project.project_id, taskId, title);
71
+ }
72
+
73
+ /**
74
+ * List tasks for a workspace.
75
+ * @param {string} workspacePath
76
+ * @param {'active' | 'archived' | 'all'} [status]
77
+ */
78
+ listTasks(workspacePath, status = 'all') {
79
+ const project = this.getProjectForWorkspace(workspacePath);
80
+ return this.store.listTasks(project.project_id, status);
81
+ }
82
+
83
+ /**
84
+ * Update the status of a task.
85
+ * @param {string} workspacePath
86
+ * @param {string} taskId
87
+ * @param {'active' | 'completed' | 'abandoned'} status
88
+ */
89
+ updateTaskStatus(workspacePath, taskId, status) {
90
+ const project = this.getProjectForWorkspace(workspacePath);
91
+ return this.store.updateTaskStatus(project.project_id, taskId, status);
92
+ }
93
+
94
+ /**
95
+ * Resolve a task for an operation, enforcing non-ambiguous selection.
96
+ * @param {string} workspacePath
97
+ * @param {string} [explicitTaskId]
98
+ * @returns {{ task: object, project: object }}
99
+ */
100
+ resolveTask(workspacePath, explicitTaskId = null) {
101
+ const project = this.getProjectForWorkspace(workspacePath);
102
+
103
+ if (explicitTaskId) {
104
+ const task = this.store.getTask(project.project_id, explicitTaskId);
105
+ if (!task) {
106
+ throw new VectorForceError(
107
+ ErrorCode.TASK_NOT_FOUND,
108
+ `Task '${explicitTaskId}' does not exist in project ${project.project_id}`
109
+ );
110
+ }
111
+ return { task, project };
112
+ }
113
+
114
+ // No explicit task ID provided: inspect active tasks
115
+ const activeTasks = this.store.listTasks(project.project_id, 'active');
116
+ if (activeTasks.length === 0) {
117
+ throw new VectorForceError(
118
+ ErrorCode.TASK_NOT_FOUND,
119
+ `No active tasks found in project. Create one using 'vf task <id> "<title>"'`
120
+ );
121
+ }
122
+ if (activeTasks.length > 1) {
123
+ const taskIds = activeTasks.map(t => t.task_id).join(', ');
124
+ throw new VectorForceError(
125
+ ErrorCode.AMBIGUOUS_TASK_SELECTION,
126
+ `Multiple active tasks found (${taskIds}). You must explicitly specify --task <id>.`
127
+ );
128
+ }
129
+
130
+ return { task: activeTasks[0], project };
131
+ }
132
+ }