vibe-coding-master 0.7.38 → 0.7.40

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 (49) hide show
  1. package/README.md +12 -6
  2. package/dist/backend/adapters/claude-adapter.js +1 -0
  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 +46 -22
  8. package/dist/backend/server.js +21 -5
  9. package/dist/backend/services/artifact-service.js +309 -5
  10. package/dist/backend/services/auto-memory-service.js +2 -2
  11. package/dist/backend/services/code-intelligence-service.js +5 -5
  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 +42 -26
  15. package/dist/backend/services/message-service.js +105 -37
  16. package/dist/backend/services/status-service.js +6 -0
  17. package/dist/backend/services/workflow-control-service.js +854 -0
  18. package/dist/backend/templates/handoff.js +158 -3
  19. package/dist/backend/templates/harness/architect-agent.js +3 -3
  20. package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
  21. package/dist/backend/templates/harness/claude-root.js +9 -1
  22. package/dist/backend/templates/harness/coder-agent.js +2 -3
  23. package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
  24. package/dist/backend/templates/harness/gate-review.js +4 -5
  25. package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
  26. package/dist/backend/templates/harness/project-manager-agent.js +3 -2
  27. package/dist/backend/templates/harness/tester-agent.js +1 -0
  28. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
  29. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -6
  30. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
  31. package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
  32. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
  33. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
  34. package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
  35. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
  36. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
  37. package/dist/backend/templates/message-envelope.js +4 -4
  38. package/dist/shared/types/workflow.js +7 -1
  39. package/dist/shared/validation/artifact-check.js +99 -95
  40. package/dist/shared/validation/artifact-contract.js +9 -0
  41. package/dist/shared/validation/artifact-registry.js +211 -0
  42. package/dist-frontend/assets/{index-VillnFRo.js → index-Bocc2DWF.js} +34 -34
  43. package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
  44. package/dist-frontend/index.html +2 -2
  45. package/package.json +1 -1
  46. package/scripts/harness-tools/run-long-check +38 -8
  47. package/scripts/harness-tools/vcm-artifact +148 -0
  48. package/scripts/harness-tools/vcm-bash-guard +42 -47
  49. 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
@@ -455,7 +462,7 @@ Harness Studio is the UI for VCM harness maintenance.
455
462
  Use it to:
456
463
 
457
464
  - inspect fixed harness status
458
- - inspect detected project languages and verified language-server readiness
465
+ - inspect detected project languages and language-server availability
459
466
  - run bootstrap
460
467
  - open Harness Engineer
461
468
  - review harness files
