vibe-coding-master 0.0.6 → 0.0.8

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 (50) hide show
  1. package/README.md +207 -66
  2. package/dist/backend/adapters/filesystem.js +13 -0
  3. package/dist/backend/adapters/git-adapter.js +79 -1
  4. package/dist/backend/adapters/translation-provider.js +145 -0
  5. package/dist/backend/api/artifact-routes.js +16 -7
  6. package/dist/backend/api/harness-routes.js +22 -0
  7. package/dist/backend/api/message-routes.js +2 -0
  8. package/dist/backend/api/project-routes.js +3 -8
  9. package/dist/backend/api/task-routes.js +14 -0
  10. package/dist/backend/api/translation-routes.js +70 -0
  11. package/dist/backend/runtime/node-pty-runtime.js +20 -18
  12. package/dist/backend/server.js +33 -2
  13. package/dist/backend/services/app-settings-service.js +128 -0
  14. package/dist/backend/services/artifact-service.js +7 -4
  15. package/dist/backend/services/claude-transcript-service.js +509 -0
  16. package/dist/backend/services/command-dispatcher.js +4 -2
  17. package/dist/backend/services/harness-service.js +196 -0
  18. package/dist/backend/services/message-service.js +1 -1
  19. package/dist/backend/services/project-service.js +50 -9
  20. package/dist/backend/services/session-service.js +13 -9
  21. package/dist/backend/services/status-service.js +79 -1
  22. package/dist/backend/services/task-service.js +118 -4
  23. package/dist/backend/services/translation-prompts.js +173 -0
  24. package/dist/backend/services/translation-queue.js +39 -0
  25. package/dist/backend/services/translation-service.js +546 -0
  26. package/dist/backend/templates/handoff.js +32 -0
  27. package/dist/backend/templates/harness/architect-agent.js +12 -0
  28. package/dist/backend/templates/harness/claude-root.js +14 -0
  29. package/dist/backend/templates/harness/coder-agent.js +11 -0
  30. package/dist/backend/templates/harness/gitignore.js +9 -0
  31. package/dist/backend/templates/harness/project-manager-agent.js +14 -0
  32. package/dist/backend/templates/harness/reviewer-agent.js +13 -0
  33. package/dist/backend/ws/translation-ws.js +35 -0
  34. package/dist/shared/types/harness.js +1 -0
  35. package/dist/shared/types/translation.js +5 -0
  36. package/dist/shared/validation/artifact-check.js +15 -1
  37. package/dist/shared/validation/language-detect.js +46 -0
  38. package/dist-frontend/assets/index-CuiNNOzj.css +32 -0
  39. package/dist-frontend/assets/index-D59GuHCR.js +58 -0
  40. package/dist-frontend/index.html +2 -2
  41. package/docs/cc-best-practices.md +109 -40
  42. package/docs/product-design.md +370 -1374
  43. package/docs/v1-architecture-design.md +595 -1114
  44. package/docs/v1-implementation-plan.md +898 -1603
  45. package/package.json +1 -1
  46. package/scripts/verify-package.mjs +8 -0
  47. package/dist/backend/templates/role-messaging-context.js +0 -44
  48. package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
  49. package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
  50. package/docs/v1-message-bus-orchestration-design.md +0 -534
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
3
+ import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
4
+ import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
5
+ import { renderGitignoreHarnessRules } from "../templates/harness/gitignore.js";
6
+ import { renderProjectManagerHarnessRules } from "../templates/harness/project-manager-agent.js";
7
+ import { renderReviewerHarnessRules } from "../templates/harness/reviewer-agent.js";
8
+ export const VCM_HARNESS_VERSION = 1;
9
+ const MANAGED_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=(\d+))? -->[\s\S]*?<!-- VCM:END -->/m;
10
+ const HASH_MANAGED_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=(\d+))?\n[\s\S]*?# VCM:END/m;
11
+ const HARNESS_FILES = [
12
+ {
13
+ kind: "root-claude",
14
+ path: "CLAUDE.md",
15
+ title: "CLAUDE.md",
16
+ renderRules: renderRootClaudeHarnessRules
17
+ },
18
+ {
19
+ kind: "gitignore",
20
+ path: ".gitignore",
21
+ title: ".gitignore",
22
+ commentStyle: "hash",
23
+ renderRules: renderGitignoreHarnessRules
24
+ },
25
+ {
26
+ kind: "agent-project-manager",
27
+ path: ".claude/agents/project-manager.md",
28
+ title: "Project Manager Agent",
29
+ frontmatter: renderAgentFrontmatter("project-manager", "User-facing VCM orchestration role for task clarification, role routing, handoffs, acceptance, and PR preparation."),
30
+ renderRules: renderProjectManagerHarnessRules
31
+ },
32
+ {
33
+ kind: "agent-architect",
34
+ path: ".claude/agents/architect.md",
35
+ title: "Architect Agent",
36
+ frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, test contracts, and post-review docs sync."),
37
+ renderRules: renderArchitectHarnessRules
38
+ },
39
+ {
40
+ kind: "agent-coder",
41
+ path: ".claude/agents/coder.md",
42
+ title: "Coder Agent",
43
+ frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes, focused tests, implementation logs, and validation evidence."),
44
+ renderRules: renderCoderHarnessRules
45
+ },
46
+ {
47
+ kind: "agent-reviewer",
48
+ path: ".claude/agents/reviewer.md",
49
+ title: "Reviewer Agent",
50
+ frontmatter: renderAgentFrontmatter("reviewer", "VCM independent review role for acceptance, test adequacy, scope checks, docs gaps, and risk findings."),
51
+ renderRules: renderReviewerHarnessRules
52
+ }
53
+ ];
54
+ export function createHarnessService(deps) {
55
+ return {
56
+ async getHarnessStatus(repoRoot) {
57
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
58
+ return renderHarnessStatus(analyses);
59
+ },
60
+ async applyHarness(repoRoot) {
61
+ const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
62
+ const changedFiles = [];
63
+ for (const analysis of analyses) {
64
+ if (analysis.status.action === "ok" || !analysis.nextContent) {
65
+ continue;
66
+ }
67
+ await deps.fs.writeText(resolveHarnessPath(repoRoot, analysis.definition.path), analysis.nextContent);
68
+ if (analysis.plannedChange) {
69
+ changedFiles.push(analysis.plannedChange);
70
+ }
71
+ }
72
+ return {
73
+ version: VCM_HARNESS_VERSION,
74
+ changedFiles,
75
+ message: changedFiles.length === 0
76
+ ? "VCM Harness is already up to date."
77
+ : "VCM Harness updated. Review these files and commit the harness changes before starting long-running work."
78
+ };
79
+ }
80
+ };
81
+ }
82
+ async function analyzeHarnessFiles(fs, repoRoot) {
83
+ const analyses = [];
84
+ for (const definition of HARNESS_FILES) {
85
+ analyses.push(await analyzeHarnessFile(fs, repoRoot, definition));
86
+ }
87
+ return analyses;
88
+ }
89
+ async function analyzeHarnessFile(fs, repoRoot, definition) {
90
+ const absolutePath = resolveHarnessPath(repoRoot, definition.path);
91
+ const expectedBlock = renderManagedBlock(definition, definition.renderRules());
92
+ const managedBlockPattern = getManagedBlockPattern(definition);
93
+ const exists = await fs.pathExists(absolutePath);
94
+ if (!exists) {
95
+ return {
96
+ definition,
97
+ status: {
98
+ kind: definition.kind,
99
+ path: definition.path,
100
+ exists: false,
101
+ hasManagedBlock: false,
102
+ action: "create"
103
+ },
104
+ plannedChange: {
105
+ path: definition.path,
106
+ action: "create",
107
+ reason: "File is missing; VCM will create a recommended default."
108
+ },
109
+ nextContent: renderNewHarnessFile(definition, expectedBlock)
110
+ };
111
+ }
112
+ const currentContent = await fs.readText(absolutePath);
113
+ const match = currentContent.match(managedBlockPattern);
114
+ if (!match) {
115
+ return {
116
+ definition,
117
+ status: {
118
+ kind: definition.kind,
119
+ path: definition.path,
120
+ exists: true,
121
+ hasManagedBlock: false,
122
+ action: "insert"
123
+ },
124
+ plannedChange: {
125
+ path: definition.path,
126
+ action: "insert",
127
+ reason: "File exists but does not contain VCM managed rules."
128
+ },
129
+ nextContent: `${currentContent.trimEnd()}\n\n${expectedBlock}\n`
130
+ };
131
+ }
132
+ const managedVersion = match[1] ? Number(match[1]) : undefined;
133
+ const currentBlock = match[0];
134
+ const action = currentBlock === expectedBlock ? "ok" : "update";
135
+ return {
136
+ definition,
137
+ status: {
138
+ kind: definition.kind,
139
+ path: definition.path,
140
+ exists: true,
141
+ hasManagedBlock: true,
142
+ managedVersion,
143
+ action
144
+ },
145
+ plannedChange: action === "ok"
146
+ ? undefined
147
+ : {
148
+ path: definition.path,
149
+ action,
150
+ reason: managedVersion === VCM_HARNESS_VERSION
151
+ ? "VCM managed rules differ from the current recommended template."
152
+ : `VCM managed block version is ${managedVersion ?? "missing"}; current version is ${VCM_HARNESS_VERSION}.`
153
+ },
154
+ nextContent: action === "ok"
155
+ ? undefined
156
+ : currentContent.replace(managedBlockPattern, expectedBlock)
157
+ };
158
+ }
159
+ function renderHarnessStatus(analyses) {
160
+ const files = analyses.map((analysis) => analysis.status);
161
+ const plannedChanges = analyses
162
+ .map((analysis) => analysis.plannedChange)
163
+ .filter((change) => Boolean(change));
164
+ return {
165
+ version: VCM_HARNESS_VERSION,
166
+ files,
167
+ needsApply: plannedChanges.length > 0,
168
+ plannedChanges,
169
+ warnings: plannedChanges.length > 0
170
+ ? ["Review and commit VCM Harness changes before starting long-running work."]
171
+ : []
172
+ };
173
+ }
174
+ function renderManagedBlock(definition, rules) {
175
+ if (definition.commentStyle === "hash") {
176
+ return `# VCM:BEGIN version=${VCM_HARNESS_VERSION}\n${rules.trimEnd()}\n# VCM:END`;
177
+ }
178
+ return `<!-- VCM:BEGIN version=${VCM_HARNESS_VERSION} -->\n${rules.trimEnd()}\n<!-- VCM:END -->`;
179
+ }
180
+ function getManagedBlockPattern(definition) {
181
+ return definition.commentStyle === "hash"
182
+ ? HASH_MANAGED_BLOCK_PATTERN
183
+ : MANAGED_BLOCK_PATTERN;
184
+ }
185
+ function renderNewHarnessFile(definition, block) {
186
+ const frontmatter = definition.frontmatter
187
+ ? `${definition.frontmatter.trimEnd()}\n\n`
188
+ : "";
189
+ return `${frontmatter}# ${definition.title}\n\n${block}\n`;
190
+ }
191
+ function renderAgentFrontmatter(name, description) {
192
+ return `---\nname: ${name}\ndescription: ${description}\ntools: Read, Grep, Glob, Bash, Edit, Write\n---`;
193
+ }
194
+ function resolveHarnessPath(repoRoot, relativePath) {
195
+ return path.join(repoRoot, relativePath);
196
+ }
@@ -30,7 +30,7 @@ export function createMessageService(deps) {
30
30
  status: "queued",
31
31
  createdAt: timestamp
32
32
  };
