vibe-coding-master 0.8.13 → 0.9.1
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 +7 -3
- package/dist/backend/adapters/claude-adapter.js +2 -1
- package/dist/backend/cli/install-vcm-harness.js +66 -16
- package/dist/backend/gateway/gateway-service.js +5 -3
- package/dist/backend/runtime/terminal-submit.js +1 -0
- package/dist/backend/server.js +14 -4
- package/dist/backend/services/app-settings-service.js +3 -2
- package/dist/backend/services/auto-memory-service.js +4 -3
- package/dist/backend/services/claude-hook-service.js +6 -0
- package/dist/backend/services/claude-transcript-service.js +2 -13
- package/dist/backend/services/claude-version-check.js +87 -0
- package/dist/backend/services/gate-review-service.js +46 -8
- package/dist/backend/services/harness-service.js +41 -20
- package/dist/backend/services/message-service.js +30 -6
- package/dist/backend/services/project-service.js +8 -2
- package/dist/backend/services/runtime-recovery-service.js +36 -9
- package/dist/backend/services/session-service.js +10 -19
- package/dist/backend/services/task-close-service.js +1 -0
- package/dist/backend/services/terminal-process-exit-service.js +4 -1
- package/dist/backend/services/workflow-control-service.js +55 -11
- package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
- package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
- package/dist/backend/templates/harness/gate-review.js +676 -643
- package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
- package/dist/backend/templates/harness/project-owned-history.js +21 -0
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
- package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
- package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
- package/dist/shared/types/app-settings.js +31 -0
- package/dist-frontend/assets/index-Bs4g_1yF.css +32 -0
- package/dist-frontend/assets/{index-DqANomt1.js → index-CjM9OqZh.js} +41 -41
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/check-durable-docs +332 -298
- package/scripts/harness-tools/generate-module-index +1047 -512
- package/scripts/harness-tools/generate-public-surface +1231 -1069
- package/scripts/harness-tools/package.json +3 -0
- package/scripts/harness-tools/run-long-check +608 -531
- package/scripts/harness-tools/vcm-artifact +181 -113
- package/scripts/harness-tools/vcm-bash-guard +839 -605
- package/scripts/harness-tools/vcm-subagent-guard +67 -63
- package/scripts/harness-tools/watch-job +313 -238
- package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
import { DEFAULT_ARCHITECT_WORKER_SETTINGS, normalizeArchitectWorkerSettings } from "../../shared/types/app-settings.js";
|
|
5
|
+
import { applyArchitectWorkerSetting } from "../templates/harness/architect-worker-frontmatter.js";
|
|
4
6
|
import { renderArchitectHarnessRules } from "../templates/harness/architect-agent.js";
|
|
5
7
|
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
6
8
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
@@ -55,8 +57,8 @@ const LEGACY_CODEX_HARNESS_PATHS = [
|
|
|
55
57
|
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'`;
|
|
56
58
|
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'`;
|
|
57
59
|
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'`;
|
|
58
|
-
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;
|
|
59
|
-
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; };
|
|
60
|
+
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; node "$guard" || exit 0'`;
|
|
61
|
+
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; }; node "$guard"'`;
|
|
60
62
|
const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
|
|
61
63
|
const VCM_AUTO_MEMORY_ENABLED = false;
|
|
62
64
|
const VCM_FAST_HOOK_COMMAND = VCM_HOOK_COMMAND.replace(" --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1", "");
|
|
@@ -256,21 +258,24 @@ const HARNESS_FILES = [
|
|
|
256
258
|
kind: "agent-architect-evidence-worker",
|
|
257
259
|
path: ".claude/agents/vcm-architect-evidence-worker.md",
|
|
258
260
|
title: "VCM Architect Evidence Worker Agent",
|
|
259
|
-
frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { tools: "Read, Grep, Glob, Write"
|
|
261
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-evidence-worker", "Foreground Architect worker for bounded project evidence collection.", { tools: "Read, Grep, Glob, Write" }),
|
|
262
|
+
architectWorkerType: "evidence",
|
|
260
263
|
renderRules: renderArchitectEvidenceWorkerHarnessRules
|
|
261
264
|
},
|
|
262
265
|
{
|
|
263
266
|
kind: "agent-architect-scaffold-worker",
|
|
264
267
|
path: ".claude/agents/vcm-architect-scaffold-worker.md",
|
|
265
268
|
title: "VCM Architect Scaffold Worker Agent",
|
|
266
|
-
frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation."
|
|
269
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-scaffold-worker", "Foreground Architect worker for exact scaffold execution and scaffold validation."),
|
|
270
|
+
architectWorkerType: "scaffold",
|
|
267
271
|
renderRules: renderArchitectScaffoldWorkerHarnessRules
|
|
268
272
|
},
|
|
269
273
|
{
|
|
270
274
|
kind: "agent-architect-validation-worker",
|
|
271
275
|
path: ".claude/agents/vcm-architect-validation-worker.md",
|
|
272
276
|
title: "VCM Architect Validation Worker Agent",
|
|
273
|
-
frontmatter: renderAgentFrontmatter("vcm-architect-validation-worker", "Foreground Architect worker for exact assigned command execution and evidence capture.", { tools: "Read, Grep, Glob, Bash, Write"
|
|
277
|
+
frontmatter: renderAgentFrontmatter("vcm-architect-validation-worker", "Foreground Architect worker for exact assigned command execution and evidence capture.", { tools: "Read, Grep, Glob, Bash, Write" }),
|
|
278
|
+
architectWorkerType: "validation",
|
|
274
279
|
renderRules: renderArchitectValidationWorkerHarnessRules
|
|
275
280
|
},
|
|
276
281
|
{
|
|
@@ -358,13 +363,18 @@ const HARNESS_FILES = [
|
|
|
358
363
|
export function createHarnessService(deps) {
|
|
359
364
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
360
365
|
const vcmVersion = deps.vcmVersion ?? "unknown";
|
|
366
|
+
async function resolveArchitectWorkerSettings() {
|
|
367
|
+
return deps.getArchitectWorkerSettings
|
|
368
|
+
? normalizeArchitectWorkerSettings(await deps.getArchitectWorkerSettings())
|
|
369
|
+
: DEFAULT_ARCHITECT_WORKER_SETTINGS;
|
|
370
|
+
}
|
|
361
371
|
const codeIntelligenceDetector = createHarnessCodeIntelligenceDetector(deps.fs, {
|
|
362
372
|
runner: deps.commandRunner
|
|
363
373
|
});
|
|
364
374
|
return {
|
|
365
375
|
async getHarnessStatus(repoRoot) {
|
|
366
376
|
const [analyses, codeIntelligence] = await Promise.all([
|
|
367
|
-
analyzeHarnessFiles(deps.fs, repoRoot),
|
|
377
|
+
analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
|
|
368
378
|
codeIntelligenceDetector.detect(repoRoot)
|
|
369
379
|
]);
|
|
370
380
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
@@ -401,7 +411,7 @@ export function createHarnessService(deps) {
|
|
|
401
411
|
}
|
|
402
412
|
const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
|
|
403
413
|
const [analyses, codeIntelligence] = await Promise.all([
|
|
404
|
-
analyzeHarnessFiles(deps.fs, repoRoot),
|
|
414
|
+
analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings()),
|
|
405
415
|
codeIntelligenceDetector.detect(repoRoot)
|
|
406
416
|
]);
|
|
407
417
|
const legacyChanges = await analyzeLegacyCodexHarnessPaths(deps.fs, repoRoot);
|
|
@@ -417,7 +427,9 @@ export function createHarnessService(deps) {
|
|
|
417
427
|
async applyHarness(repoRoot) {
|
|
418
428
|
await assertHarnessWorktreeClean(deps.git, repoRoot);
|
|
419
429
|
if (deps.runFixedInstaller) {
|
|
420
|
-
const result = await deps.runFixedInstaller(repoRoot
|
|
430
|
+
const result = await deps.runFixedInstaller(repoRoot, {
|
|
431
|
+
architectWorkers: await resolveArchitectWorkerSettings()
|
|
432
|
+
});
|
|
421
433
|
if (result.changedFiles.length > 0) {
|
|
422
434
|
await bumpHarnessRevision(deps.fs, repoRoot, now());
|
|
423
435
|
}
|
|
@@ -429,7 +441,7 @@ export function createHarnessService(deps) {
|
|
|
429
441
|
message: formatHarnessApplyMessage(committed.harnessCommit, result.changedFiles.length > 0)
|
|
430
442
|
};
|
|
431
443
|
}
|
|
432
|
-
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot);
|
|
444
|
+
const analyses = await analyzeHarnessFiles(deps.fs, repoRoot, await resolveArchitectWorkerSettings());
|
|
433
445
|
const changedFiles = [];
|
|
434
446
|
for (const analysis of analyses) {
|
|
435
447
|
if (analysis.status.action === "ok" || !analysis.nextContent) {
|
|
@@ -1191,15 +1203,15 @@ function extractManagedBlock(definition, content) {
|
|
|
1191
1203
|
}
|
|
1192
1204
|
return content.match(getManagedBlockPattern(definition))?.[0];
|
|
1193
1205
|
}
|
|
1194
|
-
async function analyzeHarnessFiles(fs, repoRoot) {
|
|
1206
|
+
async function analyzeHarnessFiles(fs, repoRoot, workerSettings = DEFAULT_ARCHITECT_WORKER_SETTINGS) {
|
|
1195
1207
|
const analyses = [];
|
|
1196
1208
|
for (const definition of HARNESS_FILES) {
|
|
1197
|
-
analyses.push(await analyzeHarnessFile(fs, repoRoot, definition));
|
|
1209
|
+
analyses.push(await analyzeHarnessFile(fs, repoRoot, definition, workerSettings));
|
|
1198
1210
|
}
|
|
1199
1211
|
analyses.push(await analyzeClaudeSettingsFile(fs, repoRoot));
|
|
1200
1212
|
return analyses;
|
|
1201
1213
|
}
|
|
1202
|
-
async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
1214
|
+
async function analyzeHarnessFile(fs, repoRoot, definition, workerSettings) {
|
|
1203
1215
|
const absolutePath = resolveHarnessPath(repoRoot, definition.path);
|
|
1204
1216
|
if (definition.ownership === "project-file") {
|
|
1205
1217
|
const exists = await fs.pathExists(absolutePath);
|
|
@@ -1233,7 +1245,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
|
1233
1245
|
: getManagedBlockPattern(definition);
|
|
1234
1246
|
const exists = await fs.pathExists(absolutePath);
|
|
1235
1247
|
if (!exists) {
|
|
1236
|
-
const newContent = expectedContent ?? renderNewHarnessFile(definition, expectedBlock ?? "");
|
|
1248
|
+
const newContent = expectedContent ?? applyWorkerSetting(renderNewHarnessFile(definition, expectedBlock ?? ""), definition, workerSettings);
|
|
1237
1249
|
return {
|
|
1238
1250
|
definition,
|
|
1239
1251
|
status: {
|
|
@@ -1281,7 +1293,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
|
1281
1293
|
const migratedContent = migrateLegacyHarnessFile(definition, currentContent, expectedBlock);
|
|
1282
1294
|
if (migratedContent) {
|
|
1283
1295
|
const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(migratedContent) : migratedContent;
|
|
1284
|
-
const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition);
|
|
1296
|
+
const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition, workerSettings);
|
|
1285
1297
|
return {
|
|
1286
1298
|
definition,
|
|
1287
1299
|
status: {
|
|
@@ -1300,7 +1312,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
|
1300
1312
|
};
|
|
1301
1313
|
}
|
|
1302
1314
|
const insertedContent = `${currentContent.trimEnd()}\n\n${expectedBlock}\n`;
|
|
1303
|
-
const nextContent = normalizeAgentFrontmatter(insertedContent, definition);
|
|
1315
|
+
const nextContent = normalizeAgentFrontmatter(insertedContent, definition, workerSettings);
|
|
1304
1316
|
return {
|
|
1305
1317
|
definition,
|
|
1306
1318
|
status: {
|
|
@@ -1322,7 +1334,7 @@ async function analyzeHarnessFile(fs, repoRoot, definition) {
|
|
|
1322
1334
|
const currentBlock = match[0];
|
|
1323
1335
|
const blockUpdatedContent = currentContent.replace(managedBlockPattern, expectedBlock);
|
|
1324
1336
|
const memoryUpdatedContent = definition.memoryBlock ? ensureVcmMemoryBlock(blockUpdatedContent) : blockUpdatedContent;
|
|
1325
|
-
const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition);
|
|
1337
|
+
const nextContent = normalizeAgentFrontmatter(memoryUpdatedContent, definition, workerSettings);
|
|
1326
1338
|
const action = currentContent === nextContent ? "ok" : "update";
|
|
1327
1339
|
return {
|
|
1328
1340
|
definition,
|
|
@@ -1488,11 +1500,17 @@ function ensureAgentAllowedTool(content, requiredTool) {
|
|
|
1488
1500
|
: frontmatter.replace(/^disallowedTools:\s*.*\r?\n?/m, "");
|
|
1489
1501
|
return content.replace(frontmatter, nextFrontmatter);
|
|
1490
1502
|
}
|
|
1491
|
-
function normalizeAgentFrontmatter(content, definition) {
|
|
1503
|
+
function normalizeAgentFrontmatter(content, definition, workerSettings) {
|
|
1492
1504
|
const allowedToolsUpdated = (definition.requiredTools ?? []).reduce(ensureAgentAllowedTool, content);
|
|
1493
1505
|
const disallowedToolsUpdated = ensureAgentDisallowedTools(allowedToolsUpdated, definition.requiredDisallowedTools);
|
|
1494
1506
|
const skillsUpdated = (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
|
|
1495
|
-
|
|
1507
|
+
const removedSkillsUpdated = (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
|
|
1508
|
+
return applyWorkerSetting(removedSkillsUpdated, definition, workerSettings);
|
|
1509
|
+
}
|
|
1510
|
+
function applyWorkerSetting(content, definition, workerSettings) {
|
|
1511
|
+
return definition.architectWorkerType
|
|
1512
|
+
? applyArchitectWorkerSetting(content, workerSettings[definition.architectWorkerType])
|
|
1513
|
+
: content;
|
|
1496
1514
|
}
|
|
1497
1515
|
function ensureAgentSkill(content, requiredSkill) {
|
|
1498
1516
|
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
@@ -2088,9 +2106,12 @@ Summarize files reviewed, files updated, generated artifacts, durable-doc audit
|
|
|
2088
2106
|
[/VCM HARNESS BOOTSTRAP]`;
|
|
2089
2107
|
}
|
|
2090
2108
|
export function createScriptFixedHarnessInstaller(scriptPath = path.resolve(process.cwd(), "scripts/install-vcm-harness.mjs")) {
|
|
2091
|
-
return async (repoRoot) => {
|
|
2109
|
+
return async (repoRoot, options = {}) => {
|
|
2092
2110
|
try {
|
|
2093
|
-
const
|
|
2111
|
+
const args = options.architectWorkers
|
|
2112
|
+
? [scriptPath, repoRoot, "--architect-workers", JSON.stringify(options.architectWorkers)]
|
|
2113
|
+
: [scriptPath, repoRoot];
|
|
2114
|
+
const { stdout, stderr } = await execFileAsync(process.execPath, args, {
|
|
2094
2115
|
cwd: process.cwd(),
|
|
2095
2116
|
maxBuffer: 10 * 1024 * 1024
|
|
2096
2117
|
});
|
|
@@ -47,11 +47,9 @@ export function createMessageService(deps) {
|
|
|
47
47
|
const candidates = selectDispatchCandidates(pendingFiles, input.stoppedRole);
|
|
48
48
|
const results = [];
|
|
49
49
|
for (const routeFile of candidates) {
|
|
50
|
+
// dispatchRouteFile persists its own message snapshots.
|
|
50
51
|
const result = await dispatchRouteFile(input, routeFile, state, timestamp);
|
|
51
52
|
results.push(result);
|
|
52
|
-
if (result.message) {
|
|
53
|
-
await appendMessageSnapshot(deps.fs, input, result.message);
|
|
54
|
-
}
|
|
55
53
|
if (result.delivered) {
|
|
56
54
|
break;
|
|
57
55
|
}
|
|
@@ -136,12 +134,25 @@ export function createMessageService(deps) {
|
|
|
136
134
|
...message,
|
|
137
135
|
deliveredAt: timestamp
|
|
138
136
|
};
|
|
137
|
+
let deliveryRecorded = false;
|
|
139
138
|
try {
|
|
140
139
|
await submitTerminalInput(deps.runtime, session.id, renderMessageEnvelope(delivered), {
|
|
141
|
-
enterDelayMs: autoDispatchEnterDelayMs
|
|
140
|
+
enterDelayMs: autoDispatchEnterDelayMs,
|
|
141
|
+
// Record delivery before Enter: a restart after this point must treat the
|
|
142
|
+
// dispatch as possibly executed instead of delivering it again.
|
|
143
|
+
beforeSubmit: async () => {
|
|
144
|
+
await appendMessageSnapshot(deps.fs, input, delivered);
|
|
145
|
+
deliveryRecorded = true;
|
|
146
|
+
}
|
|
142
147
|
});
|
|
143
148
|
}
|
|
144
149
|
catch (error) {
|
|
150
|
+
if (deliveryRecorded) {
|
|
151
|
+
await appendMessageSnapshot(deps.fs, input, {
|
|
152
|
+
...message,
|
|
153
|
+
failureReason: `Terminal submission failed: ${errorMessage(error)}`
|
|
154
|
+
});
|
|
155
|
+
}
|
|
145
156
|
if (routeFile.fromRole === PM_ROLE) {
|
|
146
157
|
await deps.workflowControlService?.releaseDispatch(toWorkflowContext(input), messageId);
|
|
147
158
|
}
|
|
@@ -523,11 +534,24 @@ async function readLatestMessages(fs, messagesPath) {
|
|
|
523
534
|
const latest = new Map();
|
|
524
535
|
const lines = (await fs.readText(messagesPath)).split(/\r?\n/).filter(Boolean);
|
|
525
536
|
for (const line of lines) {
|
|
526
|
-
const message =
|
|
527
|
-
|
|
537
|
+
const message = parseMessageLine(line);
|
|
538
|
+
// A line truncated by a crash or full disk must not block every later read;
|
|
539
|
+
// runtime recovery removes it and reports a warning.
|
|
540
|
+
if (message) {
|
|
541
|
+
latest.set(message.id, message);
|
|
542
|
+
}
|
|
528
543
|
}
|
|
529
544
|
return [...latest.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
530
545
|
}
|
|
546
|
+
export function parseMessageLine(line) {
|
|
547
|
+
try {
|
|
548
|
+
const message = JSON.parse(line);
|
|
549
|
+
return message && typeof message.id === "string" ? message : undefined;
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
531
555
|
async function appendMessageSnapshot(fs, input, message) {
|
|
532
556
|
await fs.appendText(getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug), `${JSON.stringify(message)}\n`);
|
|
533
557
|
}
|
|
@@ -33,7 +33,7 @@ export function createProjectService(deps) {
|
|
|
33
33
|
await deps.fs.ensureDir(path.join(this.getProjectDataRoot(repoRoot), "tasks"));
|
|
34
34
|
await deps.fs.ensureDir(path.join(repoRoot, ".claude", "worktrees"));
|
|
35
35
|
await this.saveConfig(config, true);
|
|
36
|
-
currentProject = await readConnectedProjectSummary(repoRoot, config);
|
|
36
|
+
currentProject = await readConnectedProjectSummary(repoRoot, config, { refreshClaudeVersion: true });
|
|
37
37
|
await deps.appSettings.recordRecentRepositoryPath(repoRoot);
|
|
38
38
|
return currentProject;
|
|
39
39
|
},
|
|
@@ -97,8 +97,14 @@ export function createProjectService(deps) {
|
|
|
97
97
|
return path.dirname(deps.appSettings.getProjectConfigPath(repoRoot));
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
|
-
async function readConnectedProjectSummary(repoRoot, config) {
|
|
100
|
+
async function readConnectedProjectSummary(repoRoot, config, options = {}) {
|
|
101
101
|
const warnings = [];
|
|
102
|
+
const claudeVersionWarning = await deps.claudeVersionCheck?.getWarning(config.claudeCommand, {
|
|
103
|
+
refresh: options.refreshClaudeVersion
|
|
104
|
+
});
|
|
105
|
+
if (claudeVersionWarning) {
|
|
106
|
+
warnings.push(claudeVersionWarning);
|
|
107
|
+
}
|
|
102
108
|
let branch = "unknown";
|
|
103
109
|
let isDirty = false;
|
|
104
110
|
let headCommit = "unknown";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { GATE_REVIEW_GATES } from "../../shared/types/gate-review.js";
|
|
3
|
+
import { parseMessageLine } from "./message-service.js";
|
|
3
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
4
5
|
const TRANSLATOR_SESSION_PATH = ".ai/vcm/translations/session.json";
|
|
5
6
|
const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
@@ -29,10 +30,16 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
29
30
|
}
|
|
30
31
|
for (const task of tasks.filter((candidate) => candidate.cleanupStatus !== "cleaned")) {
|
|
31
32
|
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const label = `recover task ${task.taskSlug}`;
|
|
34
|
+
// Each step runs independently so one failing record cannot skip the
|
|
35
|
+
// remaining recovery for the task.
|
|
36
|
+
await runStep(context, `${label} sessions`, () => recoverTaskSessions(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context));
|
|
37
|
+
let roundRecovered = false;
|
|
38
|
+
await runStep(context, `${label} round`, async () => {
|
|
39
|
+
roundRecovered = await recoverRound(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context);
|
|
40
|
+
});
|
|
41
|
+
await runStep(context, `${label} messages`, () => recoverMessages(taskRepoRoot, config.stateRoot, task.taskSlug, recoveredAt, context));
|
|
42
|
+
await runStep(context, `${label} workflow`, async () => {
|
|
36
43
|
const workflowRecovered = await deps.workflowControlService?.recoverTask({
|
|
37
44
|
taskRepoRoot,
|
|
38
45
|
stateRoot: config.stateRoot,
|
|
@@ -42,10 +49,16 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
42
49
|
if (workflowRecovered) {
|
|
43
50
|
context.changedPaths.add(path.join(config.stateRoot, "workflow-control.json"));
|
|
44
51
|
}
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
});
|
|
53
|
+
await runStep(context, `${label} gate review`, () => recoverGateReview(taskRepoRoot, recoveredAt, context));
|
|
54
|
+
await runStep(context, `${label} coder workers`, () => recoverCoderWorkers(taskRepoRoot, task.taskSlug, context));
|
|
55
|
+
await runStep(context, `${label} architect restart`, async () => {
|
|
47
56
|
await deps.architectRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
57
|
+
});
|
|
58
|
+
await runStep(context, `${label} restart with context`, async () => {
|
|
48
59
|
await deps.roleContextRestartService?.recoverTask(repoRoot, task.taskSlug);
|
|
60
|
+
});
|
|
61
|
+
await runStep(context, `${label} status`, async () => {
|
|
49
62
|
if ((roundRecovered || task.status === "running") && !hasLiveTaskSession(task.taskSlug)) {
|
|
50
63
|
await deps.taskService.updateTaskStatus(repoRoot, task.taskSlug, "stopped");
|
|
51
64
|
}
|
|
@@ -188,8 +201,17 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
188
201
|
}
|
|
189
202
|
const lines = (await deps.fs.readText(absolutePath)).split(/\r?\n/).filter((line) => line.trim());
|
|
190
203
|
let changed = false;
|
|
191
|
-
const
|
|
192
|
-
|
|
204
|
+
const parsed = [];
|
|
205
|
+
for (const [index, line] of lines.entries()) {
|
|
206
|
+
const message = parseMessageLine(line);
|
|
207
|
+
if (message) {
|
|
208
|
+
parsed.push(message);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
changed = true;
|
|
212
|
+
context.warnings.push(`${relativePath}: removed unparsable message log line ${index + 1}.`);
|
|
213
|
+
}
|
|
214
|
+
const messages = parsed.map((message) => {
|
|
193
215
|
if (message.dispatchingAt && !message.acceptedAt) {
|
|
194
216
|
const next = {
|
|
195
217
|
...message,
|
|
@@ -208,7 +230,12 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
208
230
|
return;
|
|
209
231
|
}
|
|
210
232
|
const content = messages.length ? `${messages.map((message) => JSON.stringify(message)).join("\n")}\n` : "";
|
|
211
|
-
|
|
233
|
+
if (deps.fs.writeTextAtomic) {
|
|
234
|
+
await deps.fs.writeTextAtomic(absolutePath, content);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
await deps.fs.writeText(absolutePath, content);
|
|
238
|
+
}
|
|
212
239
|
context.changedPaths.add(relativePath);
|
|
213
240
|
void timestamp;
|
|
214
241
|
}
|
|
@@ -5,7 +5,6 @@ import { isCodexBridgeSessionModel } from "../../shared/types/session.js";
|
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
8
|
-
import { claudeTranscriptPath } from "./claude-transcript-service.js";
|
|
9
8
|
import { readHarnessRevisionState } from "./harness-revision.js";
|
|
10
9
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
11
10
|
import { roleUsesLsp, prepareTaskLspPlugin, renderArchitectLspNotice, VCM_LSP_PLUGIN_DIR } from "./lsp-plugin.js";
|
|
@@ -77,11 +76,9 @@ export function createSessionService(deps) {
|
|
|
77
76
|
});
|
|
78
77
|
}
|
|
79
78
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
? claudeTranscriptPath(taskRepoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
84
|
-
: undefined;
|
|
79
|
+
// Transcript paths come from Claude Code hook payloads; VCM does not derive
|
|
80
|
+
// them from Claude Code's project-directory encoding.
|
|
81
|
+
const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
|
|
85
82
|
const lspStatus = roleUsesLsp(role) && deps.codeIntelligenceDetector
|
|
86
83
|
? await deps.codeIntelligenceDetector.detect(taskRepoRoot, { refresh: true })
|
|
87
84
|
: undefined;
|
|
@@ -222,9 +219,7 @@ export function createSessionService(deps) {
|
|
|
222
219
|
// session genuinely starts at repoRoot.
|
|
223
220
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
224
221
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
225
|
-
const transcriptPath =
|
|
226
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
227
|
-
: undefined;
|
|
222
|
+
const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
|
|
228
223
|
const startCommand = {
|
|
229
224
|
...deps.claude.buildRoleStartCommand(TRANSLATOR_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
230
225
|
cwd: launchCwd
|
|
@@ -338,9 +333,7 @@ export function createSessionService(deps) {
|
|
|
338
333
|
// restored cwd so the `/cd` migrate below fires only on an actual switch.
|
|
339
334
|
const sessionCwd = launchMode === "resume" ? persisted?.cwd ?? launchCwd : launchCwd;
|
|
340
335
|
const claudeSessionId = resumeClaudeSessionId ?? "";
|
|
341
|
-
const transcriptPath =
|
|
342
|
-
? claudeTranscriptPath(repoRoot, resumeClaudeSessionId, persisted?.claudeConfigDir)
|
|
343
|
-
: undefined;
|
|
336
|
+
const transcriptPath = launchMode === "resume" ? persisted?.transcriptPath : undefined;
|
|
344
337
|
const startCommand = {
|
|
345
338
|
...deps.claude.buildRoleStartCommand(HARNESS_ENGINEER_ROLE, config.claudeCommand, permissionMode, resumeClaudeSessionId, launchMode === "resume", model, effort, modelSettingsOverride),
|
|
346
339
|
cwd: launchCwd
|
|
@@ -597,9 +590,7 @@ export function createSessionService(deps) {
|
|
|
597
590
|
updatedAt: timestamp,
|
|
598
591
|
lastOutputAt: runtimeSession.lastOutputAt,
|
|
599
592
|
exitCode: runtimeSession.exitCode,
|
|
600
|
-
transcriptPath: session.
|
|
601
|
-
? claudeTranscriptPath(repoRoot, session.claudeSessionId, session.claudeConfigDir)
|
|
602
|
-
: session.transcriptPath
|
|
593
|
+
transcriptPath: session.transcriptPath
|
|
603
594
|
};
|
|
604
595
|
deps.registry.upsert(normalizeProjectScopedRecordForPersistence(resumed));
|
|
605
596
|
await persistProjectScopedToolSession(repoRoot, resumed);
|
|
@@ -1275,6 +1266,10 @@ function toRoleSessionRecordView(record, runtime) {
|
|
|
1275
1266
|
};
|
|
1276
1267
|
}
|
|
1277
1268
|
export function matchesRoleHookSession(record, input) {
|
|
1269
|
+
if (record.runtimeSessionToken
|
|
1270
|
+
&& record.runtimeSessionToken !== input.runtimeSessionToken) {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1278
1273
|
if (input.runtimeSessionId) {
|
|
1279
1274
|
return record.id === input.runtimeSessionId;
|
|
1280
1275
|
}
|
|
@@ -1283,10 +1278,6 @@ export function matchesRoleHookSession(record, input) {
|
|
|
1283
1278
|
&& input.eventName !== "UserPromptSubmit") {
|
|
1284
1279
|
return false;
|
|
1285
1280
|
}
|
|
1286
|
-
if (record.runtimeSessionToken
|
|
1287
|
-
&& record.runtimeSessionToken !== input.runtimeSessionToken) {
|
|
1288
|
-
return false;
|
|
1289
|
-
}
|
|
1290
1281
|
if (!record.claudeSessionId && !record.transcriptPath) {
|
|
1291
1282
|
return input.eventName === "UserPromptSubmit";
|
|
1292
1283
|
}
|
|
@@ -5,6 +5,7 @@ export function createTaskCloseService(deps) {
|
|
|
5
5
|
async closeTask(repoRoot, taskSlug) {
|
|
6
6
|
const task = await deps.taskService.markTaskCleaned(repoRoot, taskSlug);
|
|
7
7
|
const warnings = [];
|
|
8
|
+
deps.gateReviewService?.stopTask(taskSlug);
|
|
8
9
|
if (deps.architectRestartService) {
|
|
9
10
|
await bestEffort("Unable to clear Architect restart state", () => deps.architectRestartService.clear(repoRoot, taskSlug), warnings);
|
|
10
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isVcmRoleName } from "../../shared/constants.js";
|
|
1
|
+
import { isReviewerRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
3
3
|
export function createTerminalProcessExitService(deps) {
|
|
4
4
|
let unsubscribe;
|
|
@@ -12,6 +12,9 @@ export function createTerminalProcessExitService(deps) {
|
|
|
12
12
|
status: event.session.status === "crashed" ? "crashed" : "exited",
|
|
13
13
|
exitCode: event.exitCode
|
|
14
14
|
});
|
|
15
|
+
if (recorded && isReviewerRoleName(recorded.record.role)) {
|
|
16
|
+
deps.gateReviewService?.recordReviewerTurnEnded(recorded.record.taskSlug, "process-exit");
|
|
17
|
+
}
|
|
15
18
|
if (!recorded?.turnWasRunning || !isVcmRoleName(recorded.record.role)) {
|
|
16
19
|
return;
|
|
17
20
|
}
|
|
@@ -26,13 +26,46 @@ export function createWorkflowControlService(deps) {
|
|
|
26
26
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
27
27
|
const id = deps.id ?? (() => `wfauth_${randomUUID()}`);
|
|
28
28
|
const locks = new Map();
|
|
29
|
-
|
|
29
|
+
// Locked callers only: replays the commit journal and persists the template or
|
|
30
|
+
// reconstructed runtime state.
|
|
31
|
+
function getState(input) {
|
|
32
|
+
return loadState(input, true);
|
|
33
|
+
}
|
|
34
|
+
// Unlocked public reads: computes the same view without writing any file.
|
|
35
|
+
function readStateView(input) {
|
|
36
|
+
return loadState(input, false);
|
|
37
|
+
}
|
|
38
|
+
async function readProgressView(input) {
|
|
39
|
+
const pending = await readPendingCommitJournal(input);
|
|
40
|
+
if (pending)
|
|
41
|
+
return parseWorkflowProgress(pending.progressContent, input.taskSlug);
|
|
42
|
+
if (!(await deps.fs.pathExists(progressPath(input)))) {
|
|
43
|
+
return parseWorkflowProgress(renderWorkflowProgressTemplate(input.taskSlug), input.taskSlug);
|
|
44
|
+
}
|
|
45
|
+
return readProgress(deps.fs, input);
|
|
46
|
+
}
|
|
47
|
+
async function loadState(input, persist) {
|
|
30
48
|
try {
|
|
31
|
-
|
|
49
|
+
if (persist) {
|
|
50
|
+
await recoverCommitJournal(input);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const pending = await readPendingCommitJournal(input);
|
|
54
|
+
if (pending) {
|
|
55
|
+
const state = normalizeState(pending.state, input.taskSlug, now());
|
|
56
|
+
const progress = parseWorkflowProgress(pending.progressContent, input.taskSlug);
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
warnings: [...state.warnings, ...stateProgressConsistencyWarnings(state, progress)]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
32
63
|
const hasProgress = await deps.fs.pathExists(progressPath(input));
|
|
33
64
|
const hasState = await deps.fs.pathExists(statePath(input));
|
|
34
65
|
if (!hasProgress && !hasState) {
|
|
35
|
-
|
|
66
|
+
if (persist) {
|
|
67
|
+
await writeAtomic(deps.fs, progressPath(input), renderWorkflowProgressTemplate(input.taskSlug));
|
|
68
|
+
}
|
|
36
69
|
return emptyState(input.taskSlug, now());
|
|
37
70
|
}
|
|
38
71
|
if (!hasProgress) {
|
|
@@ -51,7 +84,9 @@ export function createWorkflowControlService(deps) {
|
|
|
51
84
|
}
|
|
52
85
|
if (!progress.proposal && progress.history.length > 0) {
|
|
53
86
|
const recovered = await reconstructStateFromProgress(input, progress);
|
|
54
|
-
|
|
87
|
+
if (persist) {
|
|
88
|
+
await saveState(input, recovered);
|
|
89
|
+
}
|
|
55
90
|
return recovered;
|
|
56
91
|
}
|
|
57
92
|
return {
|
|
@@ -357,18 +392,17 @@ export function createWorkflowControlService(deps) {
|
|
|
357
392
|
return true;
|
|
358
393
|
});
|
|
359
394
|
}
|
|
395
|
+
// Public reads never write; journal replay and state persistence happen only
|
|
396
|
+
// inside locked operations.
|
|
360
397
|
return {
|
|
361
|
-
getState,
|
|
362
|
-
getProgress:
|
|
363
|
-
await getState(input);
|
|
364
|
-
return readProgress(deps.fs, input);
|
|
365
|
-
},
|
|
398
|
+
getState: readStateView,
|
|
399
|
+
getProgress: readProgressView,
|
|
366
400
|
requestUserInput,
|
|
367
401
|
resolveUserInput,
|
|
368
402
|
runWhileNotAwaitingUser,
|
|
369
403
|
submitProgress,
|
|
370
|
-
assertDocsArtifactAllowed,
|
|
371
|
-
assertRouteAuthorized,
|
|
404
|
+
assertDocsArtifactAllowed: (input, kind, role) => withLock(statePath(input), () => assertDocsArtifactAllowed(input, kind, role)),
|
|
405
|
+
assertRouteAuthorized: (input) => withLock(statePath(input), () => assertRouteAuthorized(input)),
|
|
372
406
|
claimDispatch,
|
|
373
407
|
releaseDispatch,
|
|
374
408
|
markDispatchUnconfirmed,
|
|
@@ -485,6 +519,16 @@ export function createWorkflowControlService(deps) {
|
|
|
485
519
|
await deps.fs.writeJsonAtomic(transactionPath(input), { ...journal, status: "committed" });
|
|
486
520
|
await removeTransactionJournal(transactionPath(input));
|
|
487
521
|
}
|
|
522
|
+
async function readPendingCommitJournal(input) {
|
|
523
|
+
const target = transactionPath(input);
|
|
524
|
+
if (!(await deps.fs.pathExists(target)))
|
|
525
|
+
return undefined;
|
|
526
|
+
const journal = await deps.fs.readJson(target);
|
|
527
|
+
if (journal.version !== 1 || journal.taskSlug !== input.taskSlug) {
|
|
528
|
+
throw new Error("Workflow commit journal does not match the active task.");
|
|
529
|
+
}
|
|
530
|
+
return journal.status === "pending" ? journal : undefined;
|
|
531
|
+
}
|
|
488
532
|
async function recoverCommitJournal(input) {
|
|
489
533
|
const target = transactionPath(input);
|
|
490
534
|
if (!(await deps.fs.pathExists(target)))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Agent names of the Architect workers whose model and effort follow the
|
|
2
|
+
// architect worker settings. Shared by the fixed installer and harness status.
|
|
3
|
+
export const ARCHITECT_WORKER_AGENT_TYPES = {
|
|
4
|
+
"vcm-architect-evidence-worker": "evidence",
|
|
5
|
+
"vcm-architect-scaffold-worker": "scaffold",
|
|
6
|
+
"vcm-architect-validation-worker": "validation"
|
|
7
|
+
};
|
|
8
|
+
export function applyArchitectWorkerSetting(content, setting) {
|
|
9
|
+
const withModel = setAgentFrontmatterField(content, "model", setting.model, /^(?:tools|disallowedTools):.*$/m);
|
|
10
|
+
return setAgentFrontmatterField(withModel, "effort", setting.effort === "default" ? undefined : setting.effort, /^model:.*$/m);
|
|
11
|
+
}
|
|
12
|
+
function setAgentFrontmatterField(content, field, value, insertAfter) {
|
|
13
|
+
const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
|
|
14
|
+
if (!frontmatterMatch) {
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
const frontmatter = frontmatterMatch[0];
|
|
18
|
+
const fieldLine = new RegExp(`^${field}:.*$`, "m");
|
|
19
|
+
let next;
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
next = frontmatter.replace(new RegExp(`^${field}:.*(?:\\r?\\n)?`, "m"), "");
|
|
22
|
+
}
|
|
23
|
+
else if (fieldLine.test(frontmatter)) {
|
|
24
|
+
next = frontmatter.replace(fieldLine, `${field}: ${value}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const anchor = frontmatter.match(insertAfter);
|
|
28
|
+
next = anchor
|
|
29
|
+
? frontmatter.replace(anchor[0], `${anchor[0]}\n${field}: ${value}`)
|
|
30
|
+
: frontmatter.replace(/\r?\n---$/, `\n${field}: ${value}\n---`);
|
|
31
|
+
}
|
|
32
|
+
return content.replace(frontmatter, next);
|
|
33
|
+
}
|