@@ -471,11 +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`. Architect, Coder, and Reviewer do not receive the Grep tool, and the
475
- Harness guard rejects shell text-search commands for those roles so unresolved
476
- semantic relationships cannot be hidden by textual fallback. Harness Studio
477
- reports a missing executable or failed readiness probe
478
- with the backend diagnostic.
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.
479
485
 
480
486
  Harness Engineer is task-scoped and runs from the active task worktree. The
481
487
  backend automatically starts a fresh Harness Engineer for each active task or
@@ -20,6 +20,7 @@ export function createClaudeAdapter(runner) {
20
20
  },
21
21
  buildRoleStartCommand(role, command = "claude", permissionMode = "default", claudeSessionId, resume = false, model = "default", effort = "default", settingsOverride, appendSystemPrompt, pluginDirs = []) {
22
22
  const args = pluginDirs.flatMap((pluginDir) => ["--plugin-dir", pluginDir]);
23
+ args.push("--allowedTools", "Glob,Grep");
23
24
  args.push("--agent", role);
24
25
  const sessionSettings = { ...settingsOverride };
25
26
  if (claudeSessionId) {
@@ -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
+ }
@@ -28,6 +28,7 @@ import { renderVcmProposeMemorySkillRules } from "../templates/harness/vcm-propo
28
28
  import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
29
29
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
30
30
  import { renderUpdateTaskStateTool, renderVcmTaskStateSkillRules } from "../templates/harness/vcm-task-state-skill.js";
31
+ import { renderVcmWorkflowReviewSkillRules } from "../templates/harness/vcm-workflow-review-skill.js";
31
32
  import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
32
33
  import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
33
34
  import { readVcmPackageVersion } from "../app-version.js";
@@ -53,7 +54,7 @@ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
53
54
  const VCM_AUTO_MEMORY_ENABLED = false;
54
55
  const VCM_HOOK_DEFINITIONS = [
55
56
  { 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 },
57
+ { eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
57
58
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
58
59
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
59
60
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
@@ -66,18 +67,21 @@ const AGENT_FRONTMATTER = {
66
67
  },
67
68
  architect: {
68
69
  description: "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.",
69
- tools: "Read, Glob, Bash, Edit, Write, Agent, LSP"
70
+ tools: "Read, Grep, Glob, Bash, Edit, Write, Agent, LSP",
71
+ skills: ["vcm-code-navigation"]
70
72
  },
71
73
  coder: {
72
74
  description: "VCM implementation role for scoped code changes and focused tests.",
73
- tools: "Read, Glob, Bash, Edit, Write, Agent, LSP"
75
+ tools: "Read, Grep, Glob, Bash, Edit, Write, Agent, LSP",
76
+ skills: ["vcm-code-navigation"]
74
77
  },
75
78
  tester: {
76
79
  description: "VCM testing role for validation, test adequacy, approved-scope validation, and risk findings."
77
80
  },
78
81
  reviewer: {
79
82
  description: "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.",
80
- tools: "Read, Glob, Bash, Write, LSP"
83
+ tools: "Read, Grep, Glob, Bash, Write, LSP",
84
+ skills: ["vcm-code-navigation"]
81
85
  },
82
86
  translator: {
83
87
  description: "VCM task-scoped translation tool role for conversation translation, file translation, bootstrap, and memory updates."
@@ -96,14 +100,14 @@ const AGENT_FRONTMATTER = {
96
100
  }
97
101
  };
98
102
  const REQUIRED_AGENT_TOOLS = {
99
- architect: ["Agent", "LSP"],
100
- coder: ["Agent", "LSP"],
101
- reviewer: ["LSP"]
103
+ architect: ["Grep", "Agent", "LSP"],
104
+ coder: ["Grep", "Agent", "LSP"],
105
+ reviewer: ["Grep", "LSP"]
102
106
  };
103
- const FORBIDDEN_AGENT_TOOLS = {
104
- architect: ["Grep"],
105
- coder: ["Grep"],
106
- reviewer: ["Grep"]
107
+ const REQUIRED_AGENT_SKILLS = {
108
+ architect: ["vcm-code-navigation"],
109
+ coder: ["vcm-code-navigation"],
110
+ reviewer: ["vcm-code-navigation"]
107
111
  };
108
112
  const MANAGED_FILES = [
109
113
  {
@@ -304,6 +308,12 @@ const WHOLE_FILES = [
304
308
  mode: 0o644,
305
309
  content: renderSkillFile("VCM Task State Skill", "vcm-task-state", "Use only as project-manager to declare the current task workflow checkpoint to VCM.", renderVcmTaskStateSkillRules())
306
310
  },
311
+ {
312
+ path: ".claude/skills/vcm-workflow-review/SKILL.md",
313
+ category: "skill",
314
+ mode: 0o644,
315
+ 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())
316
+ },
307
317
  {
308
318
  path: ".claude/skills/vcm-gate-review/SKILL.md",
309
319
  category: "skill",
@@ -369,6 +379,12 @@ const WHOLE_FILES = [
369
379
  category: "runtime-tool",
370
380
  mode: 0o755,
371
381
  templatePath: "scripts/harness-tools/vcm-bash-guard"
382
+ },
383
+ {
384
+ path: ".ai/tools/vcm-artifact",
385
+ category: "runtime-tool",
386
+ mode: 0o755,
387
+ templatePath: "scripts/harness-tools/vcm-artifact"
372
388
  }
373
389
  ];
374
390
  const LEGACY_FLAT_SKILL_FILES = [
@@ -611,6 +627,7 @@ function fixedDirectories() {
611
627
  ".claude/skills/vcm-long-running-validation/",
612
628
  ".claude/skills/vcm-route-message/",
613
629
  ".claude/skills/vcm-task-state/",
630
+ ".claude/skills/vcm-workflow-review/",
614
631
  ".claude/skills/vcm-gate-review/",
615
632
  ".claude/skills/vcm-report-harness-issue/",
616
633
  ".claude/skills/vcm-propose-memory/",
@@ -678,8 +695,8 @@ async function installManagedFile({ projectRoot, definition, dryRun, operations
678
695
  for (const requiredTool of REQUIRED_AGENT_TOOLS[definition.agentName] ?? []) {
679
696
  nextContent = ensureAgentTool(nextContent, requiredTool);
680
697
  }
681
- for (const forbiddenTool of FORBIDDEN_AGENT_TOOLS[definition.agentName] ?? []) {
682
- nextContent = removeAgentTool(nextContent, forbiddenTool);
698
+ for (const requiredSkill of REQUIRED_AGENT_SKILLS[definition.agentName] ?? []) {
699
+ nextContent = ensureAgentSkill(nextContent, requiredSkill);
683
700
  }
684
701
  await writeIfChanged({
685
702
  targetPath,
@@ -723,7 +740,10 @@ function renderNewManagedFile(definition, block) {
723
740
  const tools = frontmatter.tools ?? "Read, Grep, Glob, Bash, Edit, Write";
724
741
  const model = frontmatter.model ? `\nmodel: ${frontmatter.model}` : "";
725
742
  const effort = frontmatter.effort ? `\neffort: ${frontmatter.effort}` : "";
726
- return `---\nname: ${definition.agentName}\ndescription: ${frontmatter.description}\ntools: ${tools}${model}${effort}\n---\n\n# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
743
+ const skills = frontmatter.skills?.length
744
+ ? `\nskills:\n${frontmatter.skills.map((skill) => ` - ${skill}`).join("\n")}`
745
+ : "";
746
+ 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`;
727
747
  }
728
748
  return `# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
