vibe-coding-master 0.9.2 → 0.9.3

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 (36) hide show
  1. package/README.md +3 -2
  2. package/dist/backend/adapters/command-runner.js +1 -0
  3. package/dist/backend/adapters/filesystem.js +5 -2
  4. package/dist/backend/api/artifact-routes.js +0 -3
  5. package/dist/backend/api/task-routes.js +0 -1
  6. package/dist/backend/cli/install-vcm-harness.js +6 -0
  7. package/dist/backend/server.js +2 -0
  8. package/dist/backend/services/artifact-service.js +1 -16
  9. package/dist/backend/services/docs-sync-check.js +32 -0
  10. package/dist/backend/services/gate-review-service.js +10 -2
  11. package/dist/backend/services/lsp-plugin.js +6 -2
  12. package/dist/backend/services/managed-artifact-validation.js +2 -1
  13. package/dist/backend/services/session-service.js +1 -1
  14. package/dist/backend/services/status-service.js +0 -1
  15. package/dist/backend/services/translation-worker-service.js +44 -6
  16. package/dist/backend/services/workflow-control-service.js +103 -165
  17. package/dist/backend/templates/handoff.js +2 -48
  18. package/dist/backend/templates/harness/architect-agent.js +42 -23
  19. package/dist/backend/templates/harness/claude-root.js +9 -9
  20. package/dist/backend/templates/harness/coder-agent.js +0 -1
  21. package/dist/backend/templates/harness/gate-review.js +55 -12
  22. package/dist/backend/templates/harness/project-manager-agent.js +33 -27
  23. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +13 -14
  24. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +2 -2
  25. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +2 -2
  26. package/dist/shared/types/gate-review.js +4 -0
  27. package/dist/shared/types/workflow.js +0 -1
  28. package/dist/shared/validation/artifact-check.js +1 -27
  29. package/dist/shared/validation/artifact-contract.js +0 -2
  30. package/dist/shared/validation/artifact-registry.js +1 -18
  31. package/package.json +1 -1
  32. package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +5 -1
  33. package/scripts/harness-tools/check-docs-sync +246 -0
  34. package/scripts/harness-tools/check-durable-docs +24 -0
  35. package/scripts/harness-tools/run-long-check +3 -3
  36. package/scripts/verify-package.mjs +1 -0
package/README.md CHANGED
@@ -211,10 +211,11 @@ Typical flow:
211
211
 
212
212
  ```text
213
213
  Project Manager
214
- -> Architect
214
+ -> Architect (plan, scaffold, and architecture docs)
215
215
  -> Coder
216
216
  -> Tester
217
- -> Architect docs sync
217
+ -> validation-adequacy Gate Review
218
+ -> code-diff Gate Review (code and docs)
218
219
  -> Project Manager final acceptance
219
220
  ```
220
221
 
@@ -7,6 +7,7 @@ export function createCommandRunner() {
7
7
  cwd: options.cwd,
8
8
  env: options.env,
9
9
  timeout: options.timeoutMs,
10
+ input: options.input,
10
11
  reject: false
11
12
  });
