vibe-coding-master 0.8.13 → 0.9.1

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 (44) hide show
  1. package/README.md +7 -3
  2. package/dist/backend/adapters/claude-adapter.js +2 -1
  3. package/dist/backend/cli/install-vcm-harness.js +66 -16
  4. package/dist/backend/gateway/gateway-service.js +5 -3
  5. package/dist/backend/runtime/terminal-submit.js +1 -0
  6. package/dist/backend/server.js +14 -4
  7. package/dist/backend/services/app-settings-service.js +3 -2
  8. package/dist/backend/services/auto-memory-service.js +4 -3
  9. package/dist/backend/services/claude-hook-service.js +6 -0
  10. package/dist/backend/services/claude-transcript-service.js +2 -13
  11. package/dist/backend/services/claude-version-check.js +87 -0
  12. package/dist/backend/services/gate-review-service.js +46 -8
  13. package/dist/backend/services/harness-service.js +41 -20
  14. package/dist/backend/services/message-service.js +30 -6
  15. package/dist/backend/services/project-service.js +8 -2
  16. package/dist/backend/services/runtime-recovery-service.js +36 -9
  17. package/dist/backend/services/session-service.js +10 -19
  18. package/dist/backend/services/task-close-service.js +1 -0
  19. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  20. package/dist/backend/services/workflow-control-service.js +55 -11
  21. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  22. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  23. package/dist/backend/templates/harness/gate-review.js +676 -643
  24. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  25. package/dist/backend/templates/harness/project-owned-history.js +21 -0
  26. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  27. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  28. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  29. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  30. package/dist/shared/types/app-settings.js +31 -0
  31. package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
  32. package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
  33. package/dist-frontend/index.html +2 -2
  34. package/package.json +1 -1
  35. package/scripts/harness-tools/check-durable-docs +332 -298
  36. package/scripts/harness-tools/generate-module-index +1047 -512
  37. package/scripts/harness-tools/generate-public-surface +1231 -1069
  38. package/scripts/harness-tools/package.json +3 -0
  39. package/scripts/harness-tools/run-long-check +608 -531
  40. package/scripts/harness-tools/vcm-artifact +181 -113
  41. package/scripts/harness-tools/vcm-bash-guard +839 -605
  42. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  43. package/scripts/harness-tools/watch-job +313 -238
  44. package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
@@ -1,6 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { promisify } from "node:util";
4
+ import { DEFAULT_ARCHITECT_WORKER_SETTINGS, normalizeArchitectWorkerSettings } from "../../shared/types/app-settings.js";
5
+ import { applyArchitectWorkerSetting } from "../templates/harness/architect-worker-frontmatter.js";
4
6
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
5
7
  import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
6
8
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
@@ -55,8 +57,8 @@ const LEGACY_CODEX_HARNESS_PATHS = [
55
57
  const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
56
58
  const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
57
59
  const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
58
- const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
59
- const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; python3 "$guard"'`;
60
+ const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; node "$guard" || exit 0'`;
61
+ const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; node "$guard"'`;
60
62
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
61
63
  const VCM_AUTO_MEMORY_ENABLED = false;
62
64
  const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
@@ -256,21 +258,24 @@ const HARNESS_FILES = [
256
258
  kind: "agent-architect-evidence-worker",
257
259
  path: ".claude/agents/vcm-architect-evidence-worker.md",
258
260
  title: "VCM Architect Evidence Worker Agent",
259
- frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { tools: "Read, Grep, Glob, Write", model: "opus", effort: "xhigh" }),
261
+ frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { tools: "Read, Grep, Glob, Write" }),
262
+ architectWorkerType: "evidence",
260
263
  renderRules: renderArchitectEvidenceWorkerHarnessRules
261
264
  },
262
265
  {
263
266
  kind: "agent-architect-scaffold-worker",
264
267
  path: ".claude/agents/vcm-architect-scaffold-worker.md",
265
268
  title: "VCM Architect Scaffold Worker Agent",
266
- frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation.", { model: "opus", effort: "xhigh" }),
269
+ frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation."),
270
+ architectWorkerType: "scaffold",
267
271
  renderRules: renderArchitectScaffoldWorkerHarnessRules
268
272
  },
