vibe-coding-master 0.9.4 → 0.9.6

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 (26) hide show
  1. package/README.md +10 -16
  2. package/dist/backend/adapters/claude-adapter.js +4 -5
  3. package/dist/backend/role-tool-policy.js +11 -10
  4. package/dist/backend/runtime/node-pty-runtime.js +39 -8
  5. package/dist/backend/server.js +2 -5
  6. package/dist/backend/services/gate-review-service.js +28 -5
  7. package/dist/backend/services/harness-service.js +6 -23
  8. package/dist/backend/services/message-service.js +5 -0
  9. package/dist/backend/services/role-context-restart-service.js +28 -4
  10. package/dist/backend/services/route-message-evidence.js +83 -0
  11. package/dist/backend/services/session-service.js +2 -19
  12. package/dist/backend/services/workflow-control-service.js +9 -3
  13. package/dist/backend/templates/harness/architect-agent.js +8 -11
  14. package/dist/backend/templates/harness/architect-evidence-worker-agent.js +3 -3
  15. package/dist/backend/templates/harness/gate-review.js +42 -35
  16. package/dist/backend/templates/harness/project-manager-agent.js +2 -2
  17. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +13 -9
  18. package/dist-frontend/assets/{index-CjM9OqZh.js → index-Bsd4k8YI.js} +35 -35
  19. package/dist-frontend/assets/{index-Bs4g_1yF.css → index-CRUqb0PX.css} +1 -1
  20. package/dist-frontend/index.html +2 -2
  21. package/package.json +1 -1
  22. package/scripts/verify-package.mjs +0 -2
  23. package/dist/backend/services/code-intelligence-service.js +0 -255
  24. package/dist/backend/services/lsp-plugin.js +0 -64
  25. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +0 -88
  26. package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +0 -273
package/README.md CHANGED
@@ -511,22 +511,16 @@ Use it to:
511
511
  - merge task harness commits back to the connected repository branch when
512
512
  appropriate
513
513
 