33
- message.bodyPath = await writeMessageBody(deps.fs, input.repoRoot, input.handoffDir, message);
33
+ message.bodyPath = await writeMessageBody(deps.fs, input.taskRepoRoot ?? input.repoRoot, input.handoffDir, message);
34
34
  const state = await this.getOrchestrationState(input);
35
35
  const session = await deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.toRole);
36
36
  if (!session || session.status !== "running") {
@@ -1,8 +1,22 @@
1
1
  import path from "node:path";
2
2
  import { ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
+ const DEFAULT_HANDOFF_ROOT = ".ai/handoffs";
5
+ const DEFAULT_STATE_ROOT = ".ai/vcm";
6
+ const LEGACY_STATE_ROOT = ".vcm";
4
7
  export function createProjectService(deps) {
5
8
  let currentProject = null;
9
+ async function migrateLegacyStateRoot(repoRoot) {
10
+ if (!deps.fs.copyDir) {
11
+ return;
12
+ }
13
+ const legacyStateRoot = path.join(repoRoot, LEGACY_STATE_ROOT);
14
+ const currentStateRoot = path.join(repoRoot, DEFAULT_STATE_ROOT);
15
+ if (!(await deps.fs.pathExists(legacyStateRoot)) || await deps.fs.pathExists(currentStateRoot)) {
16
+ return;
17
+ }
18
+ await deps.fs.copyDir(legacyStateRoot, currentStateRoot);
19
+ }
6
20
  return {
7
21
  async connectProject(input) {
8
22
  const requestedPath = input.repoPath.trim();
@@ -26,10 +40,13 @@ export function createProjectService(deps) {
26
40
  statusCode: 400
27
41
  });
28
42
  }
29
- const config = buildDefaultProjectConfig(repoRoot);
43
+ const config = await this.loadConfig(repoRoot);
30
44
  await deps.fs.ensureDir(path.join(repoRoot, config.handoffRoot));
31
45
  await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "tasks"));
32
46
  await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "sessions"));
