vibe-coding-master 0.7.39 → 0.7.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/dist/backend/adapters/claude-adapter.js +8 -6
- package/dist/backend/api/artifact-routes.js +72 -1
- package/dist/backend/api/runtime-state-routes.js +17 -5
- package/dist/backend/api/task-routes.js +6 -0
- package/dist/backend/api/workflow-control-routes.js +56 -0
- package/dist/backend/cli/install-vcm-harness.js +42 -47
- package/dist/backend/role-tool-policy.js +60 -0
- package/dist/backend/server.js +21 -5
- package/dist/backend/services/artifact-service.js +309 -5
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/gate-review-service.js +81 -33
- package/dist/backend/services/harness-feedback-service.js +11 -4
- package/dist/backend/services/harness-service.js +35 -45
- package/dist/backend/services/lsp-plugin.js +1 -4
- package/dist/backend/services/message-service.js +105 -37
- package/dist/backend/services/status-service.js +6 -0
- package/dist/backend/services/workflow-control-service.js +854 -0
- package/dist/backend/templates/handoff.js +158 -3
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/check-scaffold-ledger.js +105 -39
- package/dist/backend/templates/harness/claude-root.js +9 -1
- package/dist/backend/templates/harness/coder-agent.js +1 -2
- package/dist/backend/templates/harness/coder-worker-agent.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +3 -4
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -1
- package/dist/backend/templates/harness/project-manager-agent.js +3 -2
- package/dist/backend/templates/harness/tester-agent.js +1 -0
- package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +5 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +3 -4
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -2
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +2 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +4 -7
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -13
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +62 -0
- package/dist/backend/templates/message-envelope.js +4 -4
- package/dist/shared/types/workflow.js +7 -1
- package/dist/shared/validation/artifact-check.js +99 -95
- package/dist/shared/validation/artifact-contract.js +9 -0
- package/dist/shared/validation/artifact-registry.js +211 -0
- package/dist-frontend/assets/{index-DkM0mnQD.js → index-Bocc2DWF.js} +34 -34
- package/dist-frontend/assets/{index-CXSOe-NN.css → index-C_XHGNBD.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +2 -1
- package/scripts/harness-tools/run-long-check +38 -8
- package/scripts/harness-tools/vcm-artifact +148 -0
- package/scripts/harness-tools/vcm-bash-guard +42 -47
- package/scripts/harness-tools/watch-job +58 -4
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
5
|
+
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
5
6
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
6
7
|
import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
|
|
7
8
|
import { renderArchitectScaffoldWorkerHarnessRules } from "../templates/harness/architect-scaffold-worker-agent.js";
|
|
@@ -25,6 +26,7 @@ import { renderVcmProposeMemorySkillRules } from "../templates/harness/vcm-propo
|
|
|
25
26
|
import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
|
|
26
27
|
import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
|
|
27
28
|
import { renderUpdateTaskStateTool, renderVcmTaskStateSkillRules } from "../templates/harness/vcm-task-state-skill.js";
|
|
29
|
+
import { renderVcmWorkflowReviewSkillRules } from "../templates/harness/vcm-workflow-review-skill.js";
|
|
28
30
|
import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
|
|
29
31
|
import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
|
|
30
32
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -53,7 +55,7 @@ const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
|
53
55
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
54
56
|
const VCM_HOOK_DEFINITIONS = [
|
|
55
57
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
56
|
-
{ eventName: "PreToolUse", matcher: "
|
|
58
|
+
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
57
59
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
58
60
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
59
61
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
@@ -137,6 +139,14 @@ const HARNESS_FILES = [
|
|
|
137
139
|
ownership: "whole-file",
|
|
138
140
|
renderRules: renderVcmTaskStateSkillRules
|
|
139
141
|
},
|
|
142
|
+
{
|
|
143
|
+
kind: "skill-vcm-workflow-review",
|
|
144
|
+
path: ".claude/skills/vcm-workflow-review/SKILL.md",
|
|
145
|
+
title: "VCM Workflow Review Skill",
|
|
146
|
+
frontmatter: renderSkillFrontmatter("vcm-workflow-review", "Use before every project-manager dispatch to Architect, Coder, or Tester so VCM can approve the workflow transition."),
|
|
147
|
+
ownership: "whole-file",
|
|
148
|
+
renderRules: renderVcmWorkflowReviewSkillRules
|
|
149
|
+
},
|
|
140
150
|
{
|
|
141
151
|
kind: "skill-vcm-final-acceptance",
|
|
142
152
|
path: ".claude/skills/vcm-final-acceptance/SKILL.md",
|
|
@@ -198,10 +208,9 @@ const HARNESS_FILES = [
|
|
|
198
208
|
path: ".claude/agents/reviewer.md",
|
|
199
209
|
title: "Reviewer Agent",
|
|
200
210
|
memoryBlock: true,
|
|
201
|
-
|
|
202
|
-
forbiddenTools: ["Grep"],
|
|
211
|
+
requiredDisallowedTools: REVIEWER_DISALLOWED_TOOLS,
|
|
203
212
|
requiredSkills: ["vcm-code-navigation"],
|
|
204
|
-
frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", {
|
|
213
|
+
frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", "), skills: ["vcm-code-navigation"] }),
|
|
205
214
|
renderRules: renderReviewerAgentRules
|
|
206
215
|
},
|
|
207
216
|
{
|
|
@@ -274,11 +283,10 @@ const HARNESS_FILES = [
|
|
|
274
283
|
path: ".claude/agents/architect.md",
|
|
275
284
|
title: "Architect Agent",
|
|
276
285
|
memoryBlock: true,
|
|
277
|
-
|
|
278
|
-
forbiddenTools: ["Grep"],
|
|
286
|
+
requiredDisallowedTools: CODE_ROLE_DISALLOWED_TOOLS,
|
|
279
287
|
requiredSkills: ["vcm-code-navigation"],
|
|
280
288
|
blankLineBeforeEnd: true,
|
|
281
|
-
frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.", {
|
|
289
|
+
frontmatter: renderAgentFrontmatter("architect", "VCM architecture role for plans, module boundaries, public contracts, verifiable behavior, and docs sync.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "), skills: ["vcm-code-navigation"] }),
|
|
282
290
|
renderRules: renderArchitectHarnessRules
|
|
283
291
|
},
|
|
284
292
|
{
|
|
@@ -286,10 +294,9 @@ const HARNESS_FILES = [
|
|
|
286
294
|
path: ".claude/agents/coder.md",
|
|
287
295
|
title: "Coder Agent",
|
|
288
296
|
memoryBlock: true,
|
|
289
|
-
|
|
290
|
-
forbiddenTools: ["Grep"],
|
|
297
|
+
requiredDisallowedTools: CODE_ROLE_DISALLOWED_TOOLS,
|
|
291
298
|
requiredSkills: ["vcm-code-navigation"],
|
|
292
|
-
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", {
|
|
299
|
+
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "), skills: ["vcm-code-navigation"] }),
|
|
293
300
|
renderRules: renderCoderHarnessRules
|
|
294
301
|
},
|
|
295
302
|
{
|
|
@@ -1380,47 +1387,28 @@ function renderNewHarnessFile(definition, block, contentAfterBlock = definition.
|
|
|
1380
1387
|
const suffix = contentAfterBlock?.trim();
|
|
1381
1388
|
return `${frontmatter}# ${definition.title}\n\n${block}${suffix ? `\n\n${suffix}` : ""}\n`;
|
|
1382
1389
|
}
|
|
1383
|
-
function
|
|
1384
|
-
|
|
1385
|
-
if (!frontmatterMatch) {
|
|
1386
|
-
return content;
|
|
1387
|
-
}
|
|
1388
|
-
const toolsMatch = frontmatterMatch[0].match(/^tools:\s*(.*)$/m);
|
|
1389
|
-
if (!toolsMatch) {
|
|
1390
|
-
return content.replace(/^(---\r?\n[\s\S]*?)(\r?\n---)/, `$1\ntools: ${requiredTool}$2`);
|
|
1391
|
-
}
|
|
1392
|
-
const tools = toolsMatch[1].split(",").map((tool) => tool.trim()).filter(Boolean);
|
|
1393
|
-
if (tools.includes(requiredTool)) {
|
|
1390
|
+
function ensureAgentDisallowedTools(content, requiredTools) {
|
|
1391
|
+
if (!requiredTools) {
|
|
1394
1392
|
return content;
|
|
1395
1393
|
}
|
|
1396
|
-
const nextTools = [...tools, requiredTool].join(", ");
|
|
1397
|
-
return content.replace(frontmatterMatch[0], frontmatterMatch[0].replace(toolsMatch[0], `tools: ${nextTools}`));
|
|
1398
|
-
}
|
|
1399
|
-
function ensureAgentTools(content, requiredTools) {
|
|
1400
|
-
return (requiredTools ?? []).reduce(ensureAgentTool, content);
|
|
1401
|
-
}
|
|
1402
|
-
function removeAgentTool(content, forbiddenTool) {
|
|
1403
1394
|
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
1404
1395
|
if (!frontmatterMatch) {
|
|
1405
1396
|
return content;
|
|
1406
1397
|
}
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
function normalizeAgentTools(content, requiredTools, forbiddenTools) {
|
|
1419
|
-
const required = ensureAgentTools(content, requiredTools);
|
|
1420
|
-
return (forbiddenTools ?? []).reduce(removeAgentTool, required);
|
|
1398
|
+
const withoutAllowedTools = frontmatterMatch[0].replace(/^tools:\s*.*\r?\n?/m, "");
|
|
1399
|
+
const disallowedMatch = withoutAllowedTools.match(/^disallowedTools:\s*(.*)$/m);
|
|
1400
|
+
const existing = disallowedMatch?.[1]
|
|
1401
|
+
.split(",")
|
|
1402
|
+
.map((tool) => tool.trim())
|
|
1403
|
+
.filter(Boolean) ?? [];
|
|
1404
|
+
const disallowedTools = [...new Set([...existing, ...requiredTools])].join(", ");
|
|
1405
|
+
const nextFrontmatter = disallowedMatch
|
|
1406
|
+
? withoutAllowedTools.replace(disallowedMatch[0], `disallowedTools: ${disallowedTools}`)
|
|
1407
|
+
: withoutAllowedTools.replace(/\r?\n---$/, `\ndisallowedTools: ${disallowedTools}\n---`);
|
|
1408
|
+
return content.replace(frontmatterMatch[0], nextFrontmatter);
|
|
1421
1409
|
}
|
|
1422
1410
|
function normalizeAgentFrontmatter(content, definition) {
|
|
1423
|
-
const toolsUpdated =
|
|
1411
|
+
const toolsUpdated = ensureAgentDisallowedTools(content, definition.requiredDisallowedTools);
|
|
1424
1412
|
return (definition.requiredSkills ?? []).reduce(ensureAgentSkill, toolsUpdated);
|
|
1425
1413
|
}
|
|
1426
1414
|
function ensureAgentSkill(content, requiredSkill) {
|
|
@@ -1572,13 +1560,15 @@ function isPlainObject(value) {
|
|
|
1572
1560
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1573
1561
|
}
|
|
1574
1562
|
function renderAgentFrontmatter(name, description, options = {}) {
|
|
1575
|
-
const
|
|
1563
|
+
const toolPolicy = options.disallowedTools
|
|
1564
|
+
? `disallowedTools: ${options.disallowedTools}`
|
|
1565
|
+
: `tools: ${options.tools ?? "Read, Grep, Glob, Bash, Edit, Write"}`;
|
|
1576
1566
|
const model = options.model ? `\nmodel: ${options.model}` : "";
|
|
1577
1567
|
const effort = options.effort ? `\neffort: ${options.effort}` : "";
|
|
1578
1568
|
const skills = options.skills?.length
|
|
1579
1569
|
? `\nskills:\n${options.skills.map((skill) => ` - ${skill}`).join("\n")}`
|
|
1580
1570
|
: "";
|
|
1581
|
-
return `---\nname: ${name}\ndescription: ${description}\
|
|
1571
|
+
return `---\nname: ${name}\ndescription: ${description}\n${toolPolicy}${model}${effort}${skills}\n---`;
|
|
1582
1572
|
}
|
|
1583
1573
|
function renderSkillFrontmatter(name, description) {
|
|
1584
1574
|
return `---\nname: ${name}\ndescription: ${description}\n---`;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
+
export { roleUsesLsp } from "../role-tool-policy.js";
|
|
3
4
|
export const VCM_LSP_PLUGIN_NAME = "vcm-lsp-bridge";
|
|
4
5
|
export const VCM_LSP_PLUGIN_DIR = fileURLToPath(new URL("../../../scripts/claude-plugins/vcm-lsp-bridge", import.meta.url));
|
|
5
6
|
export const VCM_LSP_PLUGIN_MANIFEST = path.join(VCM_LSP_PLUGIN_DIR, ".claude-plugin", "plugin.json");
|
|
6
|
-
const LSP_ROLES = new Set(["architect", "coder", "reviewer"]);
|
|
7
|
-
export function roleUsesLsp(role) {
|
|
8
|
-
return LSP_ROLES.has(role);
|
|
9
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { CORE_VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
@@ -94,8 +94,28 @@ export function createMessageService(deps) {
|
|
|
94
94
|
failureReason: `${routeFile.toRole} is still running.`
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
+
const messageId = id();
|
|
98
|
+
if (routeFile.fromRole === PM_ROLE && deps.workflowControlService) {
|
|
99
|
+
try {
|
|
100
|
+
await deps.workflowControlService.claimDispatch({
|
|
101
|
+
...toWorkflowContext(input),
|
|
102
|
+
routePath: routeFile.path,
|
|
103
|
+
targetRole: routeFile.toRole,
|
|
104
|
+
routeContentHash: routeFile.contentHash ?? "",
|
|
105
|
+
messageId
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
delivered: false,
|
|
111
|
+
requiresUserApproval: false,
|
|
112
|
+
clearedRouteFile: false,
|
|
113
|
+
failureReason: errorMessage(error)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
97
117
|
const message = {
|
|
98
|
-
id:
|
|
118
|
+
id: messageId,
|
|
99
119
|
taskSlug: input.taskSlug,
|
|
100
120
|
createdAt: timestamp,
|
|
101
121
|
fromRole: routeFile.fromRole,
|
|
@@ -103,7 +123,6 @@ export function createMessageService(deps) {
|
|
|
103
123
|
type: routeFile.type,
|
|
104
124
|
body: routeFile.body,
|
|
105
125
|
artifactRefs: routeFile.artifactRefs,
|
|
106
|
-
workflow: routeFile.fromRole === PM_ROLE ? routeFile.workflow : undefined,
|
|
107
126
|
bodyPath: routeFile.path,
|
|
108
127
|
routePath: routeFile.path,
|
|
109
128
|
dispatchingAt: timestamp,
|
|
@@ -115,16 +134,24 @@ export function createMessageService(deps) {
|
|
|
115
134
|
...message,
|
|
116
135
|
deliveredAt: timestamp
|
|
117
136
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
try {
|
|
138
|
+
await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered), {
|
|
139
|
+
enterDelayMs: autoDispatchEnterDelayMs
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (routeFile.fromRole === PM_ROLE) {
|
|
144
|
+
await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
121
148
|
await deps.sessionService.markRoleActivityRunning(input.repoRoot, input.taskSlug, routeFile.toRole, session.id);
|
|
122
149
|
if (routeFile.fromRole === PM_ROLE) {
|
|
123
150
|
await deps.taskWorkflowService?.recordPmDispatch({
|
|
124
151
|
taskRepoRoot: input.taskRepoRoot ?? input.repoRoot,
|
|
125
152
|
stateRoot: input.stateRoot,
|
|
126
153
|
taskSlug: input.taskSlug
|
|
127
|
-
},
|
|
154
|
+
}, undefined, {
|
|
128
155
|
messageId: delivered.id,
|
|
129
156
|
toRole: delivered.toRole
|
|
130
157
|
}).catch(() => undefined);
|
|
@@ -158,7 +185,7 @@ export function createMessageService(deps) {
|
|
|
158
185
|
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
159
186
|
const messageId = extractVcmMessageId(input.prompt);
|
|
160
187
|
if (!messageId) {
|
|
161
|
-
return
|
|
188
|
+
return confirmManualRoute(input, timestamp);
|
|
162
189
|
}
|
|
163
190
|
const message = findDeliveredMessageForPrompt(messages, input.role, messageId);
|
|
164
191
|
if (!message) {
|
|
@@ -169,6 +196,9 @@ export function createMessageService(deps) {
|
|
|
169
196
|
acceptedAt: timestamp,
|
|
170
197
|
failureReason: undefined
|
|
171
198
|
};
|
|
199
|
+
if (accepted.fromRole === PM_ROLE) {
|
|
200
|
+
await deps.workflowControlService?.confirmDispatch(toWorkflowContext(input), accepted.id);
|
|
201
|
+
}
|
|
172
202
|
await appendMessageSnapshot(deps.fs, input, accepted);
|
|
173
203
|
await clearRouteFileIfStillMatchesMessage(deps.fs, input, accepted);
|
|
174
204
|
return accepted;
|
|
@@ -251,6 +281,58 @@ export function createMessageService(deps) {
|
|
|
251
281
|
return messages.find((message) => message.id === messageId);
|
|
252
282
|
});
|
|
253
283
|
}
|
|
284
|
+
async function confirmManualRoute(input, timestamp) {
|
|
285
|
+
if (!input.prompt?.trim() || input.role === PM_ROLE)
|
|
286
|
+
return undefined;
|
|
287
|
+
const routes = await listRouteFiles(deps.fs, toRouteContext(input));
|
|
288
|
+
const routeFile = routes.find((candidate) => candidate.pending
|
|
289
|
+
&& candidate.fromRole === PM_ROLE
|
|
290
|
+
&& candidate.toRole === input.role
|
|
291
|
+
&& candidate.body.trim() === input.prompt.trim());
|
|
292
|
+
if (!routeFile)
|
|
293
|
+
return undefined;
|
|
294
|
+
const messageId = id();
|
|
295
|
+
await deps.workflowControlService?.claimDispatch({
|
|
296
|
+
...toWorkflowContext(input),
|
|
297
|
+
routePath: routeFile.path,
|
|
298
|
+
targetRole: routeFile.toRole,
|
|
299
|
+
routeContentHash: routeFile.contentHash ?? "",
|
|
300
|
+
messageId
|
|
301
|
+
});
|
|
302
|
+
const accepted = {
|
|
303
|
+
id: messageId,
|
|
304
|
+
taskSlug: input.taskSlug,
|
|
305
|
+
createdAt: timestamp,
|
|
306
|
+
fromRole: routeFile.fromRole,
|
|
307
|
+
toRole: routeFile.toRole,
|
|
308
|
+
type: routeFile.type,
|
|
309
|
+
body: routeFile.body,
|
|
310
|
+
artifactRefs: routeFile.artifactRefs,
|
|
311
|
+
bodyPath: routeFile.path,
|
|
312
|
+
routePath: routeFile.path,
|
|
313
|
+
dispatchingAt: timestamp,
|
|
314
|
+
deliveredAt: timestamp,
|
|
315
|
+
acceptedAt: timestamp
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
await deps.workflowControlService?.confirmDispatch(toWorkflowContext(input), messageId);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
await appendMessageSnapshot(deps.fs, input, accepted);
|
|
325
|
+
await clearRouteFileIfStillMatchesMessage(deps.fs, input, accepted);
|
|
326
|
+
await deps.taskWorkflowService?.recordPmDispatch({
|
|
327
|
+
taskRepoRoot: input.taskRepoRoot ?? input.repoRoot,
|
|
328
|
+
stateRoot: input.stateRoot,
|
|
329
|
+
taskSlug: input.taskSlug
|
|
330
|
+
}, undefined, {
|
|
331
|
+
messageId: accepted.id,
|
|
332
|
+
toRole: accepted.toRole
|
|
333
|
+
}).catch(() => undefined);
|
|
334
|
+
return accepted;
|
|
335
|
+
}
|
|
254
336
|
async function markDispatchConfirmationFailure(input, messageId, failureReason) {
|
|
255
337
|
await withTaskLock(taskLocks, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), async () => {
|
|
256
338
|
const messages = await readLatestMessages(deps.fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
@@ -258,6 +340,9 @@ export function createMessageService(deps) {
|
|
|
258
340
|
if (!current || current.acceptedAt) {
|
|
259
341
|
return;
|
|
260
342
|
}
|
|
343
|
+
if (current.fromRole === PM_ROLE) {
|
|
344
|
+
await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
|
|
345
|
+
}
|
|
261
346
|
await appendMessageSnapshot(deps.fs, input, {
|
|
262
347
|
...current,
|
|
263
348
|
failureReason
|
|
@@ -272,6 +357,14 @@ function toRouteContext(input) {
|
|
|
272
357
|
handoffDir: input.handoffDir
|
|
273
358
|
};
|
|
274
359
|
}
|
|
360
|
+
function toWorkflowContext(input) {
|
|
361
|
+
return {
|
|
362
|
+
taskRepoRoot: input.taskRepoRoot ?? input.repoRoot,
|
|
363
|
+
stateRoot: input.stateRoot,
|
|
364
|
+
handoffDir: input.handoffDir,
|
|
365
|
+
taskSlug: input.taskSlug
|
|
366
|
+
};
|
|
367
|
+
}
|
|
275
368
|
async function listRouteFiles(fs, input) {
|
|
276
369
|
const repoRoot = input.taskRepoRoot ?? input.repoRoot;
|
|
277
370
|
const routeDir = path.posix.join(input.handoffDir, "messages");
|
|
@@ -296,9 +389,9 @@ async function listRouteFiles(fs, input) {
|
|
|
296
389
|
type: parsed.type,
|
|
297
390
|
body: parsed.body,
|
|
298
391
|
artifactRefs: parsed.artifactRefs,
|
|
299
|
-
workflow: parsed.workflow,
|
|
300
392
|
exists: true,
|
|
301
|
-
pending: parsed.body.trim().length > 0
|
|
393
|
+
pending: parsed.body.trim().length > 0,
|
|
394
|
+
contentHash: createHash("sha256").update(content).digest("hex")
|
|
302
395
|
});
|
|
303
396
|
}
|
|
304
397
|
return routeFiles;
|
|
@@ -320,12 +413,10 @@ function parseRouteFileContent(content, fromRole, toRole) {
|
|
|
320
413
|
const { frontmatter, body } = splitFrontmatter(content);
|
|
321
414
|
const type = parseMessageType(frontmatter.type) ?? getDefaultMessageType(fromRole, toRole);
|
|
322
415
|
const artifactRefs = parseArtifactRefs(frontmatter);
|
|
323
|
-
const workflow = fromRole === PM_ROLE ? parseWorkflowDeclaration(frontmatter) : undefined;
|
|
324
416
|
return {
|
|
325
417
|
type,
|
|
326
418
|
body: body.trim(),
|
|
327
|
-
artifactRefs
|
|
328
|
-
workflow
|
|
419
|
+
artifactRefs
|
|
329
420
|
};
|
|
330
421
|
}
|
|
331
422
|
function splitFrontmatter(content) {
|
|
@@ -376,25 +467,6 @@ function parseArtifactRefs(frontmatter) {
|
|
|
376
467
|
.map((entry) => entry.trim())
|
|
377
468
|
.filter(Boolean);
|
|
378
469
|
}
|
|
379
|
-
function parseWorkflowDeclaration(frontmatter) {
|
|
380
|
-
const declaration = {
|
|
381
|
-
flow: frontmatter.workflow_flow,
|
|
382
|
-
step: frontmatter.workflow_step,
|
|
383
|
-
branch: frontmatter.workflow_branch,
|
|
384
|
-
resumePoint: frontmatter.workflow_resume_point,
|
|
385
|
-
status: frontmatter.workflow_status,
|
|
386
|
-
evidenceRefs: parseCommaSeparated(frontmatter.workflow_evidence_refs)
|
|
387
|
-
};
|
|
388
|
-
return Object.values(declaration).some((value) => Array.isArray(value) ? value.length > 0 : value !== undefined)
|
|
389
|
-
? declaration
|
|
390
|
-
: undefined;
|
|
391
|
-
}
|
|
392
|
-
function parseCommaSeparated(value) {
|
|
393
|
-
if (value === undefined) {
|
|
394
|
-
return undefined;
|
|
395
|
-
}
|
|
396
|
-
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
397
|
-
}
|
|
398
470
|
function getDefaultMessageType(fromRole, toRole) {
|
|
399
471
|
if (fromRole === PM_ROLE && toRole !== PM_ROLE) {
|
|
400
472
|
return "task";
|
|
@@ -454,14 +526,10 @@ async function clearRouteFileIfStillMatchesMessage(fs, input, message) {
|
|
|
454
526
|
const parsed = parseRouteFileContent(routeContent, fromRole, message.toRole);
|
|
455
527
|
if (parsed.body.trim() === message.body.trim() &&
|
|
456
528
|
parsed.type === message.type &&
|
|
457
|
-
arraysEqual(parsed.artifactRefs, message.artifactRefs)
|
|
458
|
-
workflowDeclarationsEqual(parsed.workflow, message.workflow)) {
|
|
529
|
+
arraysEqual(parsed.artifactRefs, message.artifactRefs)) {
|
|
459
530
|
await fs.writeText(absolutePath, "");
|
|
460
531
|
}
|
|
461
532
|
}
|
|
462
|
-
function workflowDeclarationsEqual(left, right) {
|
|
463
|
-
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
464
|
-
}
|
|
465
533
|
function isCoreRouteRole(role) {
|
|
466
534
|
return CORE_VCM_ROLE_NAMES.some((candidate) => candidate === role);
|
|
467
535
|
}
|
|
@@ -51,10 +51,16 @@ function degradedArtifactSummary(handoffDir) {
|
|
|
51
51
|
roleCommandPaths: Object.fromEntries(DISPATCHABLE_ROLES.map((role) => [role, `${roleCommandsDir}/${role}.md`])),
|
|
52
52
|
messageRoutePaths: {},
|
|
53
53
|
architectureBriefPath: `${handoffDir}/architecture-brief.md`,
|
|
54
|
+
architectureEvidencePath: `${handoffDir}/architecture-evidence.md`,
|
|
55
|
+
planningProgressPath: `${handoffDir}/planning-progress.md`,
|
|
54
56
|
architecturePlanPath: `${handoffDir}/architecture-plan.md`,
|
|
55
57
|
knownIssuesPath: `${handoffDir}/known-issues.md`,
|
|
58
|
+
coderCompletionPath: `${handoffDir}/coder-completion.md`,
|
|
59
|
+
architectDebugPath: `${handoffDir}/architect-debug.md`,
|
|
60
|
+
architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
|
|
56
61
|
testReportPath: `${handoffDir}/test-report.md`,
|
|
57
62
|
docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
|
|
63
|
+
workflowProgressPath: `${handoffDir}/workflow-progress.md`,
|
|
58
64
|
finalAcceptancePath: `${handoffDir}/final-acceptance.md`
|
|
59
65
|
},
|
|
60
66
|
checks: []
|