vibe-coding-master 0.4.19 → 0.4.21

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.
Files changed (30) hide show
  1. package/dist/backend/adapters/git-adapter.js +15 -0
  2. package/dist/backend/api/harness-routes.js +17 -0
  3. package/dist/backend/api/task-routes.js +2 -2
  4. package/dist/backend/app-version.js +12 -0
  5. package/dist/backend/cli/install-vcm-harness.js +3 -2
  6. package/dist/backend/gateway/gateway-service.js +2 -5
  7. package/dist/backend/server.js +6 -2
  8. package/dist/backend/services/app-settings-service.js +2 -0
  9. package/dist/backend/services/claude-hook-service.js +188 -36
  10. package/dist/backend/services/diagnostics-service.js +2 -13
  11. package/dist/backend/services/harness-feedback-service.js +149 -2
  12. package/dist/backend/services/harness-service.js +159 -22
  13. package/dist/backend/services/round-service.js +58 -0
  14. package/dist/backend/services/session-service.js +6 -301
  15. package/dist/backend/templates/harness/architect-agent.js +37 -3
  16. package/dist/backend/templates/harness/coder-agent.js +6 -1
  17. package/dist/backend/templates/harness/harness-engineer-agent.js +31 -0
  18. package/dist/backend/templates/harness/project-manager-agent.js +7 -0
  19. package/dist/backend/templates/harness/reviewer-agent.js +10 -0
  20. package/dist/main.js +2 -11
  21. package/dist-frontend/assets/index-B3sODrcw.css +32 -0
  22. package/dist-frontend/assets/index-_aOTGZCq.js +96 -0
  23. package/dist-frontend/index.html +2 -2
  24. package/docs/full-harness-baseline.md +1 -2
  25. package/docs/gate-review-gates.md +8 -17
  26. package/docs/product-design.md +5 -6
  27. package/docs/v0.4-harness-optimization-plan.md +3 -2
  28. package/package.json +1 -1
  29. package/dist-frontend/assets/index-BrY-xd6U.js +0 -95
  30. package/dist-frontend/assets/index-D1LTJ-sY.css +0 -32
@@ -201,6 +201,21 @@ export function createGitAdapter(runner) {
201
201
  hint: result.stderr
202
202
  });
203
203
  },
204
+ async mergeBranchFastForward(repoRoot, branch) {
205
+ const result = await runGit(runner, repoRoot, ["merge", "--ff-only", branch]);
206
+ if (result.exitCode !== 0) {
207
+ throw new VcmError({
208
+ code: "GIT_MERGE_FAILED",
209
+ message: `Unable to fast-forward merge branch: ${branch}`,
210
+ statusCode: 409,
211
+ hint: result.stderr || result.stdout || "Rebase the task branch onto the connected repository branch, then try again."
212
+ });
213
+ }
214
+ return {
215
+ stdout: result.stdout,
216
+ stderr: result.stderr
217
+ };
218
+ },
204
219
  async addPaths(repoRoot, paths) {
205
220
  if (paths.length === 0) {
206
221
  return;
@@ -38,6 +38,13 @@ export function registerHarnessRoutes(app, deps) {
38
38
  commitSha: request.query.commit
39
39
  });
40
40
  });
41
+ app.post("/api/projects/harness/repository-diff/merge-to-current-branch", async (request) => {
42
+ const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
43
+ return deps.harnessService.mergeRepositoryDiffToCurrentBranch(project.repoRoot, {
44
+ taskRepoRoot: task.worktreePath,
45
+ taskBranch: task.branch
46
+ });
47
+ });
41
48
  app.get("/api/projects/harness/bootstrap", async (request) => {
42
49
  const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
43
50
  try {
@@ -119,6 +126,16 @@ export function registerHarnessRoutes(app, deps) {
119
126
  comment: typeof request.body?.comment === "string" ? request.body.comment : undefined
120
127
  });
121
128
  });
129
+ app.post("/api/projects/harness/task-retrospective", async (request) => {
130
+ const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
131
+ const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
132
+ return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
133
+ taskSlug: task.taskSlug,
134
+ taskRepoRoot: task.worktreePath,
135
+ handoffDir: task.handoffDir,
136
+ trigger
137
+ });
138
+ });
122
139
  }
