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.
- package/.agents/agents/AGENTS.md +12 -0
- package/.agents/agents/vectorforce-architect.md +35 -0
- package/.agents/agents/vectorforce-auditor.md +25 -0
- package/.agents/agents/vectorforce-samples-builder.md +30 -0
- package/.agents/mcp_config.json +11 -0
- package/.codex/config.toml +5 -0
- package/AGENTS.md +115 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/bin/vf.js +1159 -0
- package/codex.json +14 -0
- package/commandcode.json +11 -0
- package/musecode.json +14 -0
- package/package.json +40 -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 +546 -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 +425 -0
- package/src/storage/sqlite-store.js +387 -0
- package/src/studio/server.js +764 -0
- package/studio/dist/assets/index-C0_eVpV1.js +9 -0
- package/studio/dist/assets/index-CxTK-2ty.css +2 -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,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
|
+
}
|