47
+ await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "messages"));
48
+ await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "orchestration"));
49
+ await deps.fs.ensureDir(path.join(repoRoot, config.stateRoot, "worktrees"));
33
50
  await this.saveConfig(config, true);
34
51
  const warnings = [];
35
52
  let branch = "unknown";
@@ -59,27 +76,39 @@ export function createProjectService(deps) {
59
76
  config,
60
77
  warnings
61
78
  };
79
+ await deps.appSettings.recordRecentRepositoryPath(repoRoot);
62
80
  return currentProject;
63
81
  },
64
82
  async getCurrentProject() {
65
83
  return currentProject;
66
84
  },
85
+ async getRecentRepositoryPaths() {
86
+ return deps.appSettings.getRecentRepositoryPaths();
87
+ },
67
88
  async loadConfig(repoRoot) {
68
89
  const configPath = this.getConfigPath(repoRoot);
69
- if (!(await deps.fs.pathExists(configPath))) {
70
- return buildDefaultProjectConfig(repoRoot);
90
+ if (await deps.fs.pathExists(configPath)) {
91
+ return normalizeProjectConfig(await deps.fs.readJson(configPath), repoRoot);
92
+ }
93
+ const legacyConfigPath = path.join(repoRoot, LEGACY_STATE_ROOT, "config.json");
94
+ if (await deps.fs.pathExists(legacyConfigPath)) {
95
+ const migratedConfig = normalizeProjectConfig(await deps.fs.readJson(legacyConfigPath), repoRoot);
96
+ await migrateLegacyStateRoot(repoRoot);
97
+ await this.saveConfig(migratedConfig, true);
98
+ return migratedConfig;
71
99
  }
72
- return deps.fs.readJson(configPath);
100
+ return buildDefaultProjectConfig(repoRoot);
73
101
  },
74
102
  async saveConfig(config, force = false) {
75
- const configPath = this.getConfigPath(config.repoRoot);
103
+ const normalizedConfig = normalizeProjectConfig(config, config.repoRoot);
104
+ const configPath = this.getConfigPath(normalizedConfig.repoRoot);
76
105
  if (!force && await deps.fs.pathExists(configPath)) {
77
106
  return;
78
107
  }
79
- await deps.fs.writeJsonAtomic(configPath, config);
108
+ await deps.fs.writeJsonAtomic(configPath, normalizedConfig);
80
109
  },
81
110
  getConfigPath(repoRoot) {
82
- return path.join(repoRoot, ".vcm", "config.json");
111
+ return path.join(repoRoot, DEFAULT_STATE_ROOT, "config.json");
83
112
  }
84
113
  };
