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