vibe-coding-master 0.0.5 → 0.0.7

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 (42) hide show
  1. package/README.md +168 -63
  2. package/dist/backend/adapters/translation-provider.js +145 -0
  3. package/dist/backend/api/artifact-routes.js +3 -0
  4. package/dist/backend/api/harness-routes.js +22 -0
  5. package/dist/backend/api/project-routes.js +3 -8
  6. package/dist/backend/api/translation-routes.js +70 -0
  7. package/dist/backend/runtime/node-pty-runtime.js +20 -18
  8. package/dist/backend/server.js +31 -1
  9. package/dist/backend/services/app-settings-service.js +128 -0
  10. package/dist/backend/services/artifact-service.js +7 -4
  11. package/dist/backend/services/claude-transcript-service.js +509 -0
  12. package/dist/backend/services/harness-service.js +178 -0
  13. package/dist/backend/services/project-service.js +4 -0
  14. package/dist/backend/services/session-service.js +7 -5
  15. package/dist/backend/services/status-service.js +76 -0
  16. package/dist/backend/services/translation-prompts.js +173 -0
  17. package/dist/backend/services/translation-queue.js +39 -0
  18. package/dist/backend/services/translation-service.js +546 -0
  19. package/dist/backend/templates/handoff.js +32 -0
  20. package/dist/backend/templates/harness/architect-agent.js +12 -0
  21. package/dist/backend/templates/harness/claude-root.js +14 -0
  22. package/dist/backend/templates/harness/coder-agent.js +11 -0
  23. package/dist/backend/templates/harness/project-manager-agent.js +14 -0
  24. package/dist/backend/templates/harness/reviewer-agent.js +13 -0
  25. package/dist/backend/ws/translation-ws.js +35 -0
  26. package/dist/shared/types/harness.js +1 -0
  27. package/dist/shared/types/translation.js +5 -0
  28. package/dist/shared/validation/artifact-check.js +15 -1
  29. package/dist/shared/validation/language-detect.js +46 -0
  30. package/dist-frontend/assets/index-BNASqKEK.css +32 -0
  31. package/dist-frontend/assets/index-Bp49_End.js +58 -0
  32. package/dist-frontend/index.html +2 -2
  33. package/docs/cc-best-practices.md +93 -36
  34. package/docs/product-design.md +313 -1408
  35. package/docs/v1-architecture-design.md +500 -1153
  36. package/docs/v1-implementation-plan.md +783 -1604
  37. package/package.json +3 -1
  38. package/scripts/verify-package.mjs +121 -0
  39. package/dist/backend/templates/role-messaging-context.js +0 -44
  40. package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
  41. package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
  42. package/docs/v1-message-bus-orchestration-design.md +0 -534