514
- VCM bundles the Claude Code LSP bridge and loads it for Architect and Reviewer
515
- sessions, including Codex Bridge launches; Architect's evidence worker can use it
516
- too. While a language server is still indexing, VCM holds LSP queries until it
517
- is ready and fails them explicitly if indexing does not finish, instead of
518
- returning partial results. The project environment must still
519
- provide the language server for each detected language: `rust-analyzer`,
520
- `typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
521
- `jdtls`. Architect preloads `vcm-code-navigation` and uses LSP for semantic
522
- relationships. Accessors, backing fields, trait items, implementations, wrappers,
523
- and aliases are queried as separate symbols; a bounded source-text fallback may
524
- only locate candidates after the correct LSP query is demonstrably partial.
525
- At the architecture-plan gate, Reviewer checks that every decision-bearing
526
- relationship cites its evidence source and re-queries it with LSP. Other roles
527
- use generated context and ordinary source reads. Harness Studio
528
- reports whether the server executable and plugin can run; the role Session
529
- performs the real workspace indexing and semantic query retries.
514
+ VCM roles navigate code without language servers, so a task does not hold a
515
+ semantic index in memory per session. Architect preloads `vcm-code-navigation`
516
+ and establishes relationships with exact source-text search over the whole
517
+ project source tree, searching every spelling that can reach a symbol and
518
+ reading every match. Accessors, backing fields, trait items, implementations,
519
+ wrappers, and aliases are searched as separate symbols, and each relationship
520
+ records the search that produced it. At the architecture-plan gate, Reviewer
521
+ checks that every decision-bearing relationship cites its evidence source and
522
+ reruns the recorded search. Other roles use generated context and ordinary
523
+ source reads.
530
524
 
531
525
  Harness Engineer is task-scoped and runs from the active task worktree. The
532
526
  backend automatically starts a fresh Harness Engineer for each active task or
@@ -1,4 +1,4 @@
1
- import { roleRuntimeDisallowedTools, roleUsesLsp } from "../role-tool-policy.js";
1
+ import { roleRuntimeDisallowedTools, roleUsesRuntimeDenyList } from "../role-tool-policy.js";
2
2
  import { isCodexBridgeSessionModel } from "../../shared/types/session.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  export function createClaudeAdapter(runner) {
@@ -20,10 +20,9 @@ export function createClaudeAdapter(runner) {
20
20
  }
21
21
  return result.stdout.trim();
22
22
  },
23
- buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt, pluginDirs = []) {
24
- const args = pluginDirs.flatMap((pluginDir) => ["--plugin-dir", pluginDir]);
25
- const lspEnabled = roleUsesLsp(role) && pluginDirs.length > 0;
26
- if (lspEnabled) {
23
+ buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt) {
24
+ const args = [];
25
+ if (roleUsesRuntimeDenyList(role)) {
27
26
  args.push("--disallowedTools", roleRuntimeDisallowedTools(role).join(","));
28
27
  }
29
28
  else {
@@ -1,4 +1,4 @@
1
- const LSP_FRONTMATTER_DISALLOWED_TOOLS = [
1
+ const FRONTMATTER_DISALLOWED_TOOLS = [
2
2
  "NotebookEdit",
3
3
  "WebFetch",
4
4
  "WebSearch"
@@ -32,19 +32,19 @@ const NON_VCM_ROLE_RUNTIME_TOOLS = [
32
32
  "WebSearch",
33
33
  "Workflow"
34
34
  ];
35
- export const CODE_ROLE_DISALLOWED_TOOLS = [...LSP_FRONTMATTER_DISALLOWED_TOOLS];
35
+ export const CODE_ROLE_DISALLOWED_TOOLS = [...FRONTMATTER_DISALLOWED_TOOLS];
36
36
  export const REVIEWER_DISALLOWED_TOOLS = [
37
37
  "Agent",
38
38
  "Edit",
39
- ...LSP_FRONTMATTER_DISALLOWED_TOOLS
39
+ ...FRONTMATTER_DISALLOWED_TOOLS
40
40
  ];
41
- // The evidence worker reads and queries LSP but never runs commands, edits code,
42
- // or delegates. A deny list keeps the parent session's LSP tool available.
41
+ // The evidence worker reads and searches but never runs commands, edits code,
42
+ // or delegates.
43
43
  export const EVIDENCE_WORKER_DISALLOWED_TOOLS = [
44
44
  "Agent",
45
45
  "Bash",
46
46
  "Edit",
47
- ...LSP_FRONTMATTER_DISALLOWED_TOOLS
47
+ ...FRONTMATTER_DISALLOWED_TOOLS
48
48
  ];
49
49
  export const CODE_ROLE_RUNTIME_DISALLOWED_TOOLS = [...NON_VCM_ROLE_RUNTIME_TOOLS];
50
50
  export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
@@ -52,10 +52,11 @@ export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
52
52
  "Edit",
53
53
  ...NON_VCM_ROLE_RUNTIME_TOOLS
54
54
  ];
55
- // Architect makes semantic code claims; Reviewer verifies them at Gate Review.
56
- const LSP_ROLE_NAMES = new Set(["architect", "reviewer"]);
57
- export function roleUsesLsp(role) {
58
- return LSP_ROLE_NAMES.has(role);
55
+ // Architect and Reviewer Agent frontmatter uses a deny list, so their launch
56
+ // also removes the runtime tools no VCM workflow role may use.
57
+ const RUNTIME_DENY_LIST_ROLE_NAMES = new Set(["architect", "reviewer"]);
58
+ export function roleUsesRuntimeDenyList(role) {
59
+ return RUNTIME_DENY_LIST_ROLE_NAMES.has(role);
59
60
  }
60
61
  export function roleRuntimeDisallowedTools(role) {
61
62
  if (role === "reviewer") {
@@ -46,7 +46,7 @@ export function createNodePtyTerminalRuntime(deps) {
46
46
  process: child,
47
47
  listeners: new Set(),
48
48
  logWriter,
49
- replayBuffer: "",
49
+ replayBuffer: new TerminalReplayBuffer(),
50
50
  disposed: false
51
51
  };
52
52
  entries.set(session.id, entry);
@@ -55,7 +55,7 @@ export function createNodePtyTerminalRuntime(deps) {
55
55
  return;
56
56
  }
57
57
  entry.session.lastOutputAt = now();
58
- entry.replayBuffer = appendTerminalReplay(entry.replayBuffer, data);
58
+ entry.replayBuffer.append(data);
59
59
  entry.logWriter.append(data);
60
60
  emit(entry, {
61
61
  sessionId: session.id,
@@ -150,7 +150,8 @@ export function createNodePtyTerminalRuntime(deps) {
150
150
  subscribe(sessionId, listener, options = {}) {
151
151
  const entry = getEntry(entries, sessionId);
152
152
  entry.listeners.add(listener);
153
- if (options.replay !== false && entry.replayBuffer) {
153
+ const replay = options.replay !== false ? entry.replayBuffer.text() : "";
154
+ if (replay) {
154
155
  listener({
155
156
  id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
156
157
  sessionId,
@@ -158,7 +159,7 @@ export function createNodePtyTerminalRuntime(deps) {
158
159
  role: entry.session.role,
159
160
  type: "output",
160
161
  timestamp: now(),
161
- data: entry.replayBuffer
162
+ data: replay
162
163
  });
163
164
  }
164
165
  else if (options.replay !== false && entry.input.logPath) {
@@ -257,11 +258,41 @@ export function tailTerminalReplay(data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT
257
258
  const firstLineBreak = tail.indexOf("\n");
258
259
  return firstLineBreak >= 0 ? tail.slice(firstLineBreak + 1) : tail;
259
260
  }
260
- export function appendTerminalReplay(current, data, limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
261
- if (!data) {
262
- return current;
261
+ // Keeps the recent terminal output for subscriber replay. PTY output arrives in
262
+ // many small chunks, so appending measures only the new chunk and drops whole
263
+ // old chunks; the exact tail is cut only when a subscriber asks for it.
264
+ export class TerminalReplayBuffer {
265
+ limitBytes;
266
+ chunks = [];
267
+ head = 0;
268
+ bytes = 0;
269
+ constructor(limitBytes = TERMINAL_REPLAY_TAIL_LIMIT_BYTES) {
270
+ this.limitBytes = limitBytes;
271
+ }
272
+ append(data) {
273
+ if (!data) {
274
+ return;
275
+ }
276
+ const bytes = Buffer.byteLength(data, "utf8");
277
+ this.chunks.push({ data, bytes });
278
+ this.bytes += bytes;
279
+ // Drop a chunk only while the rest still covers the limit.
280
+ while (this.head < this.chunks.length - 1 && this.bytes - this.chunks[this.head].bytes >= this.limitBytes) {
281
+ this.bytes -= this.chunks[this.head].bytes;
282
+ this.head += 1;
283
+ }
284
+ if (this.head > 1024 && this.head * 2 > this.chunks.length) {
285
+ this.chunks = this.chunks.slice(this.head);
286
+ this.head = 0;
287
+ }
288
+ }
289
+ text() {
290
+ if (this.head >= this.chunks.length) {
291
+ return "";
292
+ }
293
+ const joined = this.chunks.slice(this.head).map((chunk) => chunk.data).join("");
294
+ return tailTerminalReplay(joined, this.limitBytes);
263
295
  }
264
- return tailTerminalReplay(`${current}${data}`, limitBytes);
265
296
  }
266
297
  export function buildPtyEnvironment(baseEnv, inputEnv = {}) {
267
298
  const env = {
@@ -35,7 +35,6 @@ import { createJobGuardService } from "./services/job-guard-service.js";
35
35
  import { createProjectService } from "./services/project-service.js";
36
36
  import { createSessionRegistry } from "./runtime/session-registry.js";
37
37
  import { createSessionService } from "./services/session-service.js";
38
- import { createHarnessCodeIntelligenceDetector } from "./services/code-intelligence-service.js";
39
38
  import { createMessageService } from "./services/message-service.js";
40
39
  import { createRoundService } from "./services/round-service.js";
41
40
  import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
@@ -258,7 +257,6 @@ export function createDefaultServerDeps(options = {}) {
258
257
  });
259
258
  const taskService = createTaskService({ fs, git, artifactService, projectService });
260
259
  const taskWorkflowService = createTaskWorkflowService({ fs });
261
- const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(fs, { runner });
262
260
  const sessionService = createSessionService({
263
261
  fs,
264
262
  runtime,
@@ -268,13 +266,11 @@ export function createDefaultServerDeps(options = {}) {
268
266
  projectService,
269
267
  taskService,
270
268
  taskWorkflowService,
271
- codeIntelligenceDetector,
272
269
  codexBridgeIntegration,
273
270
  apiUrl: options.apiUrl
274
271
  });
275
272
  const harnessService = createHarnessService({
276
273
  fs,
277
- commandRunner: runner,
278
274
  git,
279
275
  runtime,
280
276
  harnessEngineerSessions: sessionService,
@@ -320,7 +316,8 @@ export function createDefaultServerDeps(options = {}) {
320
316
  fs,
321
317
  projectService,
322
318
  taskService,
323
- sessionService
319
+ sessionService,
320
+ workflowControlService
324
321
  });
325
322
  const messageService = createMessageService({
326
323
  fs,
@@ -7,6 +7,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
7
7
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
8
8
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
9
9
  import { parseGateReviewReportArtifact } from "./managed-artifact-validation.js";
10
+ import { collectFlowRouteMessages, renderRouteMessages } from "./route-message-evidence.js";
10
11
  const REVIEWER_AGENT_PATH = ".claude/agents/reviewer.md";
11
12
  const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
12
13
  const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
@@ -39,10 +40,7 @@ const CODE_DIFF_SOURCE_ARTIFACTS = {
39
40
  ".ai/vcm/handoffs/architecture-plan.md",
40
41
  ".ai/vcm/handoffs/coder-completion.md"
41
42
  ],
42
- "architect-debug": [
43
- ".ai/vcm/handoffs/role-commands/architect.md",
44
- ".ai/vcm/handoffs/architect-debug.md"
45
- ],
43
+ "architect-debug": [".ai/vcm/handoffs/architect-debug.md"],
46
44
  "architect-diagnosis": [
47
45
  ".ai/vcm/handoffs/architecture-diagnosis.md"
48
46
  ]
@@ -438,6 +436,9 @@ export function createGateReviewService(deps) {
438
436
  const promptPath = path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
439
437
  const requestReportPath = reportPathForRequest(requestId);
440
438
  const inputSnapshots = await captureGateInputSnapshots(deps.fs, context.taskRepoRoot, requestId, getSourceArtifacts(gate, codeDiffSources));
439
+ if (gate === "code-diff" && codeDiffSources) {
440
+ inputSnapshots.push(await captureRouteMessageSnapshot(deps, context, requestId, codeDiffSources));
441
+ }
441
442
  const nextRecord = {
442
443
  ...record,
443
444
  status: "running",
@@ -1487,7 +1488,12 @@ function splitLines(value) {
1487
1488
  function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots = []) {
1488
1489
  const reportPath = reportPathForRequest(requestId);
1489
1490
  const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
1490
- const evidence = getSourceArtifacts(gate, codeDiffSources)
1491
+ const evidence = [
1492
+ ...getSourceArtifacts(gate, codeDiffSources),
1493
+ ...(gate === "code-diff"
1494
+ ? [`${path.posix.join(context.stateRoot, "messages", `${context.taskSlug}.jsonl`)} (PM route messages for the reviewed flow)`]
1495
+ : [])
1496
+ ]
1491
1497
  .map((relativePath) => `- ${relativePath}`)
1492
1498
  .join("\n");
1493
1499
  const capturedEvidence = inputSnapshots.length > 0
@@ -1561,6 +1567,23 @@ async function captureGateInputSnapshots(fs, taskRepoRoot, requestId, sourcePath
1561
1567
  }
1562
1568
  return snapshots;
1563
1569
  }
1570
+ // The reviewer judges authorization and scope against the flow's PM route
1571
+ // messages, captured like any other immutable Gate input. Tester commits are
1572
+ // part of every reviewed range, so routes to Tester are always included.
1573
+ async function captureRouteMessageSnapshot(deps, context, requestId, codeDiffSources) {
1574
+ const sourcePath = path.posix.join(context.stateRoot, "messages", `${context.taskSlug}.jsonl`);
1575
+ const roles = new Set([
1576
+ ...codeDiffSources.map((source) => source === "coder" ? "coder" : "architect"),
1577
+ "tester"
1578
+ ]);
1579
+ const flow = await collectFlowRouteMessages(deps.fs, deps.workflowControlService, context, roles);
1580
+ if (flow.messages.length === 0) {
1581
+ return { sourcePath, status: "missing" };
1582
+ }
1583
+ const snapshotPath = inputSnapshotPathForRequest(requestId, ".ai/vcm/messages/route-messages.md");
1584
+ await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, snapshotPath), renderRouteMessages(context.taskSlug, [`Code sources: ${codeDiffSources.join(" -> ")}`], flow));
1585
+ return { sourcePath, snapshotPath, status: "captured" };
1586
+ }
1564
1587
  function isTaskEvidencePath(sourcePath) {
1565
1588
  return sourcePath.startsWith(".ai/vcm/handoffs/")
1566
1589
  || sourcePath.startsWith(".ai/vcm/gate-reviews/");
@@ -38,8 +38,6 @@ import { renderResolveDurableDocAssignmentTool } from "../templates/harness/reso
38
38
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
39
39
  import { VcmError } from "../errors.js";
40
40
  import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
41
- import { createHarnessCodeIntelligenceDetector } from "./code-intelligence-service.js";
42
- import { lspUnavailableWarning } from "./lsp-plugin.js";
43
41
  const execFileAsync = promisify(execFile);
44
42
  const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
45
43
  const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
@@ -369,20 +367,14 @@ export function createHarnessService(deps) {
369
367
  ? normalizeArchitectWorkerSettings(await deps.getArchitectWorkerSettings())
370
368
  : DEFAULT_ARCHITECT_WORKER_SETTINGS;
371
369
  }
372
- const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(deps.fs, {
373
- runner: deps.commandRunner
374
- });
375
370
  return {
376
371
  async getHarnessStatus(repoRoot) {
377
- const [analyses, codeIntelligence] = await Promise.all([
378
- analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
379
- codeIntelligenceDetector.detect(repoRoot)
380
- ]);
372
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
381
373
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
382
374
  const manifestChange = deps.runFixedInstaller
383
375
  ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
384
376
  : undefined;
385
- return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange, codeIntelligence);
377
+ return renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange);
386
378
  },
387
379
  async getHarnessFileContent(repoRoot, filePath) {
388
380
  return readHarnessFileContent(deps.fs, repoRoot, filePath);
@@ -411,17 +403,14 @@ export function createHarnessService(deps) {
411
403
  harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "[VCM Harness] Update harness file")).harnessCommit;
412
404
  }
413
405
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
414
- const [analyses, codeIntelligence] = await Promise.all([
415
- analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
416
- codeIntelligenceDetector.detect(repoRoot)
417
- ]);
406
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
418
407
  const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
419
408
  const manifestChange = deps.runFixedInstaller
420
409
  ? await analyzeHarnessManifest(deps.fs, repoRoot, vcmVersion)
421
410
  : undefined;
422
411
  return {
423
412
  file,
424
- status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange, codeIntelligence),
413
+ status: renderHarnessStatus(await readHarnessRevisionValue(deps.fs, repoRoot), analyses, legacyChanges, manifestChange),
425
414
  harnessCommit
426
415
  };
427
416
  },
@@ -1393,7 +1382,7 @@ async function removeLegacyCodexHarnessPaths(fs, repoRoot) {
1393
1382
  }
1394
1383
  return changes;
1395
1384
  }
1396
- function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange, codeIntelligence) {
1385
+ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], manifestChange) {
1397
1386
  const files = analyses.map((analysis) => analysis.status);
1398
1387
  const plannedChanges = analyses
1399
1388
  .map((analysis) => analysis.plannedChange)
@@ -1419,13 +1408,7 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
1419
1408
  files,
1420
1409
  needsApply: plannedChanges.length > 0,
1421
1410
  plannedChanges,
1422
- codeIntelligence,
1423
- warnings: [
1424
- ...(plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []),
1425
- ...((codeIntelligence?.languages ?? [])
1426
- .filter((language) => language.state !== "server_runnable")
1427
- .map(lspUnavailableWarning))
1428
- ]
1411
+ warnings: plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []
1429
1412
  };
1430
1413
  }
1431
1414
  async function readHarnessRevisionValue(fs, repoRoot) {
@@ -527,6 +527,11 @@ function arraysEqual(left, right) {
527
527
  }
528
528
  return left.every((value, index) => value === right[index]);
529
529
  }
530
+ // Reads the task's message log outside the service's task lock, for consumers
531
+ // that only need a point-in-time view, such as Gate Review evidence capture.
532
+ export function readTaskMessages(fs, input) {
533
+ return readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
534
+ }
530
535
  async function readLatestMessages(fs, messagesPath) {
531
536
  if (!(await fs.pathExists(messagesPath))) {
532
537
  return [];
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { VCM_ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { resolveRepoPath } from "../adapters/filesystem.js";
4
4
  import { toVcmError, VcmError } from "../errors.js";
5
+ import { collectFlowRouteMessages, renderRouteMessages } from "./route-message-evidence.js";
5
6
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
6
7
  const STATE_DIR = "restart-with-context";
7
8
  export function createRoleContextRestartService(deps) {
@@ -126,7 +127,11 @@ export function createRoleContextRestartService(deps) {
126
127
  ]);
127
128
  const handoffDir = task.handoffDir;
128
129
  const stateRoot = config.stateRoot;
129
- const files = roleContextFiles(role, handoffDir, stateRoot);
130
+ const routeMessagesPath = await writeRouteMessages(repoRoot, task, stateRoot, role);
131
+ const files = [
132
+ ...(routeMessagesPath ? [routeMessagesPath] : []),
133
+ ...roleContextFiles(role, handoffDir, stateRoot)
134
+ ];
130
135
  return {
131
136
  system: [
132
137
  `This fresh ${role} session continues the current VCM task after Restart With Context.`,
@@ -140,6 +145,28 @@ export function createRoleContextRestartService(deps) {
140
145
  user: `[VCM RESTART WITH CONTEXT]\nRestore the current ${role} work from the listed task artifacts and continue the accepted assignment.`
141
146
  };
142
147
  }
148
+ // A delivery role's assignment lives only in the PM route messages, so render
149
+ // the current flow run's messages to that role for the fresh session.
150
+ async function writeRouteMessages(repoRoot, task, stateRoot, role) {
151
+ if (role !== "architect" && role !== "coder" && role !== "tester") {
152
+ return undefined;
153
+ }
154
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
155
+ const context = {
156
+ repoRoot,
157
+ taskRepoRoot,
158
+ stateRoot,
159
+ handoffDir: task.handoffDir,
160
+ taskSlug: task.taskSlug
161
+ };
162
+ const flow = await collectFlowRouteMessages(deps.fs, deps.workflowControlService, context, new Set([role]));
163
+ if (flow.messages.length === 0) {
164
+ return undefined;
165
+ }
166
+ const relativePath = path.posix.join(stateRoot, STATE_DIR, `${role}-route-messages.md`);
167
+ await deps.fs.writeText(resolveRepoPath(taskRepoRoot, relativePath), renderRouteMessages(task.taskSlug, [`Role: ${role}`], flow));
168
+ return relativePath;
169
+ }
143
170
  async function persist(repoRoot, state) {
144
171
  await deps.fs.writeJsonAtomic(await statePath(repoRoot, state.taskSlug, state.role), state);
145
172
  pending.set(restartKey(repoRoot, state.taskSlug, state.role), state);
@@ -205,7 +232,6 @@ function roleContextFiles(role, handoffDir, stateRoot) {
205
232
  ];
206
233
  case "architect":
207
234
  return [
208
- `${handoffDir}/role-commands/architect.md`,
209
235
  `${handoffDir}/architecture-brief.md`,
210
236
  `${handoffDir}/architecture-evidence.md`,
211
237
  `${handoffDir}/planning-progress.md`,
@@ -216,14 +242,12 @@ function roleContextFiles(role, handoffDir, stateRoot) {
216
242
  ];
217
243
  case "coder":
218
244
  return [
219
- `${handoffDir}/role-commands/coder.md`,
220
245
  `${handoffDir}/architecture-plan.md`,
221
246
  `${handoffDir}/coder-completion.md`,
222
247
  ".ai/vcm/coder-workers/"
223
248
  ];
224
249
  case "tester":
225
250
  return [
226
- `${handoffDir}/role-commands/tester.md`,
227
251
  `${handoffDir}/architecture-plan.md`,
228
252
  `${handoffDir}/coder-completion.md`,
229
253
  `${handoffDir}/test-report.md`
@@ -0,0 +1,83 @@
1
+ import { readTaskMessages } from "./message-service.js";
2
+ // PM route messages are the operative instructions for a role: the dispatch
3
+ // that started the current flow run and every later relay of a user decision.
4
+ // They exist only in the task message log, so callers render this slice of it.
5
+ export async function collectFlowRouteMessages(fs, workflowControlService, context, roles) {
6
+ const messages = await readTaskMessages(fs, {
7
+ repoRoot: context.repoRoot,
8
+ stateRepoRoot: context.taskRepoRoot,
9
+ stateRoot: context.stateRoot,
10
+ taskSlug: context.taskSlug
11
+ });
12
+ const flowRunStart = await findFlowRunStart(workflowControlService, context);
13
+ const acceptedPmMessages = messages
14
+ .filter((message) => message.fromRole === "project-manager" && message.acceptedAt)
15
+ .sort((left, right) => left.acceptedAt.localeCompare(right.acceptedAt));
16
+ // A PM message is accepted immediately before Workflow Progress confirms its
17
+ // dispatch, so the flow's starting message is the last one accepted at or
18
+ // before the first confirmation of the flow run.
19
+ const windowStart = flowRunStart
20
+ ? acceptedPmMessages.filter((message) => message.acceptedAt <= flowRunStart).at(-1)?.acceptedAt ?? flowRunStart
21
+ : undefined;
22
+ return {
23
+ windowStart,
24
+ messages: messages
25
+ .filter((message) => message.fromRole === "user"
26
+ ? !windowStart || message.createdAt >= windowStart
27
+ : message.fromRole === "project-manager"
28
+ && roles.has(message.toRole)
29
+ && Boolean(message.acceptedAt)
30
+ && (!windowStart || message.acceptedAt >= windowStart))
31
+ .sort((left, right) => messageTime(left).localeCompare(messageTime(right)))
32
+ };
33
+ }
34
+ export function renderRouteMessages(taskSlug, header, flow) {
35
+ return [
36
+ "# PM Route Messages",
37
+ "",
38
+ `Task: ${taskSlug}`,
39
+ ...header,
40
+ flow.windowStart
41
+ ? `Scope: messages since the current flow run started at ${flow.windowStart}`
42
+ : "Scope: the whole task message log; the current flow run start was unavailable",
43
+ "",
44
+ "PM route messages to the listed roles and user messages to PM, oldest first.",
45
+ "A later message overrides an earlier one where they conflict.",
46
+ ...flow.messages.flatMap((message, index) => [
47
+ "",
48
+ `## ${index + 1}. ${message.fromRole} -> ${message.toRole} (${message.type})`,
49
+ "",
50
+ `Message: ${message.id}`,
51
+ `Time: ${messageTime(message)}`,
52
+ ...(message.artifactRefs.length > 0 ? [`Artifacts: ${message.artifactRefs.join(", ")}`] : []),
53
+ "",
54
+ message.body.trimEnd()
55
+ ]),
56
+ ""
57
+ ].join("\n");
58
+ }
59
+ async function findFlowRunStart(workflowControlService, context) {
60
+ if (!workflowControlService) {
61
+ return undefined;
62
+ }
63
+ try {
64
+ const workflowContext = {
65
+ taskRepoRoot: context.taskRepoRoot,
66
+ stateRoot: context.stateRoot,
67
+ handoffDir: context.handoffDir,
68
+ taskSlug: context.taskSlug
69
+ };
70
+ const [state, progress] = await Promise.all([
71
+ workflowControlService.getState(workflowContext),
72
+ workflowControlService.getProgress(workflowContext)
73
+ ]);
74
+ const startedAtSequence = state.flowRun?.startedAtSequence;
75
+ return progress.history.find((entry) => entry.sequence === startedAtSequence)?.confirmedAt;
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ }
81
+ function messageTime(message) {
82
+ return message.acceptedAt ?? message.createdAt;
83
+ }
@@ -7,7 +7,6 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
7
7
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
8
8
  import { readHarnessRevisionState } from "./harness-revision.js";
9
9
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
10
- import { roleUsesLsp, prepareTaskLspPlugin, renderLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
11
10
  const TRANSLATOR_ROLE = "translator";
12
11
  const HARNESS_ENGINEER_ROLE = "harness-engineer";
13
12
  const TRANSLATION_DIR = ".ai/vcm/translations";
@@ -79,16 +78,8 @@ export function createSessionService(deps) {
79
78
  // Transcript paths come from Claude Code hook payloads; VCM does not derive
80
79
  // them from Claude Code's project-directory encoding.
81
80
  const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
82
- const lspStatus = roleUsesLsp(role) && deps.codeIntelligenceDetector
83
- ? await deps.codeIntelligenceDetector.detect(taskRepoRoot, { refresh: true })
84
- : undefined;
85
- const lspPluginDirs = roleUsesLsp(role)
86
- ? lspStatus ? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, lspStatus) : [VCM_LSP_PLUGIN_DIR]
87
- : [];
88
- const appendSystemPrompt = [input.appendSystemPrompt, lspStatus && renderLspNotice(lspStatus, role)]
89
- .filter(Boolean).join("\n\n") || undefined;
90
81
  const startCommand = {
91
- ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, appendSystemPrompt, lspPluginDirs),
82
+ ...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt),
92
83
  cwd: taskRepoRoot
93
84
  };
