vibe-coding-master 0.8.9 → 0.8.11
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 +3 -4
- package/dist/backend/cli/install-vcm-harness.js +5 -4
- package/dist/backend/server.js +3 -0
- package/dist/backend/services/app-settings-service.js +6 -0
- package/dist/backend/services/gate-review-service.js +44 -10
- package/dist/backend/services/harness-feedback-service.js +21 -11
- package/dist/backend/services/harness-service.js +11 -7
- package/dist/backend/services/lsp-plugin.js +13 -0
- package/dist/backend/services/message-service.js +3 -1
- package/dist/backend/services/runtime-coordinator-service.js +1 -1
- package/dist/backend/services/runtime-recovery-service.js +5 -3
- package/dist/backend/services/session-service.js +15 -6
- package/dist/backend/services/workflow-control-service.js +94 -13
- package/dist/backend/templates/harness/architect-agent.js +1 -1
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +5 -0
- package/dist/shared/types/session.js +1 -15
- package/dist-frontend/assets/{index-BWbworWs.js → index-Db0OTAez.js} +37 -37
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
- package/scripts/harness-tools/run-long-check +25 -6
package/README.md
CHANGED
|
@@ -253,10 +253,9 @@ such as a Dev Container or VM.
|
|
|
253
253
|
|
|
254
254
|
Model and effort can be selected before start/resume/restart. Changes affect the
|
|
255
255
|
next launched process, not a currently running Claude Code process.
|
|
256
|
-
The native model list
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
aliases, while `Opus 4.8` remains available as a pinned model.
|
|
256
|
+
The native model list is `Default`, `Fable`, `Opus`, and `Sonnet`. The latter
|
|
257
|
+
three use Claude Code aliases, so the resolved model depends on the installed
|
|
258
|
+
Claude Code version and provider.
|
|
260
259
|
|
|
261
260
|
## Usage Analytics
|
|
262
261
|
|
|
@@ -51,21 +51,22 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
51
51
|
".claude/skills/vcm-codex-review-gate",
|
|
52
52
|
".ai/tools/request-codex-review"
|
|
53
53
|
];
|
|
54
|
-
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
54
|
+
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
55
55
|
const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
56
56
|
const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
|
|
57
57
|
const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
|
|
58
58
|
const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; python3 "$guard"'`;
|
|
59
59
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
60
60
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
61
|
+
const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
|
|
61
62
|
const VCM_HOOK_DEFINITIONS = [
|
|
62
63
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
63
64
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
65
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
|
-
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout:
|
|
66
|
+
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
|
|
66
67
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
67
|
-
{ eventName: "StopFailure", command:
|
|
68
|
-
{ eventName: "PostCompact", command:
|
|
68
|
+
{ eventName: "StopFailure", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
|
+
{ eventName: "PostCompact", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
70
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
70
71
|
];
|
|
71
72
|
const AGENT_FRONTMATTER = {
|
package/dist/backend/server.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createJobGuardService } from "./services/job-guard-service.js";
|
|
|
33
33
|
import { createProjectService } from "./services/project-service.js";
|
|
34
34
|
import { createSessionRegistry } from "./runtime/session-registry.js";
|
|
35
35
|
import { createSessionService } from "./services/session-service.js";
|
|
36
|
+
import { createHarnessCodeIntelligenceDetector } from "./services/code-intelligence-service.js";
|
|
36
37
|
import { createMessageService } from "./services/message-service.js";
|
|
37
38
|
import { createRoundService } from "./services/round-service.js";
|
|
38
39
|
import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-service.js";
|
|
@@ -250,6 +251,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
250
251
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
251
252
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
252
253
|
const taskWorkflowService = createTaskWorkflowService({ fs });
|
|
254
|
+
const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(fs, { runner });
|
|
253
255
|
const sessionService = createSessionService({
|
|
254
256
|
fs,
|
|
255
257
|
runtime,
|
|
@@ -259,6 +261,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
259
261
|
projectService,
|
|
260
262
|
taskService,
|
|
261
263
|
taskWorkflowService,
|
|
264
|
+
codeIntelligenceDetector,
|
|
262
265
|
codexBridgeIntegration,
|
|
263
266
|
apiUrl: options.apiUrl
|
|
264
267
|
});
|
|
@@ -355,6 +355,12 @@ function normalizeClaudeModel(input, fallback) {
|
|
|
355
355
|
if (typeof input !== "string") {
|
|
356
356
|
return fallback;
|
|
357
357
|
}
|
|
358
|
+
if (input === "claude-fable-5-1") {
|
|
359
|
+
return "fable";
|
|
360
|
+
}
|
|
361
|
+
if (input === "claude-opus-4-8") {
|
|
362
|
+
return "opus";
|
|
363
|
+
}
|
|
358
364
|
if (isCodexBridgeSessionModel(input)) {
|
|
359
365
|
return input;
|
|
360
366
|
}
|
|
@@ -148,7 +148,15 @@ export function createGateReviewService(deps) {
|
|
|
148
148
|
};
|
|
149
149
|
}
|
|
150
150
|
if (gate === "code-diff") {
|
|
151
|
-
|
|
151
|
+
let dirtyStatus;
|
|
152
|
+
try {
|
|
153
|
+
dirtyStatus = await getDirtyCodeDiffStatus(deps.runner, context.taskRepoRoot);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = errorMessage(error);
|
|
157
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message);
|
|
158
|
+
return { status: "failed_to_start", gate, record: index.gates[gate], message };
|
|
159
|
+
}
|
|
152
160
|
if (dirtyStatus) {
|
|
153
161
|
const message = `code-diff requires committed inputs; commit or clean these changes first: ${dirtyStatus}`;
|
|
154
162
|
index = applyGateState(index, gate, {
|
|
@@ -319,9 +327,17 @@ export function createGateReviewService(deps) {
|
|
|
319
327
|
};
|
|
320
328
|
}
|
|
321
329
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
330
|
+
let codeDiffInput;
|
|
331
|
+
if (gate === "code-diff") {
|
|
332
|
+
try {
|
|
333
|
+
codeDiffInput = await resolveCodeDiffInput(deps, context, record);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
const message = errorMessage(error);
|
|
337
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, message);
|
|
338
|
+
return { status: "failed_to_start", gate, record: index.gates[gate], message };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
325
341
|
if (gate === "code-diff" && (!codeDiffInput || codeDiffInput.commits.length === 0)) {
|
|
326
342
|
index = applyGateState(index, gate, {
|
|
327
343
|
status: "not_required",
|
|
@@ -1157,7 +1173,7 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1157
1173
|
"merge-base",
|
|
1158
1174
|
"HEAD",
|
|
1159
1175
|
rootBranch
|
|
1160
|
-
])).trim();
|
|
1176
|
+
], { allowFailure: true })).trim();
|
|
1161
1177
|
if (mergeBase
|
|
1162
1178
|
&& mergeBase !== headCommit
|
|
1163
1179
|
&& await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
|
|
@@ -1169,13 +1185,13 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1169
1185
|
"--abbrev-ref",
|
|
1170
1186
|
"--symbolic-full-name",
|
|
1171
1187
|
"@{upstream}"
|
|
1172
|
-
])).trim();
|
|
1188
|
+
], { allowFailure: true })).trim();
|
|
1173
1189
|
if (upstream) {
|
|
1174
1190
|
const mergeBase = (await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1175
1191
|
"merge-base",
|
|
1176
1192
|
"HEAD",
|
|
1177
1193
|
upstream
|
|
1178
|
-
])).trim();
|
|
1194
|
+
], { allowFailure: true })).trim();
|
|
1179
1195
|
if (mergeBase
|
|
1180
1196
|
&& mergeBase !== headCommit
|
|
1181
1197
|
&& await isAncestor(deps.runner, context.taskRepoRoot, mergeBase, headCommit)) {
|
|
@@ -1395,9 +1411,27 @@ async function readCodeDiffSourceArtifactError(fs, taskRepoRoot, source) {
|
|
|
1395
1411
|
}
|
|
1396
1412
|
return undefined;
|
|
1397
1413
|
}
|
|
1398
|
-
async function commandStdout(runner, cwd, args) {
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1414
|
+
async function commandStdout(runner, cwd, args, options = {}) {
|
|
1415
|
+
const retryHead = args.length === 2 && args[0] === "rev-parse" && args[1] === "HEAD";
|
|
1416
|
+
const attempts = retryHead ? 3 : 1;
|
|
1417
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1418
|
+
const result = await runner.run("git", args, { cwd });
|
|
1419
|
+
if (result.exitCode === 0)
|
|
1420
|
+
return result.stdout;
|
|
1421
|
+
if (attempt + 1 < attempts) {
|
|
1422
|
+
await delay(100 * (attempt + 1));
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (options.allowFailure)
|
|
1426
|
+
return "";
|
|
1427
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "Git did not provide an error message.";
|
|
1428
|
+
throw new VcmError({
|
|
1429
|
+
code: "GATE_REVIEW_GIT_COMMAND_FAILED",
|
|
1430
|
+
message: `Git ${args.join(" ")} failed in ${cwd} (exit ${result.exitCode}): ${detail}`,
|
|
1431
|
+
statusCode: 409
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
return "";
|
|
1401
1435
|
}
|
|
1402
1436
|
function splitLines(value) {
|
|
1403
1437
|
return value
|
|
@@ -52,15 +52,6 @@ export function createHarnessFeedbackService(deps) {
|
|
|
52
52
|
statusCode: 409
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
56
|
-
if (existingMarker && existingMarker.status !== "failed") {
|
|
57
|
-
throw new VcmError({
|
|
58
|
-
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
59
|
-
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
60
|
-
statusCode: 409,
|
|
61
|
-
hint: "Review the existing retrospective result instead of starting another retrospective for the same task."
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
55
|
const finalAcceptancePath = path.posix.join(input.handoffDir, "final-acceptance.md");
|
|
65
56
|
const finalAcceptanceAbsolutePath = resolveRepoPath(input.taskRepoRoot, finalAcceptancePath);
|
|
66
57
|
const finalAcceptanceContent = await readAbsoluteOptionalText(finalAcceptanceAbsolutePath);
|
|
@@ -78,10 +69,25 @@ export function createHarnessFeedbackService(deps) {
|
|
|
78
69
|
hint: `${finalAcceptancePath} must pass the final-acceptance artifact check before Task Harness Retrospective can start.`
|
|
79
70
|
});
|
|
80
71
|
}
|
|
72
|
+
const finalAcceptanceHash = `sha256:${sha256(finalAcceptanceContent)}`;
|
|
73
|
+
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
74
|
+
if (existingMarker && (existingMarker.status === "running"
|
|
75
|
+
|| existingMarker.status === "triggered"
|
|
76
|
+
|| existingMarker.status === "waiting-docs"
|
|
77
|
+
|| (existingMarker.finalAcceptanceHash === finalAcceptanceHash && existingMarker.status !== "failed"))) {
|
|
78
|
+
throw new VcmError({
|
|
79
|
+
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
80
|
+
message: `Task Harness Retrospective has already been triggered for this acceptance or is still active: ${taskSlug}`,
|
|
81
|
+
statusCode: 409,
|
|
82
|
+
hint: "Wait for the active retrospective, or review its result before retrying the same acceptance."
|
|
83
|
+
});
|
|
84
|
+
}
|
|
81
85
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
82
86
|
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
83
87
|
const timestamp = now();
|
|
84
|
-
const analysisPath =
|
|
88
|
+
const analysisPath = existingMarker && existingMarker.finalAcceptanceHash !== finalAcceptanceHash
|
|
89
|
+
? `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.${finalAcceptanceHash.slice(7)}.md`
|
|
90
|
+
: existingMarker?.analysisPath ?? `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
85
91
|
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
86
92
|
const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
|
|
87
93
|
const marker = {
|
|
@@ -90,13 +96,17 @@ export function createHarnessFeedbackService(deps) {
|
|
|
90
96
|
trigger: input.trigger,
|
|
91
97
|
status: "running",
|
|
92
98
|
analysisPath,
|
|
93
|
-
finalAcceptanceHash
|
|
99
|
+
finalAcceptanceHash,
|
|
94
100
|
pendingFeedbackPaths: pendingFeedback.map((item) => item.path),
|
|
95
101
|
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
96
102
|
createdAt: timestamp,
|
|
97
103
|
updatedAt: timestamp
|
|
98
104
|
};
|
|
99
105
|
try {
|
|
106
|
+
if (existingMarker && existingMarker.finalAcceptanceHash !== finalAcceptanceHash) {
|
|
107
|
+
const archivePath = resolveRepoPath(repoRoot, `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.${existingMarker.finalAcceptanceHash.slice(7)}.json`);
|
|
108
|
+
await deps.fs.writeJsonAtomic(archivePath, existingMarker);
|
|
109
|
+
}
|
|
100
110
|
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
101
111
|
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
102
112
|
}
|
|
@@ -51,21 +51,22 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
51
51
|
".claude/skills/vcm-codex-review-gate",
|
|
52
52
|
".ai/tools/request-codex-review"
|
|
53
53
|
];
|
|
54
|
-
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
54
|
+
const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
|
|
55
55
|
const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
|
|
56
56
|
const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,runtimeSessionToken:process.env.VCM_RUNTIME_SESSION_TOKEN,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
|
|
57
57
|
const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
|
|
58
58
|
const VCM_SUBAGENT_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-subagent-guard" ]; then guard="$repo/.ai/tools/vcm-subagent-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-subagent-guard" ]; then guard="$dir/.ai/tools/vcm-subagent-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-subagent-guard"; fi; fi; [ -n "$guard" ] || { printf "%s\\n" "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"VCM subagent policy is unavailable. Refresh the VCM Harness before invoking subagents.\\"}}"; exit 0; }; python3 "$guard"'`;
|
|
59
59
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
60
60
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
61
|
+
const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
|
|
61
62
|
const VCM_HOOK_DEFINITIONS = [
|
|
62
63
|
{ eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
63
64
|
{ eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
64
65
|
{ eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
|
|
65
|
-
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout:
|
|
66
|
+
{ eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
|
|
66
67
|
{ eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
|
|
67
|
-
{ eventName: "StopFailure", command:
|
|
68
|
-
{ eventName: "PostCompact", command:
|
|
68
|
+
{ eventName: "StopFailure", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
|
+
{ eventName: "PostCompact", command: VCM_FAST_HOOK_COMMAND, timeout: 5 },
|
|
69
70
|
{ eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
|
|
70
71
|
];
|
|
71
72
|
const HARNESS_FILES = [
|
|
@@ -1405,9 +1406,12 @@ function renderHarnessStatus(harnessRevision, analyses, legacyChanges = [], mani
|
|
|
1405
1406
|
needsApply: plannedChanges.length > 0,
|
|
1406
1407
|
plannedChanges,
|
|
1407
1408
|
codeIntelligence,
|
|
1408
|
-
warnings:
|
|
1409
|
-
? ["Review and commit VCM Harness changes before starting long-running work."]
|
|
1410
|
-
|
|
1409
|
+
warnings: [
|
|
1410
|
+
...(plannedChanges.length > 0 ? ["Review and commit VCM Harness changes before starting long-running work."] : []),
|
|
1411
|
+
...((codeIntelligence?.languages ?? [])
|
|
1412
|
+
.filter((language) => language.state !== "server_runnable")
|
|
1413
|
+
.map((language) => `Architect LSP for ${language.label} is unavailable: ${language.error ?? `${language.serverCommand} is not runnable.`}`))
|
|
1414
|
+
]
|
|
1411
1415
|
};
|
|
1412
1416
|
}
|
|
1413
1417
|
async function readHarnessRevisionValue(fs, repoRoot) {
|
|
@@ -4,3 +4,16 @@ export { roleUsesLsp } from "../role-tool-policy.js";
|
|
|
4
4
|
export const VCM_LSP_PLUGIN_NAME = "vcm-lsp-bridge";
|
|
5
5
|
export const VCM_LSP_PLUGIN_DIR = fileURLToPath(new URL("../../../scripts/claude-plugins/vcm-lsp-bridge", import.meta.url));
|
|
6
6
|
export const VCM_LSP_PLUGIN_MANIFEST = path.join(VCM_LSP_PLUGIN_DIR, ".claude-plugin", "plugin.json");
|
|
7
|
+
export async function prepareTaskLspPlugin(fs, taskRepoRoot, status) {
|
|
8
|
+
const runnable = status.languages.filter((language) => language.state === "server_runnable");
|
|
9
|
+
if (runnable.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const manifest = await fs.readJson(VCM_LSP_PLUGIN_MANIFEST);
|
|
12
|
+
const commands = new Set(runnable.map((language) => language.serverCommand));
|
|
13
|
+
const lspServers = Object.fromEntries(Object.entries(manifest.lspServers).filter(([, server]) => commands.has(server.command)));
|
|
14
|
+
const pluginDir = path.join(taskRepoRoot, ".ai/vcm/lsp-plugin");
|
|
15
|
+
const pluginManifestPath = path.join(pluginDir, ".claude-plugin/plugin.json");
|
|
16
|
+
await fs.ensureDir(path.dirname(pluginManifestPath));
|
|
17
|
+
await fs.writeJsonAtomic(pluginManifestPath, { ...manifest, lspServers });
|
|
18
|
+
return [pluginDir];
|
|
19
|
+
}
|
|
@@ -193,6 +193,8 @@ export function createMessageService(deps) {
|
|
|
193
193
|
if (!message) {
|
|
194
194
|
return undefined;
|
|
195
195
|
}
|
|
196
|
+
if (message.acceptedAt)
|
|
197
|
+
return message;
|
|
196
198
|
const accepted = {
|
|
197
199
|
...message,
|
|
198
200
|
acceptedAt: timestamp,
|
|
@@ -353,7 +355,7 @@ export function createMessageService(deps) {
|
|
|
353
355
|
return;
|
|
354
356
|
}
|
|
355
357
|
if (current.fromRole === PM_ROLE) {
|
|
356
|
-
await deps.workflowControlService?.
|
|
358
|
+
await deps.workflowControlService?.markDispatchUnconfirmed(toWorkflowContext(input), messageId, failureReason);
|
|
357
359
|
}
|
|
358
360
|
await appendMessageSnapshot(deps.fs, input, {
|
|
359
361
|
...current,
|
|
@@ -192,7 +192,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
|
-
if (error instanceof VcmError &&
|
|
195
|
+
if (error instanceof VcmError && EXPECTED_AUTO_RETROSPECTIVE_SKIP_CODES.has(error.code)) {
|
|
196
196
|
return;
|
|
197
197
|
}
|
|
198
198
|
throw error;
|
|
@@ -193,10 +193,12 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
193
193
|
if (message.dispatchingAt && !message.acceptedAt) {
|
|
194
194
|
const next = {
|
|
195
195
|
...message,
|
|
196
|
-
failureReason:
|
|
196
|
+
failureReason: message.deliveredAt
|
|
197
|
+
? "VCM restarted before this delivered dispatch was confirmed. Inspect the target session before retrying."
|
|
198
|
+
: "VCM restarted before this dispatch was delivered. Resend the message if it is still needed."
|
|
197
199
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
if (!message.deliveredAt)
|
|
201
|
+
delete next.dispatchingAt;
|
|
200
202
|
changed = true;
|
|
201
203
|
return next;
|
|
202
204
|
}
|
|
@@ -8,7 +8,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
|
8
8
|
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
9
9
|
import { readHarnessRevisionState } from "./harness-revision.js";
|
|
10
10
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
11
|
-
import { roleUsesLsp, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
|
|
11
|
+
import { roleUsesLsp, prepareTaskLspPlugin, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
|
|
12
12
|
const TRANSLATOR_ROLE = "translator";
|
|
13
13
|
const HARNESS_ENGINEER_ROLE = "harness-engineer";
|
|
14
14
|
const TRANSLATION_DIR = ".ai/vcm/translations";
|
|
@@ -82,8 +82,13 @@ export function createSessionService(deps) {
|
|
|
82
82
|
: resumeClaudeSessionId
|
|
83
83
|
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
84
84
|
: undefined;
|
|
85
|
+
const lspPluginDirs = roleUsesLsp(role)
|
|
86
|
+
? deps.codeIntelligenceDetector
|
|
87
|
+
? await prepareTaskLspPlugin(deps.fs, taskRepoRoot, await deps.codeIntelligenceDetector.detect(taskRepoRoot))
|
|
88
|
+
: [VCM_LSP_PLUGIN_DIR]
|
|
89
|
+
: [];
|
|
85
90
|
const startCommand = {
|
|
86
|
-
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt,
|
|
91
|
+
...deps.claude.buildRoleStartCommand(role, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride, input.appendSystemPrompt, lspPluginDirs),
|
|
87
92
|
cwd: taskRepoRoot
|
|
88
93
|
};
|
|
89
94
|
const runtimeSessionToken = randomUUID();
|
|
@@ -104,7 +109,7 @@ export function createSessionService(deps) {
|
|
|
104
109
|
VCM_RUNTIME_SESSION_TOKEN: runtimeSessionToken
|
|
105
110
|
}, modelEnvironment, {
|
|
106
111
|
...buildUsageTelemetryEnvironment(deps.apiUrl, role, model),
|
|
107
|
-
...(
|
|
112
|
+
...(lspPluginDirs.length > 0
|
|
108
113
|
? {
|
|
109
114
|
ENABLE_LSP_TOOL: "true",
|
|
110
115
|
ENABLE_TOOL_SEARCH: "false"
|
|
@@ -1608,9 +1613,13 @@ function normalizeClaudePermissionMode(value) {
|
|
|
1608
1613
|
return "default";
|
|
1609
1614
|
}
|
|
1610
1615
|
function normalizeClaudeModel(value) {
|
|
1611
|
-
if (value === "claude-fable-5-1"
|
|
1612
|
-
|
|
1613
|
-
|
|
1616
|
+
if (value === "claude-fable-5-1") {
|
|
1617
|
+
return "fable";
|
|
1618
|
+
}
|
|
1619
|
+
if (value === "claude-opus-4-8") {
|
|
1620
|
+
return "opus";
|
|
1621
|
+
}
|
|
1622
|
+
if (value === "opus"
|
|
1614
1623
|
|| value === "sonnet"
|
|
1615
1624
|
|| value === "fable"
|
|
1616
1625
|
|| isCodexBridgeSessionModel(value)) {
|
|
@@ -215,18 +215,36 @@ export function createWorkflowControlService(deps) {
|
|
|
215
215
|
status: "pending",
|
|
216
216
|
routeContentHash: undefined,
|
|
217
217
|
messageId: undefined,
|
|
218
|
+
confirmationError: undefined,
|
|
218
219
|
updatedAt: now()
|
|
219
220
|
},
|
|
220
221
|
updatedAt: now()
|
|
221
222
|
});
|
|
222
223
|
});
|
|
223
224
|
}
|
|
225
|
+
async function markDispatchUnconfirmed(input, messageId, reason) {
|
|
226
|
+
await withLock(statePath(input), async () => {
|
|
227
|
+
const state = await getState(input);
|
|
228
|
+
const pending = state.pendingDispatch;
|
|
229
|
+
if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId)
|
|
230
|
+
return;
|
|
231
|
+
await saveState(input, {
|
|
232
|
+
...state,
|
|
233
|
+
pendingDispatch: { ...pending, confirmationError: reason, updatedAt: now() },
|
|
234
|
+
updatedAt: now()
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
224
238
|
async function cancelPendingDispatch(input) {
|
|
225
239
|
await withLock(statePath(input), async () => {
|
|
226
240
|
const state = await getState(input);
|
|
227
241
|
const pending = state.pendingDispatch;
|
|
228
242
|
if (!pending)
|
|
229
243
|
return;
|
|
244
|
+
if (pending.status === "dispatching" && !pending.confirmationError) {
|
|
245
|
+
throw workflowError("WORKFLOW_DISPATCH_PENDING", `Message ${pending.messageId} is being delivered and cannot be cleared before confirmation.`);
|
|
246
|
+
}
|
|
247
|
+
const timestamp = now();
|
|
230
248
|
const progress = await readProgress(deps.fs, input);
|
|
231
249
|
await commitStateAndProgress(input, renderWorkflowProgress({
|
|
232
250
|
...progress,
|
|
@@ -234,16 +252,18 @@ export function createWorkflowControlService(deps) {
|
|
|
234
252
|
}), {
|
|
235
253
|
...state,
|
|
236
254
|
pendingDispatch: null,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
updatedAt: now()
|
|
255
|
+
...abandonPendingApprovals(state, pending, "Pending route was cleared before confirmation.", timestamp),
|
|
256
|
+
updatedAt: timestamp
|
|
240
257
|
});
|
|
241
258
|
});
|
|
242
259
|
}
|
|
243
260
|
async function confirmDispatch(input, messageId) {
|
|
244
261
|
await withLock(statePath(input), async () => {
|
|
245
262
|
const state = await getState(input);
|
|
246
|
-
failOnStateWarnings(
|
|
263
|
+
failOnStateWarnings({
|
|
264
|
+
...state,
|
|
265
|
+
warnings: state.warnings.filter((warning) => warning !== unconfirmedDispatchWarning(state.pendingDispatch))
|
|
266
|
+
});
|
|
247
267
|
failWhileAwaitingUser(state);
|
|
248
268
|
const pending = state.pendingDispatch;
|
|
249
269
|
if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId) {
|
|
@@ -293,8 +313,24 @@ export function createWorkflowControlService(deps) {
|
|
|
293
313
|
return withLock(statePath(input), async () => {
|
|
294
314
|
await recoverCommitJournal(input);
|
|
295
315
|
const state = await getState(input);
|
|
296
|
-
if (state.
|
|
316
|
+
if (state.pendingDispatch?.status !== "dispatching"
|
|
317
|
+
|| state.warnings.some((warning) => warning !== unconfirmedDispatchWarning(state.pendingDispatch)))
|
|
297
318
|
return false;
|
|
319
|
+
if (await wasDispatchDelivered(deps.fs, input, state.pendingDispatch.messageId)) {
|
|
320
|
+
if (state.pendingDispatch.confirmationError)
|
|
321
|
+
return false;
|
|
322
|
+
const timestamp = now();
|
|
323
|
+
await saveState(input, {
|
|
324
|
+
...state,
|
|
325
|
+
pendingDispatch: {
|
|
326
|
+
...state.pendingDispatch,
|
|
327
|
+
confirmationError: "VCM restarted before the delivered message received UserPromptSubmit confirmation.",
|
|
328
|
+
updatedAt: timestamp
|
|
329
|
+
},
|
|
330
|
+
updatedAt: timestamp
|
|
331
|
+
});
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
298
334
|
await saveState(input, {
|
|
299
335
|
...state,
|
|
300
336
|
pendingDispatch: {
|
|
@@ -302,6 +338,7 @@ export function createWorkflowControlService(deps) {
|
|
|
302
338
|
status: "pending",
|
|
303
339
|
routeContentHash: undefined,
|
|
304
340
|
messageId: undefined,
|
|
341
|
+
confirmationError: undefined,
|
|
305
342
|
updatedAt: now()
|
|
306
343
|
},
|
|
307
344
|
updatedAt: now()
|
|
@@ -323,6 +360,7 @@ export function createWorkflowControlService(deps) {
|
|
|
323
360
|
assertRouteAuthorized,
|
|
324
361
|
claimDispatch,
|
|
325
362
|
releaseDispatch,
|
|
363
|
+
markDispatchUnconfirmed,
|
|
326
364
|
cancelPendingDispatch,
|
|
327
365
|
confirmDispatch,
|
|
328
366
|
recoverTask
|
|
@@ -361,10 +399,12 @@ export function createWorkflowControlService(deps) {
|
|
|
361
399
|
async function requestUserInput(input, question) {
|
|
362
400
|
return withLock(statePath(input), async () => {
|
|
363
401
|
const state = await getState(input);
|
|
364
|
-
failOnStateWarnings(state);
|
|
365
402
|
if (state.pendingDispatch?.status === "dispatching") {
|
|
366
|
-
throw workflowError("WORKFLOW_DISPATCH_PENDING",
|
|
403
|
+
throw workflowError("WORKFLOW_DISPATCH_PENDING", state.pendingDispatch.confirmationError
|
|
404
|
+
? `A delivered role dispatch is unconfirmed: ${state.pendingDispatch.confirmationError}`
|
|
405
|
+
: "A role dispatch is already being submitted and cannot be canceled by a user question.", "Inspect the target session and confirm or clear the pending dispatch before asking the user.");
|
|
367
406
|
}
|
|
407
|
+
failOnStateWarnings(state);
|
|
368
408
|
const normalizedQuestion = question.trim();
|
|
369
409
|
if (!normalizedQuestion) {
|
|
370
410
|
throw workflowError("WORKFLOW_USER_QUESTION_REQUIRED", "A non-empty user question is required.");
|
|
@@ -378,8 +418,7 @@ export function createWorkflowControlService(deps) {
|
|
|
378
418
|
requestedAt: timestamp
|
|
379
419
|
},
|
|
380
420
|
pendingDispatch: null,
|
|
381
|
-
|
|
382
|
-
userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending?.followUpApprovalId),
|
|
421
|
+
...(pending ? abandonPendingApprovals(state, pending, "PM asked the user before dispatch confirmation.", timestamp) : {}),
|
|
383
422
|
updatedAt: timestamp
|
|
384
423
|
};
|
|
385
424
|
if (pending) {
|
|
@@ -1226,6 +1265,9 @@ function validateCandidateAgainstCurrent(current, candidate) {
|
|
|
1226
1265
|
function stateProgressConsistencyWarnings(state, progress) {
|
|
1227
1266
|
const warnings = [];
|
|
1228
1267
|
const pending = state.pendingDispatch;
|
|
1268
|
+
const unconfirmedWarning = unconfirmedDispatchWarning(pending);
|
|
1269
|
+
if (unconfirmedWarning)
|
|
1270
|
+
warnings.push(unconfirmedWarning);
|
|
1229
1271
|
if (!pending && progress.proposal) {
|
|
1230
1272
|
warnings.push("Workflow Progress contains a dispatch proposal without a runtime approval.");
|
|
1231
1273
|
}
|
|
@@ -1248,6 +1290,40 @@ function stateProgressConsistencyWarnings(state, progress) {
|
|
|
1248
1290
|
}
|
|
1249
1291
|
return warnings;
|
|
1250
1292
|
}
|
|
1293
|
+
function unconfirmedDispatchWarning(pending) {
|
|
1294
|
+
return pending?.confirmationError
|
|
1295
|
+
? `PM dispatch ${pending.messageId ?? "unknown"} to ${pending.targetRole} was delivered but not confirmed: ${pending.confirmationError} Inspect the target session before clearing or retrying this route.`
|
|
1296
|
+
: undefined;
|
|
1297
|
+
}
|
|
1298
|
+
function abandonPendingApprovals(state, pending, reason, timestamp) {
|
|
1299
|
+
return {
|
|
1300
|
+
userAuthorizations: state.userAuthorizations.map((entry) => entry.id === pending.overrideAuthorizationId
|
|
1301
|
+
? { ...entry, status: "abandoned", abandonedAt: timestamp, abandonReason: reason }
|
|
1302
|
+
: entry),
|
|
1303
|
+
userApprovedFollowUps: state.userApprovedFollowUps.map((entry) => entry.id === pending.followUpApprovalId
|
|
1304
|
+
? { ...entry, status: "abandoned", abandonedAt: timestamp, abandonReason: reason }
|
|
1305
|
+
: entry)
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
async function wasDispatchDelivered(fs, input, messageId) {
|
|
1309
|
+
if (!messageId)
|
|
1310
|
+
return false;
|
|
1311
|
+
const messagesPath = path.join(input.taskRepoRoot, input.stateRoot, "messages", `${input.taskSlug}.jsonl`);
|
|
1312
|
+
if (!(await fs.pathExists(messagesPath)))
|
|
1313
|
+
return false;
|
|
1314
|
+
try {
|
|
1315
|
+
let latest;
|
|
1316
|
+
for (const line of (await fs.readText(messagesPath)).split(/\r?\n/).filter(Boolean)) {
|
|
1317
|
+
const message = JSON.parse(line);
|
|
1318
|
+
if (message.id === messageId)
|
|
1319
|
+
latest = message;
|
|
1320
|
+
}
|
|
1321
|
+
return Boolean(latest?.deliveredAt);
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1251
1327
|
function createUserAuthorization(authorizationId, current, candidate, effectiveFlow, baseHistoryHash, violation, authorizationText, timestamp) {
|
|
1252
1328
|
return {
|
|
1253
1329
|
id: authorizationId,
|
|
@@ -1492,6 +1568,7 @@ function isPendingDispatch(value) {
|
|
|
1492
1568
|
&& (value.status === "pending" || value.status === "dispatching")
|
|
1493
1569
|
&& (value.routeContentHash === undefined || typeof value.routeContentHash === "string")
|
|
1494
1570
|
&& (value.messageId === undefined || typeof value.messageId === "string")
|
|
1571
|
+
&& (value.confirmationError === undefined || typeof value.confirmationError === "string")
|
|
1495
1572
|
&& typeof value.createdAt === "string"
|
|
1496
1573
|
&& typeof value.updatedAt === "string";
|
|
1497
1574
|
}
|
|
@@ -1499,7 +1576,7 @@ function isUserAuthorization(value) {
|
|
|
1499
1576
|
if (!isRecord(value))
|
|
1500
1577
|
return false;
|
|
1501
1578
|
return typeof value.id === "string"
|
|
1502
|
-
&& (value.status === "accepted" || value.status === "consumed")
|
|
1579
|
+
&& (value.status === "accepted" || value.status === "consumed" || value.status === "abandoned")
|
|
1503
1580
|
&& value.role === "project-manager"
|
|
1504
1581
|
&& value.operation === "workflow-dispatch"
|
|
1505
1582
|
&& Number.isInteger(value.baseRevision)
|
|
@@ -1511,13 +1588,15 @@ function isUserAuthorization(value) {
|
|
|
1511
1588
|
&& typeof value.violatedRule === "string"
|
|
1512
1589
|
&& typeof value.authorizationText === "string"
|
|
1513
1590
|
&& typeof value.createdAt === "string"
|
|
1514
|
-
&& (value.consumedAt === undefined || typeof value.consumedAt === "string")
|
|
1591
|
+
&& (value.consumedAt === undefined || typeof value.consumedAt === "string")
|
|
1592
|
+
&& (value.abandonedAt === undefined || typeof value.abandonedAt === "string")
|
|
1593
|
+
&& (value.abandonReason === undefined || typeof value.abandonReason === "string");
|
|
1515
1594
|
}
|
|
1516
1595
|
function isUserApprovedFollowUp(value) {
|
|
1517
1596
|
if (!isRecord(value))
|
|
1518
1597
|
return false;
|
|
1519
1598
|
return typeof value.id === "string"
|
|
1520
|
-
&& (value.status === "accepted" || value.status === "consumed")
|
|
1599
|
+
&& (value.status === "accepted" || value.status === "consumed" || value.status === "abandoned")
|
|
1521
1600
|
&& value.role === "project-manager"
|
|
1522
1601
|
&& value.operation === "post-validation-follow-up"
|
|
1523
1602
|
&& Number.isInteger(value.baseRevision)
|
|
@@ -1529,7 +1608,9 @@ function isUserApprovedFollowUp(value) {
|
|
|
1529
1608
|
&& typeof value.evidence === "string"
|
|
1530
1609
|
&& typeof value.approvalText === "string"
|
|
1531
1610
|
&& typeof value.createdAt === "string"
|
|
1532
|
-
&& (value.consumedAt === undefined || typeof value.consumedAt === "string")
|
|
1611
|
+
&& (value.consumedAt === undefined || typeof value.consumedAt === "string")
|
|
1612
|
+
&& (value.abandonedAt === undefined || typeof value.abandonedAt === "string")
|
|
1613
|
+
&& (value.abandonReason === undefined || typeof value.abandonReason === "string");
|
|
1533
1614
|
}
|
|
1534
1615
|
function normalizeDispatchEvidenceBaseline(value) {
|
|
1535
1616
|
if (!isRecord(value) || !isRecord(value.artifactHashes) || !isRecord(value.gateFingerprints))
|