vibe-coding-master 0.7.41 → 0.7.43
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 +10 -5
- package/dist/backend/api/task-routes.js +31 -2
- package/dist/backend/api/workflow-control-routes.js +0 -31
- package/dist/backend/cli/install-vcm-harness.js +84 -9
- package/dist/backend/role-tool-policy.js +1 -2
- package/dist/backend/server.js +12 -5
- package/dist/backend/services/artifact-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +2 -2
- package/dist/backend/services/claude-hook-service.js +103 -4
- package/dist/backend/services/harness-feedback-service.js +105 -3
- package/dist/backend/services/harness-service.js +77 -10
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +10 -0
- package/dist/backend/services/session-service.js +76 -4
- package/dist/backend/services/workflow-control-service.js +439 -203
- package/dist/backend/templates/handoff.js +2 -3
- package/dist/backend/templates/harness/architect-agent.js +7 -4
- package/dist/backend/templates/harness/coder-agent.js +4 -5
- package/dist/backend/templates/harness/gate-review.js +7 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +18 -8
- package/dist/backend/templates/harness/project-manager-agent.js +5 -5
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +6 -7
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +14 -0
- package/dist/shared/validation/artifact-registry.js +1 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/{index-Bocc2DWF.js → index-VW9tYPP5.js} +38 -38
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-bash-guard +203 -13
|
@@ -76,6 +76,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
79
|
+
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
79
80
|
const timestamp = now();
|
|
80
81
|
const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
81
82
|
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
@@ -87,11 +88,11 @@ export function createHarnessFeedbackService(deps) {
|
|
|
87
88
|
status: "running",
|
|
88
89
|
analysisPath,
|
|
89
90
|
finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
|
|
91
|
+
pendingFeedbackPaths: pendingFeedback.map((item) => item.path),
|
|
90
92
|
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
91
93
|
createdAt: timestamp,
|
|
92
94
|
updatedAt: timestamp
|
|
93
95
|
};
|
|
94
|
-
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
95
96
|
try {
|
|
96
97
|
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
97
98
|
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
@@ -135,7 +136,12 @@ export function createHarnessFeedbackService(deps) {
|
|
|
135
136
|
const reportContent = await deps.fs.pathExists(reportPath)
|
|
136
137
|
? await deps.fs.readText(reportPath)
|
|
137
138
|
: "";
|
|
138
|
-
const reportErrors = reportContent.trim()
|
|
139
|
+
const reportErrors = reportContent.trim()
|
|
140
|
+
? [
|
|
141
|
+
...getRetrospectiveReportErrors(reportContent),
|
|
142
|
+
...getFeedbackDispositionErrors(repoRoot, marker.pendingFeedbackPaths ?? [], reportContent)
|
|
143
|
+
]
|
|
144
|
+
: ["Report is empty."];
|
|
139
145
|
const reportReady = reportErrors.length === 0;
|
|
140
146
|
if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
|
|
141
147
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
@@ -149,6 +155,19 @@ export function createHarnessFeedbackService(deps) {
|
|
|
149
155
|
});
|
|
150
156
|
return true;
|
|
151
157
|
}
|
|
158
|
+
try {
|
|
159
|
+
await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
163
|
+
...marker,
|
|
164
|
+
status: "failed",
|
|
165
|
+
failedAt: timestamp,
|
|
166
|
+
updatedAt: timestamp,
|
|
167
|
+
error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
|
|
168
|
+
});
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
152
171
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
153
172
|
...marker,
|
|
154
173
|
status: "completed",
|
|
@@ -245,7 +264,15 @@ export function createHarnessFeedbackService(deps) {
|
|
|
245
264
|
...(pendingFeedbackPaths.length > 0
|
|
246
265
|
? [
|
|
247
266
|
"",
|
|
248
|
-
"Process every listed feedback inside this retrospective. Record every disposition in the retrospective report
|
|
267
|
+
"Process every listed feedback inside this retrospective. Record every disposition in the retrospective report using this exact block for each assigned path:",
|
|
268
|
+
"",
|
|
269
|
+
"### Feedback: <exact assigned absolute path>",
|
|
270
|
+
"Decision: confirmed|rejected|duplicate|already-covered",
|
|
271
|
+
"Evidence: <concise evidence>",
|
|
272
|
+
"Impact: <impact>",
|
|
273
|
+
"Required action: <action or none>",
|
|
274
|
+
"",
|
|
275
|
+
"Do not edit or delete pending feedback files. VCM removes the assigned files after validating every disposition in the accepted report."
|
|
249
276
|
]
|
|
250
277
|
: []),
|
|
251
278
|
"",
|
|
@@ -375,6 +402,20 @@ export function createHarnessFeedbackService(deps) {
|
|
|
375
402
|
await deps.fs.removePath?.(statePath, { force: true });
|
|
376
403
|
}
|
|
377
404
|
}
|
|
405
|
+
async function removeProcessedFeedback(repoRoot, feedbackPaths) {
|
|
406
|
+
if (feedbackPaths.length === 0) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!deps.fs.removePath) {
|
|
410
|
+
throw new Error("The filesystem adapter does not support feedback removal.");
|
|
411
|
+
}
|
|
412
|
+
for (const feedbackPath of feedbackPaths) {
|
|
413
|
+
assertPendingFeedbackPath(feedbackPath);
|
|
414
|
+
}
|
|
415
|
+
for (const feedbackPath of feedbackPaths) {
|
|
416
|
+
await deps.fs.removePath(resolveRepoPath(repoRoot, feedbackPath), { force: true });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
378
419
|
async function readOptionalText(repoRoot, relativePath) {
|
|
379
420
|
const absolutePath = resolveRepoPath(repoRoot, relativePath);
|
|
380
421
|
return readAbsoluteOptionalText(absolutePath);
|
|
@@ -393,6 +434,67 @@ export function createHarnessFeedbackService(deps) {
|
|
|
393
434
|
assertHarnessEngineerAvailable
|
|
394
435
|
};
|
|
395
436
|
}
|
|
437
|
+
function getFeedbackDispositionErrors(repoRoot, feedbackPaths, reportContent) {
|
|
438
|
+
if (feedbackPaths.length === 0) {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const section = readLevelTwoSection(reportContent, "Feedback Dispositions");
|
|
442
|
+
if (!section) {
|
|
443
|
+
return ["Feedback Dispositions is empty."];
|
|
444
|
+
}
|
|
445
|
+
const errors = [];
|
|
446
|
+
const lines = section.split(/\r?\n/);
|
|
447
|
+
for (const feedbackPath of feedbackPaths) {
|
|
448
|
+
try {
|
|
449
|
+
assertPendingFeedbackPath(feedbackPath);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
errors.push(errorMessage(error));
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const absolutePath = resolveRepoPath(repoRoot, feedbackPath);
|
|
456
|
+
const heading = `### Feedback: ${absolutePath}`;
|
|
457
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
458
|
+
if (start < 0) {
|
|
459
|
+
errors.push(`Missing disposition for assigned feedback: ${absolutePath}.`);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const endOffset = lines.slice(start + 1).findIndex((line) => /^###\s+/.test(line.trim()));
|
|
463
|
+
const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
|
|
464
|
+
const block = lines.slice(start + 1, end);
|
|
465
|
+
const decision = readDispositionField(block, "Decision");
|
|
466
|
+
if (!decision || !["confirmed", "rejected", "duplicate", "already-covered"].includes(decision)) {
|
|
467
|
+
errors.push(`Feedback disposition Decision for ${absolutePath} must be confirmed|rejected|duplicate|already-covered.`);
|
|
468
|
+
}
|
|
469
|
+
for (const field of ["Evidence", "Impact", "Required action"]) {
|
|
470
|
+
if (!readDispositionField(block, field)) {
|
|
471
|
+
errors.push(`Feedback disposition ${field} is required for ${absolutePath}.`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return errors;
|
|
476
|
+
}
|
|
477
|
+
function readLevelTwoSection(content, heading) {
|
|
478
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
479
|
+
const match = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "im").exec(content);
|
|
480
|
+
if (!match || match.index === undefined) {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
const afterHeading = content.slice(match.index + match[0].length);
|
|
484
|
+
const nextHeading = /\n##\s+\S/.exec(afterHeading);
|
|
485
|
+
return (nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading).trim();
|
|
486
|
+
}
|
|
487
|
+
function readDispositionField(lines, field) {
|
|
488
|
+
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
489
|
+
return lines
|
|
490
|
+
.map((line) => new RegExp(`^${escapedField}:\\s*(.+)$`, "i").exec(line.trim())?.[1]?.trim())
|
|
491
|
+
.find((value) => Boolean(value));
|
|
492
|
+
}
|
|
493
|
+
function assertPendingFeedbackPath(feedbackPath) {
|
|
494
|
+
if (!/^\.ai\/vcm\/harness-feedback\/pending\/[A-Za-z0-9._-]+\.md$/.test(feedbackPath)) {
|
|
495
|
+
throw new Error(`Invalid assigned Harness Feedback path: ${feedbackPath}.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
396
498
|
function parseSimpleMetadata(content) {
|
|
397
499
|
const result = {};
|
|
398
500
|
for (const line of content.split(/\r?\n/).slice(0, 80)) {
|
|
@@ -57,8 +57,15 @@ const VCM_HOOK_DEFINITIONS = [
|
|
|
57
57
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
58
58
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
59
59
|
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
60
|
+
{ eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
61
|
+
{ eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
62
|
+
{ eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
63
|
+
{ eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
64
|
+
{ eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
65
|
+
{ eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
60
66
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
61
67
|
{ eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
68
|
+
{ eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
62
69
|
{ eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
|
|
63
70
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
64
71
|
];
|
|
@@ -119,7 +126,7 @@ const HARNESS_FILES = [
|
|
|
119
126
|
kind: "skill-vcm-code-navigation",
|
|
120
127
|
path: ".claude/skills/vcm-code-navigation/SKILL.md",
|
|
121
128
|
title: "VCM Code Navigation Skill",
|
|
122
|
-
frontmatter: renderSkillFrontmatter("vcm-code-navigation", "Use when Architect
|
|
129
|
+
frontmatter: renderSkillFrontmatter("vcm-code-navigation", "Use when Architect must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths."),
|
|
123
130
|
ownership: "whole-file",
|
|
124
131
|
renderRules: renderVcmCodeNavigationSkillRules
|
|
125
132
|
},
|
|
@@ -209,8 +216,8 @@ const HARNESS_FILES = [
|
|
|
209
216
|
title: "Reviewer Agent",
|
|
210
217
|
memoryBlock: true,
|
|
211
218
|
requiredDisallowedTools: REVIEWER_DISALLOWED_TOOLS,
|
|
212
|
-
|
|
213
|
-
frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", ")
|
|
219
|
+
removedSkills: ["vcm-code-navigation"],
|
|
220
|
+
frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", ") }),
|
|
214
221
|
renderRules: renderReviewerAgentRules
|
|
215
222
|
},
|
|
216
223
|
{
|
|
@@ -225,7 +232,8 @@ const HARNESS_FILES = [
|
|
|
225
232
|
path: ".claude/agents/harness-engineer.md",
|
|
226
233
|
title: "Harness Engineer Agent",
|
|
227
234
|
memoryBlock: true,
|
|
228
|
-
|
|
235
|
+
requiredTools: ["Skill"],
|
|
236
|
+
frontmatter: renderAgentFrontmatter("harness-engineer", "VCM task-scoped harness maintenance role for harness diagnosis, diff proposals, and VCM issue drafts.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Skill" }),
|
|
229
237
|
renderRules: renderHarnessEngineerHarnessRules
|
|
230
238
|
},
|
|
231
239
|
{
|
|
@@ -275,7 +283,8 @@ const HARNESS_FILES = [
|
|
|
275
283
|
path: ".claude/agents/project-manager.md",
|
|
276
284
|
title: "Project Manager Agent",
|
|
277
285
|
memoryBlock: true,
|
|
278
|
-
|
|
286
|
+
requiredTools: ["Skill"],
|
|
287
|
+
frontmatter: renderAgentFrontmatter("project-manager", "User-facing VCM orchestration role for task clarification, role routing, handoffs, acceptance, and PR preparation.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Skill" }),
|
|
279
288
|
renderRules: renderProjectManagerHarnessRules
|
|
280
289
|
},
|
|
281
290
|
{
|
|
@@ -295,8 +304,8 @@ const HARNESS_FILES = [
|
|
|
295
304
|
title: "Coder Agent",
|
|
296
305
|
memoryBlock: true,
|
|
297
306
|
requiredDisallowedTools: CODE_ROLE_DISALLOWED_TOOLS,
|
|
298
|
-
|
|
299
|
-
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", ")
|
|
307
|
+
removedSkills: ["vcm-code-navigation"],
|
|
308
|
+
frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", ") }),
|
|
300
309
|
renderRules: renderCoderHarnessRules
|
|
301
310
|
},
|
|
302
311
|
{
|
|
@@ -304,7 +313,8 @@ const HARNESS_FILES = [
|
|
|
304
313
|
path: ".claude/agents/tester.md",
|
|
305
314
|
title: "Tester Agent",
|
|
306
315
|
memoryBlock: true,
|
|
307
|
-
|
|
316
|
+
requiredTools: ["Skill"],
|
|
317
|
+
frontmatter: renderAgentFrontmatter("tester", "VCM testing role for validation, test adequacy, approved-scope validation, and risk findings.", { tools: "Read, Grep, Glob, Bash, Edit, Write, Skill" }),
|
|
308
318
|
renderRules: renderTesterHarnessRules
|
|
309
319
|
}
|
|
310
320
|
];
|
|
@@ -1407,9 +1417,42 @@ function ensureAgentDisallowedTools(content, requiredTools) {
|
|
|
1407
1417
|
: withoutAllowedTools.replace(/\r?\n---$/, `\ndisallowedTools: ${disallowedTools}\n---`);
|
|
1408
1418
|
return content.replace(frontmatterMatch[0], nextFrontmatter);
|
|
1409
1419
|
}
|
|
1420
|
+
function ensureAgentAllowedTool(content, requiredTool) {
|
|
1421
|
+
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
1422
|
+
if (!frontmatterMatch) {
|
|
1423
|
+
return content;
|
|
1424
|
+
}
|
|
1425
|
+
const frontmatter = frontmatterMatch[0];
|
|
1426
|
+
const toolsMatch = frontmatter.match(/^tools:\s*(.*)$/m);
|
|
1427
|
+
if (toolsMatch) {
|
|
1428
|
+
const existing = toolsMatch[1]
|
|
1429
|
+
.split(",")
|
|
1430
|
+
.map((tool) => tool.trim())
|
|
1431
|
+
.filter(Boolean);
|
|
1432
|
+
if (existing.includes(requiredTool)) {
|
|
1433
|
+
return content;
|
|
1434
|
+
}
|
|
1435
|
+
const tools = [...existing, requiredTool].join(", ");
|
|
1436
|
+
return content.replace(frontmatter, frontmatter.replace(toolsMatch[0], `tools: ${tools}`));
|
|
1437
|
+
}
|
|
1438
|
+
const disallowedMatch = frontmatter.match(/^disallowedTools:\s*(.*)$/m);
|
|
1439
|
+
if (!disallowedMatch) {
|
|
1440
|
+
return content;
|
|
1441
|
+
}
|
|
1442
|
+
const disallowedTools = disallowedMatch[1]
|
|
1443
|
+
.split(",")
|
|
1444
|
+
.map((tool) => tool.trim())
|
|
1445
|
+
.filter((tool) => tool && tool !== requiredTool);
|
|
1446
|
+
const nextFrontmatter = disallowedTools.length > 0
|
|
1447
|
+
? frontmatter.replace(disallowedMatch[0], `disallowedTools: ${disallowedTools.join(", ")}`)
|
|
1448
|
+
: frontmatter.replace(/^disallowedTools:\s*.*\r?\n?/m, "");
|
|
1449
|
+
return content.replace(frontmatter, nextFrontmatter);
|
|
1450
|
+
}
|
|
1410
1451
|
function normalizeAgentFrontmatter(content, definition) {
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1452
|
+
const allowedToolsUpdated = (definition.requiredTools ?? []).reduce(ensureAgentAllowedTool, content);
|
|
1453
|
+
const disallowedToolsUpdated = ensureAgentDisallowedTools(allowedToolsUpdated, definition.requiredDisallowedTools);
|
|
1454
|
+
const skillsUpdated = (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
|
|
1455
|
+
return (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
|
|
1413
1456
|
}
|
|
1414
1457
|
function ensureAgentSkill(content, requiredSkill) {
|
|
1415
1458
|
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
@@ -1431,6 +1474,30 @@ function ensureAgentSkill(content, requiredSkill) {
|
|
|
1431
1474
|
const nextSkills = `${skillsMatch[0].trimEnd()}\n - ${requiredSkill}`;
|
|
1432
1475
|
return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], nextSkills));
|
|
1433
1476
|
}
|
|
1477
|
+
function removeAgentSkill(content, removedSkill) {
|
|
1478
|
+
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
1479
|
+
if (!frontmatterMatch) {
|
|
1480
|
+
return content;
|
|
1481
|
+
}
|
|
1482
|
+
const frontmatter = frontmatterMatch[0];
|
|
1483
|
+
const skillsMatch = frontmatter.match(/^skills:[ \t]*(?:\r?\n((?:\s+-\s+[^\r\n]+\r?\n?)*))?/m);
|
|
1484
|
+
if (!skillsMatch) {
|
|
1485
|
+
return content;
|
|
1486
|
+
}
|
|
1487
|
+
const listedSkills = (skillsMatch[1] ?? "")
|
|
1488
|
+
.split(/\r?\n/)
|
|
1489
|
+
.map((line) => line.match(/^\s+-\s+(.+)$/)?.[1]?.trim())
|
|
1490
|
+
.filter((skill) => Boolean(skill));
|
|
1491
|
+
const remainingSkills = listedSkills.filter((skill) => skill !== removedSkill);
|
|
1492
|
+
if (remainingSkills.length === listedSkills.length) {
|
|
1493
|
+
return content;
|
|
1494
|
+
}
|
|
1495
|
+
const lineEnding = skillsMatch[0].includes("\r\n") ? "\r\n" : "\n";
|
|
1496
|
+
const replacement = remainingSkills.length > 0
|
|
1497
|
+
? `skills:${lineEnding}${remainingSkills.map((skill) => ` - ${skill}`).join(lineEnding)}${lineEnding}`
|
|
1498
|
+
: "";
|
|
1499
|
+
return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], replacement));
|
|
1500
|
+
}
|
|
1434
1501
|
function migrateLegacyHarnessFile(definition, currentContent, block) {
|
|
1435
1502
|
const legacyContent = definition.legacyWholeFile?.trimEnd();
|
|
1436
1503
|
if (!legacyContent) {
|
|
@@ -2,6 +2,19 @@ export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
|
2
2
|
export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
3
3
|
export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
4
4
|
export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
|
|
5
|
+
const MEMORY_REVIEW_ROLE_DRAFT_PATTERN = new RegExp(`^${escapeRegExp(MEMORY_REVIEW_RUNS_ROOT)}/[A-Za-z0-9._-]+/drafts/([A-Za-z0-9._-]+)\\.md$`);
|
|
6
|
+
export function memoryReviewRoleDraftPath(runId, role) {
|
|
7
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`;
|
|
8
|
+
}
|
|
9
|
+
export function isMemoryProposalSubmissionPath(artifactPath, role) {
|
|
10
|
+
if (artifactPath === ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH) {
|
|
11
|
+
return role === "architect";
|
|
12
|
+
}
|
|
13
|
+
return MEMORY_REVIEW_ROLE_DRAFT_PATTERN.exec(artifactPath)?.[1] === role;
|
|
14
|
+
}
|
|
5
15
|
export function architectPlanningCandidateSnapshotPath(runId) {
|
|
6
16
|
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
|
|
7
17
|
}
|
|
18
|
+
function escapeRegExp(value) {
|
|
19
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { isVcmRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export const ROLE_MODEL_STALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
4
|
+
export const ROLE_TOOL_STALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
5
|
+
export const ROLE_SUBAGENT_STALL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
6
|
+
const TOOL_TIMEOUT_GRACE_MS = 30_000;
|
|
7
|
+
export function createRoleStallDetectorService(deps) {
|
|
8
|
+
const modelTimeoutMs = deps.modelTimeoutMs ?? ROLE_MODEL_STALL_TIMEOUT_MS;
|
|
9
|
+
const toolTimeoutMs = deps.toolTimeoutMs ?? ROLE_TOOL_STALL_TIMEOUT_MS;
|
|
10
|
+
const subagentTimeoutMs = deps.subagentTimeoutMs ?? ROLE_SUBAGENT_STALL_TIMEOUT_MS;
|
|
11
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
12
|
+
const setTimer = deps.setTimeout ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
|
|
13
|
+
const clearTimer = deps.clearTimeout ?? ((timer) => globalThis.clearTimeout(timer));
|
|
14
|
+
const monitors = new Map();
|
|
15
|
+
return {
|
|
16
|
+
async recordHook(input) {
|
|
17
|
+
if (!isVcmRoleName(input.role)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
21
|
+
if (input.eventName === "Stop" || input.eventName === "StopFailure") {
|
|
22
|
+
clearMonitor(key);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const [session, round] = await Promise.all([
|
|
26
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, input.role),
|
|
27
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
28
|
+
]);
|
|
29
|
+
if (!session
|
|
30
|
+
|| session.status !== "running"
|
|
31
|
+
|| session.activityStatus !== "running"
|
|
32
|
+
|| round.status !== "running"
|
|
33
|
+
|| round.activeRole !== input.role
|
|
34
|
+
|| !round.roundId) {
|
|
35
|
+
clearMonitor(key);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let monitor = monitors.get(key);
|
|
39
|
+
if (!monitor
|
|
40
|
+
|| monitor.sessionId !== session.id
|
|
41
|
+
|| monitor.runtimeSessionToken !== session.runtimeSessionToken
|
|
42
|
+
|| monitor.roundId !== round.roundId
|
|
43
|
+
|| monitor.role !== input.role) {
|
|
44
|
+
clearMonitor(key);
|
|
45
|
+
monitor = {
|
|
46
|
+
context: taskInput(input),
|
|
47
|
+
role: input.role,
|
|
48
|
+
sessionId: session.id,
|
|
49
|
+
runtimeSessionToken: session.runtimeSessionToken,
|
|
50
|
+
roundId: round.roundId,
|
|
51
|
+
phase: "awaiting-model",
|
|
52
|
+
phaseStartedAt: now(),
|
|
53
|
+
generation: 0,
|
|
54
|
+
activeTools: new Map()
|
|
55
|
+
};
|
|
56
|
+
monitors.set(key, monitor);
|
|
57
|
+
}
|
|
58
|
+
advanceMonitor(key, monitor, input.eventName, input.event);
|
|
59
|
+
},
|
|
60
|
+
async reconcileTask(input) {
|
|
61
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
62
|
+
for (const [candidateKey, monitor] of monitors) {
|
|
63
|
+
if (monitor.context.repoRoot === input.repoRoot && candidateKey !== key) {
|
|
64
|
+
clearMonitor(candidateKey);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const monitor = monitors.get(key);
|
|
68
|
+
if (!monitor) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const [session, round] = await Promise.all([
|
|
72
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, monitor.role),
|
|
73
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
74
|
+
]);
|
|
75
|
+
if (!session
|
|
76
|
+
|| session.id !== monitor.sessionId
|
|
77
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
78
|
+
|| session.status !== "running"
|
|
79
|
+
|| session.activityStatus !== "running"
|
|
80
|
+
|| round.status !== "running"
|
|
81
|
+
|| round.activeRole !== monitor.role
|
|
82
|
+
|| round.roundId !== monitor.roundId) {
|
|
83
|
+
clearMonitor(key);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
getWarning(repoRoot, taskSlug) {
|
|
87
|
+
const warning = monitors.get(monitorKey(repoRoot, taskSlug))?.warning;
|
|
88
|
+
return warning ? { ...warning } : null;
|
|
89
|
+
},
|
|
90
|
+
ignoreWarning(repoRoot, taskSlug, warningId) {
|
|
91
|
+
const monitor = requireWarning(repoRoot, taskSlug, warningId);
|
|
92
|
+
monitor.ignoredGeneration = monitor.generation;
|
|
93
|
+
monitor.warning = undefined;
|
|
94
|
+
cancelTimer(monitor);
|
|
95
|
+
return { ok: true, warning: null };
|
|
96
|
+
},
|
|
97
|
+
async recoverWarning(input) {
|
|
98
|
+
const key = monitorKey(input.repoRoot, input.taskSlug);
|
|
99
|
+
const monitor = requireWarning(input.repoRoot, input.taskSlug, input.warningId);
|
|
100
|
+
const [session, round] = await Promise.all([
|
|
101
|
+
deps.sessionService.getRoleSession(input.repoRoot, input.taskSlug, monitor.role),
|
|
102
|
+
deps.roundService.getSessionRoundState(roundInput(input))
|
|
103
|
+
]);
|
|
104
|
+
if (!session
|
|
105
|
+
|| session.id !== monitor.sessionId
|
|
106
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
107
|
+
|| session.status !== "running"
|
|
108
|
+
|| session.activityStatus !== "running"
|
|
109
|
+
|| round.status !== "running"
|
|
110
|
+
|| round.activeRole !== monitor.role
|
|
111
|
+
|| round.roundId !== monitor.roundId) {
|
|
112
|
+
clearMonitor(key);
|
|
113
|
+
return { ok: true, warning: null };
|
|
114
|
+
}
|
|
115
|
+
await deps.sessionService.recoverRoleSession(input.repoRoot, input.taskSlug, {
|
|
116
|
+
role: monitor.role,
|
|
117
|
+
expectedSessionId: monitor.sessionId,
|
|
118
|
+
expectedRuntimeSessionToken: monitor.runtimeSessionToken,
|
|
119
|
+
recoveryPrompt: renderRecoveryPrompt(monitor)
|
|
120
|
+
});
|
|
121
|
+
clearMonitor(key);
|
|
122
|
+
return { ok: true, warning: null };
|
|
123
|
+
},
|
|
124
|
+
clearProject(repoRoot) {
|
|
125
|
+
for (const [key, monitor] of monitors) {
|
|
126
|
+
if (monitor.context.repoRoot === repoRoot) {
|
|
127
|
+
clearMonitor(key);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
stop() {
|
|
132
|
+
for (const key of [...monitors.keys()]) {
|
|
133
|
+
clearMonitor(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function advanceMonitor(key, monitor, eventName, event) {
|
|
138
|
+
monitor.generation += 1;
|
|
139
|
+
monitor.warning = undefined;
|
|
140
|
+
monitor.ignoredGeneration = undefined;
|
|
141
|
+
cancelTimer(monitor);
|
|
142
|
+
switch (eventName) {
|
|
143
|
+
case "UserPromptSubmit":
|
|
144
|
+
case "PostCompact":
|
|
145
|
+
case "PostToolBatch":
|
|
146
|
+
monitor.activeTools.clear();
|
|
147
|
+
monitor.currentTool = undefined;
|
|
148
|
+
setPhase(key, monitor, "awaiting-model", modelTimeoutMs);
|
|
149
|
+
return;
|
|
150
|
+
case "PreToolUse": {
|
|
151
|
+
const tool = readTool(event, toolTimeoutMs, subagentTimeoutMs);
|
|
152
|
+
const toolKey = tool.toolUseId ?? `anonymous-${monitor.generation}`;
|
|
153
|
+
monitor.activeTools.set(toolKey, tool);
|
|
154
|
+
monitor.currentTool = tool;
|
|
155
|
+
setPhase(key, monitor, tool.toolName === "Agent" ? "subagent-running" : "tool-running", tool.timeoutMs);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
case "SubagentStart":
|
|
159
|
+
setPhase(key, monitor, "subagent-running", subagentTimeoutMs);
|
|
160
|
+
return;
|
|
161
|
+
case "PostToolUse":
|
|
162
|
+
case "PostToolUseFailure": {
|
|
163
|
+
const toolUseId = stringValue(event.tool_use_id);
|
|
164
|
+
if (toolUseId) {
|
|
165
|
+
monitor.activeTools.delete(toolUseId);
|
|
166
|
+
}
|
|
167
|
+
monitor.currentTool = firstTool(monitor.activeTools);
|
|
168
|
+
if (monitor.currentTool) {
|
|
169
|
+
setPhase(key, monitor, monitor.currentTool.toolName === "Agent" ? "subagent-running" : "tool-running", monitor.currentTool.timeoutMs);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "SubagentStop":
|
|
174
|
+
monitor.currentTool = firstTool(monitor.activeTools);
|
|
175
|
+
if (monitor.currentTool) {
|
|
176
|
+
setPhase(key, monitor, monitor.currentTool.toolName === "Agent" ? "subagent-running" : "tool-running", monitor.currentTool.timeoutMs);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
case "PreCompact":
|
|
180
|
+
monitor.activeTools.clear();
|
|
181
|
+
monitor.currentTool = undefined;
|
|
182
|
+
setPhase(key, monitor, "compacting", modelTimeoutMs);
|
|
183
|
+
return;
|
|
184
|
+
case "PermissionRequest":
|
|
185
|
+
setPhase(key, monitor, "awaiting-permission");
|
|
186
|
+
return;
|
|
187
|
+
default:
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function setPhase(key, monitor, phase, timeoutMs) {
|
|
192
|
+
monitor.phase = phase;
|
|
193
|
+
monitor.phaseStartedAt = now();
|
|
194
|
+
if (timeoutMs === undefined) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const generation = monitor.generation;
|
|
198
|
+
monitor.timer = setTimer(() => {
|
|
199
|
+
void detectStall(key, generation).catch(() => undefined);
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
}
|
|
202
|
+
async function detectStall(key, generation) {
|
|
203
|
+
const monitor = monitors.get(key);
|
|
204
|
+
if (!monitor || monitor.generation !== generation || monitor.ignoredGeneration === generation) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const [session, round] = await Promise.all([
|
|
208
|
+
deps.sessionService.getRoleSession(monitor.context.repoRoot, monitor.context.taskSlug, monitor.role),
|
|
209
|
+
deps.roundService.getSessionRoundState(roundInput(monitor.context))
|
|
210
|
+
]);
|
|
211
|
+
if (!session
|
|
212
|
+
|| session.id !== monitor.sessionId
|
|
213
|
+
|| session.runtimeSessionToken !== monitor.runtimeSessionToken
|
|
214
|
+
|| session.status !== "running"
|
|
215
|
+
|| session.activityStatus !== "running"
|
|
216
|
+
|| round.status !== "running"
|
|
217
|
+
|| round.activeRole !== monitor.role
|
|
218
|
+
|| round.roundId !== monitor.roundId) {
|
|
219
|
+
clearMonitor(key);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const detectedAt = now();
|
|
223
|
+
monitor.timer = undefined;
|
|
224
|
+
monitor.warning = {
|
|
225
|
+
id: `${monitor.sessionId}:${monitor.roundId}:${generation}:${detectedAt}`,
|
|
226
|
+
taskSlug: monitor.context.taskSlug,
|
|
227
|
+
role: monitor.role,
|
|
228
|
+
sessionId: monitor.sessionId,
|
|
229
|
+
runtimeSessionToken: monitor.runtimeSessionToken,
|
|
230
|
+
roundId: monitor.roundId,
|
|
231
|
+
phase: monitor.phase,
|
|
232
|
+
phaseStartedAt: monitor.phaseStartedAt,
|
|
233
|
+
detectedAt,
|
|
234
|
+
toolUseId: monitor.currentTool?.toolUseId,
|
|
235
|
+
toolName: monitor.currentTool?.toolName
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function requireWarning(repoRoot, taskSlug, warningId) {
|
|
239
|
+
const monitor = monitors.get(monitorKey(repoRoot, taskSlug));
|
|
240
|
+
if (!monitor?.warning || monitor.warning.id !== warningId) {
|
|
241
|
+
throw new VcmError({
|
|
242
|
+
code: "ROLE_STALL_WARNING_STALE",
|
|
243
|
+
message: "The role stall warning is no longer active.",
|
|
244
|
+
statusCode: 409,
|
|
245
|
+
hint: "Refresh the task workspace before retrying the action."
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return monitor;
|
|
249
|
+
}
|
|
250
|
+
function cancelTimer(monitor) {
|
|
251
|
+
if (monitor.timer === undefined) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
clearTimer(monitor.timer);
|
|
255
|
+
monitor.timer = undefined;
|
|
256
|
+
}
|
|
257
|
+
function clearMonitor(key) {
|
|
258
|
+
const monitor = monitors.get(key);
|
|
259
|
+
if (!monitor) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
cancelTimer(monitor);
|
|
263
|
+
monitors.delete(key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function monitorKey(repoRoot, taskSlug) {
|
|
267
|
+
return `${repoRoot}:${taskSlug}`;
|
|
268
|
+
}
|
|
269
|
+
function taskInput(input) {
|
|
270
|
+
return {
|
|
271
|
+
repoRoot: input.repoRoot,
|
|
272
|
+
taskRepoRoot: input.taskRepoRoot,
|
|
273
|
+
stateRoot: input.stateRoot,
|
|
274
|
+
taskSlug: input.taskSlug
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function roundInput(input) {
|
|
278
|
+
return {
|
|
279
|
+
repoRoot: input.repoRoot,
|
|
280
|
+
stateRepoRoot: input.taskRepoRoot,
|
|
281
|
+
stateRoot: input.stateRoot,
|
|
282
|
+
taskSlug: input.taskSlug
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function readTool(event, defaultTimeoutMs, subagentTimeoutMs) {
|
|
286
|
+
const toolName = stringValue(event.tool_name);
|
|
287
|
+
const toolUseId = stringValue(event.tool_use_id);
|
|
288
|
+
const input = objectValue(event.tool_input);
|
|
289
|
+
const requestedTimeout = numberValue(input?.timeout);
|
|
290
|
+
return {
|
|
291
|
+
toolName,
|
|
292
|
+
toolUseId,
|
|
293
|
+
timeoutMs: toolName === "Agent"
|
|
294
|
+
? subagentTimeoutMs
|
|
295
|
+
: Math.max(defaultTimeoutMs, requestedTimeout ? requestedTimeout + TOOL_TIMEOUT_GRACE_MS : 0)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function firstTool(tools) {
|
|
299
|
+
return tools.values().next().value;
|
|
300
|
+
}
|
|
301
|
+
function renderRecoveryPrompt(monitor) {
|
|
302
|
+
const phase = monitor.currentTool?.toolName
|
|
303
|
+
? `${monitor.phase} (${monitor.currentTool.toolName})`
|
|
304
|
+
: monitor.phase;
|
|
305
|
+
return [
|
|
306
|
+
"[VCM Session Recovery]",
|
|
307
|
+
`The previous ${monitor.role} turn may have stalled during ${phase}.`,
|
|
308
|
+
"Continue the current assigned work from the existing Claude session context.",
|
|
309
|
+
"Do not repeat completed work unless it is needed to verify the current state."
|
|
310
|
+
].join("\n");
|
|
311
|
+
}
|
|
312
|
+
function objectValue(value) {
|
|
313
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
314
|
+
? value
|
|
315
|
+
: undefined;
|
|
316
|
+
}
|
|
317
|
+
function stringValue(value) {
|
|
318
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
319
|
+
}
|
|
320
|
+
function numberValue(value) {
|
|
321
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
322
|
+
}
|