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.
- package/README.md +10 -16
- package/dist/backend/adapters/claude-adapter.js +4 -5
- package/dist/backend/role-tool-policy.js +11 -10
- package/dist/backend/runtime/node-pty-runtime.js +39 -8
- package/dist/backend/server.js +2 -5
- package/dist/backend/services/gate-review-service.js +28 -5
- package/dist/backend/services/harness-service.js +6 -23
- package/dist/backend/services/message-service.js +5 -0
- package/dist/backend/services/role-context-restart-service.js +28 -4
- package/dist/backend/services/route-message-evidence.js +83 -0
- package/dist/backend/services/session-service.js +2 -19
- package/dist/backend/services/workflow-control-service.js +9 -3
- package/dist/backend/templates/harness/architect-agent.js +8 -11
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +42 -35
- package/dist/backend/templates/harness/project-manager-agent.js +2 -2
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +13 -9
- package/dist-frontend/assets/{index-CjM9OqZh.js → index-Bsd4k8YI.js} +35 -35
- package/dist-frontend/assets/{index-Bs4g_1yF.css → index-CRUqb0PX.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/verify-package.mjs +0 -2
- package/dist/backend/services/code-intelligence-service.js +0 -255
- package/dist/backend/services/lsp-plugin.js +0 -64
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +0 -88
- 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
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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,
|
|
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
|
|
24
|
-
const args =
|
|
25
|
-
|
|
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
|
|
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 = [...
|
|
35
|
+
export const CODE_ROLE_DISALLOWED_TOOLS = [...FRONTMATTER_DISALLOWED_TOOLS];
|
|
36
36
|
export const REVIEWER_DISALLOWED_TOOLS = [
|
|
37
37
|
"Agent",
|
|
38
38
|
"Edit",
|
|
39
|
-
...
|
|
39
|
+
...FRONTMATTER_DISALLOWED_TOOLS
|
|
40
40
|
];
|
|
41
|
-
// The evidence worker reads and
|
|
42
|
-
// or delegates.
|
|
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
|
-
...
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 = {
|
package/dist/backend/server.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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))
|