85
114
  }
@@ -97,9 +126,21 @@ export function buildDefaultProjectConfig(repoRoot) {
97
126
  version: 1,
98
127
  repoRoot,
99
128
  defaultRoles: [...ROLE_NAMES],
100
- handoffRoot: ".ai/handoffs",
101
- stateRoot: ".vcm",
129
+ handoffRoot: DEFAULT_HANDOFF_ROOT,
130
+ stateRoot: DEFAULT_STATE_ROOT,
102
131
  terminalBackend: "node-pty",
103
132
  claudeCommand: process.env.VCM_CLAUDE_COMMAND || "claude"
104
133
  };
105
134
  }
135
+ function normalizeProjectConfig(input, repoRoot) {
136
+ const fallback = buildDefaultProjectConfig(repoRoot);
137
+ return {
138
+ version: 1,
139
+ repoRoot,
140
+ defaultRoles: input.defaultRoles?.length ? input.defaultRoles : fallback.defaultRoles,
141
+ handoffRoot: input.handoffRoot || fallback.handoffRoot,
142
+ stateRoot: DEFAULT_STATE_ROOT,
143
+ terminalBackend: "node-pty",
144
+ claudeCommand: input.claudeCommand || fallback.claudeCommand
145
+ };
146
+ }
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { ROLE_NAMES, isDispatchableRole } from "../../shared/constants.js";
4
4
  import { VcmError } from "../errors.js";
