vibe-coding-master 0.0.6 → 0.0.8

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 (50) hide show
  1. package/README.md +207 -66
  2. package/dist/backend/adapters/filesystem.js +13 -0
  3. package/dist/backend/adapters/git-adapter.js +79 -1
  4. package/dist/backend/adapters/translation-provider.js +145 -0
  5. package/dist/backend/api/artifact-routes.js +16 -7
  6. package/dist/backend/api/harness-routes.js +22 -0
  7. package/dist/backend/api/message-routes.js +2 -0
  8. package/dist/backend/api/project-routes.js +3 -8
  9. package/dist/backend/api/task-routes.js +14 -0
  10. package/dist/backend/api/translation-routes.js +70 -0
  11. package/dist/backend/runtime/node-pty-runtime.js +20 -18
  12. package/dist/backend/server.js +33 -2
  13. package/dist/backend/services/app-settings-service.js +128 -0
  14. package/dist/backend/services/artifact-service.js +7 -4
  15. package/dist/backend/services/claude-transcript-service.js +509 -0
  16. package/dist/backend/services/command-dispatcher.js +4 -2
  17. package/dist/backend/services/harness-service.js +196 -0
  18. package/dist/backend/services/message-service.js +1 -1
  19. package/dist/backend/services/project-service.js +50 -9
  20. package/dist/backend/services/session-service.js +13 -9
  21. package/dist/backend/services/status-service.js +79 -1
  22. package/dist/backend/services/task-service.js +118 -4
  23. package/dist/backend/services/translation-prompts.js +173 -0
  24. package/dist/backend/services/translation-queue.js +39 -0
  25. package/dist/backend/services/translation-service.js +546 -0
  26. package/dist/backend/templates/handoff.js +32 -0
  27. package/dist/backend/templates/harness/architect-agent.js +12 -0
  28. package/dist/backend/templates/harness/claude-root.js +14 -0
  29. package/dist/backend/templates/harness/coder-agent.js +11 -0
  30. package/dist/backend/templates/harness/gitignore.js +9 -0
  31. package/dist/backend/templates/harness/project-manager-agent.js +14 -0
  32. package/dist/backend/templates/harness/reviewer-agent.js +13 -0
  33. package/dist/backend/ws/translation-ws.js +35 -0
  34. package/dist/shared/types/harness.js +1 -0
  35. package/dist/shared/types/translation.js +5 -0
  36. package/dist/shared/validation/artifact-check.js +15 -1
  37. package/dist/shared/validation/language-detect.js +46 -0
  38. package/dist-frontend/assets/index-CuiNNOzj.css +32 -0
  39. package/dist-frontend/assets/index-D59GuHCR.js +58 -0
  40. package/dist-frontend/index.html +2 -2
  41. package/docs/cc-best-practices.md +109 -40
  42. package/docs/product-design.md +370 -1374
  43. package/docs/v1-architecture-design.md +595 -1114
  44. package/docs/v1-implementation-plan.md +898 -1603
  45. package/package.json +1 -1
  46. package/scripts/verify-package.mjs +8 -0
  47. package/dist/backend/templates/role-messaging-context.js +0 -44
  48. package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
  49. package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
  50. package/docs/v1-message-bus-orchestration-design.md +0 -534
@@ -1,23 +1,26 @@
1
1
  import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
