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.
- package/README.md +207 -66
- package/dist/backend/adapters/filesystem.js +13 -0
- package/dist/backend/adapters/git-adapter.js +79 -1
- package/dist/backend/adapters/translation-provider.js +145 -0
- package/dist/backend/api/artifact-routes.js +16 -7
- package/dist/backend/api/harness-routes.js +22 -0
- package/dist/backend/api/message-routes.js +2 -0
- package/dist/backend/api/project-routes.js +3 -8
- package/dist/backend/api/task-routes.js +14 -0
- package/dist/backend/api/translation-routes.js +70 -0
- package/dist/backend/runtime/node-pty-runtime.js +20 -18
- package/dist/backend/server.js +33 -2
- package/dist/backend/services/app-settings-service.js +128 -0
- package/dist/backend/services/artifact-service.js +7 -4
- package/dist/backend/services/claude-transcript-service.js +509 -0
- package/dist/backend/services/command-dispatcher.js +4 -2
- package/dist/backend/services/harness-service.js +196 -0
- package/dist/backend/services/message-service.js +1 -1
- package/dist/backend/services/project-service.js +50 -9
- package/dist/backend/services/session-service.js +13 -9
- package/dist/backend/services/status-service.js +79 -1
- package/dist/backend/services/task-service.js +118 -4
- package/dist/backend/services/translation-prompts.js +173 -0
- package/dist/backend/services/translation-queue.js +39 -0
- package/dist/backend/services/translation-service.js +546 -0
- package/dist/backend/templates/handoff.js +32 -0
- package/dist/backend/templates/harness/architect-agent.js +12 -0
- package/dist/backend/templates/harness/claude-root.js +14 -0
- package/dist/backend/templates/harness/coder-agent.js +11 -0
- package/dist/backend/templates/harness/gitignore.js +9 -0
- package/dist/backend/templates/harness/project-manager-agent.js +14 -0
- package/dist/backend/templates/harness/reviewer-agent.js +13 -0
- package/dist/backend/ws/translation-ws.js +35 -0
- package/dist/shared/types/harness.js +1 -0
- package/dist/shared/types/translation.js +5 -0
- package/dist/shared/validation/artifact-check.js +15 -1
- package/dist/shared/validation/language-detect.js +46 -0
- package/dist-frontend/assets/index-CuiNNOzj.css +32 -0
- package/dist-frontend/assets/index-D59GuHCR.js +58 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +109 -40
- package/docs/product-design.md +370 -1374
- package/docs/v1-architecture-design.md +595 -1114
- package/docs/v1-implementation-plan.md +898 -1603
- package/package.json +1 -1
- package/scripts/verify-package.mjs +8 -0
- package/dist/backend/templates/role-messaging-context.js +0 -44
- package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
- package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
- 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 =
|
|
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 (
|
|
70
|
-
return
|
|
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
|
|
100
|
+
return buildDefaultProjectConfig(repoRoot);
|
|
73
101
|
},
|
|
74
102
|
async saveConfig(config, force = false) {
|
|
75
|
-
const
|
|
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,
|
|
108
|
+
await deps.fs.writeJsonAtomic(configPath, normalizedConfig);
|
|
80
109
|
},
|
|
81
110
|
getConfigPath(repoRoot) {
|
|
82
|
-
return path.join(repoRoot,
|
|
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:
|
|
101
|
-
stateRoot:
|
|
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 {
|
|
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
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|