vibe-coding-master 0.7.39 → 0.7.41

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 (51) hide show
  1. package/README.md +11 -6
  2. package/dist/backend/adapters/claude-adapter.js +8 -6
  3. package/dist/backend/api/artifact-routes.js +72 -1
  4. package/dist/backend/api/runtime-state-routes.js +17 -5
  5. package/dist/backend/api/task-routes.js +6 -0
  6. package/dist/backend/api/workflow-control-routes.js +56 -0
  7. package/dist/backend/cli/install-vcm-harness.js +42 -47
  8. package/dist/backend/role-tool-policy.js +60 -0
  9. package/dist/backend/server.js +21 -5
  10. package/dist/backend/services/artifact-service.js +309 -5
  11. package/dist/backend/services/auto-memory-service.js +2 -2
  12. package/dist/backend/services/gate-review-service.js +81 -33
  13. package/dist/backend/services/harness-feedback-service.js +11 -4
  14. package/dist/backend/services/harness-service.js +35 -45
  15. package/dist/backend/services/lsp-plugin.js +1 -4
  16. package/dist/backend/services/message-service.js +105 -37
  17. package/dist/backend/services/status-service.js +6 -0
  18. package/dist/backend/services/workflow-control-service.js +854 -0
  19. package/dist/backend/templates/handoff.js +158 -3
  20. package/dist/backend/templates/harness/architect-agent.js +3 -2
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
  22. package/dist/backend/templates/harness/claude-root.js +9 -1
  23. package/dist/backend/templates/harness/coder-agent.js +1 -2
  24. package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
  25. package/dist/backend/templates/harness/gate-review.js +3 -4
  26. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
  27. package/dist/backend/templates/harness/project-manager-agent.js +3 -2
  28. package/dist/backend/templates/harness/tester-agent.js +1 -0
  29. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
  30. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +3 -4
  31. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
  32. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
  33. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
  34. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
  35. package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
  38. package/dist/backend/templates/message-envelope.js +4 -4
  39. package/dist/shared/types/workflow.js +7 -1
  40. package/dist/shared/validation/artifact-check.js +99 -95
  41. package/dist/shared/validation/artifact-contract.js +9 -0
  42. package/dist/shared/validation/artifact-registry.js +211 -0
  43. package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
  44. package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +2 -1
  48. package/scripts/harness-tools/run-long-check +38 -8
  49. package/scripts/harness-tools/vcm-artifact +148 -0
  50. package/scripts/harness-tools/vcm-bash-guard +42 -47
  51. package/scripts/harness-tools/watch-job +58 -4
package/README.md CHANGED
@@ -324,6 +324,13 @@ Automatic mode:
324
324
  - the UI switches to the target role before dispatch
325
325
  - Claude Code hooks confirm whether the prompt was accepted
326
326
 
327
+ Before Project Manager can dispatch Architect, Coder, or Tester, it submits a
328
+ strict `workflow-progress.md` transition through `vcm-workflow-review`. VCM
329
+ checks the confirmed dispatch history and current task artifacts, then grants
330
+ one matching route. The target role's `UserPromptSubmit` consumes that approval
331
+ and appends the confirmed transition. A rejected transition can be bypassed
332
+ only by an exact one-time user authorization recorded through the VCM dialog.
333
+
327
334
  If the flow stops, VCM always shows a blocking pause alert. `Pause alert sound`
328
335
  only controls the looping sound. Enabling Gateway turns that preference off once;
329
336
  it can be turned back on afterward. A new Gateway command closes an open pause
@@ -471,12 +478,10 @@ VCM bundles the Claude Code LSP bridge and loads it for Architect, Coder, and
471
478
  Reviewer sessions, including CCR launches. The project environment must still
472
479
  provide the language server for each detected language: `rust-analyzer`,
473
480
  `typescript-language-server`, `pyright-langserver`, `gopls`, `clangd`, or
474
- `jdtls`. Their Agent definitions preload `vcm-code-navigation`, and their
475
- launches explicitly enable Glob while leaving Grep unavailable. The Harness
476
- guard also rejects shell text-search commands for those roles so unresolved
477
- semantic relationships cannot be hidden by textual fallback. Harness Studio
478
- reports whether the server executable and plugin can run; the role Session
479
- performs the real workspace warm-up and semantic query retries.
481
+ `jdtls`. Their Agent definitions preload `vcm-code-navigation`. Architect,
482
+ Coder, and Reviewer use LSP for semantic relationships. Harness Studio reports
483
+ whether the server executable and plugin can run; the role Session performs the
484
+ real workspace warm-up and semantic query retries.
480
485
 