+ import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
3
4
  export function registerArtifactRoutes(app, deps) {
4
5
  app.get("/api/tasks/:taskSlug/artifacts", async (request) => {
5
6
  const project = await requireCurrentProject(deps.projectService);
6
7
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
8
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
7
9
  return deps.artifactService.listArtifacts({
8
- repoRoot: project.repoRoot,
10
+ repoRoot: taskRepoRoot,
9
11
  handoffDir: task.handoffDir
10
12
  });
11
13
  });
12
14
  app.get("/api/tasks/:taskSlug/artifacts/:artifactName", async (request) => {
13
15
  const project = await requireCurrentProject(deps.projectService);
14
16
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
15
- const paths = deps.artifactService.getHandoffPaths(project.repoRoot, task.handoffDir);
17
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
18
+ const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
16
19
  const artifactPath = artifactNameToPath(paths, request.params.artifactName);
17
20
  return {
18
21
  path: artifactPath,
19
22
  content: await deps.artifactService.readArtifact({
20
- repoRoot: project.repoRoot,
23
+ repoRoot: taskRepoRoot,
21
24
  artifactPath
22
25
  })
23
26
  };
@@ -26,10 +29,11 @@ export function registerArtifactRoutes(app, deps) {
26
29
  const project = await requireCurrentProject(deps.projectService);
27
30
  const role = parseDispatchableRole(request.params.role);
28
31
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
32
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
29
33
  return {
30
34
  role,
31
35
  content: await deps.artifactService.readRoleCommand({
32
- repoRoot: project.repoRoot,
36
+ repoRoot: taskRepoRoot,
33
37
  handoffDir: task.handoffDir,
34
38
  role
35
39
  })
@@ -39,8 +43,9 @@ export function registerArtifactRoutes(app, deps) {
39
43
  const project = await requireCurrentProject(deps.projectService);
40
44
  const role = parseDispatchableRole(request.params.role);
41
45
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
46
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
42
47
  await deps.artifactService.saveRoleCommand({
43
- repoRoot: project.repoRoot,
48
+ repoRoot: taskRepoRoot,
44
49
  handoffDir: task.handoffDir,
45
50
  role,
46
51
  content: request.body.content
@@ -57,11 +62,12 @@ export function registerArtifactRoutes(app, deps) {
57
62
  });
58
63
  }
59
64
  const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
60
- const paths = deps.artifactService.getHandoffPaths(project.repoRoot, task.handoffDir);
65
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
66
+ const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
61
67
  return {
62
68
  role: request.params.role,
63
69
  content: await deps.artifactService.readArtifact({
64
- repoRoot: project.repoRoot,
70
+ repoRoot: taskRepoRoot,
65
71
  artifactPath: paths.roleLogPaths[request.params.role]
66
72
  })
67
73
  };
@@ -90,6 +96,9 @@ function artifactNameToPath(paths, artifactName) {
90
96
  if (artifactName === "review-report.md") {
91
97
  return paths.reviewReportPath;
92
98
  }
99
+ if (artifactName === "docs-sync-report.md") {
100
+ return paths.docsSyncReportPath;
101
+ }
93
102
  throw new VcmError({
94
103
  code: "ARTIFACT_UNKNOWN",
95
104
  message: `Unknown artifact: ${artifactName}`,
@@ -0,0 +1,22 @@
1
+ import { VcmError } from "../errors.js";
2
+ export function registerHarnessRoutes(app, deps) {
3
+ app.get("/api/projects/harness", async () => {
4
+ const project = await requireCurrentProject(deps.projectService);
5
+ return deps.harnessService.getHarnessStatus(project.repoRoot);
6
+ });
7
+ app.post("/api/projects/harness/apply", async () => {
8
+ const project = await requireCurrentProject(deps.projectService);
9
+ return deps.harnessService.applyHarness(project.repoRoot);
10
+ });
11
+ }
12
+ async function requireCurrentProject(projectService) {
13
+ const project = await projectService.getCurrentProject();
14
+ if (!project) {
15
+ throw new VcmError({
16
+ code: "PROJECT_NOT_CONNECTED",
17
+ message: "Connect a repository first.",
18
+ statusCode: 409
19
+ });
20
+ }
21
+ return project;
22
+ }
@@ -1,4 +1,5 @@
1
1
  import { VcmError } from "../errors.js";
2
+ import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
2
3
  export function registerMessageRoutes(app, deps) {
3
4
  app.get("/api/tasks/:taskSlug/messages", async (request) => {
4
5
  const context = await getRouteContext(deps, request.params.taskSlug);
@@ -72,6 +73,7 @@ async function getRouteContext(deps, taskSlug) {
72
73
  const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
73
74
  return {
74
75
  repoRoot: project.repoRoot,
76
+ taskRepoRoot: getTaskRuntimeRepoRoot(task),
75
77
  stateRoot: config.stateRoot,
76
78
  handoffDir: task.handoffDir,
77
79
  taskSlug
@@ -1,17 +1,12 @@
1
1
  export function registerProjectRoutes(app, deps) {
2
2
  app.get("/api/health", async () => ({ ok: true }));
3
+ app.get("/api/projects/recent", async () => {
4
+ return deps.projectService.getRecentRepositoryPaths();
5
+ });
3
6
  app.post("/api/projects/connect", async (request) => {
4
7
  return deps.projectService.connectProject(request.body);
5
8
  });
6
9
  app.get("/api/projects/current", async () => {
7
10
  return deps.projectService.getCurrentProject();
8
11
  });
9
- app.get("/api/projects/harness", async () => ({
10
- checks: [
11
- {
12
- name: "local-gui",
13
- status: "ok"
14
- }
15
- ]
16
- }));
17
12
  }
@@ -16,6 +16,20 @@ export function registerTaskRoutes(app, deps) {
16
16
  const project = await requireCurrentProject(deps.projectService);
17
17
  return deps.statusService.getTaskStatus(project.repoRoot, request.params.taskSlug);
18
18
  });
19
+ app.post("/api/tasks/:taskSlug/cleanup", async (request) => {
20
+ const project = await requireCurrentProject(deps.projectService);
21
+ const sessions = await deps.sessionService.listRoleSessions(project.repoRoot, request.params.taskSlug);
22
+ const running = sessions.filter((session) => session.status === "running");
23
+ if (running.length > 0) {
24
+ throw new VcmError({
25
+ code: "TASK_SESSIONS_RUNNING",
26
+ message: "Stop all role sessions before cleaning up the task.",
27
+ statusCode: 409,
28
+ hint: running.map((session) => session.role).join(", ")
29
+ });
30
+ }
31
+ return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
32
+ });
19
33
  }
20
34
  async function requireCurrentProject(projectService) {
21
35
  const project = await projectService.getCurrentProject();
@@ -0,0 +1,70 @@
1
+ import { isRoleName } from "../../shared/constants.js";
2
+ import { VcmError } from "../errors.js";
3
+ export function registerTranslationRoutes(app, deps) {
4
+ app.get("/api/translation/settings", async () => {
5
+ return deps.translationService.getSettings();
6
+ });
7
+ app.put("/api/translation/settings", async (request) => {
8
+ const { apiKey, ...settings } = request.body ?? {};
9
+ return deps.translationService.updateSettings(settings, apiKey !== undefined ? { apiKey } : undefined);
10
+ });
11
+ app.get("/api/translation/prompts", async () => {
12
+ return deps.translationService.getPromptPreviews();
13
+ });
14
+ app.post("/api/translation/test", async () => {
15
+ return deps.translationService.testProvider();
16
+ });
17
+ app.post("/api/tasks/:taskSlug/sessions/:role/translation/input", async (request) => {
18
+ const project = await requireCurrentProject(deps.projectService);
19
+ const role = parseRole(request.params.role);
20
+ await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
21
+ return deps.translationService.translateUserInput({
22
+ repoRoot: project.repoRoot,
23
+ taskSlug: request.params.taskSlug,
24
+ role,
25
+ ...(request.body ?? { text: "" })
26
+ });
27
+ });
28
+ app.post("/api/tasks/:taskSlug/sessions/:role/translation/send", async (request) => {
29
+ const project = await requireCurrentProject(deps.projectService);
30
+ const role = parseRole(request.params.role);
31
+ await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
32
+ await deps.translationService.sendTranslatedInput({
33
+ repoRoot: project.repoRoot,
34
+ taskSlug: request.params.taskSlug,
35
+ role,
36
+ englishText: request.body?.englishText ?? ""
37
+ });
38
+ return { ok: true };
39
+ });
40
+ app.post("/api/translation/sessions/:sessionId/clear", async (request) => {
41
+ await requireCurrentProject(deps.projectService);
42
+ deps.translationService.clearSession(request.params.sessionId);
43
+ return { ok: true };
44
+ });
45
+ app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
46
+ await requireCurrentProject(deps.projectService);
47
+ return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
48
+ });
49
+ }
50
+ function parseRole(role) {
51
+ if (!isRoleName(role)) {
52
+ throw new VcmError({
53
+ code: "UNKNOWN_ROLE",
54
+ message: `Unknown role: ${role}`,
55
+ statusCode: 400
56
+ });
57
+ }
58
+ return role;
59
+ }
60
+ async function requireCurrentProject(projectService) {
61
+ const project = await projectService.getCurrentProject();
62
+ if (!project) {
63
+ throw new VcmError({
64
+ code: "PROJECT_NOT_CONNECTED",
65
+ message: "Connect a repository first.",
66
+ statusCode: 409
67
+ });
68
+ }
69
+ return project;
70
+ }
@@ -109,27 +109,29 @@ export function createNodePtyTerminalRuntime(deps) {
109
109
  entry.process.kill();
110
110
  return create(entry.input, sessionId);
111
111
  },
112
- subscribe(sessionId, listener) {
112
+ subscribe(sessionId, listener, options = {}) {
113
113
  const entry = getEntry(entries, sessionId);
114
114
  entry.listeners.add(listener);
115
- void deps.fs.readText(entry.input.logPath)
116
- .then((data) => {
117
- if (!data || !entry.listeners.has(listener)) {
118
- return;
119
- }
120
- listener({
121
- id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
122
- sessionId,
123
- taskSlug: entry.session.taskSlug,
124
- role: entry.session.role,
125
- type: "output",
126
- timestamp: now(),
127
- data
115
+ if (options.replay !== false) {
116
+ void deps.fs.readText(entry.input.logPath)
117
+ .then((data) => {
118
+ if (!data || !entry.listeners.has(listener)) {
119
+ return;
120
+ }
121
+ listener({
122
+ id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
123
+ sessionId,
124
+ taskSlug: entry.session.taskSlug,
125
+ role: entry.session.role,
126
+ type: "output",
127
+ timestamp: now(),
128
+ data
129
+ });
130
+ })
131
+ .catch(() => {
132
+ // The log file may not exist yet for a brand-new session.
128
133
  });
129
- })
130
- .catch(() => {
131
- // The log file may not exist yet for a brand-new session.
132
- });
134
+ }
133
135
  return () => {
134
136
  entry.listeners.delete(listener);
135
137
  };
@@ -8,20 +8,28 @@ import { createClaudeAdapter } from "./adapters/claude-adapter.js";
8
8
  import { createCommandRunner } from "./adapters/command-runner.js";
9
9
  import { createCommandDispatcher } from "./services/command-dispatcher.js";
10
10
  import { createGitAdapter } from "./adapters/git-adapter.js";
11
+ import { createAppSettingsService } from "./services/app-settings-service.js";
12
+ import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
13
+ import { createHarnessService } from "./services/harness-service.js";
11
14
  import { createNodeFileSystemAdapter } from "./adapters/filesystem.js";
12
15
  import { createNodePtyTerminalRuntime } from "./runtime/node-pty-runtime.js";
16
+ import { createOpenAiCompatibleTranslationProvider } from "./adapters/translation-provider.js";
13
17
  import { createProjectService } from "./services/project-service.js";
14
18
  import { createSessionRegistry } from "./runtime/session-registry.js";
15
19
  import { createSessionService } from "./services/session-service.js";
16
20
  import { createMessageService } from "./services/message-service.js";
17
21
  import { createStatusService } from "./services/status-service.js";
18
22
  import { createTaskService } from "./services/task-service.js";
23
+ import { createTranslationService } from "./services/translation-service.js";
19
24
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
25
+ import { registerHarnessRoutes } from "./api/harness-routes.js";
20
26
  import { registerMessageRoutes } from "./api/message-routes.js";
21
27
  import { registerProjectRoutes } from "./api/project-routes.js";
22
28
  import { registerSessionRoutes } from "./api/session-routes.js";
23
29
  import { registerTaskRoutes } from "./api/task-routes.js";
30
+ import { registerTranslationRoutes } from "./api/translation-routes.js";
24
31
  import { registerTerminalWs } from "./ws/terminal-ws.js";
32
+ import { registerTranslationWs } from "./ws/translation-ws.js";
25
33
  import { toVcmError } from "./errors.js";
26
34
  export async function createServer(deps, options = {}) {
27
35
  const app = Fastify({
@@ -38,10 +46,15 @@ export async function createServer(deps, options = {}) {
38
46
  });
39
47
  });
40
48
  registerProjectRoutes(app, { projectService: deps.projectService });
49
+ registerHarnessRoutes(app, {
50
+ projectService: deps.projectService,
51
+ harnessService: deps.harnessService
52
+ });
41
53
  registerTaskRoutes(app, {
42
54
  projectService: deps.projectService,
43
55
  taskService: deps.taskService,
44
- statusService: deps.statusService
56
+ statusService: deps.statusService,
57
+ sessionService: deps.sessionService
45
58
  });
46
59
  registerSessionRoutes(app, {
47
60
  projectService: deps.projectService,
@@ -58,7 +71,13 @@ export async function createServer(deps, options = {}) {
58
71
  taskService: deps.taskService,
59
72
  messageService: deps.messageService
60
73
  });
74
+ registerTranslationRoutes(app, {
75
+ projectService: deps.projectService,
76
+ taskService: deps.taskService,
77
+ translationService: deps.translationService
78
+ });
61
79
  registerTerminalWs(app, { runtime: deps.runtime });
80
+ registerTranslationWs(app, { translationService: deps.translationService });
62
81
  if (options.staticDir) {
63
82
  await app.register(fastifyStatic, {
64
83
  root: options.staticDir,
@@ -90,10 +109,12 @@ export function createDefaultServerDeps(options = {}) {
90
109
  const runner = createCommandRunner();
91
110
  const git = createGitAdapter(runner);
92
111
  const claude = createClaudeAdapter(runner);
112
+ const appSettings = createAppSettingsService({ fs });
93
113
  const runtime = createNodePtyTerminalRuntime({ fs });
94
114
  const registry = createSessionRegistry();
95
115
  const artifactService = createArtifactService(fs);
96
- const projectService = createProjectService({ fs, git, claude });
116
+ const harnessService = createHarnessService({ fs });
117
+ const projectService = createProjectService({ fs, git, claude, appSettings });
97
118
  const taskService = createTaskService({ fs, git, artifactService, projectService });
98
119
  const sessionService = createSessionService({
99
120
  fs,
@@ -123,14 +144,24 @@ export function createDefaultServerDeps(options = {}) {
123
144
  sessionService,
124
145
  taskService
125
146
  });
147
+ const translationService = createTranslationService({
148
+ runtime,
149
+ sessionRegistry: registry,
150
+ transcripts: createClaudeTranscriptService(),
151
+ sessionService,
152
+ appSettings,
153
+ provider: createOpenAiCompatibleTranslationProvider()
154
+ });
126
155
  return {
127
156
  projectService,
128
157
  taskService,
129
158
  sessionService,
130
159
  artifactService,
160
+ harnessService,
131
161
  commandDispatcher,
132
162
  messageService,
133
163
  statusService,
164
+ translationService,
134
165
  runtime
135
166
  };
136
167
  }
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+ import { homedir } from "node:os";
3
+ const MAX_RECENT_REPOSITORIES = 5;
4
+ export function createAppSettingsService(deps) {
5
+ const settingsPath = deps.settingsPath ?? path.join(homedir(), ".vcm", "settings.json");
6
+ const legacySettingsPath = deps.legacySettingsPath
7
+ ?? path.join(homedir(), ".vibe-coding-master", "settings.json");
8
+ const legacyTranslationPath = deps.legacyTranslationPath
9
+ ?? path.join(homedir(), ".vibe-coding-master", "translation.json");
10
+ let cachedSettings = null;
11
+ async function loadSettings() {
12
+ if (cachedSettings) {
13
+ return cachedSettings;
14
+ }
15
+ let raw = {};
16
+ let shouldSave = false;
17
+ if (await deps.fs.pathExists(settingsPath)) {
18
+ raw = await deps.fs.readJson(settingsPath);
19
+ }
20
+ else if (await deps.fs.pathExists(legacySettingsPath)) {
21
+ raw = await deps.fs.readJson(legacySettingsPath);
22
+ shouldSave = true;
23
+ }
24
+ else {
25
+ shouldSave = true;
26
+ }
27
+ if (!raw.translation && await deps.fs.pathExists(legacyTranslationPath)) {
28
+ raw = {
29
+ ...raw,
30
+ translation: normalizeTranslationConfig(await deps.fs.readJson(legacyTranslationPath))
31
+ };
32
+ shouldSave = true;
33
+ }
34
+ cachedSettings = normalizeSettingsFile(raw);
35
+ if (shouldSave) {
36
+ await saveSettings(cachedSettings);
37
+ }
38
+ return cachedSettings;
39
+ }
40
+ async function saveSettings(settings) {
41
+ cachedSettings = settings;
42
+ await deps.fs.writeJsonAtomic(settingsPath, settings);
43
+ }
44
+ return {
45
+ loadSettings,
46
+ async updateTranslationConfig(config) {
47
+ const current = await loadSettings();
48
+ const translation = normalizeTranslationConfig(config) ?? { settings: {}, secrets: {} };
49
+ await saveSettings({
50
+ ...current,
51
+ translation
52
+ });
53
+ return translation;
54
+ },
55
+ async getTranslationConfig() {
56
+ return (await loadSettings()).translation;
57
+ },
58
+ async getRecentRepositoryPaths() {
59
+ return (await loadSettings()).recentRepositoryPaths;
60
+ },
61
+ async recordRecentRepositoryPath(repoRoot) {
62
+ const normalizedPath = repoRoot.trim();
63
+ if (!normalizedPath) {
64
+ return (await loadSettings()).recentRepositoryPaths;
65
+ }
66
+ const current = await loadSettings();
67
+ const recentRepositoryPaths = normalizeRecentRepositoryPaths([
68
+ normalizedPath,
69
+ ...current.recentRepositoryPaths
70
+ ]);
71
+ await saveSettings({
72
+ ...current,
73
+ recentRepositoryPaths
74
+ });
75
+ return recentRepositoryPaths;
76
+ },
77
+ getSettingsPath() {
78
+ return settingsPath;
79
+ }
80
+ };
81
+ }
82
+ function normalizeSettingsFile(input) {
83
+ return {
84
+ version: 1,
85
+ translation: normalizeTranslationConfig(input.translation),
86
+ recentRepositoryPaths: normalizeRecentRepositoryPaths(input.recentRepositoryPaths)
87
+ };
88
+ }
89
+ function normalizeTranslationConfig(input) {
90
+ if (!input || typeof input !== "object") {
91
+ return undefined;
92
+ }
93
+ const candidate = input;
94
+ const rawSettings = isObject(candidate.settings) ? candidate.settings : {};
95
+ const rawSecrets = isObject(candidate.secrets) ? candidate.secrets : {};
96
+ const { apiKey: settingsApiKey, ...settings } = rawSettings;
97
+ const apiKey = rawSecrets.apiKey ?? settingsApiKey;
98
+ return {
99
+ settings,
100
+ secrets: {
101
+ ...rawSecrets,
102
+ ...(apiKey !== undefined ? { apiKey } : {})
103
+ }
104
+ };
105
+ }
106
+ function normalizeRecentRepositoryPaths(input) {
107
+ const paths = Array.isArray(input) ? input : [];
108
+ const seen = new Set();
109
+ const normalized = [];
110
+ for (const value of paths) {
111
+ if (typeof value !== "string") {
112
+ continue;
113
+ }
114
+ const repoPath = value.trim();
115
+ if (!repoPath || seen.has(repoPath)) {
116
+ continue;
117
+ }
118
+ seen.add(repoPath);
119
+ normalized.push(repoPath);
120
+ if (normalized.length >= MAX_RECENT_REPOSITORIES) {
121
+ break;
122
+ }
123
+ }
124
+ return normalized;
125
+ }
126
+ function isObject(value) {
127
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
128
+ }
@@ -3,13 +3,14 @@ import { DISPATCHABLE_ROLES, ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
- import { renderArchitecturePlanTemplate, renderImplementationLogTemplate, renderReviewReportTemplate, renderValidationLogTemplate } from "../templates/handoff.js";
6
+ import { renderArchitecturePlanTemplate, renderDocsSyncReportTemplate, renderImplementationLogTemplate, renderReviewReportTemplate, renderValidationLogTemplate } from "../templates/handoff.js";
7
7
  import { renderRoleCommandTemplate } from "../templates/role-command.js";
8
8
  const ARTIFACT_PATH_KEYS = [
9
9
  ["architecture-plan", "architecturePlanPath"],
10
10
  ["implementation-log", "implementationLogPath"],
11
11
  ["validation-log", "validationLogPath"],
12
- ["review-report", "reviewReportPath"]
12
+ ["review-report", "reviewReportPath"],
13
+ ["docs-sync-report", "docsSyncReportPath"]
13
14
  ];
14
15
  const ROLE_COMMAND_PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|status:\s*draft)\s*(\n|$)/i;
15
16
  export function createArtifactService(fs) {
@@ -35,7 +36,8 @@ export function createArtifactService(fs) {
35
36
  architecturePlanPath: path.posix.join(handoffDir, "architecture-plan.md"),
36
37
  implementationLogPath: path.posix.join(handoffDir, "implementation-log.md"),
37
38
  validationLogPath: path.posix.join(handoffDir, "validation-log.md"),
38
- reviewReportPath: path.posix.join(handoffDir, "review-report.md")
39
+ reviewReportPath: path.posix.join(handoffDir, "review-report.md"),
40
+ docsSyncReportPath: path.posix.join(handoffDir, "docs-sync-report.md")
39
41
  };
40
42
  },
41
43
  async ensureHandoffStructure(input) {
@@ -54,7 +56,8 @@ export function createArtifactService(fs) {
54
56
  [paths.architecturePlanPath, renderArchitecturePlanTemplate(input.taskSlug)],
55
57
  [paths.implementationLogPath, renderImplementationLogTemplate(input.taskSlug)],
56
58
  [paths.validationLogPath, renderValidationLogTemplate(input.taskSlug)],
57
- [paths.reviewReportPath, renderReviewReportTemplate(input.taskSlug)]
59
+ [paths.reviewReportPath, renderReviewReportTemplate(input.taskSlug)],
60
+ [paths.docsSyncReportPath, renderDocsSyncReportTemplate(input.taskSlug)]
58
61
  ];
59
62
  const created = [];
60
63
  for (const [artifactPath, content] of files) {