5
5
  import { resolveRepoPath } from "../adapters/filesystem.js";
6
- import { renderRoleMessagingContext } from "../templates/role-messaging-context.js";
6
+ import { claudeTranscriptPath } from "./claude-transcript-service.js";
7
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
7
8
  export function createSessionService(deps) {
8
9
  const now = deps.now ?? (() => new Date().toISOString());
9
10
  async function launchRoleSession(repoRoot, taskSlug, role, input, launchMode) {
@@ -13,7 +14,8 @@ export function createSessionService(deps) {
13
14
  }
14
15
  const config = await deps.projectService.loadConfig(repoRoot);
15
16
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
16
- const paths = deps.artifactService.getHandoffPaths(repoRoot, task.handoffDir);
17
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
18
+ const paths = deps.artifactService.getHandoffPaths(taskRepoRoot, task.handoffDir);
17
19
  const persisted = await loadPersistedRoleRecord(deps.fs, repoRoot, config.stateRoot, taskSlug, role);
18
20
  const permissionMode = input.permissionMode ?? persisted?.permissionMode ?? "default";
19
21
  const claudeSessionId = launchMode === "resume"
@@ -27,13 +29,16 @@ export function createSessionService(deps) {
27
29
  hint: "Start the role once before using Resume."
28
30
  });
29
31
  }
32
+ const transcriptPath = launchMode === "resume" && persisted?.transcriptPath
33
+ ? persisted.transcriptPath
34
+ : claudeTranscriptPath(taskRepoRoot, claudeSessionId);
30
35
  const startCommand = deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, claudeSessionId, launchMode === "resume");