481
486
  Harness Engineer is task-scoped and runs from the active task worktree. The
482
487
  backend automatically starts a fresh Harness Engineer for each active task or
@@ -1,3 +1,4 @@
1
+ import { roleRuntimeDisallowedTools, roleUsesLsp } from "../role-tool-policy.js";
1
2
  import { isCcrSessionModel } from "../../shared/types/session.js";
2
3
  import { VcmError } from "../errors.js";
3
4
  export function createClaudeAdapter(runner) {
@@ -20,7 +21,13 @@ export function createClaudeAdapter(runner) {
20
21
  },
21
22
  buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt, pluginDirs = []) {
22
23
  const args = pluginDirs.flatMap((pluginDir) => ["--plugin-dir", pluginDir]);
23
- args.push("--allowedTools", roleSearchToolOptIns(role).join(","));
24
+ const lspEnabled = roleUsesLsp(role) && pluginDirs.length > 0;
25
+ if (lspEnabled) {
26
+ args.push("--disallowedTools", roleRuntimeDisallowedTools(role).join(","));
27
+ }
28
+ else {
29
+ args.push("--allowedTools", "Glob,Grep");
30
+ }
24
31
  args.push("--agent", role);
25
32
  const sessionSettings = { ...settingsOverride };
26
33
  if (claudeSessionId) {
@@ -52,11 +59,6 @@ export function createClaudeAdapter(runner) {
52
59
  }
53
60
  };
54
61
  }
55
- function roleSearchToolOptIns(role) {
56
- return role === "architect" || role === "coder" || role === "reviewer"
57
- ? ["Glob"]
58
- : ["Glob", "Grep"];
59
- }
60
62
  function formatDisplayArg(value) {
61
63
  if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
62
64
  return value;
@@ -1,4 +1,4 @@
1
- import { isDispatchableRole } from "../../shared/constants.js";
1
+ import { isDispatchableRole, isRoleName } from "../../shared/constants.js";
2
2
  import { VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
4
  export function registerArtifactRoutes(app, deps) {
@@ -52,6 +52,59 @@ export function registerArtifactRoutes(app, deps) {
52
52
  });
53
53
  return { ok: true };
54
54
  });
55
+ app.post("/api/tasks/:taskSlug/artifacts/submit", async (request) => {
56
+ const project = await requireCurrentProject(deps.projectService);
57
+ const config = await deps.projectService.loadConfig(project.repoRoot);
58
+ const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
59
+ const body = request.body;
60
+ if (!body || !isRoleName(body.role)) {
61
+ throw new VcmError({
62
+ code: "ARTIFACT_ROLE_INVALID",
63
+ message: `Unknown artifact role: ${body?.role ?? "missing"}`,
64
+ statusCode: 400
65
+ });
66
+ }
67
+ if (body.mode !== "draft" && body.mode !== "final") {
68
+ throw new VcmError({
69
+ code: "ARTIFACT_MODE_INVALID",
70
+ message: "Artifact mode must be draft or final.",
71
+ statusCode: 400
72
+ });
73
+ }
74
+ if (typeof body.content !== "string") {
75
+ throw new VcmError({
76
+ code: "ARTIFACT_CONTENT_INVALID",
77
+ message: "Artifact content must be text.",
78
+ statusCode: 400
79
+ });
80
+ }
81
+ const session = body.role === "translator"
82
+ ? await deps.sessionService.getProjectTranslatorSession(project.repoRoot)
83
+ : body.role === "harness-engineer"
84
+ ? await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, body.role)
85
+ ?? await deps.sessionService.getProjectHarnessEngineerSession(project.repoRoot)
86
+ : await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, body.role);
87
+ if (!session || !session.runtimeSessionToken || session.runtimeSessionToken !== body.runtimeSessionToken) {
88
+ throw new VcmError({
89
+ code: "ARTIFACT_SESSION_INVALID",
90
+ message: `${body.role} does not have the active VCM Session that owns this artifact submission.`,
91
+ statusCode: 409,
92
+ hint: "Submit from the active role terminal with .ai/tools/vcm-artifact."
93
+ });
94
+ }
95
+ return deps.artifactService.submitArtifact({
96
+ repoRoot: getTaskRuntimeRepoRoot(task),
97
+ baseRepoRoot: project.repoRoot,
98
+ stateRoot: config.stateRoot,
99
+ handoffDir: task.handoffDir,
100
+ taskSlug: task.taskSlug,
101
+ kind: body.kind,
102
+ mode: body.mode,
103
+ role: body.role,
104
+ content: body.content,
105
+ artifactPath: body.path
106
+ });
107
+ });
55
108
  }
