vibe-coding-master 0.8.10 → 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.
@@ -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: 5 },
66
+ { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
66
67
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
67
- { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
68
- { eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
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 = {
@@ -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
  });
@@ -148,7 +148,15 @@ export function createGateReviewService(deps) {
148
148
  };
149
149
  }
150
150
  if (gate === "code-diff") {
151
- const dirtyStatus = await getDirtyCodeDiffStatus(deps.runner, context.taskRepoRoot);
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
- const codeDiffInput = gate === "code-diff"
323
- ? await resolveCodeDiffInput(deps, context, record)
324
- : undefined;
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 result = await runner.run("git", args, { cwd });
1400
- return result.exitCode === 0 ? result.stdout : "";
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 = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
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: `sha256:${sha256(finalAcceptanceContent)}`,
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: 5 },
66
+ { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 10 },
66
67
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
67
- { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
68
- { eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
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: plannedChanges.length > 0
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?.releaseDispatch(toWorkflowContext(input), messageId);
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 && (EXPECTED_AUTO_RETROSPECTIVE_SKIP_CODES.has(error.code) || error.statusCode === 409)) {
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: "VCM restarted before this dispatch was confirmed. Resend the message if it is still needed."
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
- delete next.dispatchingAt;
199
- delete next.deliveredAt;
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, roleUsesLsp(role) ? [VCM_LSP_PLUGIN_DIR] : []),
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
- ...(roleUsesLsp(role)
112
+ ...(lspPluginDirs.length > 0
108
113
  ? {
109
114
  ENABLE_LSP_TOOL: "true",
110
115
  ENABLE_TOOL_SEARCH: "false"
@@ -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
- userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending.overrideAuthorizationId),
238
- userApprovedFollowUps: state.userApprovedFollowUps.filter((entry) => entry.id !== pending.followUpApprovalId),
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(state);
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.warnings.length > 0 || state.pendingDispatch?.status !== "dispatching")
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", "A role dispatch is already being submitted and cannot be canceled by a user question.", "Wait for the target UserPromptSubmit confirmation or recover the dispatch before asking the user.");
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
- userAuthorizations: state.userAuthorizations.filter((entry) => entry.id !== pending?.overrideAuthorizationId),
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))
@@ -141,7 +141,7 @@ ${renderRoleMemoryRules("architect")}
141
141
  #### Code Scaffolding
142
142
 
143
143
  - Use the Agent tool to invoke \`vcm-architect-scaffold-worker\` in the foreground after the plan and Scaffold Manifest are complete. Give it the exact plan path and require it to return before this Architect turn continues.
144
- - Use one scaffold worker. Do not run it in the background or end the Architect turn while it is active.
144
+ - Run at most one scaffold worker at a time. Do not run it in the background or end the Architect turn while it is active. If its report leaves scaffold items unfinished, verify the completed work and invoke another foreground scaffold worker with the previous report and exact remaining items. Continue until every item is complete or report the concrete failure to project-manager.
145
145
  - Review the worker commit, actual diff, callable surfaces, marker placement, ledger reconciliation, and L0 results yourself. Architect owns every final scaffold claim and must correct any worker error before marking planning complete.
146
146
  - Create or update only the minimum module/file scaffolding needed to make boundaries, callable surfaces, and placeholders unambiguous. Minimum limits depth (no business implementation), never breadth: every \`create\`, \`change\`, and \`delete\` item must be scaffolded.
147
147
  - When a required configuration, package manifest, or build-definition change cannot safely contain a \`VCM:CODE\` marker, complete and commit it directly as Architect-owned scaffold work. Record it in the Module/File Plan and Scaffold Build Evidence. Do not add it to the Scaffold Manifest.
@@ -12,6 +12,11 @@ planning-session memory candidate and provides an exact path.
12
12
  - If VCM did not provide a path, do not create a proposal.
13
13
  - Propose only verified, durable, reusable project knowledge supported by task
14
14
  evidence.
15
+ - Before proposing an add or update, check current durable documents, Harness
16
+ rules and skills, and the tool's help, usage, or error output. Cite the checked
17
+ source in Evidence. Do not duplicate existing knowledge in memory; propose
18
+ \`durable-doc\` only when the existing document is wrong or incomplete, or use
19
+ \`memory-reference\` when roles need a short pointer across tasks.
15
20
  - Target shared project knowledge to \`shared\`. Target knowledge used only by
16
21
  the current role to \`current-role\`.
17
22
  - For every add or update, explain why the memory is necessary, what future