729
749
  }
@@ -743,21 +763,25 @@ function ensureAgentTool(content, requiredTool) {
743
763
  const nextTools = [...tools, requiredTool].join(", ");
744
764
  return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
745
765
  }
746
- function removeAgentTool(content, forbiddenTool) {
766
+ function ensureAgentSkill(content, requiredSkill) {
747
767
  const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
748
768
  if (!frontmatterMatch) {
749
769
  return content;
750
770
  }
751
- const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
752
- if (!toolsMatch) {
753
- return content;
771
+ const frontmatter = frontmatterMatch[0];
772
+ const skillsMatch = frontmatter.match(/^skills:[ \t]*(?:\r?\n((?:\s+-\s+[^\r\n]+\r?\n?)*))?/m);
773
+ if (!skillsMatch) {
774
+ return content.replace(frontmatter, frontmatter.replace(/\r?\n---$/, `\nskills:\n - ${requiredSkill}\n---`));
754
775
  }
755
- const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
756
- const nextTools = tools.filter((tool) => tool !== forbiddenTool);
757
- if (nextTools.length === tools.length) {
776
+ const listedSkills = (skillsMatch[1] ?? "")
777
+ .split(/\r?\n/)
778
+ .map((line) => line.match(/^\s+-\s+(.+)$/)?.[1]?.trim())
779
+ .filter(Boolean);
780
+ if (listedSkills.includes(requiredSkill)) {
758
781
  return content;
759
782
  }
760
- return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools.join(", ")}`));
783
+ const nextSkills = `${skillsMatch[0].trimEnd()}\n - ${requiredSkill}`;
784
+ return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], nextSkills));
761
785
  }
762
786
  function migrateLegacyManagedFile(definition, currentContent, block) {
763
787
  const legacyContent = definition.legacyWholeFile?.trimEnd();
@@ -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() {