56
109
  function parseDispatchableRole(role) {
57
110
  if (!isDispatchableRole(role)) {
@@ -67,18 +120,36 @@ function artifactNameToPath(paths, artifactName) {
67
120
  if (artifactName === "architecture-brief.md") {
68
121
  return paths.architectureBriefPath;
69
122
  }
123
+ if (artifactName === "architecture-evidence.md") {
124
+ return paths.architectureEvidencePath;
125
+ }
126
+ if (artifactName === "planning-progress.md") {
127
+ return paths.planningProgressPath;
128
+ }
70
129
  if (artifactName === "architecture-plan.md") {
71
130
  return paths.architecturePlanPath;
72
131
  }
73
132
  if (artifactName === "known-issues.md") {
74
133
  return paths.knownIssuesPath;
75
134
  }
135
+ if (artifactName === "coder-completion.md") {
136
+ return paths.coderCompletionPath;
137
+ }
138
+ if (artifactName === "architect-debug.md") {
139
+ return paths.architectDebugPath;
140
+ }
141
+ if (artifactName === "architecture-diagnosis.md") {
142
+ return paths.architectureDiagnosisPath;
143
+ }
76
144
  if (artifactName === "test-report.md") {
77
145
  return paths.testReportPath;
78
146
  }
79
147
  if (artifactName === "docs-sync-report.md") {
80
148
  return paths.docsSyncReportPath;
81
149
  }
150
+ if (artifactName === "workflow-progress.md") {
151
+ return paths.workflowProgressPath;
152
+ }
82
153
  if (artifactName === "final-acceptance.md") {
83
154
  return paths.finalAcceptancePath;
84
155
  }
@@ -23,15 +23,25 @@ export function registerRuntimeStateRoutes(app, deps) {
23
23
  harnessBootstrapStatus: null,
24
24
  harnessFeedbackState,
25
25
  autoMemoryState: null,
26
- gatewayStatus: coordinated.gatewayStatus
26
+ gatewayStatus: coordinated.gatewayStatus,
27
+ workflowControlState: null
27
28
  };
28
29
  }
29
30
  try {
30
31
  const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
31
- const [harnessStatus, harnessBootstrapStatus, autoMemoryState] = await Promise.all([
32
+ const config = deps.workflowControlService
33
+ ? await deps.projectService.loadConfig(project.repoRoot)
34
+ : null;
35
+ const [harnessStatus, harnessBootstrapStatus, autoMemoryState, workflowControlState] = await Promise.all([
32
36
  withOpenFileLimitFallback(() => deps.harnessService.getHarnessStatus(task.worktreePath), (error) => degradedHarnessStatus(error)),
33
37
  withOpenFileLimitFallback(() => deps.harnessService.getBootstrapStatus(project.repoRoot, task.worktreePath, task.taskSlug), (error) => degradedBootstrapStatus(error)),
34
- deps.autoMemoryService.getState(project.repoRoot, task.worktreePath)
38
+ deps.autoMemoryService.getState(project.repoRoot, task.worktreePath),
39
+ deps.workflowControlService && config ? deps.workflowControlService.getState({
40
+ taskRepoRoot: task.worktreePath,
41
+ stateRoot: config.stateRoot,
42
+ handoffDir: task.handoffDir,
43
+ taskSlug: task.taskSlug
44
+ }) : Promise.resolve(null)
35
45
  ]);
36
46
  return {
37
47
  translatorSession,
@@ -41,7 +51,8 @@ export function registerRuntimeStateRoutes(app, deps) {
41
51
  harnessBootstrapStatus,
42
52
  harnessFeedbackState,
43
53
  autoMemoryState,
44
- gatewayStatus: coordinated.gatewayStatus
54
+ gatewayStatus: coordinated.gatewayStatus,
55
+ workflowControlState
45
56
  };
46
57
  }
47
58
  catch (error) {
@@ -54,7 +65,8 @@ export function registerRuntimeStateRoutes(app, deps) {
54
65
  harnessBootstrapStatus: degradedBootstrapStatus(error),
55
66
  harnessFeedbackState,
56
67
  autoMemoryState: null,
57
- gatewayStatus: coordinated.gatewayStatus
68
+ gatewayStatus: coordinated.gatewayStatus,
69
+ workflowControlState: null
58
70
  };
59
71
  }
60
72
  throw error;
@@ -198,10 +198,16 @@ function degradedArtifactSummary(handoffDir) {
198
198
  roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
199
199
  messageRoutePaths: {},
200
200
  architectureBriefPath: `${handoffDir}/architecture-brief.md`,
201
+ architectureEvidencePath: `${handoffDir}/architecture-evidence.md`,
202
+ planningProgressPath: `${handoffDir}/planning-progress.md`,
201
203
  architecturePlanPath: `${handoffDir}/architecture-plan.md`,
202
204
  knownIssuesPath: `${handoffDir}/known-issues.md`,
205
+ coderCompletionPath: `${handoffDir}/coder-completion.md`,
206
+ architectDebugPath: `${handoffDir}/architect-debug.md`,
207
+ architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
203
208
  testReportPath: `${handoffDir}/test-report.md`,
204
209
  docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
210
+ workflowProgressPath: `${handoffDir}/workflow-progress.md`,
205
211
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
206
212
  },
207
213
  checks: []
@@ -0,0 +1,56 @@
1
+ import { VcmError } from "../errors.js";
2
+ import { submitTerminalInput } from "../runtime/terminal-submit.js";
3
+ import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
+ export function registerWorkflowControlRoutes(app, deps) {
5
+ app.get("/api/tasks/:taskSlug/workflow-control", async (request) => {
6
+ const context = await getContext(deps, request.params.taskSlug);
7
+ return deps.workflowControlService.getState(context);
8
+ });
9
+ app.post("/api/tasks/:taskSlug/workflow-overrides/:overrideId/approve", async (request) => {
10
+ const authorizationText = request.body?.authorizationText?.trim();
11
+ if (!authorizationText) {
12
+ throw new VcmError({
13
+ code: "WORKFLOW_OVERRIDE_AUTHORIZATION_REQUIRED",
14
+ message: "Enter the exact workflow exception that the user authorizes.",
15
+ statusCode: 400
16
+ });
17
+ }
18
+ const context = await getContext(deps, request.params.taskSlug);
19
+ const state = await deps.workflowControlService.approveOverride(context, request.params.overrideId, authorizationText);
20
+ await notifyProjectManager(deps, context, request.params.overrideId, "approved", authorizationText);
21
+ return state;
22
+ });
23
+ app.post("/api/tasks/:taskSlug/workflow-overrides/:overrideId/reject", async (request) => {
24
+ const context = await getContext(deps, request.params.taskSlug);
25
+ const state = await deps.workflowControlService.rejectOverride(context, request.params.overrideId);
26
+ await notifyProjectManager(deps, context, request.params.overrideId, "rejected");
27
+ return state;
28
+ });
29
+ }
30
+ async function getContext(deps, taskSlug) {
31
+ const project = await deps.projectService.getCurrentProject();
32
+ if (!project) {
33
+ throw new VcmError({ code: "PROJECT_NOT_CONNECTED", message: "Connect a repository first.", statusCode: 409 });
34
+ }
35
+ const [config, task] = await Promise.all([
36
+ deps.projectService.loadConfig(project.repoRoot),
37
+ deps.taskService.loadTask(project.repoRoot, taskSlug)
38
+ ]);
39
+ return {
40
+ repoRoot: project.repoRoot,
41
+ taskRepoRoot: getTaskRuntimeRepoRoot(task),
42
+ stateRoot: config.stateRoot,
43
+ handoffDir: task.handoffDir,
44
+ taskSlug: task.taskSlug
45
+ };
46
+ }
47
+ async function notifyProjectManager(deps, context, overrideId, decision, authorizationText) {
48
+ const session = await deps.sessionService.getRoleSession(context.repoRoot, context.taskSlug, "project-manager");
49
+ if (!session || session.status !== "running" || session.activityStatus === "running")
50
+ return;
51
+ const prompt = decision === "approved"
52
+ ? `[VCM Workflow Override Decision]\nDecision: approved\nAuthorization ID: ${overrideId}\nAuthorization Text: ${authorizationText}\n\nResubmit workflow-progress.md with this Authorization ID and exact Authorization Text.`
53
+ : `[VCM Workflow Override Decision]\nDecision: rejected\nAuthorization ID: ${overrideId}\n\nKeep the current workflow and choose a legal next dispatch.`;
54
+ await submitTerminalInput(deps.runtime, session.id, prompt);
55
+ await deps.sessionService.markRoleActivityRunning(context.repoRoot, context.taskSlug, "project-manager", session.id);
56
+ }
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
8
+ import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
8
9
  import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
9
10
  import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
10
11
  import { renderArchitectScaffoldWorkerHarnessRules } from "../templates/harness/architect-scaffold-worker-agent.js";
@@ -28,6 +29,7 @@ import { renderVcmProposeMemorySkillRules } from "../templates/harness/vcm-propo
28
29
  import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
29
30
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
30
31
  import { renderUpdateTaskStateTool, renderVcmTaskStateSkillRules } from "../templates/harness/vcm-task-state-skill.js";
32
+ import { renderVcmWorkflowReviewSkillRules } from "../templates/harness/vcm-workflow-review-skill.js";
31
33
  import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
32
34
  import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
33
35
  import { readVcmPackageVersion } from "../app-version.js";
@@ -53,7 +55,7 @@ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
53
55
  const VCM_AUTO_MEMORY_ENABLED = false;
54
56
  const VCM_HOOK_DEFINITIONS = [
55
57
  { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
56
- { eventName: "PreToolUse", matcher: "Grep", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
58
+ { eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
57
59
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
58
60
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
59
61
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
@@ -66,12 +68,12 @@ const AGENT_FRONTMATTER = {
66
68
  },
67
69
  architect: {
68
70
  description: "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.",
69
- tools: "Read, Glob, Bash, Edit, Write, Agent, LSP",
71
+ disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "),
70
72
  skills: ["vcm-code-navigation"]
71
73
  },
72
74
  coder: {
73
75
  description: "VCM implementation role for scoped code changes and focused tests.",
74
- tools: "Read, Glob, Bash, Edit, Write, Agent, LSP",
76
+ disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "),
75
77
  skills: ["vcm-code-navigation"]
76
78
  },
77
79
  tester: {
@@ -79,7 +81,7 @@ const AGENT_FRONTMATTER = {
79
81
  },
80
82
  reviewer: {
81
83
  description: "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.",
82
- tools: "Read, Glob, Bash, Write, LSP",
84
+ disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", "),
83
85
  skills: ["vcm-code-navigation"]
84
86
  },
85
87
  translator: {
@@ -98,15 +100,10 @@ const AGENT_FRONTMATTER = {
98
100
  effort: "xhigh"
99
101
  }
100
102
  };
101
- const REQUIRED_AGENT_TOOLS = {
102
- architect: ["Agent", "LSP"],
103
- coder: ["Agent", "LSP"],
104
- reviewer: ["LSP"]
105
- };
106
- const FORBIDDEN_AGENT_TOOLS = {
107
- architect: ["Grep"],
108
- coder: ["Grep"],
109
- reviewer: ["Grep"]
103
+ const REQUIRED_AGENT_DISALLOWED_TOOLS = {
104
+ architect: CODE_ROLE_DISALLOWED_TOOLS,
105
+ coder: CODE_ROLE_DISALLOWED_TOOLS,
106
+ reviewer: REVIEWER_DISALLOWED_TOOLS
110
107
  };
111
108
  const REQUIRED_AGENT_SKILLS = {
112
109
  architect: ["vcm-code-navigation"],
@@ -312,6 +309,12 @@ const WHOLE_FILES = [
312
309
  mode: 0o644,
313
310
  content: renderSkillFile("VCM Task State Skill", "vcm-task-state", "Use only as project-manager to declare the current task workflow checkpoint to VCM.", renderVcmTaskStateSkillRules())
314
311
  },
312
+ {
313
+ path: ".claude/skills/vcm-workflow-review/SKILL.md",
314
+ category: "skill",
315
+ mode: 0o644,
316
+ content: renderSkillFile("VCM Workflow Review Skill", "vcm-workflow-review", "Use before every project-manager dispatch to Architect, Coder, or Tester so VCM can approve the workflow transition.", renderVcmWorkflowReviewSkillRules())
317
+ },
315
318
  {
316
319
  path: ".claude/skills/vcm-gate-review/SKILL.md",
317
320
  category: "skill",
@@ -377,6 +380,12 @@ const WHOLE_FILES = [
377
380
  category: "runtime-tool",
378
381
  mode: 0o755,
379
382
  templatePath: "scripts/harness-tools/vcm-bash-guard"
383
+ },
384
+ {
385
+ path: ".ai/tools/vcm-artifact",
386
+ category: "runtime-tool",
387
+ mode: 0o755,
388
+ templatePath: "scripts/harness-tools/vcm-artifact"
380
389
  }
381
390
  ];
382
391
  const LEGACY_FLAT_SKILL_FILES = [
@@ -619,6 +628,7 @@ function fixedDirectories() {
619
628
  ".claude/skills/vcm-long-running-validation/",
620
629
  ".claude/skills/vcm-route-message/",
621
630
  ".claude/skills/vcm-task-state/",
631
+ ".claude/skills/vcm-workflow-review/",
622
632
  ".claude/skills/vcm-gate-review/",
623
633
  ".claude/skills/vcm-report-harness-issue/",
624
634
  ".claude/skills/vcm-propose-memory/",
@@ -683,11 +693,9 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
683
693
  if (definition.memoryBlock) {
684
694
  nextContent = ensureVcmMemoryBlock(nextContent);
685
695
  }
686
- for (const requiredTool of REQUIRED_AGENT_TOOLS[definition.agentName] ?? []) {
687
- nextContent = ensureAgentTool(nextContent, requiredTool);
688
- }
689
- for (const forbiddenTool of FORBIDDEN_AGENT_TOOLS[definition.agentName] ?? []) {
690
- nextContent = removeAgentTool(nextContent, forbiddenTool);
696
+ const requiredDisallowedTools = REQUIRED_AGENT_DISALLOWED_TOOLS[definition.agentName];
697
+ if (requiredDisallowedTools) {
698
+ nextContent = ensureAgentDisallowedTools(nextContent, requiredDisallowedTools);
691
699
  }
692
700
  for (const requiredSkill of REQUIRED_AGENT_SKILLS[definition.agentName] ?? []) {
693
701
  nextContent = ensureAgentSkill(nextContent, requiredSkill);
@@ -731,47 +739,34 @@ function renderNewManagedFile(definition, block) {
731
739
  const suffix = definition.contentAfterBlock?.trim();
732
740
  if (definition.agentName) {
733
741
  const frontmatter = AGENT_FRONTMATTER[definition.agentName];
734
- const tools = frontmatter.tools ?? "Read, Grep, Glob, Bash, Edit, Write";
742
+ const toolPolicy = frontmatter.disallowedTools
743
+ ? `disallowedTools: ${frontmatter.disallowedTools}`
744
+ : `tools: ${frontmatter.tools ?? "Read, Grep, Glob, Bash, Edit, Write"}`;
735
745
  const model = frontmatter.model ? `\nmodel: ${frontmatter.model}` : "";
736
746
  const effort = frontmatter.effort ? `\neffort: ${frontmatter.effort}` : "";
737
747
  const skills = frontmatter.skills?.length
738
748
  ? `\nskills:\n${frontmatter.skills.map((skill) => ` - ${skill}`).join("\n")}`
739
749
  : "";
740
- return `---\nname: ${definition.agentName}\ndescription: ${frontmatter.description}\ntools: ${tools}${model}${effort}${skills}\n---\n\n# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
750
+ return `---\nname: ${definition.agentName}\ndescription: ${frontmatter.description}\n${toolPolicy}${model}${effort}${skills}\n---\n\n# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
741
751
  }
742
752
  return `# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
743
753
  }
744
- function ensureAgentTool(content, requiredTool) {
745
- const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
746
- if (!frontmatterMatch) {
747
- return content;
748
- }
749
- const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
750
- if (!toolsMatch) {
751
- return content.replace(/^(---\r?\n[\s\S]*?)(\r?\n---)/, `$1\ntools: ${requiredTool}$2`);
752
- }
753
- const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
754
- if (tools.includes(requiredTool)) {
755
- return content;
756
- }
757
- const nextTools = [...tools, requiredTool].join(", ");
758
- return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
759
- }
760
- function removeAgentTool(content, forbiddenTool) {
754
+ function ensureAgentDisallowedTools(content, requiredTools) {
761
755
  const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
762
756
  if (!frontmatterMatch) {
763
757
  return content;
764
758
  }
765
- const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
766
- if (!toolsMatch) {
767
- return content;
768
- }
769
- const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
770
- const nextTools = tools.filter((tool) => tool !== forbiddenTool);
771
- if (nextTools.length === tools.length) {
772
- return content;
773
- }
774
- return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools.join(", ")}`));
759
+ const withoutAllowedTools = frontmatterMatch[0].replace(/^tools:\s*.*\r?\n?/m, "");
760
+ const disallowedMatch = withoutAllowedTools.match(/^disallowedTools:\s*(.*)$/m);
761
+ const existing = disallowedMatch?.[1]
762
+ .split(",")
763
+ .map((tool) => tool.trim())
764
+ .filter(Boolean) ?? [];
765
+ const disallowedTools = [...new Set([...existing, ...requiredTools])].join(", ");
766
+ const nextFrontmatter = disallowedMatch
767
+ ? withoutAllowedTools.replace(disallowedMatch[0], `disallowedTools: ${disallowedTools}`)
768
+ : withoutAllowedTools.replace(/\r?\n---$/, `\ndisallowedTools: ${disallowedTools}\n---`);
769
+ return content.replace(frontmatterMatch[0], nextFrontmatter);
775
770
  }
776
771
  function ensureAgentSkill(content, requiredSkill) {
777
772
  const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
@@ -0,0 +1,60 @@
1
+ const LSP_FRONTMATTER_DISALLOWED_TOOLS = [
2
+ "NotebookEdit",
3
+ "WebFetch",
4
+ "WebSearch"
5
+ ];
6
+ const NON_VCM_ROLE_RUNTIME_TOOLS = [
7
+ "AskUserQuestion",
8
+ "CronCreate",
9
+ "CronDelete",
10
+ "CronList",
11
+ "DesignSync",
12
+ "EnterPlanMode",
13
+ "EnterWorktree",
14
+ "ExitPlanMode",
15
+ "ExitWorktree",
16
+ "KillShell",
17
+ "Monitor",
18
+ "NotebookEdit",
19
+ "PushNotification",
20
+ "RemoteTrigger",
21
+ "ReportFindings",
22
+ "ScheduleWakeup",
23
+ "SendMessage",
24
+ "Skill",
25
+ "TaskCreate",
26
+ "TaskGet",
27
+ "TaskList",
28
+ "TaskOutput",
29
+ "TaskStop",
30
+ "TaskUpdate",
31
+ "TodoWrite",
32
+ "WebFetch",
33
+ "WebSearch",
34
+ "Workflow"
35
+ ];
36
+ export const CODE_ROLE_DISALLOWED_TOOLS = [...LSP_FRONTMATTER_DISALLOWED_TOOLS];
37
+ export const REVIEWER_DISALLOWED_TOOLS = [
38
+ "Agent",
39
+ "Edit",
40
+ ...LSP_FRONTMATTER_DISALLOWED_TOOLS
41
+ ];
42
+ export const CODE_ROLE_RUNTIME_DISALLOWED_TOOLS = [...NON_VCM_ROLE_RUNTIME_TOOLS];
43
+ export const REVIEWER_RUNTIME_DISALLOWED_TOOLS = [
44
+ "Agent",
45
+ "Edit",
46
+ ...NON_VCM_ROLE_RUNTIME_TOOLS
47
+ ];
48
+ const LSP_ROLE_NAMES = new Set(["architect", "coder", "reviewer"]);
49
+ export function roleUsesLsp(role) {
50
+ return LSP_ROLE_NAMES.has(role);
51
+ }
52
+ export function roleRuntimeDisallowedTools(role) {
53
+ if (role === "reviewer") {
54
+ return REVIEWER_RUNTIME_DISALLOWED_TOOLS;
55
+ }
56
+ if (role === "architect" || role === "coder") {
57
+ return CODE_ROLE_RUNTIME_DISALLOWED_TOOLS;
58
+ }
59
+ return [];
60
+ }
@@ -45,6 +45,7 @@ import { createTerminalInterruptService } from "./services/terminal-interrupt-se
45
45
  import { createTerminalProcessExitService } from "./services/terminal-process-exit-service.js";
46
46
  import { createTranslationService } from "./services/translation-service.js";
47
47
  import { createUsageAnalyticsService } from "./services/usage-analytics-service.js";
48
+ import { createWorkflowControlService } from "./services/workflow-control-service.js";
48
49
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
49
50
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
50
51
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
@@ -60,6 +61,7 @@ import { registerSessionRoutes } from "./api/session-routes.js";
60
61
  import { registerTaskRoutes } from "./api/task-routes.js";
61
62
  import { registerTranslationRoutes } from "./api/translation-routes.js";
62
63
  import { registerUsageAnalyticsRoutes } from "./api/usage-analytics-routes.js";
64
+ import { registerWorkflowControlRoutes } from "./api/workflow-control-routes.js";
63
65
  import { registerTerminalWs } from "./ws/terminal-ws.js";
64
66
  import { toVcmError } from "./errors.js";
65
67
  import { readVcmPackageVersion } from "./app-version.js";
@@ -120,7 +122,8 @@ export async function createServer(deps, options = {}) {
120
122
  harnessService: deps.harnessService,
121
123
  harnessFeedbackService: deps.harnessFeedbackService,
122
124
  autoMemoryService: deps.autoMemoryService,
123
- runtimeCoordinator: deps.runtimeCoordinator
125
+ runtimeCoordinator: deps.runtimeCoordinator,
126
+ workflowControlService: deps.workflowControlService
124
127
  });
125
128
  registerTaskRoutes(app, {
126
129
  projectService: deps.projectService,
@@ -144,8 +147,18 @@ export async function createServer(deps, options = {}) {
144
147
  registerArtifactRoutes(app, {
145
148
  projectService: deps.projectService,
146
149
  taskService: deps.taskService,
147
- artifactService: deps.artifactService
148
- });
150
+ artifactService: deps.artifactService,
151
+ sessionService: deps.sessionService
152
+ });
153
+ if (deps.workflowControlService) {
154
+ registerWorkflowControlRoutes(app, {
155
+ projectService: deps.projectService,
156
+ taskService: deps.taskService,
157
+ sessionService: deps.sessionService,
158
+ workflowControlService: deps.workflowControlService,
159
+ runtime: deps.runtime
160
+ });
161
+ }
149
162
  registerMessageRoutes(app, {
150
163
  projectService: deps.projectService,
151
164
  taskService: deps.taskService,
@@ -228,7 +241,8 @@ export function createDefaultServerDeps(options = {}) {
228
241
  });
229
242
  const runtime = createNodePtyTerminalRuntime({ fs });
230
243
  const registry = createSessionRegistry();
231
- const artifactService = createArtifactService(fs);
244
+ const workflowControlService = createWorkflowControlService({ fs });
245
+ const artifactService = createArtifactService(fs, { workflowControlService });
232
246
  const projectService = createProjectService({ fs, git, appSettings });
233
247
  const taskService = createTaskService({ fs, git, artifactService, projectService });
234
248
  const taskWorkflowService = createTaskWorkflowService({ fs });
@@ -292,6 +306,7 @@ export function createDefaultServerDeps(options = {}) {
292
306
  sessionService,
293
307
  taskService,
294
308
  taskWorkflowService,
309
+ workflowControlService,
295
310
  onRouteDelivered: ({ repoRoot, taskSlug, message }) => architectRestartService.recordRouteDelivered(repoRoot, taskSlug, message)
296
311
  });
297
312
  const taskLaunchService = createTaskLaunchService({
@@ -462,7 +477,8 @@ export function createDefaultServerDeps(options = {}) {
462
477
  terminalProcessExitService,
463
478
  runtime,
464
479
  diagnosticsService,
465
- usageAnalyticsService
480
+ usageAnalyticsService,
481
+ workflowControlService
466
482
  };
467
483
  }
468
484
  export function getDefaultStaticDir() {