123
140
  function degradedHarnessStatus(error) {
124
141
  return {
@@ -1,4 +1,4 @@
1
- import { CORE_VCM_ROLE_NAMES, DISPATCHABLE_ROLES } from "../../shared/constants.js";
1
+ import { DISPATCHABLE_ROLES, VCM_ROLE_NAMES } from "../../shared/constants.js";
2
2
  import { isOpenFileLimitError, VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
4
  export function registerTaskRoutes(app, deps) {
@@ -41,7 +41,7 @@ export function registerTaskRoutes(app, deps) {
41
41
  async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
42
42
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
43
43
  for (const session of sessions) {
44
- if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
44
+ if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
45
45
  await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
46
46
  }
47
47
  }
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ export function readVcmPackageVersion(appRoot) {
4
+ const packageJsonPath = path.join(appRoot, "package.json");
5
+ try {
6
+ const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
7
+ return typeof raw.version === "string" && raw.version.trim() ? raw.version : "unknown";
8
+ }
9
+ catch {
10
+ return process.env.npm_package_version || "unknown";
11
+ }
12
+ }
@@ -18,9 +18,10 @@ import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-ha
18
18
  import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
19
19
  import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
20
20
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
21
- const HARNESS_VERSION = "0.3.0-fixed";
21
+ import { readVcmPackageVersion } from "../app-version.js";
22
22
  const CLI_DIR = path.dirname(fileURLToPath(import.meta.url));
23
23
  const APP_ROOT = path.resolve(CLI_DIR, "../../..");
24
+ const VCM_PACKAGE_VERSION = readVcmPackageVersion(APP_ROOT);
24
25
  const MANIFEST_PATH = ".ai/vcm-harness-manifest.json";
25
26
  const HTML_BLOCK_PATTERN = /<!-- VCM:BEGIN(?:\s+version=\d+)? -->[\s\S]*?<!-- VCM:END -->/m;
26
27
  const HASH_BLOCK_PATTERN = /# VCM:BEGIN(?:\s+version=\d+)?\n[\s\S]*?# VCM:END/m;
@@ -331,7 +332,7 @@ async function buildManifest(projectRoot) {
331
332
  return {
332
333
  schemaVersion: 1,
333
334
  manager: "vcm",
334
- harnessVersion: HARNESS_VERSION,
335
+ harnessVersion: VCM_PACKAGE_VERSION,
335
336
  installMode: "fixed",
336
337
  installedAt: typeof current?.installedAt === "string" ? current.installedAt : now,
337
338
  updatedAt: now,
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { CORE_VCM_ROLE_DEFINITIONS, CORE_VCM_ROLE_NAMES, GATE_REVIEWER_ROLE_DEFINITION } from "../../shared/constants.js";
2
+ import { CORE_VCM_ROLE_DEFINITIONS, GATE_REVIEWER_ROLE_DEFINITION, VCM_ROLE_NAMES } from "../../shared/constants.js";
3
3
  import { VcmError } from "../errors.js";
4
4
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
5
5
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -425,9 +425,6 @@ export function createGatewayService(deps) {
425
425
  };
426
426
  const existing = await deps.sessionService.getRoleSession(project.repoRoot, task.taskSlug, definition.name);
427
427
  if (existing?.status === "running") {
428
- if (definition.name === "gate-reviewer") {
429
- await deps.sessionService.resumeRoleSession(project.repoRoot, task.taskSlug, definition.name, sessionInput);
430
- }
431
428
  startedRoles.push(definition.name);
432
429
  continue;
433
430
  }
@@ -556,7 +553,7 @@ export function createGatewayService(deps) {
556
553
  async function stopRunningRoleSessions(repoRoot, taskSlug) {
557
554
  const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
558
555
  for (const session of sessions) {
559
- if (session.status === "running" && CORE_VCM_ROLE_NAMES.some((role) => role === session.role)) {
556
+ if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
560
557
  await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
561
558
  }
562
559
  }
@@ -46,6 +46,7 @@ import { registerTaskRoutes } from "./api/task-routes.js";
46
46
  import { registerTranslationRoutes } from "./api/translation-routes.js";
47
47
  import { registerTerminalWs } from "./ws/terminal-ws.js";
48
48
  import { toVcmError } from "./errors.js";
49
+ import { readVcmPackageVersion } from "./app-version.js";
49
50
  export async function createServer(deps, options = {}) {
50
51
  const app = Fastify({
51
52
  logger: false
@@ -161,6 +162,8 @@ export async function startServer(options = {}) {
161
162
  }
162
163
  export function createDefaultServerDeps(options = {}) {
163
164
  const fs = createNodeFileSystemAdapter();
165
+ const appRoot = getAppRoot();
166
+ const vcmVersion = readVcmPackageVersion(appRoot);
164
167
  const runner = createCommandRunner();
165
168
  const git = createGitAdapter(runner);
166
169
  const claude = createClaudeAdapter(runner);
@@ -185,7 +188,8 @@ export function createDefaultServerDeps(options = {}) {
185
188
  git,
186
189
  runtime,
187
190
  harnessEngineerSessions: sessionService,
188
- runFixedInstaller: createScriptFixedHarnessInstaller(path.join(getAppRoot(), "scripts/install-vcm-harness.mjs"))
191
+ runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
192
+ vcmVersion
189
193
  });
190
194
  const harnessFeedbackService = createHarnessFeedbackService({
191
195
  fs,
@@ -277,7 +281,7 @@ export function createDefaultServerDeps(options = {}) {
277
281
  translationWorkerService
278
282
  });
279
283
  const diagnosticsService = createDiagnosticsService({
280
- appRoot: getAppRoot(),
284
+ appRoot,
281
285
  runtime,
282
286
  gatewayService,
283
287
  translationService
@@ -230,7 +230,9 @@ function normalizePreferences(input) {
230
230
  return {
231
231
  themeMode: normalizeThemeMode(candidate.themeMode),
232
232
  flowPauseAlerts: rawFlowPauseAlerts !== false,
233
+ roleRetryEnabled: candidate.roleRetryEnabled !== false,
233
234
  permissionRequestMode: normalizePermissionRequestMode(candidate.permissionRequestMode),
235
+ autoTaskHarnessReviewEnabled: candidate.autoTaskHarnessReviewEnabled === true,
234
236
  translationEnabled: candidate.translationEnabled === true,
235
237
  translationAutoSendEnabled: candidate.translationAutoSendEnabled === true,
236
238
  translationTargetLanguage: normalizeTranslationTargetLanguage(candidate.translationTargetLanguage),
@@ -2,9 +2,22 @@ import { isGateReviewerRoleName, isHarnessEngineerToolRoleName, isTranslatorTool
2
2
  import { VcmError } from "../errors.js";
3
3
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
4
4
  import { getTaskRuntimeRepoRoot } from "./task-service.js";
5
- const MAX_STOP_FAILURE_RECOVERY_ATTEMPTS = 2;
5
+ const MAX_ROLE_RETRY_ATTEMPTS = 20;
6
+ const ROLE_RETRY_BASE_DELAY_MS = 60_000;
7
+ const NON_RETRYABLE_STOP_FAILURE_ERRORS = new Set([
8
+ "authentication_failed",
9
+ "oauth_org_not_allowed",
10
+ "billing_error",
11
+ "invalid_request",
12
+ "model_not_found",
13
+ "max_output_tokens"
14
+ ]);
15
+ const DIAGNOSTIC_SNIPPET_MAX_LENGTH = 2000;
6
16
  export function createClaudeHookService(deps) {
7
- const stopFailureRecoveryAttempts = new Map();
17
+ const stopFailureRetryTimers = new Map();
18
+ const now = deps.now ?? (() => new Date().toISOString());
19
+ const retrySetTimeout = deps.retrySetTimeout ?? ((callback, delayMs) => globalThis.setTimeout(callback, delayMs));
20
+ const retryClearTimeout = deps.retryClearTimeout ?? ((timer) => globalThis.clearTimeout(timer));
8
21
  async function getHookContext(input) {
9
22
  if (!isVcmRoleName(input.role)) {
10
23
  throw new VcmError({
@@ -99,19 +112,8 @@ export function createClaudeHookService(deps) {
99
112
  };
100
113
  }
101
114
  async function resolveHookTaskSlug(repoRoot, input) {
102
- if (!isGateReviewerRoleName(input.role)) {
103
- return input.taskSlug;
104
- }
105
- const session = await deps.sessionService.getProjectGateReviewerSession(repoRoot);
106
- if (session?.activeTaskSlug) {
107
- return session.activeTaskSlug;
108
- }
109
- throw new VcmError({
110
- code: "GATE_REVIEWER_TASK_UNBOUND",
111
- message: "Gate Reviewer hook arrived without an active task binding.",
112
- statusCode: 409,
113
- hint: "Start or resume Gate Reviewer from the current task before submitting work."
114
- });
115
+ void repoRoot;
116
+ return input.taskSlug;
115
117
  }
116
118
  async function handleUserPromptSubmitHook(input) {
117
119
  const eventName = parseHookEvent(input.event.hook_event_name);
@@ -177,7 +179,7 @@ export function createClaudeHookService(deps) {
177
179
  throwUnsupportedEvent(eventName);
178
180
  }
179
181
  const context = await getHookContext(input);
180
- clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
182
+ await clearStopFailureRecoveryState(context, input.role);
181
183
  if (options.allowBlock && deps.jobGuard) {
182
184
  const verdict = await deps.jobGuard.evaluateStop({
183
185
  repoRoot: context.project.repoRoot,
@@ -215,15 +217,24 @@ export function createClaudeHookService(deps) {
215
217
  const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
216
218
  const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
217
219
  if (hasCompletionEvidence) {
218
- clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, input.role);
220
+ await clearStopFailureRecoveryState(context, input.role);
219
221
  return recordTurnEnd(input, context, eventName, {
220
222
  dispatchRouteFiles: !isGateReviewerRoleName(input.role),
221
223
  notifyGateway: false,
222
224
  settleGuard: true
223
225
  });
224
226
  }
225
- const recovered = await dispatchStopFailureRecovery(input, context);
226
- if (recovered) {
227
+ const failure = parseStopFailureDiagnostic(input.event);
228
+ if (!failure.retryable) {
229
+ await markStopFailureRecoveryFailed(input, context, failure, 0);
230
+ return recordTurnEnd(input, context, eventName, {
231
+ dispatchRouteFiles: false,
232
+ notifyGateway: false,
233
+ settleGuard: false
234
+ });
235
+ }
236
+ const retryScheduled = await scheduleStopFailureRetry(input, context, failure);
237
+ if (retryScheduled) {
227
238
  return {
228
239
  ok: true,
229
240
  eventName,
@@ -336,38 +347,147 @@ export function createClaudeHookService(deps) {
336
347
  ...(stoppedRole ? { stoppedRole } : {})
337
348
  };
338
349
  }
339
- async function dispatchStopFailureRecovery(input, context) {
340
- if (!deps.runtime) {
350
+ async function scheduleStopFailureRetry(input, context, failure) {
351
+ const preferences = await deps.appSettings.getPreferences();
352
+ if (!preferences.roleRetryEnabled || !deps.runtime) {
341
353
  return false;
342
354
  }
343
- const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
344
- const attempt = (stopFailureRecoveryAttempts.get(key) ?? 0) + 1;
345
- if (attempt > MAX_STOP_FAILURE_RECOVERY_ATTEMPTS) {
355
+ const stateInput = createRoundStateInput(context);
356
+ const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
357
+ const previousAttempt = currentRoundState.roleRecovery?.role === input.role &&
358
+ currentRoundState.roleRecovery.status !== "failed"
359
+ ? currentRoundState.roleRecovery.attempt
360
+ : 0;
361
+ const attempt = previousAttempt + 1;
362
+ const timestamp = now();
363
+ if (attempt > MAX_ROLE_RETRY_ATTEMPTS) {
364
+ clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
365
+ await markStopFailureRecoveryFailed(input, context, failure, MAX_ROLE_RETRY_ATTEMPTS, timestamp);
346
366
  return false;
347
367
  }
368
+ const nextRetryAt = new Date(Date.parse(timestamp) + attempt * ROLE_RETRY_BASE_DELAY_MS).toISOString();
369
+ await deps.roundService.setRoleRecovery({
370
+ ...stateInput,
371
+ recovery: {
372
+ role: input.role,
373
+ status: "waiting",
374
+ attempt,
375
+ maxAttempts: MAX_ROLE_RETRY_ATTEMPTS,
376
+ lastFailureAt: timestamp,
377
+ error: failure.error,
378
+ errorDetails: failure.errorDetails,
379
+ lastAssistantMessage: failure.lastAssistantMessage,
380
+ retryable: failure.retryable,
381
+ nextRetryAt
382
+ }
383
+ });
384
+ await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
385
+ scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt);
386
+ return true;
387
+ }
388
+ function scheduleStopFailureRetryTimer(input, context, attempt, nextRetryAt) {
389
+ const key = stopFailureRecoveryKey(context.project.repoRoot, context.taskSlug, input.role);
390
+ clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
391
+ const delayMs = Math.max(0, Date.parse(nextRetryAt) - Date.parse(now()));
392
+ const timer = retrySetTimeout(() => {
393
+ stopFailureRetryTimers.delete(key);
394
+ void runScheduledStopFailureRetry(input, context, attempt).catch(() => undefined);
395
+ }, delayMs);
396
+ stopFailureRetryTimers.set(key, timer);
397
+ }
398
+ async function runScheduledStopFailureRetry(input, context, attempt) {
399
+ const stateInput = createRoundStateInput(context);
400
+ const currentRoundState = await deps.roundService.getSessionRoundState(stateInput);
401
+ const recovery = currentRoundState.roleRecovery;
402
+ if (!recovery || recovery.role !== input.role || recovery.attempt !== attempt || recovery.status !== "waiting") {
403
+ return;
404
+ }
405
+ const timestamp = now();
348
406
  const session = await deps.sessionService.getRoleSession(context.project.repoRoot, context.taskSlug, input.role);
349
- if (!session || session.status !== "running") {
350
- return false;
407
+ if (!session || session.status !== "running" || !deps.runtime) {
408
+ await deps.roundService.setRoleRecovery({
409
+ ...stateInput,
410
+ recovery: {
411
+ ...recovery,
412
+ status: "failed",
413
+ failedAt: timestamp
414
+ }
415
+ });
416
+ await recordTurnEnd(input, context, "StopFailure", {
417
+ dispatchRouteFiles: false,
418
+ notifyGateway: false,
419
+ settleGuard: false
420
+ });
421
+ return;
351
422
  }
352
- stopFailureRecoveryAttempts.set(key, attempt);
353
- await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt(attempt));
423
+ await deps.roundService.setRoleRecovery({
424
+ ...stateInput,
425
+ recovery: {
426
+ ...recovery,
427
+ status: "retrying",
428
+ nextRetryAt: undefined,
429
+ lastRetryAt: timestamp
430
+ }
431
+ });
432
+ await submitTerminalInput(deps.runtime, session.id, renderStopFailureRecoveryPrompt());
354
433
  await deps.sessionService.markRoleActivityRunning(context.project.repoRoot, context.taskSlug, input.role);
355
- return true;
434
+ }
435
+ async function markStopFailureRecoveryFailed(input, context, failure, attempt, timestamp = now()) {
436
+ clearStopFailureRetryTimer(context.project.repoRoot, context.taskSlug, input.role);
437
+ await deps.roundService.setRoleRecovery({
438
+ ...createRoundStateInput(context),
439
+ recovery: {
440
+ role: input.role,
441
+ status: "failed",
442
+ attempt,
443
+ maxAttempts: MAX_ROLE_RETRY_ATTEMPTS,
444
+ lastFailureAt: timestamp,
445
+ error: failure.error,
446
+ errorDetails: failure.errorDetails,
447
+ lastAssistantMessage: failure.lastAssistantMessage,
448
+ retryable: failure.retryable,
449
+ failedAt: timestamp
450
+ }
451
+ });
356
452
  }
357
453
  function clearStopFailureRecovery(repoRoot, taskSlug, role) {
358
- stopFailureRecoveryAttempts.delete(stopFailureRecoveryKey(repoRoot, taskSlug, role));
454
+ clearStopFailureRetryTimer(repoRoot, taskSlug, role);
455
+ }
456
+ async function clearStopFailureRecoveryState(context, role) {
457
+ clearStopFailureRecovery(context.project.repoRoot, context.taskSlug, role);
458
+ await deps.roundService.clearRoleRecovery?.({
459
+ ...createRoundStateInput(context),
460
+ role
461
+ });
462
+ }
463
+ function clearStopFailureRetryTimer(repoRoot, taskSlug, role) {
464
+ const key = stopFailureRecoveryKey(repoRoot, taskSlug, role);
465
+ const timer = stopFailureRetryTimers.get(key);
466
+ if (timer === undefined) {
467
+ return;
468
+ }
469
+ retryClearTimeout(timer);
470
+ stopFailureRetryTimers.delete(key);
359
471
  }
360
472
  function stopFailureRecoveryKey(repoRoot, taskSlug, role) {
361
473
  return `${repoRoot}:${taskSlug}:${role}`;
362
474
  }
363
- function renderStopFailureRecoveryPrompt(attempt) {
475
+ function createRoundStateInput(context) {
476
+ return {
477
+ repoRoot: context.project.repoRoot,
478
+ stateRepoRoot: context.taskRepoRoot,
479
+ stateRoot: context.config.stateRoot,
480
+ taskSlug: context.taskSlug
481
+ };
482
+ }
483
+ function renderStopFailureRecoveryPrompt() {
364
484
  return [
365
485
  "[VCM Recovery]",
366
- "Your previous turn ended unexpectedly after context compaction or an API error.",
367
- "Continue the same assigned work from the current repository and VCM handoff state.",
368
- "Do not repeat completed edits, duplicate validation, or duplicate route messages.",
369
- "If the assigned work is already complete, write/send the expected VCM handoff now.",
370
- `Recovery attempt: ${attempt}/${MAX_STOP_FAILURE_RECOVERY_ATTEMPTS}.`
486
+ "Previous turn ended unexpectedly. Continue from current repo + VCM handoff state.",
487
+ "",
488
+ "Check whether your assigned work is already complete.",
489
+ "If complete, write the expected VCM completion artifact now.",
490
+ "Do not repeat completed edits, validation, or route messages."
371
491
  ].join("\n");
372
492
  }
373
493
  async function handlePermissionRequestHook(input) {
@@ -470,6 +590,38 @@ function throwUnsupportedEvent(eventName) {
470
590
  hint: "Use the matching VCM hook endpoint for this event."
471
591
  });
472
592
  }
593
+ function parseStopFailureDiagnostic(event) {
594
+ const error = (normalizeDiagnosticString(event.error, 200) ?? "unknown").toLowerCase();
595
+ const errorDetails = normalizeDiagnosticString(event.error_details, DIAGNOSTIC_SNIPPET_MAX_LENGTH);
596
+ const lastAssistantMessage = normalizeDiagnosticString(event.last_assistant_message, DIAGNOSTIC_SNIPPET_MAX_LENGTH);
597
+ return {
598
+ error,
599
+ errorDetails,
600
+ lastAssistantMessage,
601
+ retryable: !NON_RETRYABLE_STOP_FAILURE_ERRORS.has(error)
602
+ };
603
+ }
604
+ function normalizeDiagnosticString(value, maxLength) {
605
+ let text;
606
+ if (typeof value === "string") {
607
+ text = value;
608
+ }
609
+ else if (value !== undefined && value !== null) {
610
+ try {
611
+ text = JSON.stringify(value);
612
+ }
613
+ catch {
614
+ text = String(value);
615
+ }
616
+ }
617
+ const trimmed = text?.trim();
618
+ if (!trimmed) {
619
+ return undefined;
620
+ }
621
+ return trimmed.length > maxLength
622
+ ? `${trimmed.slice(0, maxLength)}...`
623
+ : trimmed;
624
+ }
473
625
  function stringOrUndefined(value) {
474
626
  return typeof value === "string" ? value : undefined;
475
627
  }
@@ -1,8 +1,7 @@
1
- import { readFileSync } from "node:fs";
2
1
  import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { readVcmPackageVersion } from "../app-version.js";
4
3
  export function createDiagnosticsService(deps) {
5
- const version = readPackageVersion(deps.appRoot);
4
+ const version = readVcmPackageVersion(deps.appRoot);
6
5
  return {
7
6
  async getRuntimeDiagnostics() {
8
7
  const runtimeSessions = deps.runtime.listSessions();
@@ -34,16 +33,6 @@ export function createDiagnosticsService(deps) {
34
33
  }
35
34
  };
36
35
  }
37
- function readPackageVersion(appRoot) {
38
- const packageJsonPath = path.join(appRoot, "package.json");
39
- try {
40
- const raw = JSON.parse(readFileSync(packageJsonPath, "utf8"));
41
- return typeof raw.version === "string" && raw.version.trim() ? raw.version : "unknown";
42
- }
43
- catch {
44
- return process.env.npm_package_version || "unknown";
45
- }
46
- }
47
36
  async function readFdCount() {
48
37
  try {
49
38
  return (await fs.readdir("/proc/self/fd")).length;