269
273
  {
270
274
  kind: "agent-architect-validation-worker",
271
275
  path: ".claude/agents/vcm-architect-validation-worker.md",
272
276
  title: "VCM Architect Validation Worker Agent",
273
- frontmatter: renderAgentFrontmatter("vcm-architect-validation-worker", "Foreground Architect worker for exact assigned command execution and evidence capture.", { tools: "Read, Grep, Glob, Bash, Write", model: "opus", effort: "xhigh" }),
277
+ frontmatter: renderAgentFrontmatter("vcm-architect-validation-worker", "Foreground Architect worker for exact assigned command execution and evidence capture.", { tools: "Read, Grep, Glob, Bash, Write" }),
278
+ architectWorkerType: "validation",
274
279
  renderRules: renderArchitectValidationWorkerHarnessRules
275
280
  },
276
281
  {
@@ -358,13 +363,18 @@ const HARNESS_FILES = [
358
363
  export function createHarnessService(deps) {
359
364
  const now = deps.now ?? (() => new Date().toISOString());
360
365
  const vcmVersion = deps.vcmVersion ?? "unknown";
366
+ async function resolveArchitectWorkerSettings() {
367
+ return deps.getArchitectWorkerSettings
368
+ ? normalizeArchitectWorkerSettings(await deps.getArchitectWorkerSettings())
369
+ : DEFAULT_ARCHITECT_WORKER_SETTINGS;
370
+ }
361
371
  const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(deps.fs, {
362
372
  runner: deps.commandRunner
363
373
  });
364
374
  return {
365
375
  async getHarnessStatus(repoRoot) {
366
376
  const [analyses, codeIntelligence] = await Promise.all([
367
- analyzeHarnessFiles(deps.fs, repoRoot),
377
+ analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
368
378
  codeIntelligenceDetector.detect(repoRoot)
369
379
  ]);
370
380
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
@@ -401,7 +411,7 @@ export function createHarnessService(deps) {
401
411
  }
402
412
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
403
413
  const [analyses, codeIntelligence] = await Promise.all([
404
- analyzeHarnessFiles(deps.fs, repoRoot),
414
+ analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
405
415
  codeIntelligenceDetector.detect(repoRoot)
406
416
  ]);
407
417
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
@@ -417,7 +427,9 @@ export function createHarnessService(deps) {
417
427
  async applyHarness(repoRoot) {
418
428
  await assertHarnessWorktreeClean(deps.git, repoRoot);
419
429
  if (deps.runFixedInstaller) {
420
- const result = await deps.runFixedInstaller(repoRoot);
430
+ const result = await deps.runFixedInstaller(repoRoot, {
431
+ architectWorkers: await resolveArchitectWorkerSettings()
432
+ });
421
433
  if (result.changedFiles.length > 0) {
422
434
  await bumpHarnessRevision(deps.fs, repoRoot, now());
423
435
  }
@@ -429,7 +441,7 @@ export function createHarnessService(deps) {
429
441
  message: formatHarnessApplyMessage(committed.harnessCommit, result.changedFiles.length > 0)
430
442
  };
431
443
  }
432
- const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
444
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
433
445
  const changedFiles = [];
434
446
  for (const analysis of analyses) {
435
447
  if (analysis.status.action === "ok" || !analysis.nextContent) {
@@ -1191,15 +1203,15 @@ function extractManagedBlock(definition, content) {
1191
1203
  }
1192
1204
  return content.match(getManagedBlockPattern(definition))?.[0];
1193
1205
  }
1194
- async function analyzeHarnessFiles(fs, repoRoot) {
1206
+ async function analyzeHarnessFiles(fs, repoRoot, workerSettings = DEFAULT_ARCHITECT_WORKER_SETTINGS) {
1195
1207
  const analyses = [];
1196
1208
  for (const definition of HARNESS_FILES) {
1197
- analyses.push(await analyzeHarnessFile(fs, repoRoot, definition));
1209
+ analyses.push(await analyzeHarnessFile(fs, repoRoot, definition, workerSettings));
1198
1210
  }
1199
1211
  analyses.push(await analyzeClaudeSettingsFile(fs, repoRoot));
1200
1212
  return analyses;
1201
1213
  }
1202
- async function analyzeHarnessFile(fs, repoRoot, definition) {
1214
+ async function analyzeHarnessFile(fs, repoRoot, definition, workerSettings) {
1203
1215
  const absolutePath = resolveHarnessPath(repoRoot, definition.path);
1204
1216
  if (definition.ownership === "project-file") {
1205
1217
  const exists = await fs.pathExists(absolutePath);
@@ -1233,7 +1245,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1233
1245
  : getManagedBlockPattern(definition);
1234
1246
  const exists = await fs.pathExists(absolutePath);
1235
1247
  if (!exists) {
1236
- const newContent = expectedContent ?? renderNewHarnessFile(definition, expectedBlock ?? "");
1248
+ const newContent = expectedContent ?? applyWorkerSetting(renderNewHarnessFile(definition, expectedBlock ?? ""), definition, workerSettings);
1237
1249
  return {
1238
1250
  definition,
1239
1251
  status: {
@@ -1281,7 +1293,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1281
1293
  const migratedContent = migrateLegacyHarnessFile(definition, currentContent, expectedBlock);
1282
1294
  if (migratedContent) {
1283
1295
  const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(migratedContent) : migratedContent;
1284
- const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition);
1296
+ const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition, workerSettings);
1285
1297
  return {
1286
1298
  definition,
1287
1299
  status: {
@@ -1300,7 +1312,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1300
1312
  };
1301
1313
  }
1302
1314
  const insertedContent = `${currentContent.trimEnd()}\n\n${expectedBlock}\n`;
1303
- const nextContent = normalizeAgentFrontmatter(insertedContent, definition);
1315
+ const nextContent = normalizeAgentFrontmatter(insertedContent, definition, workerSettings);
1304
1316
  return {
1305
1317
  definition,
1306
1318
  status: {
@@ -1322,7 +1334,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
1322
1334
  const currentBlock = match[0];
1323
1335
  const blockUpdatedContent = currentContent.replace(managedBlockPattern, expectedBlock);
1324
1336
  const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(blockUpdatedContent) : blockUpdatedContent;
1325
- const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition);
1337
+ const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition, workerSettings);
1326
1338
  const action = currentContent === nextContent ? "ok" : "update";
1327
1339
  return {
1328
1340
  definition,
@@ -1488,11 +1500,17 @@ function ensureAgentAllowedTool(content, requiredTool) {
1488
1500
  : frontmatter.replace(/^disallowedTools:\s*.*\r?\n?/m, "");
1489
1501
  return content.replace(frontmatter, nextFrontmatter);
1490
1502
  }
1491
- function normalizeAgentFrontmatter(content, definition) {
1503
+ function normalizeAgentFrontmatter(content, definition, workerSettings) {
1492
1504
  const allowedToolsUpdated = (definition.requiredTools ?? []).reduce(ensureAgentAllowedTool, content);
1493
1505
  const disallowedToolsUpdated = ensureAgentDisallowedTools(allowedToolsUpdated, definition.requiredDisallowedTools);
1494
1506
  const skillsUpdated = (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
1495
- return (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
1507
+ const removedSkillsUpdated = (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
1508
+ return applyWorkerSetting(removedSkillsUpdated, definition, workerSettings);
1509
+ }
1510
+ function applyWorkerSetting(content, definition, workerSettings) {
1511
+ return definition.architectWorkerType
1512
+ ? applyArchitectWorkerSetting(content, workerSettings[definition.architectWorkerType])
1513
+ : content;
1496
1514
  }
1497
1515
  function ensureAgentSkill(content, requiredSkill) {
1498
1516
  const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
@@ -2088,9 +2106,12 @@ Summarize files reviewed, files updated, generated artifacts, durable-doc audit
2088
2106
  [/VCM HARNESS BOOTSTRAP]`;
2089
2107
  }
2090
2108
  export function createScriptFixedHarnessInstaller(scriptPath = path.resolve(process.cwd(), "scripts/install-vcm-harness.mjs")) {
2091
- return async (repoRoot) => {
2109
+ return async (repoRoot, options = {}) => {
2092
2110
  try {
2093
- const { stdout, stderr } = await execFileAsync(process.execPath, [scriptPath, repoRoot], {
2111
+ const args = options.architectWorkers
2112
+ ? [scriptPath, repoRoot, "--architect-workers", JSON.stringify(options.architectWorkers)]
2113
+ : [scriptPath, repoRoot];
2114
+ const { stdout, stderr } = await execFileAsync(process.execPath, args, {
2094
2115
  cwd: process.cwd(),
2095
2116
  maxBuffer: 10 * 1024 * 1024
2096
2117
  });
@@ -47,11 +47,9 @@ export function createMessageService(deps) {
47
47
  const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
48
48
  const results = [];
49
49
  for (const routeFile of candidates) {
50
+ // dispatchRouteFile persists its own message snapshots.
50
51
  const result = await dispatchRouteFile(input, routeFile, state, timestamp);
51
52
  results.push(result);
52
- if (result.message) {
53
- await appendMessageSnapshot(deps.fs, input, result.message);
54
- }
55
53
  if (result.delivered) {
56
54
  break;
57
55
  }
@@ -136,12 +134,25 @@ export function createMessageService(deps) {
136
134
  ...message,
137
135
  deliveredAt: timestamp
138
136
  };
137
+ let deliveryRecorded = false;
139
138
  try {
140
139
  await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered), {
141
- enterDelayMs: autoDispatchEnterDelayMs
140
+ enterDelayMs: autoDispatchEnterDelayMs,
141
+ // Record delivery before Enter: a restart after this point must treat the
142
+ // dispatch as possibly executed instead of delivering it again.
143
+ beforeSubmit: async () => {
144
+ await appendMessageSnapshot(deps.fs, input, delivered);
145
+ deliveryRecorded = true;
146
+ }
142
147
  });
143
148
  }
144
149
  catch (error) {
150
+ if (deliveryRecorded) {
151
+ await appendMessageSnapshot(deps.fs, input, {
152
+ ...message,
153
+ failureReason: `Terminal submission failed: ${errorMessage(error)}`
154
+ });
155
+ }
145
156
  if (routeFile.fromRole === PM_ROLE) {
146
157
  await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
147
158
  }
@@ -523,11 +534,24 @@ async function readLatestMessages(fs, messagesPath) {
523
534
  const latest = new Map();
524
535
  const lines = (await fs.readText(messagesPath)).split(/\r?\n/).filter(Boolean);
525
536
  for (const line of lines) {
526
- const message = JSON.parse(line);
527
- latest.set(message.id, message);
537
+ const message = parseMessageLine(line);
538
+ // A line truncated by a crash or full disk must not block every later read;
539
+ // runtime recovery removes it and reports a warning.
540
+ if (message) {
541
+ latest.set(message.id, message);
542
+ }
528
543
  }
529
544
  return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
530
545
  }
546
+ export function parseMessageLine(line) {
547
+ try {
548
+ const message = JSON.parse(line);
549
+ return message && typeof message.id === "string" ? message : undefined;
550
+ }
551
+ catch {
552
+ return undefined;
553
+ }
554
+ }
531
555
  async function appendMessageSnapshot(fs, input, message) {
532
556
  await fs.appendText(getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), `${JSON.stringify(message)}\n`);
533
557
  }
@@ -33,7 +33,7 @@ export function createProjectService(deps) {
33
33
  await deps.fs.ensureDir(path.join(this.getProjectDataRoot(repoRoot), "tasks"));
34
34
  await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
35
35
  await this.saveConfig(config, true);
36
- currentProject = await readConnectedProjectSummary(repoRoot, config);
36
+ currentProject = await readConnectedProjectSummary(repoRoot, config, { refreshClaudeVersion: true });
37
37
  await deps.appSettings.recordRecentRepositoryPath(repoRoot);
38
38
  return currentProject;
39
39
  },
@@ -97,8 +97,14 @@ export function createProjectService(deps) {
97
97
  return path.dirname(deps.appSettings.getProjectConfigPath(repoRoot));
98
98
  }
99
99
  };
100
- async function readConnectedProjectSummary(repoRoot, config) {
100
+ async function readConnectedProjectSummary(repoRoot, config, options = {}) {
101
101
  const warnings = [];
102
+ const claudeVersionWarning = await deps.claudeVersionCheck?.getWarning(config.claudeCommand, {
103
+ refresh: options.refreshClaudeVersion
104
+ });
105
+ if (claudeVersionWarning) {
106
+ warnings.push(claudeVersionWarning);
107
+ }
102
108
  let branch = "unknown";
103
109
  let isDirty = false;
104
110
  let headCommit = "unknown";
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
3
+ import { parseMessageLine } from "./message-service.js";
3
4
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
4
5
  const TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
5
6
  const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
@@ -29,10 +30,16 @@ export function createRuntimeRecoveryService(deps) {
29
30
  }
30
31
  for (const task of tasks.filter((candidate) => candidate.cleanupStatus !== "cleaned")) {
31
32
  const taskRepoRoot = getTaskRuntimeRepoRoot(task);
32
- await runStep(context, `recover task ${task.taskSlug}`, async () => {
33
- await recoverTaskSessions(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
34
- const roundRecovered = await recoverRound(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
35
- await recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
33
+ const label = `recover task ${task.taskSlug}`;
34
+ // Each step runs independently so one failing record cannot skip the
35
+ // remaining recovery for the task.
36
+ await runStep(context, `${label} sessions`, () => recoverTaskSessions(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context));
37
+ let roundRecovered = false;
38
+ await runStep(context, `${label} round`, async () => {
39
+ roundRecovered = await recoverRound(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
40
+ });
41
+ await runStep(context, `${label} messages`, () => recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context));
42
+ await runStep(context, `${label} workflow`, async () => {
36
43
  const workflowRecovered = await deps.workflowControlService?.recoverTask({
37
44
  taskRepoRoot,
38
45
  stateRoot: config.stateRoot,
@@ -42,10 +49,16 @@ export function createRuntimeRecoveryService(deps) {
42
49
  if (workflowRecovered) {
43
50
  context.changedPaths.add(path.join(config.stateRoot, "workflow-control.json"));
44
51
  }
45
- await recoverGateReview(taskRepoRoot, recoveredAt, context);
46
- await recoverCoderWorkers(taskRepoRoot, task.taskSlug, context);
52
+ });
53
+ await runStep(context, `${label} gate review`, () => recoverGateReview(taskRepoRoot, recoveredAt, context));
54
+ await runStep(context, `${label} coder workers`, () => recoverCoderWorkers(taskRepoRoot, task.taskSlug, context));
55
+ await runStep(context, `${label} architect restart`, async () => {
47
56
  await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
57
+ });
58
+ await runStep(context, `${label} restart with context`, async () => {
48
59
  await deps.roleContextRestartService?.recoverTask(repoRoot, task.taskSlug);
60
+ });
61
+ await runStep(context, `${label} status`, async () => {
49
62
  if ((roundRecovered || task.status === "running") && !hasLiveTaskSession(task.taskSlug)) {
50
63
  await deps.taskService.updateTaskStatus(repoRoot, task.taskSlug, "stopped");
51
64
  }
@@ -188,8 +201,17 @@ export function createRuntimeRecoveryService(deps) {
188
201
  }
189
202
  const lines = (await deps.fs.readText(absolutePath)).split(/\r?\n/).filter((line) => line.trim());
190
203
  let changed = false;
191
- const messages = lines.map((line) => {
192
- const message = JSON.parse(line);
204
+ const parsed = [];
205
+ for (const [index, line] of lines.entries()) {
206
+ const message = parseMessageLine(line);
207
+ if (message) {
208
+ parsed.push(message);
209
+ continue;
210
+ }
211
+ changed = true;
212
+ context.warnings.push(`${relativePath}: removed unparsable message log line ${index + 1}.`);
213
+ }
214
+ const messages = parsed.map((message) => {
193
215
  if (message.dispatchingAt && !message.acceptedAt) {
194
216
  const next = {
195
217
  ...message,
@@ -208,7 +230,12 @@ export function createRuntimeRecoveryService(deps) {
208
230
  return;
209
231
  }
210
232
  const content = messages.length ? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n` : "";
211
- await deps.fs.writeText(absolutePath, content);
233
+ if (deps.fs.writeTextAtomic) {
234
+ await deps.fs.writeTextAtomic(absolutePath, content);
235
+ }
236
+ else {
237
+ await deps.fs.writeText(absolutePath, content);
238
+ }
212
239
  context.changedPaths.add(relativePath);
213
240
  void timestamp;
214
241
  }
@@ -5,7 +5,6 @@ import { isCodexBridgeSessionModel } from "../../shared/types/session.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { resolveRepoPath } from "../adapters/filesystem.js";
7
7
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
8
- import { claudeTranscriptPath } from "./claude-transcript-service.js";
9
8
  import { readHarnessRevisionState } from "./harness-revision.js";
10
9
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
11
10
  import { roleUsesLsp, prepareTaskLspPlugin, renderArchitectLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
@@ -77,11 +76,9 @@ export function createSessionService(deps) {
77
76
  });
78
77
  }
79
78
  const claudeSessionId = resumeClaudeSessionId ?? "";
80
- const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
81
- ? persisted.transcriptPath
82
- : resumeClaudeSessionId
83
- ? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
84
- : undefined;
79
+ // Transcript paths come from Claude Code hook payloads; VCM does not derive
80
+ // them from Claude Code's project-directory encoding.
81
+ const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
85
82
  const lspStatus = roleUsesLsp(role) && deps.codeIntelligenceDetector
86
83
  ? await deps.codeIntelligenceDetector.detect(taskRepoRoot, { refresh: true })
87
84
  : undefined;
@@ -222,9 +219,7 @@ export function createSessionService(deps) {
222
219
  // session genuinely starts at repoRoot.
223
220
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
224
221
  const claudeSessionId = resumeClaudeSessionId ?? "";
225
- const transcriptPath = resumeClaudeSessionId
226
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
227
- : undefined;
222
+ const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
228
223
  const startCommand = {
229
224
  ...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
230
225
  cwd: launchCwd
@@ -338,9 +333,7 @@ export function createSessionService(deps) {
338
333
  // restored cwd so the `/cd` migrate below fires only on an actual switch.
339
334
  const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
340
335
  const claudeSessionId = resumeClaudeSessionId ?? "";
341
- const transcriptPath = resumeClaudeSessionId
342
- ? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
343
- : undefined;
336
+ const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
344
337
  const startCommand = {
345
338
  ...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
346
339
  cwd: launchCwd
@@ -597,9 +590,7 @@ export function createSessionService(deps) {
597
590
  updatedAt: timestamp,
598
591
  lastOutputAt: runtimeSession.lastOutputAt,
599
592
  exitCode: runtimeSession.exitCode,
600
- transcriptPath: session.claudeSessionId
601
- ? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
602
- : session.transcriptPath
593
+ transcriptPath: session.transcriptPath
603
594
  };
604
595
  deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
605
596
  await persistProjectScopedToolSession(repoRoot, resumed);
@@ -1275,6 +1266,10 @@ function toRoleSessionRecordView(record, runtime) {
1275
1266
  };
1276
1267
  }
1277
1268
  export function matchesRoleHookSession(record, input) {
1269
+ if (record.runtimeSessionToken
1270
+ && record.runtimeSessionToken !== input.runtimeSessionToken) {
1271
+ return false;
1272
+ }
1278
1273
  if (input.runtimeSessionId) {
1279
1274
  return record.id === input.runtimeSessionId;
1280
1275
  }
@@ -1283,10 +1278,6 @@ export function matchesRoleHookSession(record, input) {
1283
1278
  && input.eventName !== "UserPromptSubmit") {
1284
1279
  return false;
1285
1280
  }
1286
- if (record.runtimeSessionToken
1287
- && record.runtimeSessionToken !== input.runtimeSessionToken) {
1288
- return false;
1289
- }
1290
1281
  if (!record.claudeSessionId && !record.transcriptPath) {
1291
1282
  return input.eventName === "UserPromptSubmit";
1292
1283
  }
@@ -5,6 +5,7 @@ export function createTaskCloseService(deps) {
5
5
  async closeTask(repoRoot, taskSlug) {
6
6
  const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
7
7
  const warnings = [];
8
+ deps.gateReviewService?.stopTask(taskSlug);
8
9
  if (deps.architectRestartService) {
9
10
  await bestEffort("Unable to clear Architect restart state", () => deps.architectRestartService.clear(repoRoot, taskSlug), warnings);
10
11
  }
@@ -1,4 +1,4 @@
1
- import { isVcmRoleName } from "../../shared/constants.js";
1
+ import { isReviewerRoleName, isVcmRoleName } from "../../shared/constants.js";
2
2
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
3
3
  export function createTerminalProcessExitService(deps) {
4
4
  let unsubscribe;
@@ -12,6 +12,9 @@ export function createTerminalProcessExitService(deps) {
12
12
  status: event.session.status === "crashed" ? "crashed" : "exited",
13
13
  exitCode: event.exitCode
14
14
  });
15
+ if (recorded && isReviewerRoleName(recorded.record.role)) {
16
+ deps.gateReviewService?.recordReviewerTurnEnded(recorded.record.taskSlug, "process-exit");
17
+ }
15
18
  if (!recorded?.turnWasRunning || !isVcmRoleName(recorded.record.role)) {
16
19
  return;
17
20
  }
@@ -26,13 +26,46 @@ export function createWorkflowControlService(deps) {
26
26
  const now = deps.now ?? (() => new Date().toISOString());
27
27
  const id = deps.id ?? (() => `wfauth_${randomUUID()}`);
28
28
  const locks = new Map();
29
- async function getState(input) {
29
+ // Locked callers only: replays the commit journal and persists the template or
30
+ // reconstructed runtime state.
31
+ function getState(input) {
32
+ return loadState(input, true);
33
+ }
34
+ // Unlocked public reads: computes the same view without writing any file.
35
+ function readStateView(input) {
36
+ return loadState(input, false);
37
+ }
38
+ async function readProgressView(input) {
39
+ const pending = await readPendingCommitJournal(input);
40
+ if (pending)
41
+ return parseWorkflowProgress(pending.progressContent, input.taskSlug);
42
+ if (!(await deps.fs.pathExists(progressPath(input)))) {
43
+ return parseWorkflowProgress(renderWorkflowProgressTemplate(input.taskSlug), input.taskSlug);
44
+ }
45
+ return readProgress(deps.fs, input);
46
+ }
47
+ async function loadState(input, persist) {
30
48
  try {
31
- await recoverCommitJournal(input);
49
+ if (persist) {
50
+ await recoverCommitJournal(input);
51
+ }
52
+ else {
53
+ const pending = await readPendingCommitJournal(input);
54
+ if (pending) {
55
+ const state = normalizeState(pending.state, input.taskSlug, now());
56
+ const progress = parseWorkflowProgress(pending.progressContent, input.taskSlug);
57
+ return {
58
+ ...state,
59
+ warnings: [...state.warnings, ...stateProgressConsistencyWarnings(state, progress)]
60
+ };
61
+ }
62
+ }
32
63
  const hasProgress = await deps.fs.pathExists(progressPath(input));
33
64
  const hasState = await deps.fs.pathExists(statePath(input));
34
65
  if (!hasProgress && !hasState) {
35
- await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgressTemplate(input.taskSlug));
66
+ if (persist) {
67
+ await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgressTemplate(input.taskSlug));
68
+ }
36
69
  return emptyState(input.taskSlug, now());
37
70
  }
38
71
  if (!hasProgress) {
@@ -51,7 +84,9 @@ export function createWorkflowControlService(deps) {
51
84
  }
52
85
  if (!progress.proposal && progress.history.length > 0) {
53
86
  const recovered = await reconstructStateFromProgress(input, progress);
54
- await saveState(input, recovered);
87
+ if (persist) {
88
+ await saveState(input, recovered);
89
+ }
55
90
  return recovered;
56
91
  }
57
92
  return {
@@ -357,18 +392,17 @@ export function createWorkflowControlService(deps) {
357
392
  return true;
358
393
  });
359
394
  }
395
+ // Public reads never write; journal replay and state persistence happen only
396
+ // inside locked operations.
360
397
  return {
361
- getState,
362
- getProgress: async (input) => {
363
- await getState(input);
364
- return readProgress(deps.fs, input);
365
- },
398
+ getState: readStateView,
399
+ getProgress: readProgressView,
366
400
  requestUserInput,
367
401
  resolveUserInput,
368
402
  runWhileNotAwaitingUser,
369
403
  submitProgress,
370
- assertDocsArtifactAllowed,
371
- assertRouteAuthorized,
404
+ assertDocsArtifactAllowed: (input, kind, role) => withLock(statePath(input), () => assertDocsArtifactAllowed(input, kind, role)),
405
+ assertRouteAuthorized: (input) => withLock(statePath(input), () => assertRouteAuthorized(input)),
372
406
  claimDispatch,
373
407
  releaseDispatch,
374
408
  markDispatchUnconfirmed,
@@ -485,6 +519,16 @@ export function createWorkflowControlService(deps) {
485
519
  await deps.fs.writeJsonAtomic(transactionPath(input), { ...journal, status: "committed" });
486
520
  await removeTransactionJournal(transactionPath(input));
487
521
  }
522
+ async function readPendingCommitJournal(input) {
523
+ const target = transactionPath(input);
524
+ if (!(await deps.fs.pathExists(target)))
525
+ return undefined;
526
+ const journal = await deps.fs.readJson(target);
527
+ if (journal.version !== 1 || journal.taskSlug !== input.taskSlug) {
528
+ throw new Error("Workflow commit journal does not match the active task.");
529
+ }
530
+ return journal.status === "pending" ? journal : undefined;
531
+ }
488
532
  async function recoverCommitJournal(input) {
489
533
  const target = transactionPath(input);
490
534
  if (!(await deps.fs.pathExists(target)))
@@ -0,0 +1,33 @@
1
+ // Agent names of the Architect workers whose model and effort follow the
2
+ // architect worker settings. Shared by the fixed installer and harness status.
3
+ export const ARCHITECT_WORKER_AGENT_TYPES = {
4
+ "vcm-architect-evidence-worker": "evidence",
5
+ "vcm-architect-scaffold-worker": "scaffold",
6
+ "vcm-architect-validation-worker": "validation"
7
+ };
8
+ export function applyArchitectWorkerSetting(content, setting) {
9
+ const withModel = setAgentFrontmatterField(content, "model", setting.model, /^(?:tools|disallowedTools):.*$/m);
10
+ return setAgentFrontmatterField(withModel, "effort", setting.effort === "default" ? undefined : setting.effort, /^model:.*$/m);
11
+ }
12
+ function setAgentFrontmatterField(content, field, value, insertAfter) {
13
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
14
+ if (!frontmatterMatch) {
15
+ return content;
16
+ }
17
+ const frontmatter = frontmatterMatch[0];
18
+ const fieldLine = new RegExp(`^${field}:.*$`, "m");
19
+ let next;
20
+ if (value === undefined) {
21
+ next = frontmatter.replace(new RegExp(`^${field}:.*(?:\\r?\\n)?`, "m"), "");
22
+ }
23
+ else if (fieldLine.test(frontmatter)) {
24
+ next = frontmatter.replace(fieldLine, `${field}: ${value}`);
25
+ }
26
+ else {
27
+ const anchor = frontmatter.match(insertAfter);
28
+ next = anchor
29
+ ? frontmatter.replace(anchor[0], `${anchor[0]}\n${field}: ${value}`)
30
+ : frontmatter.replace(/\r?\n---$/, `\n${field}: ${value}\n---`);
31
+ }
32
+ return content.replace(frontmatter, next);
33
+ }