31
36
  const runtimeSession = await deps.runtime.createSession({
32
37
  taskSlug,
33
38
  role,
34
39
  command: startCommand.command,
35
40
  args: startCommand.args,
36
- cwd: repoRoot,
41
+ cwd: taskRepoRoot,
37
42
  env: {
38
43
  VCM_API_URL: deps.apiUrl,
39
44
  VCM_CTL_COMMAND: deps.vcmctlCommand,
@@ -42,18 +47,19 @@ export function createSessionService(deps) {
42
47
  },
43
48
  cols: input.cols,
44
49
  rows: input.rows,
45
- logPath: resolveRepoPath(repoRoot, paths.roleLogPaths[role])
50
+ logPath: resolveRepoPath(taskRepoRoot, paths.roleLogPaths[role])
46
51
  });
47
52
  const timestamp = now();
48
53
  const record = {
49
54
  id: runtimeSession.id,
50
55
  claudeSessionId,
56
+ transcriptPath,
51
57
  taskSlug,
52
58
  role,
53
59
  status: runtimeSession.status,
54
60
  command: startCommand.display,
55
61
  permissionMode,
56
- cwd: repoRoot,
62
+ cwd: taskRepoRoot,
57
63
  terminalBackend: "node-pty",
58
64
  pid: runtimeSession.pid,
59
65
  logPath: paths.roleLogPaths[role],
@@ -69,7 +75,6 @@ export function createSessionService(deps) {
69
75
  deps.registry.upsert(record);
70
76
  await persistTaskSession(deps.fs, repoRoot, config.stateRoot, record);
71
77
  await deps.taskService.updateTaskStatus(repoRoot, taskSlug, "running");
72
- deps.runtime.write(runtimeSession.id, `${renderRoleMessagingContext(task, paths, role, deps.vcmctlCommand)}\r`);
73
78
  return record;
74
79
  }
75
80
  return {
@@ -110,9 +115,7 @@ export function createSessionService(deps) {
110
115
  await deps.runtime.stop(existing.id);
111
116
  }
112
117
  deps.registry.remove(existing.id);
113
- return existing.claudeSessionId
114
- ? launchRoleSession(repoRoot, taskSlug, role, input, "resume")
115
- : launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
118
+ return launchRoleSession(repoRoot, taskSlug, role, input, "fresh");
116
119
  },
117
120
  async getRoleSession(repoRoot, taskSlug, role) {
118
121
  const config = await deps.projectService.loadConfig(repoRoot);
@@ -197,6 +200,7 @@ async function persistTaskSession(fs, repoRoot, stateRoot, session) {
197
200
  [session.role]: {
198
201
  id: session.id,
199
202
  claudeSessionId: session.claudeSessionId,
203
+ transcriptPath: session.transcriptPath,
200
204
  status: session.status,
201
205
  record
202
206
  }
@@ -1,9 +1,11 @@
1
+ import { getTaskRuntimeRepoRoot } from "./task-service.js";
1
2
  export function createStatusService(deps) {
2
3
  return {
3
4
  async getTaskStatus(repoRoot, taskSlug) {
4
5
  const task = await deps.taskService.loadTask(repoRoot, taskSlug);
6
+ const taskRepoRoot = getTaskRuntimeRepoRoot(task);
5
7
  const artifacts = await deps.artifactService.listArtifacts({
6
- repoRoot,
8
+ repoRoot: taskRepoRoot,
7
9
  handoffDir: task.handoffDir
8
10
  });
9
11
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
@@ -14,8 +16,84 @@ export function createStatusService(deps) {
14
16
  task,
15
17
  sessions,
16
18
  artifacts,
19
+ workflow: buildWorkflowReport(artifacts, sessions),
17
20
  warnings
18
21
  };
19
22
  }
20
23
  };
21
24
  }
25
+ function buildWorkflowReport(artifacts, sessions) {
26
+ const isComplete = (kind) => artifacts.checks.find((check) => check.kind === kind)?.status === "ok";
27
+ const roleIsRunning = (role) => sessions.some((session) => session.role === role && session.status === "running");
28
+ const architectureComplete = isComplete("architecture-plan");
29
+ const implementationComplete = isComplete("implementation-log") && isComplete("validation-log");
30
+ const reviewComplete = isComplete("review-report");
31
+ const docsSyncComplete = isComplete("docs-sync-report");
32
+ const steps = [
33
+ {
34
+ id: "architecture-plan",
35
+ label: "Architecture",
36
+ role: "architect",
37
+ artifactPaths: [artifacts.paths.architecturePlanPath],
38
+ status: architectureComplete ? "complete" : "ready",
39
+ detail: architectureComplete
40
+ ? "architecture-plan.md is ready."
41
+ : roleIsRunning("architect")
42
+ ? "Architect is running; produce architecture-plan.md before coder work."
43
+ : "Start architect and produce architecture-plan.md before coder work."
44
+ },
45
+ {
46
+ id: "implementation",
47
+ label: "Implementation",
48
+ role: "coder",
49
+ artifactPaths: [artifacts.paths.implementationLogPath, artifacts.paths.validationLogPath],
50
+ status: implementationComplete ? "complete" : architectureComplete ? "ready" : "blocked",
51
+ detail: implementationComplete
52
+ ? "implementation-log.md and validation-log.md are ready."
53
+ : architectureComplete
54
+ ? "Start coder, then update implementation-log.md and validation-log.md."
55
+ : "Blocked until architecture-plan.md is complete."
56
+ },
57
+ {
58
+ id: "review",
59
+ label: "Review",
60
+ role: "reviewer",
61
+ artifactPaths: [artifacts.paths.reviewReportPath],
62
+ status: reviewComplete ? "complete" : implementationComplete ? "ready" : "blocked",
63
+ detail: reviewComplete
64
+ ? "review-report.md is ready."
65
+ : implementationComplete
66
+ ? "Start reviewer for independent review and final test adequacy."
67
+ : "Blocked until implementation-log.md and validation-log.md are complete."
68
+ },
69
+ {
70
+ id: "docs-sync",
71
+ label: "Docs Sync",
72
+ role: "architect",
73
+ artifactPaths: [artifacts.paths.docsSyncReportPath],
74
+ status: docsSyncComplete ? "complete" : reviewComplete ? "ready" : "blocked",
75
+ detail: docsSyncComplete
76
+ ? "docs-sync-report.md is ready."
77
+ : reviewComplete
78
+ ? "Send architect a docs-sync / architecture drift check task."
79
+ : "Blocked until review-report.md is complete."
80
+ },
81
+ {
82
+ id: "final-acceptance",
83
+ label: "PM Final",
84
+ role: "project-manager",
85
+ artifactPaths: [],
86
+ status: docsSyncComplete ? "ready" : "blocked",
87
+ detail: docsSyncComplete
88
+ ? "Project Manager can prepare final acceptance, commit, and PR."
89
+ : "Blocked until architect docs sync is complete."
90
+ }
91
+ ];
92
+ const current = steps.find((step) => step.status !== "complete") ?? steps[steps.length - 1];
93
+ return {
94
+ currentStepId: current.id,
95
+ nextAction: current.detail,
96
+ blocked: current.status === "blocked",
97
+ steps
98
+ };
99
+ }
@@ -8,6 +8,8 @@ export function createTaskService(deps) {
8
8
  assertValidTaskSlug(input.taskSlug);
9
9
  const config = await deps.projectService.loadConfig(repoRoot);
10
10
  const taskPath = getTaskPath(repoRoot, config.stateRoot, input.taskSlug);
11
+ const branch = `feature/${input.taskSlug}`;
12
+ const worktreePath = getTaskWorktreePath(repoRoot, config.stateRoot, input.taskSlug);
11
13
  if (await deps.fs.pathExists(taskPath)) {
12
14
  throw new VcmError({
13
15
  code: "TASK_EXISTS",
@@ -15,6 +17,46 @@ export function createTaskService(deps) {
15
17
  statusCode: 409
16
18
  });
17
19
  }
20
+ if (await deps.git.branchExists(repoRoot, branch)) {
21
+ throw new VcmError({
22
+ code: "TASK_BRANCH_EXISTS",
23
+ message: `Task branch already exists: ${branch}`,
24
+ statusCode: 409,
25
+ hint: "Choose a different task name or clean up the existing branch."
26
+ });
27
+ }
28
+ if (await deps.fs.pathExists(worktreePath)) {
29
+ throw new VcmError({
30
+ code: "TASK_WORKTREE_EXISTS",
31
+ message: `Task worktree already exists: ${worktreePath}`,
32
+ statusCode: 409,
33
+ hint: "Choose a different task name or clean up the existing worktree."
34
+ });
35
+ }
36
+ if (!(await deps.git.isIgnored(repoRoot, `${config.stateRoot}/config.json`))) {
37
+ throw new VcmError({
38
+ code: "VCM_STATE_NOT_IGNORED",
39
+ message: `${config.stateRoot}/ is not ignored by Git.`,
40
+ statusCode: 409,
41
+ hint: "Apply VCM Harness first so .gitignore contains the VCM managed block."
42
+ });
43
+ }
44
+ const baseStatus = await deps.git.getStatusPorcelain(repoRoot);
45
+ if (baseStatus.trim()) {
46
+ throw new VcmError({
47
+ code: "BASE_REPO_DIRTY",
48
+ message: "The connected repository has uncommitted changes.",
49
+ statusCode: 409,
50
+ hint: "Commit, stash, or discard base repository changes before creating a task worktree."
51
+ });
52
+ }
53
+ await deps.fs.ensureDir(path.dirname(worktreePath));
54
+ await deps.git.createWorktree({
55
+ repoRoot,
56
+ branch,
57
+ worktreePath,
58
+ baseRef: "HEAD"
59
+ });
18
60
  const timestamp = now();
19
61
  const task = {
20
62
  version: 1,
@@ -23,18 +65,20 @@ export function createTaskService(deps) {
23
65
  createdAt: timestamp,
24
66
  updatedAt: timestamp,
25
67
  repoRoot,
26
- branch: await deps.git.getCurrentBranch(repoRoot),
68
+ worktreePath,
69
+ branch,
27
70
  handoffDir: path.posix.join(config.handoffRoot, input.taskSlug),
28
71
  status: "created",
29
- specPath: input.specPath
72
+ specPath: input.specPath,
73
+ cleanupStatus: "active"
30
74
  };
31
75
  await deps.artifactService.ensureHandoffStructure({
32
- repoRoot,
76
+ repoRoot: worktreePath,
33
77
  taskSlug: input.taskSlug,
34
78
  handoffDir: task.handoffDir
35
79
  });
36
80
  await deps.artifactService.createArtifactTemplates({
37
- repoRoot,
81
+ repoRoot: worktreePath,
38
82
  taskSlug: input.taskSlug,
39
83
  handoffDir: task.handoffDir
40
84
  });
@@ -80,9 +124,79 @@ export function createTaskService(deps) {
80
124
  };
81
125
  await this.saveTask(repoRoot, updated);
82
126
  return updated;
127
+ },
128
+ async cleanupTask(repoRoot, taskSlug, options = {}) {
129
+ assertValidTaskSlug(taskSlug);
130
+ if (!deps.fs.removePath) {
131
+ throw new VcmError({
132
+ code: "FILESYSTEM_REMOVE_UNAVAILABLE",
133
+ message: "This VCM runtime cannot remove task files.",
134
+ statusCode: 500
135
+ });
136
+ }
137
+ const config = await deps.projectService.loadConfig(repoRoot);
138
+ const task = await this.loadTask(repoRoot, taskSlug);
139
+ const removedStatePaths = [];
140
+ const cleanedAt = now();
141
+ if (task.worktreePath) {
142
+ assertTaskWorktreePath(repoRoot, config.stateRoot, task.worktreePath);
143
+ const status = await deps.git.getStatusPorcelain(task.worktreePath);
144
+ if (status.trim() && !options.force) {
145
+ throw new VcmError({
146
+ code: "TASK_WORKTREE_DIRTY",
147
+ message: `Task worktree has uncommitted changes: ${task.worktreePath}`,
148
+ statusCode: 409,
149
+ hint: "Commit, stash, or discard the task worktree changes before cleanup, or retry with force."
150
+ });
151
+ }
152
+ await deps.git.removeWorktree(repoRoot, task.worktreePath, { force: options.force });
153
+ }
154
+ for (const statePath of getTaskStatePaths(repoRoot, config.stateRoot, taskSlug)) {
155
+ await deps.fs.removePath(statePath, { force: true });
156
+ removedStatePaths.push(statePath);
157
+ }
158
+ let deletedBranch;
159
+ if (options.deleteBranch) {
160
+ await deps.git.deleteBranch(repoRoot, task.branch, { force: options.forceDeleteBranch });
161
+ deletedBranch = task.branch;
162
+ }
163
+ return {
164
+ taskSlug,
165
+ removedWorktreePath: task.worktreePath,
166
+ removedStatePaths,
167
+ deletedBranch,
168
+ cleanedAt
169
+ };
83
170
  }
84
171
  };
85
172
  }
173
+ export function getTaskRuntimeRepoRoot(task) {
174
+ return task.worktreePath ?? task.repoRoot;
175
+ }
86
176
  function getTaskPath(repoRoot, stateRoot, taskSlug) {
87
177
  return path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`);
88
178
  }
179
+ function getTaskWorktreePath(repoRoot, stateRoot, taskSlug) {
180
+ return path.join(repoRoot, stateRoot, "worktrees", taskSlug);
181
+ }
182
+ function getTaskStatePaths(repoRoot, stateRoot, taskSlug) {
183
+ return [
184
+ path.join(repoRoot, stateRoot, "tasks", `${taskSlug}.json`),
185
+ path.join(repoRoot, stateRoot, "sessions", `${taskSlug}.json`),
186
+ path.join(repoRoot, stateRoot, "messages", `${taskSlug}.jsonl`),
187
+ path.join(repoRoot, stateRoot, "orchestration", `${taskSlug}.json`)
188
+ ];
189
+ }
190
+ function assertTaskWorktreePath(repoRoot, stateRoot, worktreePath) {
191
+ const worktreeRoot = path.resolve(repoRoot, stateRoot, "worktrees");
192
+ const resolvedWorktreePath = path.resolve(worktreePath);
193
+ const relative = path.relative(worktreeRoot, resolvedWorktreePath);
194
+ if (relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
195
+ throw new VcmError({
196
+ code: "TASK_WORKTREE_PATH_INVALID",
197
+ message: `Refusing to clean up worktree outside ${worktreeRoot}.`,
198
+ statusCode: 400,
199
+ hint: resolvedWorktreePath
200
+ });
201
+ }
202
+ }