94
85
  const runtimeSessionToken = randomUUID();
@@ -107,15 +98,7 @@ export function createSessionService(deps) {
107
98
  VCM_ROLE: role,
108
99
  VCM_SESSION_ID: claudeSessionId || undefined,
109
100
  VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
110
- }, modelEnvironment, {
111
- ...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
112
- ...(lspPluginDirs.length > 0
113
- ? {
114
- ENABLE_LSP_TOOL: "true",
115
- ENABLE_TOOL_SEARCH: "false"
116
- }
117
- : {})
118
- }),
101
+ }, modelEnvironment, buildUsageTelemetryEnvironment(deps.apiUrl, role, model)),
119
102
  cols: input.cols,
120
103
  rows: input.rows
121
104
  });
@@ -890,10 +890,16 @@ async function allowedAfterTester(fs, input, state, currentFlow, codeSource, opt
890
890
  if (test.value === "incomplete" || test.infrastructure === "repair-required")
891
891
  return [`${currentFlow}/tester`];
892
892
  const validation = await gateState(fs, input, "validation-adequacy");
893
+ const unapprovedFailure = test.value === "fail" && !test.hasUserApprovedCoverageGap;
893
894
  if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
894
- return [`${currentFlow}/tester`];
895
- }
896
- if (test.value === "fail" && !test.hasUserApprovedCoverageGap) {
895
+ // Findings on a genuine failure may be Tester-owned (wrong tests or
896
+ // evidence) or need the production investigation that Tester Failure
897
+ // starts; PM routes by who can dispose of them.
898
+ return unapprovedFailure && options.testerFailureFlow
899
+ ? [`${currentFlow}/tester`, `${options.testerFailureFlow}/architect`]
900
+ : [`${currentFlow}/tester`];
901
+ }
902
+ if (unapprovedFailure) {
897
903
  return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
898
904
  }
899
905
  if (!gatePassedForDispatch(state, currentFlow, "tester", "validation-adequacy", validation))