vibe-coding-master 0.7.42 → 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 +40 -6
- package/dist/backend/role-tool-policy.js +1 -1
- 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 +38 -6
- 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 +70 -3
- 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 +2 -2
- 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
|
{
|
|
@@ -297,8 +304,8 @@ const HARNESS_FILES = [
|
|
|
297
304
|
title: "Coder Agent",
|
|
298
305
|
memoryBlock: true,
|
|
299
306
|
requiredDisallowedTools: CODE_ROLE_DISALLOWED_TOOLS,
|
|
300
|
-
|
|
301
|
-
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(", ") }),
|
|
302
309
|
renderRules: renderCoderHarnessRules
|
|
303
310
|
},
|
|
304
311
|
{
|
|
@@ -1444,7 +1451,8 @@ function ensureAgentAllowedTool(content, requiredTool) {
|
|
|
1444
1451
|
function normalizeAgentFrontmatter(content, definition) {
|
|
1445
1452
|
const allowedToolsUpdated = (definition.requiredTools ?? []).reduce(ensureAgentAllowedTool, content);
|
|
1446
1453
|
const disallowedToolsUpdated = ensureAgentDisallowedTools(allowedToolsUpdated, definition.requiredDisallowedTools);
|
|
1447
|
-
|
|
1454
|
+
const skillsUpdated = (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
|
|
1455
|
+
return (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
|
|
1448
1456
|
}
|
|
1449
1457
|
function ensureAgentSkill(content, requiredSkill) {
|
|
1450
1458
|
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
@@ -1466,6 +1474,30 @@ function ensureAgentSkill(content, requiredSkill) {
|
|
|
1466
1474
|
const nextSkills = `${skillsMatch[0].trimEnd()}\n - ${requiredSkill}`;
|
|
1467
1475
|
return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], nextSkills));
|
|
1468
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
|
+
}
|
|
1469
1501
|
function migrateLegacyHarnessFile(definition, currentContent, block) {
|
|
1470
1502
|
const legacyContent = definition.legacyWholeFile?.trimEnd();
|
|
1471
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
|
+
}
|
|
@@ -280,6 +280,31 @@ export function createRoundService(deps) {
|
|
|
280
280
|
return toSessionRoundState(next, timestamp);
|
|
281
281
|
});
|
|
282
282
|
},
|
|
283
|
+
async recordRoleRecoveryFailure(input) {
|
|
284
|
+
return withTaskLock(input, async () => {
|
|
285
|
+
const timestamp = now();
|
|
286
|
+
const state = await load(input);
|
|
287
|
+
const stopped = applyTerminalExit({
|
|
288
|
+
state,
|
|
289
|
+
taskSlug: input.taskSlug,
|
|
290
|
+
role: input.recovery.role,
|
|
291
|
+
timestamp
|
|
292
|
+
});
|
|
293
|
+
const next = {
|
|
294
|
+
...stopped,
|
|
295
|
+
roleRecovery: normalizeRoleRecovery({
|
|
296
|
+
...input.recovery,
|
|
297
|
+
status: "failed",
|
|
298
|
+
failedAt: input.recovery.failedAt ?? timestamp
|
|
299
|
+
}),
|
|
300
|
+
updatedAt: timestamp
|
|
301
|
+
};
|
|
302
|
+
await save(input, next);
|
|
303
|
+
clearSettleTimer(input);
|
|
304
|
+
await updateSessionStatus(input, next.currentRound?.status === "running" ? "running" : "stopped");
|
|
305
|
+
return toSessionRoundState(next, timestamp);
|
|
306
|
+
});
|
|
307
|
+
},
|
|
283
308
|
async setRoleRecovery(input) {
|
|
284
309
|
return withTaskLock(input, async () => {
|
|
285
310
|
const timestamp = now();
|
|
@@ -42,9 +42,17 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
42
42
|
]);
|
|
43
43
|
const preferences = await deps.appSettings.getPreferences();
|
|
44
44
|
if (!activeTask) {
|
|
45
|
+
deps.roleStallDetector.clearProject(repoRoot);
|
|
45
46
|
return { activeTask: null, gatewayStatus };
|
|
46
47
|
}
|
|
47
48
|
const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
|
|
49
|
+
const stateRoot = await deps.getStateRoot(repoRoot);
|
|
50
|
+
await deps.roleStallDetector.reconcileTask({
|
|
51
|
+
repoRoot,
|
|
52
|
+
taskRepoRoot,
|
|
53
|
+
stateRoot,
|
|
54
|
+
taskSlug: activeTask.taskSlug
|
|
55
|
+
});
|
|
48
56
|
const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
|
|
49
57
|
.then((status) => status.initialized)
|
|
50
58
|
.catch(() => false);
|
|
@@ -77,6 +85,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
77
85
|
void reconcileCurrentProject().catch(() => undefined);
|
|
78
86
|
},
|
|
79
87
|
stop() {
|
|
88
|
+
deps.roleStallDetector.stop();
|
|
80
89
|
if (reconcileTimer === undefined) {
|
|
81
90
|
return;
|
|
82
91
|
}
|
|
@@ -88,6 +97,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
88
97
|
async function reconcileCurrentProject() {
|
|
89
98
|
const project = await deps.projectService.getCurrentProject();
|
|
90
99
|
if (!project) {
|
|
100
|
+
deps.roleStallDetector.stop();
|
|
91
101
|
return;
|
|
92
102
|
}
|
|
93
103
|
await reconcileProject(project.repoRoot);
|