@@ -0,0 +1,509 @@
1
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync, watch as fsWatch } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const DEFAULT_TAIL_POLL_INTERVAL_MS = 500;
5
+ /**
6
+ * Adapted from CodingForMoney/cc-pm's transcript tailer.
7
+ *
8
+ * Claude Code writes semantic JSONL events under ~/.claude/projects. Tailing
9
+ * those files is much more reliable than trying to infer answer boundaries
10
+ * from the terminal TUI's raw PTY output.
11
+ */
12
+ export class TranscriptTail {
13
+ path;
14
+ handlers;
15
+ offset = 0;
16
+ buffer = "";
17
+ watcher = null;
18
+ pollTimer = null;
19
+ flushing = false;
20
+ flushScheduled = false;
21
+ constructor(path, handlers) {
22
+ this.path = path;
23
+ this.handlers = handlers;
24
+ }
25
+ start(opts) {
26
+ if (!existsSync(this.path)) {
27
+ throw new Error(`transcript not found: ${this.path}`);
28
+ }
29
+ const stat = statSync(this.path);
30
+ if (opts?.replaySince) {
31
+ this.replaySince(opts.replaySince);
32
+ }
33
+ else if (opts?.replayLastN && opts.replayLastN > 0) {
34
+ this.replayHistory(opts.replayLastN);
35
+ }
36
+ this.offset = stat.size;
37
+ this.buffer = "";
38
+ try {
39
+ this.watcher = fsWatch(this.path, () => {
40
+ this.scheduleFlush();
41
+ });
42
+ }
43
+ catch {
44
+ this.watcher = null;
45
+ }
46
+ this.pollTimer = setInterval(() => {
47
+ this.scheduleFlush();
48
+ }, opts?.pollIntervalMs ?? DEFAULT_TAIL_POLL_INTERVAL_MS);
49
+ this.scheduleFlush();
50
+ }
51
+ stop() {
52
+ if (this.watcher) {
53
+ this.watcher.close();
54
+ this.watcher = null;
55
+ }
56
+ if (this.pollTimer) {
57
+ clearInterval(this.pollTimer);
58
+ this.pollTimer = null;
59
+ }
60
+ }
61
+ scheduleFlush() {
62
+ if (this.flushing || this.flushScheduled) {
63
+ return;
64
+ }
65
+ this.flushScheduled = true;
66
+ setImmediate(() => {
67
+ this.flushScheduled = false;
68
+ this.flush();
69
+ });
70
+ }
71
+ flush() {
72
+ if (this.flushing) {
73
+ return;
74
+ }
75
+ this.flushing = true;
76
+ try {
77
+ const stat = statSync(this.path);
78
+ if (stat.size < this.offset) {
79
+ this.offset = stat.size;
80
+ this.buffer = "";
81
+ return;
82
+ }
83
+ if (stat.size === this.offset) {
84
+ return;
85
+ }
86
+ const toRead = stat.size - this.offset;
87
+ const fd = openSync(this.path, "r");
88
+ try {
89
+ const buf = Buffer.alloc(toRead);
90
+ readSync(fd, buf, 0, toRead, this.offset);
91
+ this.offset = stat.size;
92
+ this.buffer += buf.toString("utf-8");
93
+ }
94
+ finally {
95
+ closeSync(fd);
96
+ }
97
+ const lines = this.buffer.split("\n");
98
+ this.buffer = lines.pop() ?? "";
99
+ for (const line of lines) {
100
+ if (line.trim()) {
101
+ this.tryEmit(line);
102
+ }
103
+ }
104
+ }
105
+ catch (error) {
106
+ this.handlers.onError?.(error);
107
+ }
108
+ finally {
109
+ this.flushing = false;
110
+ }
111
+ }
112
+ replayHistory(n) {
113
+ try {
114
+ const text = readFileSync(this.path, "utf-8");
115
+ const events = [];
116
+ for (const line of text.split("\n")) {
117
+ if (line.trim()) {
118
+ events.push(...parseAssistantContent(line));
119
+ }
120
+ }
121
+ for (const event of events.slice(-n)) {
122
+ this.handlers.onContent(event);
123
+ }
124
+ }
125
+ catch (error) {
126
+ this.handlers.onError?.(error);
127
+ }
128
+ }
129
+ replaySince(replaySince) {
130
+ const sinceMs = Date.parse(replaySince);
131
+ if (!Number.isFinite(sinceMs)) {
132
+ return;
133
+ }
134
+ try {
135
+ const text = readFileSync(this.path, "utf-8");
136
+ for (const line of text.split("\n")) {
137
+ if (!line.trim()) {
138
+ continue;
139
+ }
140
+ for (const event of parseAssistantContent(line)) {
141
+ const eventMs = Date.parse(event.timestamp);
142
+ if (Number.isFinite(eventMs) && eventMs >= sinceMs) {
143
+ this.handlers.onContent(event);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ catch (error) {
149
+ this.handlers.onError?.(error);
150
+ }
151
+ }
152
+ tryEmit(line) {
153
+ for (const event of parseAssistantContent(line)) {
154
+ this.handlers.onContent(event);
155
+ }
156
+ }
157
+ }
158
+ export function createClaudeTranscriptService() {
159
+ return {
160
+ subscribeToRoleSession(session, listener, options = {}) {
161
+ let tail;
162
+ let retryTimer;
163
+ let stopped = false;
164
+ const start = () => {
165
+ if (stopped) {
166
+ return;
167
+ }
168
+ const transcriptPath = resolveExistingClaudeTranscriptPath(session);
169
+ if (!transcriptPath) {
170
+ retryTimer = setTimeout(start, 500);
171
+ return;
172
+ }
173
+ try {
174
+ tail = new TranscriptTail(transcriptPath, {
175
+ onContent: listener,
176
+ onError: options.onError
177
+ });
178
+ tail.start({
179
+ replayLastN: options.replayLastN,
180
+ replaySince: options.replaySince
181
+ });
182
+ options.onTranscriptPathResolved?.(transcriptPath);
183
+ }
184
+ catch (error) {
185
+ options.onError?.(error);
186
+ tail?.stop();
187
+ tail = undefined;
188
+ retryTimer = setTimeout(start, 500);
189
+ }
190
+ };
191
+ start();
192
+ return () => {
193
+ stopped = true;
194
+ if (retryTimer) {
195
+ clearTimeout(retryTimer);
196
+ }
197
+ tail?.stop();
198
+ };
199
+ }
200
+ };
201
+ }
202
+ export function resolveExistingClaudeTranscriptPath(session) {
203
+ const sessionPath = existingFile(session.transcriptPath);
204
+ if (sessionPath) {
205
+ return sessionPath;
206
+ }
207
+ const cwdPath = existingFile(claudeTranscriptPath(session.cwd, session.claudeSessionId));
208
+ if (cwdPath) {
209
+ return cwdPath;
210
+ }
211
+ return findClaudeTranscriptPathBySessionId(session.claudeSessionId);
212
+ }
213
+ export function findClaudeTranscriptPathBySessionId(claudeSessionId) {
214
+ const root = claudeProjectsRoot();
215
+ let projectDirs;
216
+ try {
217
+ projectDirs = readdirSync(root, { withFileTypes: true });
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ const matches = [];
223
+ for (const dirent of projectDirs) {
224
+ if (!dirent.isDirectory()) {
225
+ continue;
226
+ }
227
+ const candidate = existingFile(join(root, dirent.name, `${claudeSessionId}.jsonl`));
228
+ if (!candidate) {
229
+ continue;
230
+ }
231
+ try {
232
+ matches.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs });
233
+ }
234
+ catch {
235
+ // Ignore files that disappear while scanning Claude's transcript folder.
236
+ }
237
+ }
238
+ matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
239
+ return matches[0]?.path;
240
+ }
241
+ function existingFile(candidate) {
242
+ if (!candidate) {
243
+ return undefined;
244
+ }
245
+ try {
246
+ return statSync(candidate).isFile() ? candidate : undefined;
247
+ }
248
+ catch {
249
+ return undefined;
250
+ }
251
+ }
252
+ export function claudeProjectsRoot() {
253
+ return join(homedir(), ".claude", "projects");
254
+ }
255
+ export function projectHash(projectDir) {
256
+ return projectDir.replace(/[\/\s]+/g, "-");
257
+ }
258
+ export function projectsTranscriptDir(projectDir) {
259
+ return join(claudeProjectsRoot(), projectHash(projectDir));
260
+ }
261
+ export function claudeTranscriptPath(projectDir, claudeSessionId) {
262
+ return join(projectsTranscriptDir(projectDir), `${claudeSessionId}.jsonl`);
263
+ }
264
+ export function parseAssistantContent(line) {
265
+ let obj;
266
+ try {
267
+ obj = JSON.parse(line);
268
+ }
269
+ catch {
270
+ return [];
271
+ }
272
+ if (obj.type === "user") {
273
+ return parseUserToolResults(obj);
274
+ }
275
+ if (obj.type !== "assistant") {
276
+ return [];
277
+ }
278
+ const message = obj.message;
279
+ const content = message?.content;
280
+ if (!Array.isArray(content)) {
281
+ return [];
282
+ }
283
+ const textParts = [];
284
+ const thinkingParts = [];
285
+ const questions = [];
286
+ const todos = [];
287
+ const agents = [];
288
+ const rawTools = [];
289
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : new Date().toISOString();
290
+ const uuid = typeof obj.uuid === "string" ? obj.uuid : undefined;
291
+ const rawStopReason = message?.stop_reason;
292
+ const stopReason = typeof rawStopReason === "string" ? rawStopReason : undefined;
293
+ for (const entry of content) {
294
+ if (!entry || typeof entry !== "object") {
295
+ continue;
296
+ }
297
+ const block = entry;
298
+ if (block.type === "text" && typeof block.text === "string" && block.text.length > 0) {
299
+ textParts.push(block.text);
300
+ continue;
301
+ }
302
+ if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.length > 0) {
303
+ thinkingParts.push(block.thinking);
304
+ continue;
305
+ }
306
+ if (block.type !== "tool_use") {
307
+ continue;
308
+ }
309
+ const toolId = typeof block.id === "string" ? block.id : undefined;
310
+ const toolName = typeof block.name === "string" ? block.name : undefined;
311
+ if (!toolId || !toolName) {
312
+ continue;
313
+ }
314
+ if (toolName === "AskUserQuestion") {
315
+ const payload = parseQuestionInput(block.input);
316
+ if (payload) {
317
+ questions.push({ id: toolId, payload });
318
+ }
319
+ }
320
+ else if (toolName === "TodoWrite") {
321
+ const payload = parseTodoInput(block.input);
322
+ if (payload) {
323
+ todos.push({ id: toolId, payload });
324
+ }
325
+ }
326
+ else if (toolName === "Agent" || toolName === "Task") {
327
+ const payload = parseAgentInput(block.input);
328
+ if (payload) {
329
+ agents.push({ id: toolId, payload });
330
+ }
331
+ }
332
+ else {
333
+ rawTools.push({
334
+ id: toolId,
335
+ payload: { name: toolName, input: block.input }
336
+ });
337
+ }
338
+ }
339
+ const out = [];
340
+ if (textParts.length > 0) {
341
+ const text = textParts.join("\n\n");
342
+ out.push({
343
+ kind: "text",
344
+ timestamp,
345
+ text,
346
+ id: uuid ?? `${timestamp}-${text.slice(0, 16)}`,
347
+ ...(stopReason !== undefined ? { stopReason } : {})
348
+ });
349
+ }
350
+ if (thinkingParts.length > 0) {
351
+ const text = thinkingParts.join("\n\n");
352
+ out.push({
353
+ kind: "thinking",
354
+ timestamp,
355
+ text,
356
+ id: uuid ? `${uuid}#thinking` : `${timestamp}-thinking-${text.slice(0, 16)}`,
357
+ ...(stopReason !== undefined ? { stopReason } : {})
358
+ });
359
+ }
360
+ for (const question of questions) {
361
+ out.push({
362
+ kind: "question",
363
+ timestamp,
364
+ id: question.id,
365
+ question: question.payload
366
+ });
367
+ }
368
+ for (const todo of todos) {
369
+ out.push({
370
+ kind: "todo",
371
+ timestamp,
372
+ id: todo.id,
373
+ todo: todo.payload
374
+ });
375
+ }
376
+ for (const agent of agents) {
377
+ out.push({
378
+ kind: "agent",
379
+ timestamp,
380
+ id: agent.id,
381
+ agent: agent.payload
382
+ });
383
+ }
384
+ for (const tool of rawTools) {
385
+ out.push({
386
+ kind: "tool_use",
387
+ timestamp,
388
+ id: tool.id,
389
+ toolUse: tool.payload
390
+ });
391
+ }
392
+ return out;
393
+ }
394
+ function parseUserToolResults(obj) {
395
+ const message = obj.message;
396
+ const content = message?.content;
397
+ if (!Array.isArray(content)) {
398
+ return [];
399
+ }
400
+ const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : new Date().toISOString();
401
+ const out = [];
402
+ for (const entry of content) {
403
+ if (!entry || typeof entry !== "object") {
404
+ continue;
405
+ }
406
+ const block = entry;
407
+ if (block.type !== "tool_result") {
408
+ continue;
409
+ }
410
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
411
+ if (!toolUseId) {
412
+ continue;
413
+ }
414
+ out.push({
415
+ kind: "tool_result",
416
+ timestamp,
417
+ id: `${toolUseId}#result`,
418
+ toolResult: {
419
+ tool_use_id: toolUseId,
420
+ content: block.content,
421
+ isError: block.is_error === true
422
+ }
423
+ });
424
+ }
425
+ return out;
426
+ }
427
+ function parseQuestionInput(raw) {
428
+ if (!raw || typeof raw !== "object") {
429
+ return null;
430
+ }
431
+ const rawQuestions = raw.questions;
432
+ if (!Array.isArray(rawQuestions) || rawQuestions.length === 0) {
433
+ return null;
434
+ }
435
+ const questions = [];
436
+ for (const rawQuestion of rawQuestions) {
437
+ if (!rawQuestion || typeof rawQuestion !== "object") {
438
+ continue;
439
+ }
440
+ const questionRecord = rawQuestion;
441
+ const rawOptions = questionRecord.options;
442
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0) {
443
+ continue;
444
+ }
445
+ const options = [];
446
+ for (const rawOption of rawOptions) {
447
+ if (!rawOption || typeof rawOption !== "object") {
448
+ continue;
449
+ }
450
+ const optionRecord = rawOption;
451
+ const option = {
452
+ label: typeof optionRecord.label === "string" ? optionRecord.label : "",
453
+ description: typeof optionRecord.description === "string" ? optionRecord.description : ""
454
+ };
455
+ if (typeof optionRecord.preview === "string") {
456
+ option.preview = optionRecord.preview;
457
+ }
458
+ options.push(option);
459
+ }
460
+ if (options.length > 0) {
461
+ questions.push({
462
+ question: typeof questionRecord.question === "string" ? questionRecord.question : "",
463
+ header: typeof questionRecord.header === "string" ? questionRecord.header : "",
464
+ multiSelect: questionRecord.multiSelect === true,
465
+ options
466
+ });
467
+ }
468
+ }
469
+ return questions.length > 0 ? { questions } : null;
470
+ }
471
+ function parseTodoInput(raw) {
472
+ if (!raw || typeof raw !== "object") {
473
+ return null;
474
+ }
475
+ const rawTodos = raw.todos;
476
+ if (!Array.isArray(rawTodos) || rawTodos.length === 0) {
477
+ return null;
478
+ }
479
+ const todos = [];
480
+ for (const rawTodo of rawTodos) {
481
+ if (!rawTodo || typeof rawTodo !== "object") {
482
+ continue;
483
+ }
484
+ const todoRecord = rawTodo;
485
+ const content = typeof todoRecord.content === "string" ? todoRecord.content : "";
486
+ const activeForm = typeof todoRecord.activeForm === "string" ? todoRecord.activeForm : "";
487
+ const rawStatus = todoRecord.status;
488
+ const status = rawStatus === "in_progress" || rawStatus === "completed"
489
+ ? rawStatus
490
+ : "pending";
491
+ if (content || activeForm) {
492
+ todos.push({ content, activeForm, status });
493
+ }
494
+ }
495
+ return todos.length > 0 ? { todos } : null;
496
+ }
497
+ function parseAgentInput(raw) {
498
+ if (!raw || typeof raw !== "object") {
499
+ return null;
500
+ }
501
+ const record = raw;
502
+ const description = typeof record.description === "string" ? record.description : "";
503
+ const prompt = typeof record.prompt === "string" ? record.prompt : "";
504
+ const subagent_type = typeof record.subagent_type === "string" ? record.subagent_type : "";
505
+ if (!description && !prompt) {
506
+ return null;
507
+ }
508
+ return { description, prompt, subagent_type };
509
+ }
@@ -0,0 +1,178 @@
1
+ import path from "node:path";
2
+ import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
3
+ import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
4
+ import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
5
+ import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
6
+ import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.js";
7
+ export const VCM_HARNESS_VERSION = 1;
8
+ const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
9
+ const HARNESS_FILES = [
10
+ {
11
+ kind: "root-claude",
12
+ path: "CLAUDE.md",
13
+ title: "CLAUDE.md",
14
+ renderRules: renderRootClaudeHarnessRules
15
+ },
16
+ {
17
+ kind: "agent-project-manager",
18
+ path: ".claude/agents/project-manager.md",
19
+ title: "Project Manager Agent",
20
+ frontmatter: renderAgentFrontmatter("project-manager", "User-facing VCM orchestration role for task clarification, role routing, handoffs, acceptance, and PR preparation."),
21
+ renderRules: renderProjectManagerHarnessRules
22
+ },
23
+ {
24
+ kind: "agent-architect",
25
+ path: ".claude/agents/architect.md",
26
+ title: "Architect Agent",
27
+ frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, test contracts, and post-review docs sync."),
28
+ renderRules: renderArchitectHarnessRules
29
+ },
30
+ {
31
+ kind: "agent-coder",
32
+ path: ".claude/agents/coder.md",
33
+ title: "Coder Agent",
34
+ frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes, focused tests, implementation logs, and validation evidence."),
35
+ renderRules: renderCoderHarnessRules
36
+ },
37
+ {
38
+ kind: "agent-reviewer",
39
+ path: ".claude/agents/reviewer.md",
40
+ title: "Reviewer Agent",
41
+ frontmatter: renderAgentFrontmatter("reviewer", "VCM independent review role for acceptance, test adequacy, scope checks, docs gaps, and risk findings."),
42
+ renderRules: renderReviewerHarnessRules
43
+ }
44
+ ];
45
+ export function createHarnessService(deps) {
46
+ return {
47
+ async getHarnessStatus(repoRoot) {
48
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
49
+ return renderHarnessStatus(analyses);
50
+ },
51
+ async applyHarness(repoRoot) {
52
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
53
+ const changedFiles = [];
54
+ for (const analysis of analyses) {
55
+ if (analysis.status.action === "ok" || !analysis.nextContent) {
56
+ continue;
57
+ }
58
+ await deps.fs.writeText(resolveHarnessPath(repoRoot, analysis.definition.path), analysis.nextContent);
59
+ if (analysis.plannedChange) {
60
+ changedFiles.push(analysis.plannedChange);
61
+ }
62
+ }
63
+ return {
64
+ version: VCM_HARNESS_VERSION,
65
+ changedFiles,
66
+ message: changedFiles.length === 0
67
+ ? "VCM Harness is already up to date."
68
+ : "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
69
+ };
70
+ }
71
+ };
72
+ }
73
+ async function analyzeHarnessFiles(fs, repoRoot) {
74
+ const analyses = [];
75
+ for (const definition of HARNESS_FILES) {
76
+ analyses.push(await analyzeHarnessFile(fs, repoRoot, definition));
77
+ }
78
+ return analyses;
79
+ }
80
+ async function analyzeHarnessFile(fs, repoRoot, definition) {
81
+ const absolutePath = resolveHarnessPath(repoRoot, definition.path);
82
+ const expectedBlock = renderManagedBlock(definition.renderRules());
83
+ const exists = await fs.pathExists(absolutePath);
84
+ if (!exists) {
85
+ return {
86
+ definition,
87
+ status: {
88
+ kind: definition.kind,
89
+ path: definition.path,
90
+ exists: false,
91
+ hasManagedBlock: false,
92
+ action: "create"
93
+ },
94
+ plannedChange: {
95
+ path: definition.path,
96
+ action: "create",
97
+ reason: "File is missing; VCM will create a recommended default."
98
+ },
99
+ nextContent: renderNewHarnessFile(definition, expectedBlock)
100
+ };
101
+ }
102
+ const currentContent = await fs.readText(absolutePath);
103
+ const match = currentContent.match(MANAGED_BLOCK_PATTERN);
104
+ if (!match) {
105
+ return {
106
+ definition,
107
+ status: {
108
+ kind: definition.kind,
109
+ path: definition.path,
110
+ exists: true,
111
+ hasManagedBlock: false,
112
+ action: "insert"
113
+ },
114
+ plannedChange: {
115
+ path: definition.path,
116
+ action: "insert",
117
+ reason: "File exists but does not contain VCM managed rules."
118
+ },
119
+ nextContent: `${currentContent.trimEnd()}\n\n${expectedBlock}\n`
120
+ };
121
+ }
122
+ const managedVersion = match[1] ? Number(match[1]) : undefined;
123
+ const currentBlock = match[0];
124
+ const action = currentBlock === expectedBlock ? "ok" : "update";
125
+ return {
126
+ definition,
127
+ status: {
128
+ kind: definition.kind,
129
+ path: definition.path,
130
+ exists: true,
131
+ hasManagedBlock: true,
132
+ managedVersion,
133
+ action
134
+ },
135
+ plannedChange: action === "ok"
136
+ ? undefined
137
+ : {
138
+ path: definition.path,
139
+ action,
140
+ reason: managedVersion === VCM_HARNESS_VERSION
141
+ ? "VCM managed rules differ from the current recommended template."
142
+ : `VCM managed block version is ${managedVersion ?? "missing"}; current version is ${VCM_HARNESS_VERSION}.`
143
+ },
144
+ nextContent: action === "ok"
145
+ ? undefined
146
+ : currentContent.replace(MANAGED_BLOCK_PATTERN, expectedBlock)
147
+ };
148
+ }
149
+ function renderHarnessStatus(analyses) {
150
+ const files = analyses.map((analysis) => analysis.status);
151
+ const plannedChanges = analyses
152
+ .map((analysis) => analysis.plannedChange)
153
+ .filter((change) => Boolean(change));
154
+ return {
155
+ version: VCM_HARNESS_VERSION,
156
+ files,
157
+ needsApply: plannedChanges.length > 0,
158
+ plannedChanges,
159
+ warnings: plannedChanges.length > 0
160
+ ? ["Review and commit VCM Harness changes before starting long-running work."]
161
+ : []
162
+ };
163
+ }
164
+ function renderManagedBlock(rules) {
165
+ return `<!-- VCM:BEGIN version=${VCM_HARNESS_VERSION} -->\n${rules.trimEnd()}\n<!-- VCM:END -->`;
166
+ }
167
+ function renderNewHarnessFile(definition, block) {
168
+ const frontmatter = definition.frontmatter
169
+ ? `${definition.frontmatter.trimEnd()}\n\n`
170
+ : "";
171
+ return `${frontmatter}# ${definition.title}\n\n${block}\n`;
172
+ }
173
+ function renderAgentFrontmatter(name, description) {
174
+ return `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash, Edit, Write\n---`;
175
+ }
176
+ function resolveHarnessPath(repoRoot, relativePath) {
177
+ return path.join(repoRoot, relativePath);
178
+ }
@@ -59,11 +59,15 @@ export function createProjectService(deps) {
59
59
  config,
60
60
  warnings
61
61
  };
62
+ await deps.appSettings.recordRecentRepositoryPath(repoRoot);
62
63
  return currentProject;
63
64
  },
64
65
  async getCurrentProject() {
65
66
  return currentProject;
66
67
  },
68
+ async getRecentRepositoryPaths() {
69
+ return deps.appSettings.getRecentRepositoryPaths();
70
+ },
67
71
  async loadConfig(repoRoot) {
68
72
  const configPath = this.getConfigPath(repoRoot);
69
73
  if (!(await deps.fs.pathExists(configPath))) {