12
13
  return {
@@ -70,13 +70,16 @@ export function createNodeFileSystemAdapter() {
70
70
  async readJson(targetPath) {
71
71
  let attempt = 0;
72
72
  for (;;) {
73
- const content = await runFileOperation(() => fs.readFile(targetPath, "utf8"));
74
73
  try {
74
+ const content = await runFileOperation(() => fs.readFile(targetPath, "utf8"));
75
75
  return JSON.parse(content);
76
76
  }
77
77
  catch (error) {
78
+ // A concurrent rename-over can leave the target briefly missing on
79
+ // some shared mounts, so ENOENT gets the same short retry as a
80
+ // partially written file. A file that stays missing still throws.
78
81
  const delayMs = JSON_READ_RETRY_DELAYS_MS[attempt];
79
- if (!(error instanceof SyntaxError) || delayMs === undefined) {
82
+ if (!(error instanceof SyntaxError || getErrorCode(error) === "ENOENT") || delayMs === undefined) {
80
83
  throw error;
81
84
  }
82
85
  attempt += 1;
@@ -147,9 +147,6 @@ function artifactNameToPath(paths, artifactName) {
147
147
  if (artifactName === "docs-update-report.md") {
148
148
  return paths.docsUpdateReportPath;
149
149
  }
150
- if (artifactName === "docs-sync-report.md") {
151
- return paths.docsSyncReportPath;
152
- }
153
150
  if (artifactName === "workflow-progress.md") {
154
151
  return paths.workflowProgressPath;
155
152
  }
@@ -226,7 +226,6 @@ function degradedArtifactSummary(handoffDir) {
226
226
  architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
227
227
  testReportPath: `${handoffDir}/test-report.md`,
228
228
  docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
229
- docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
230
229
  workflowProgressPath: `${handoffDir}/workflow-progress.md`,
231
230
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
232
231
  },
@@ -320,6 +320,12 @@ const WHOLE_FILES = [
320
320
  mode: 0o755,
321
321
  templatePath: "scripts/harness-tools/check-durable-docs"
322
322
  },
323
+ {
324
+ path: ".ai/tools/check-docs-sync",
325
+ category: "durable-docs-tool",
326
+ mode: 0o755,
327
+ templatePath: "scripts/harness-tools/check-docs-sync"
328
+ },
323
329
  {
324
330
  path: ".claude/skills/vcm-architecture-interview/SKILL.md",
325
331
  category: "skill",
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import Fastify from "fastify";
4
4
  import fastifyStatic from "@fastify/static";
5
+ import { createDocsCoverageCheck } from "./services/docs-sync-check.js";
5
6
  import { createArtifactService } from "./services/artifact-service.js";
6
7
  import { createClaudeAdapter } from "./adapters/claude-adapter.js";
7
8
  import { createCodexBridgeAdapter } from "./adapters/codex-bridge-adapter.js";
@@ -354,6 +355,7 @@ export function createDefaultServerDeps(options = {}) {
354
355
  sessionService,
355
356
  roundService,
356
357
  workflowControlService,
358
+ checkDocsCoverage: createDocsCoverageCheck(runner),
357
359
  onArchitecturePlanDisposition: ({ repoRoot, taskSlug, accepted }) => architectRestartService.recordArchitectureGateDisposition(repoRoot, taskSlug, accepted)
358
360
  });
359
361
  const translationWorkerService = createTranslationWorkerService({
@@ -4,7 +4,7 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
4
4
  import { getManagedArtifactDefinition, isArtifactKind, isManagedArtifactKind } from "../../shared/validation/artifact-registry.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
7
- import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsUpdateReportTemplate, renderDocsSyncReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
7
+ import { renderArchitectureBriefTemplate, renderArchitectureDiagnosisTemplate, renderArchitectureEvidenceTemplate, renderArchitecturePlanTemplate, renderArchitectDebugTemplate, renderCoderCompletionTemplate, renderDocsUpdateReportTemplate, renderFinalAcceptanceTemplate, renderKnownIssuesTemplate, renderPlanningProgressTemplate, renderMessageRouteTemplate, renderTestReportTemplate, renderWorkflowProgressTemplate } from "../templates/handoff.js";
8
8
  import { renderRoleCommandTemplate } from "../templates/role-command.js";
9
9
  import { isMemoryProposalSubmissionPath } from "./memory-review-paths.js";
10
10
  import { validateManagedArtifactContent } from "./managed-artifact-validation.js";
@@ -19,7 +19,6 @@ const ARTIFACT_PATH_KEYS = [
19
19
  ["architecture-diagnosis", "architectureDiagnosisPath"],
20
20
  ["test-report", "testReportPath"],
21
21
  ["docs-update-report", "docsUpdateReportPath"],
22
- ["docs-sync-report", "docsSyncReportPath"],
23
22
  ["workflow-progress", "workflowProgressPath"],
24
23
  ["final-acceptance", "finalAcceptancePath"]
25
24
  ];
@@ -57,7 +56,6 @@ export function createArtifactService(fs, deps = {}) {
57
56
  architectureDiagnosisPath: path.posix.join(handoffDir, "architecture-diagnosis.md"),
58
57
  testReportPath: path.posix.join(handoffDir, "test-report.md"),
59
58
  docsUpdateReportPath: path.posix.join(handoffDir, "docs-update-report.md"),
60
- docsSyncReportPath: path.posix.join(handoffDir, "docs-sync-report.md"),
61
59
  workflowProgressPath: path.posix.join(handoffDir, "workflow-progress.md"),
62
60
  finalAcceptancePath: path.posix.join(handoffDir, "final-acceptance.md")
63
61
  };
@@ -88,7 +86,6 @@ export function createArtifactService(fs, deps = {}) {
88
86
  [paths.architectureDiagnosisPath, renderArchitectureDiagnosisTemplate(input.taskSlug)],
89
87
  [paths.testReportPath, renderTestReportTemplate(input.taskSlug)],
90
88
  [paths.docsUpdateReportPath, renderDocsUpdateReportTemplate(input.taskSlug)],
91
- [paths.docsSyncReportPath, renderDocsSyncReportTemplate(input.taskSlug)],
92
89
  [paths.workflowProgressPath, renderWorkflowProgressTemplate(input.taskSlug)],
93
90
  [paths.finalAcceptancePath, renderFinalAcceptanceTemplate(input.taskSlug)],
94
91
  ...Object.values(paths.messageRoutePaths).map((messagePath) => [
@@ -230,18 +227,6 @@ export function createArtifactService(fs, deps = {}) {
230
227
  if (validation.errors.length > 0) {
231
228
  throw artifactRejected(input.kind, validation.errors);
232
229
  }
233
- if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
234
- && deps.workflowControlService) {
235
- // Memory-owned documentation work is independent of PM's active dispatch.
236
- if (!assignment) {
237
- await deps.workflowControlService.assertDocsArtifactAllowed({
238
- taskRepoRoot: input.repoRoot,
239
- stateRoot: input.stateRoot ?? ".ai/vcm",
240
- handoffDir: input.handoffDir,
241
- taskSlug: input.taskSlug
242
- }, input.kind, input.role);
243
- }
244
- }
245
230
  if (input.kind === "workflow-progress") {
246
231
  if (input.mode !== "final") {
247
232
  throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const DOCS_SYNC_TOOL = ".ai/tools/check-docs-sync";
4
+ const DOCS_SYNC_TIMEOUT_MS = 60_000;
5
+ // Runs the task worktree's check-docs-sync, which compares the task's commits
6
+ // with the architecture plan's Docs Impact, and returns one error per finding.
7
+ // A worktree whose harness predates the tool is not checked.
8
+ export function createDocsCoverageCheck(runner) {
9
+ return async (taskRepoRoot) => {
10
+ const tool = path.join(taskRepoRoot, DOCS_SYNC_TOOL);
11
+ if (!fs.existsSync(tool)) {
12
+ return [];
13
+ }
14
+ const result = await runner.run(process.execPath, [tool, "--json"], {
15
+ cwd: taskRepoRoot,
16
+ timeoutMs: DOCS_SYNC_TIMEOUT_MS
17
+ });
18
+ if (result.exitCode === 0) {
19
+ return [];
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(result.stdout);
23
+ if (result.exitCode === 1 && Array.isArray(parsed.findings) && parsed.findings.length > 0) {
24
+ return parsed.findings.map((finding) => `${finding.code} ${finding.subject}: ${finding.message}`);
25
+ }
26
+ }
27
+ catch {
28
+ // Fall through to the raw tool error below.
29
+ }
30
+ return [`${DOCS_SYNC_TOOL} could not check the task: ${(result.stderr || result.stdout).trim() || `exit ${result.exitCode}`}`];
31
+ };
32
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
3
+ import { CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX, CODE_DIFF_FINDING_SCOPES, CODE_DIFF_SOURCES, GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
4
4
  import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
@@ -379,6 +379,14 @@ export function createGateReviewService(deps) {
379
379
  message: prerequisiteError
380
380
  };
381
381
  }
382
+ const coverageFindings = deps.checkDocsCoverage
383
+ ? await deps.checkDocsCoverage(context.taskRepoRoot)
384
+ : [];
385
+ if (coverageFindings.length > 0) {
386
+ const message = `${CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX} ${coverageFindings.join(" ")}`;
387
+ index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message, codeDiffInput);
388
+ return { status: "failed_to_start", gate, record: index.gates[gate], message };
389
+ }
382
390
  }
383
391
  async function recordCodeDiffStartFailure(currentIndex, currentContext, currentGate, currentSource, message, currentCodeDiffInput) {
384
392
  const failedIndex = applyGateState(currentIndex, currentGate, {
@@ -1293,7 +1301,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
1293
1301
  }
1294
1302
  }
1295
1303
  if (gate === "validation-adequacy") {
1296
- const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**"];
1304
+ const evidencePathspec = ["--", ".", ":(exclude).ai/vcm/**", ":(exclude)docs/**", ":(exclude,glob)**/ARCHITECTURE.md", ":(exclude).ai/generated/**"];
1297
1305
  digest.update("trackedEvidence");
1298
1306
  digest.update(await commandStdout(deps.runner, taskRepoRoot, ["ls-files", "-s", ...evidencePathspec]));
1299
1307
  digest.update("workingEvidence");
@@ -41,7 +41,9 @@ export function lspUnavailableWarning(language) {
41
41
  : `Install or repair ${language.serverCommand} in the environment running the VCM backend${installCommands[language.serverCommand] ? ` (${installCommands[language.serverCommand]})` : ""} and ensure it is executable on PATH.`;
42
42
  return `LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`} ${recovery} Restart the LSP roles (Architect, Reviewer) after fixing the environment so VCM can detect and register the server again.`;
43
43
  }
44
- export function renderLspNotice(status) {
44
+ // Architect stops semantic work in an unavailable language; Reviewer completes
45
+ // the review and records those relationships as not LSP-verified.
46
+ export function renderLspNotice(status, role = "architect") {
45
47
  if (status.languages.length === 0)
46
48
  return undefined;
47
49
  const available = status.languages.filter((language) => language.state === "server_runnable");
@@ -54,7 +56,9 @@ export function renderLspNotice(status) {
54
56
  ] : []),
55
57
  ...unavailable.map(lspUnavailableWarning),
56
58
  ...(unavailable.length > 0 ? [
57
- "If your assignment requires a semantic query in an unavailable language, stop that work and report the environment prerequisite through the current communication channel. Do not substitute source text search or claim the relationship is verified. Continue only after the server is available."
59
+ role === "reviewer"
60
+ ? "For relationships in an unavailable language, complete the review by reading the complete candidate code, record each such relationship as not LSP-verified in the report, and name the missing server. Do not stop the review or treat text matches as verified relationships."
61
+ : "If your assignment requires a semantic query in an unavailable language, stop that work and report the environment prerequisite through the current communication channel. Do not substitute source text search or claim the relationship is verified. Continue only after the server is available."
58
62
  ] : [])
59
63
  ].join("\n");
60
64
  }
@@ -26,6 +26,7 @@ export const GATE_ANALYSIS_FIELDS = {
26
26
  "Invalidated Assumptions",
27
27
  "Existing-Class Completeness",
28
28
  "Relationship Evidence",
29
+ "Architecture Docs",
29
30
  "Ownership",
30
31
  "Data Flow",
31
32
  "Lifecycle",
@@ -60,7 +61,7 @@ export const GATE_ANALYSIS_FIELDS = {
60
61
  "State Lifecycle And Failure Paths",
61
62
  "Coding Standards",
62
63
  "Baseline Test Integrity",
63
- "Generated Context And Durable Docs",
64
+ "Documentation Fit",
64
65
  "Code Readiness"
65
66
  ]
66
67
  };
@@ -85,7 +85,7 @@ export function createSessionService(deps) {
85
85
  const lspPluginDirs = roleUsesLsp(role)
86
86
  ? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
87
87
  : [];
88
- const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus)]
88
+ const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus, role)]
89
89
  .filter(Boolean).join("\n\n") || undefined;
90
90
  const startCommand = {
91
91
  ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt, lspPluginDirs),
@@ -60,7 +60,6 @@ function degradedArtifactSummary(handoffDir) {
60
60
  architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
61
61
  testReportPath: `${handoffDir}/test-report.md`,
62
62
  docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
63
- docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
64
63
  workflowProgressPath: `${handoffDir}/workflow-progress.md`,
65
64
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
66
65
  },
@@ -22,6 +22,7 @@ const MEMORY_TOTAL_LIMIT_BYTES = 80 * 1024;
22
22
  const MEMORY_INITIALIZED_MIN_FILES = 2;
23
23
  const MEMORY_FILE_NAMES = ["glossary.md", "style-guide.md", "project-context.md", "decisions.md"];
24
24
  const TRANSLATOR_ROLE = "translator";
25
+ const QUEUE_HOOK_EVENTS = new Set(["UserPromptSubmit", "Stop", "StopFailure"]);
25
26
  const FILE_BROWSER_DEFAULT_LIMIT = 200;
26
27
  const FILE_BROWSER_MAX_LIMIT = 500;
27
28
  const FILE_BROWSER_SEARCH_MAX_DEPTH = 6;
@@ -99,6 +100,26 @@ const BINARY_LIKE_EXTENSIONS = new Set([
99
100
  export function createTranslationWorkerService(deps) {
100
101
  const now = deps.now ?? (() => new Date().toISOString());
101
102
  const createId = deps.id ?? (() => randomUUID());
103
+ const queueLocks = new Map();
104
+ // Every queue and index access happens in this process. Serializing them per
105
+ // repository keeps concurrent load-modify-save cycles from dropping each
106
+ // other's updates and keeps readers out of an in-flight rename.
107
+ function withQueueLock(repoRoot, operation) {
108
+ const key = path.resolve(repoRoot);
109
+ const previous = queueLocks.get(key) ?? Promise.resolve();
110
+ const result = previous.then(operation);
111
+ const settled = result.then(() => undefined, () => undefined);
112
+ queueLocks.set(key, settled);
113
+ void settled.then(() => {
114
+ if (queueLocks.get(key) === settled) {
115
+ queueLocks.delete(key);
116
+ }
117
+ });
118
+ return result;
119
+ }
120
+ function scheduleDispatch(repoRoot) {
121
+ void withQueueLock(repoRoot, () => dispatchNext(repoRoot)).catch(() => undefined);
122
+ }
102
123
  async function ensureLayout(repoRoot) {
103
124
  await Promise.all([
104
125
  deps.fs.ensureDir(resolveRepoPath(repoRoot, MEMORY_DIR)),
@@ -917,7 +938,7 @@ export function createTranslationWorkerService(deps) {
917
938
  queue.updatedAt = now();
918
939
  await saveQueue(repoRoot, queue);
919
940
  }
920
- return {
941
+ const service = {
921
942
  async cleanupStartupRuntime(repoRoot) {
922
943
  await removeRepoPath(repoRoot, TRANSLATIONS_RUNTIME_DIR, true);
923
944
  await cleanupStartupFileIndex(repoRoot);
@@ -1006,7 +1027,7 @@ export function createTranslationWorkerService(deps) {
1006
1027
  const activeExisting = (await loadRuntimeFileJobs(repoRoot)).find((job) => fileTranslationReplacementKey(job) === replacementKey &&
1007
1028
  isActiveFileTranslationJobStatus(job.status));
1008
1029
  if (activeExisting) {
1009
- void dispatchNext(repoRoot);
1030
+ scheduleDispatch(repoRoot);
1010
1031
  return activeExisting;
1011
1032
  }
1012
1033
  const timestamp = now();
@@ -1106,7 +1127,7 @@ export function createTranslationWorkerService(deps) {
1106
1127
  lastUpdatedAt: timestamp
1107
1128
  });
1108
1129
  await deps.fs.writeText(resolveRepoPath(repoRoot, job.reportPath), `# Translation Report\n\nStatus: queued\nJob: ${jobId}\n`);
1109
- void dispatchNext(repoRoot);
1130
+ scheduleDispatch(repoRoot);
1110
1131
  return job;
1111
1132
  },
1112
1133
  async createBootstrapRun(repoRoot, input) {
@@ -1165,7 +1186,7 @@ export function createTranslationWorkerService(deps) {
1165
1186
  index.runs = [run, ...index.runs];
1166
1187
  index.updatedAt = timestamp;
1167
1188
  await saveBootstrapIndex(repoRoot, index);
1168
- void dispatchNext(repoRoot);
1189
+ scheduleDispatch(repoRoot);
1169
1190
  return run;
1170
1191
  },
1171
1192
  async createMemoryUpdate(repoRoot, input) {
@@ -1223,7 +1244,7 @@ export function createTranslationWorkerService(deps) {
1223
1244
  "Delete or merge stale, duplicate, temporary, and task-local content."
1224
1245
  ]
1225
1246
  });
1226
- void dispatchNext(repoRoot);
1247
+ scheduleDispatch(repoRoot);
1227
1248
  return queueItem;
1228
1249
  },
1229
1250
  async readFileJobOutput(repoRoot, jobId) {
@@ -1297,7 +1318,7 @@ export function createTranslationWorkerService(deps) {
1297
1318
  });
1298
1319
  job.queueItemId = queueItem.id;
1299
1320
  if (!input.deferDispatch) {
1300
- void dispatchNext(repoRoot);
1321
+ scheduleDispatch(repoRoot);
1301
1322
  }
1302
1323
  return job;
1303
1324
  },
@@ -1410,6 +1431,23 @@ export function createTranslationWorkerService(deps) {
1410
1431
  });
1411
1432
  }
1412
1433
  };
1434
+ return {
1435
+ ...service,
1436
+ cleanupStartupRuntime: (repoRoot) => withQueueLock(repoRoot, () => service.cleanupStartupRuntime(repoRoot)),
1437
+ getState: (repoRoot, options) => withQueueLock(repoRoot, () => service.getState(repoRoot, options)),
1438
+ createFileJob: (repoRoot, input) => withQueueLock(repoRoot, () => service.createFileJob(repoRoot, input)),
1439
+ readFileJobOutput: (repoRoot, jobId) => withQueueLock(repoRoot, () => service.readFileJobOutput(repoRoot, jobId)),
1440
+ createBootstrapRun: (repoRoot, input) => withQueueLock(repoRoot, () => service.createBootstrapRun(repoRoot, input)),
1441
+ createMemoryUpdate: (repoRoot, input) => withQueueLock(repoRoot, () => service.createMemoryUpdate(repoRoot, input)),
1442
+ createConversationJob: (repoRoot, input) => withQueueLock(repoRoot, () => service.createConversationJob(repoRoot, input)),
1443
+ validateConversationResult: (repoRoot, input) => withQueueLock(repoRoot, () => service.validateConversationResult(repoRoot, input)),
1444
+ promoteFileJob: (repoRoot, jobId, targetPath) => withQueueLock(repoRoot, () => service.promoteFileJob(repoRoot, jobId, targetPath)),
1445
+ // Only the events that touch the queue wait for the lock, so a SessionStart
1446
+ // hook fired while a dispatch is starting the Translator never waits on it.
1447
+ handleTranslatorHook: (repoRoot, eventName, taskSlug) => QUEUE_HOOK_EVENTS.has(eventName)
1448
+ ? withQueueLock(repoRoot, () => service.handleTranslatorHook(repoRoot, eventName, taskSlug))
1449
+ : service.handleTranslatorHook(repoRoot, eventName, taskSlug)
1450
+ };
1413
1451
  }
1414
1452
  function requireTranslationTaskSlug(value) {
1415
1453
  const taskSlug = value?.trim();