vibe-coding-master 0.7.43 → 0.7.44

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 (42) hide show
  1. package/README.md +40 -13
  2. package/dist/backend/api/artifact-routes.js +3 -0
  3. package/dist/backend/api/harness-routes.js +16 -0
  4. package/dist/backend/api/task-routes.js +1 -0
  5. package/dist/backend/api/workflow-control-routes.js +12 -0
  6. package/dist/backend/cli/install-vcm-harness.js +21 -0
  7. package/dist/backend/server.js +3 -1
  8. package/dist/backend/services/artifact-service.js +7 -30
  9. package/dist/backend/services/auto-memory-service.js +640 -12
  10. package/dist/backend/services/claude-hook-service.js +80 -2
  11. package/dist/backend/services/gate-review-service.js +173 -74
  12. package/dist/backend/services/harness-feedback-service.js +76 -78
  13. package/dist/backend/services/harness-service.js +27 -3
  14. package/dist/backend/services/runtime-coordinator-service.js +2 -1
  15. package/dist/backend/services/status-service.js +1 -0
  16. package/dist/backend/services/translation-worker-service.js +19 -4
  17. package/dist/backend/services/workflow-control-service.js +96 -12
  18. package/dist/backend/templates/handoff.js +44 -2
  19. package/dist/backend/templates/harness/architect-agent.js +13 -7
  20. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
  21. package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
  22. package/dist/backend/templates/harness/claude-root.js +3 -2
  23. package/dist/backend/templates/harness/coder-agent.js +8 -0
  24. package/dist/backend/templates/harness/gate-review.js +144 -49
  25. package/dist/backend/templates/harness/harness-engineer-agent.js +25 -10
  26. package/dist/backend/templates/harness/project-manager-agent.js +16 -10
  27. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
  28. package/dist/backend/templates/harness/tester-agent.js +13 -0
  29. package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
  30. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -5
  31. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
  32. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +1 -1
  33. package/dist/shared/types/workflow.js +1 -0
  34. package/dist/shared/validation/artifact-check.js +3 -3
  35. package/dist/shared/validation/artifact-contract.js +1 -1
  36. package/dist/shared/validation/artifact-registry.js +16 -0
  37. package/dist-frontend/assets/{index-VW9tYPP5.js → index-BvCmrFlN.js} +49 -49
  38. package/dist-frontend/index.html +1 -1
  39. package/package.json +1 -1
  40. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
  41. package/scripts/harness-tools/vcm-artifact +1 -2
  42. package/scripts/harness-tools/vcm-bash-guard +1 -1
@@ -4,7 +4,6 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
4
4
  import { resolveRepoPath } from "../adapters/filesystem.js";
5
5
  import { VcmError } from "../errors.js";
6
6
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
7
- import { getRetrospectiveReportErrors } from "./artifact-service.js";
8
7
  const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
9
8
  const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
10
9
  const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
@@ -143,7 +142,7 @@ export function createHarnessFeedbackService(deps) {
143
142
  ]
144
143
  : ["Report is empty."];
145
144
  const reportReady = reportErrors.length === 0;
146
- if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
145
+ if (!reportReady || (marker.memoryRunId && input.memoryReviewStatus === "failed")) {
147
146
  await persistTaskRetrospectiveMarker(repoRoot, {
148
147
  ...marker,
149
148
  status: "failed",
@@ -155,6 +154,41 @@ export function createHarnessFeedbackService(deps) {
155
154
  });
156
155
  return true;
157
156
  }
157
+ if (marker.memoryRunId && input.memoryReviewStatus === "documenting") {
158
+ await persistTaskRetrospectiveMarker(repoRoot, {
159
+ ...marker,
160
+ status: "waiting-docs",
161
+ updatedAt: timestamp
162
+ });
163
+ return true;
164
+ }
165
+ await completeTaskRetrospective(repoRoot, marker);
166
+ return true;
167
+ }
168
+ async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
169
+ const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
170
+ if (!marker || marker.status !== "waiting-docs") {
171
+ return false;
172
+ }
173
+ if (memoryStatus === "documenting") {
174
+ return true;
175
+ }
176
+ if (memoryStatus === "failed") {
177
+ const timestamp = now();
178
+ await persistTaskRetrospectiveMarker(repoRoot, {
179
+ ...marker,
180
+ status: "failed",
181
+ failedAt: timestamp,
182
+ updatedAt: timestamp,
183
+ error: "Task Harness Retrospective durable-document assignment failed."
184
+ });
185
+ return true;
186
+ }
187
+ await completeTaskRetrospective(repoRoot, marker);
188
+ return true;
189
+ }
190
+ async function completeTaskRetrospective(repoRoot, marker) {
191
+ const timestamp = now();
158
192
  try {
159
193
  await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
160
194
  }
@@ -166,7 +200,7 @@ export function createHarnessFeedbackService(deps) {
166
200
  updatedAt: timestamp,
167
201
  error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
168
202
  });
169
- return true;
203
+ return;
170
204
  }
171
205
  await persistTaskRetrospectiveMarker(repoRoot, {
172
206
  ...marker,
@@ -174,7 +208,6 @@ export function createHarnessFeedbackService(deps) {
174
208
  completedAt: timestamp,
175
209
  updatedAt: timestamp
176
210
  });
177
- return true;
178
211
  }
179
212
  async function assertHarnessEngineerAvailable(_repoRoot) {
180
213
  return undefined;
@@ -285,6 +318,16 @@ export function createHarnessFeedbackService(deps) {
285
318
  `Current memory snapshot: ${memoryReview.currentMemoryPath}`,
286
319
  "Active memory files:",
287
320
  ...memoryReview.activeMemoryPaths.map((memoryPath) => `- ${memoryPath}`),
321
+ "Proposal candidates:",
322
+ ...(memoryReview.proposalCandidates.length > 0
323
+ ? memoryReview.proposalCandidates.map((candidate) => [
324
+ candidate.id,
325
+ `source=${candidate.source}`,
326
+ `operation=${candidate.operation}`,
327
+ `target=${candidate.target}`,
328
+ `entry=${candidate.content ?? candidate.existing ?? "none"}`
329
+ ].join(" | "))
330
+ : ["none"]),
288
331
  ...(memoryReview.planningCandidatePath
289
332
  ? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
290
333
  : []),
@@ -292,88 +335,30 @@ export function createHarnessFeedbackService(deps) {
292
335
  "Review every memory candidate against final task evidence while performing this retrospective.",
293
336
  "The snapshot files contain only the matching pre-review <VCM-memory> block content.",
294
337
  "Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
295
- "For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
296
- "Complete this full existing-memory review even when every proposal says no-change.",
297
- "Then evaluate every proposal item independently. Do not accept or reject a whole role draft as one decision.",
298
- "For every Add or Update candidate, independently explain why the memory is necessary, what fails if it is absent, and why memory or a durable document is the correct destination.",
299
- "Do not copy the proposer rationale as the review. Verify it against current code, durable documentation, and final task evidence.",
300
- "Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
301
- "Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
302
- "Final content must be one exact line written to the selected active <VCM-memory> block. Use none when the decision does not keep memory.",
303
- "For keep-in-memory use Durable doc disposition: memory. For keep-memory-reference use memory-reference. For move-to-durable-doc use durable-doc.",
304
- "Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
305
- "Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
306
- "Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
307
- "Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
308
- "Apply the reviewed result directly to the <VCM-memory> blocks in the listed active memory files. Do not change any content outside those blocks.",
309
- "If memory changes, commit only the changed active memory files before ending the turn. Use commit message: chore: update VCM memory. If memory is unchanged, do not create a commit.",
310
- "Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
338
+ "Evaluate each proposal independently. Keep only verified, durable, reusable project knowledge; do not keep task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
339
+ "For every existing entry and proposal, record why the decision is necessary, the impact if the knowledge is absent, the evidence checked, and whether a durable document is the correct source.",
340
+ "When the decision is move-to-durable-doc, remove the entry from memory now and add one durableDocAssignment. Do not wait for the durable document update before removing memory.",
341
+ "Apply the reviewed result directly to the listed <VCM-memory> blocks. Do not change content outside those blocks.",
342
+ "If memory changes, commit only the changed active memory files with message [VCM Harness] Update VCM memory. If memory is unchanged, do not create a commit.",
343
+ `Write the complete machine-readable review to: ${memoryReview.reviewResultPath}`,
344
+ "The JSON root must be: {\"version\":1,\"runId\":\"<assigned run id>\",\"memoryCommit\":\"<full commit or none>\",\"decisions\":[],\"durableDocAssignments\":[]}.",
345
+ "Each decision must contain itemId, source (existing|proposal), target, entry, decision, reason, impactIfAbsent, evidence (non-empty array), finalContent, and durableDocPath.",
346
+ "Existing decisions use retain|update|remove|move-to-durable-doc. Add or Update proposal decisions use keep-in-memory|keep-memory-reference|move-to-durable-doc|reject; Remove proposal decisions use remove|retain. Proposal itemId must equal the assigned candidate ID.",
347
+ "Each durableDocAssignment must contain sourceMemoryPath, sourceEntry, targetPath, content, reason, and evidence (non-empty array). Use a project-relative Markdown target outside .ai/vcm.",
348
+ "In the retrospective report, include only this memory summary:",
311
349
  "",
312
350
  "## Memory Review",
313
- "Existing memory reviewed: complete",
314
- "",
315
- "### Proposal Decisions",
316
- ...(memoryReview.proposalCandidates.length > 0
317
- ? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
318
- : ["none"]),
319
- "",
320
- "### Existing Memory Decisions",
321
- "#### Item 1",
322
- "Target: shared",
323
- "Existing: <exact existing memory entry>",
324
- "Decision: retain",
325
- "Reason: <why this decision is correct>",
326
- "Impact if removed: <specific future role or task failure>",
327
- "Durable doc disposition: memory",
328
- "Durable doc path: none",
329
- "Evidence: <current code, durable documentation, or final task evidence>",
330
- "",
331
- "### Existing Memory Changes",
332
- "- retained: <summary or none>",
333
- "- updated: <summary or none>",
334
- "- removed: <summary or none>",
335
- "",
336
- "Reviewed memory set: complete"
351
+ "Memory commit: <full commit or none>",
352
+ `Review result: ${memoryReview.reviewResultPath}`,
353
+ "Durable document assignments: <count>"
337
354
  ]
338
355
  : []),
339
356
  "",
340
357
  `Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
341
- `Submit with: .ai/tools/vcm-artifact retrospective-report --file <candidate> --path ${analysisPath} --mode final`,
342
- "End your turn after VCM accepts the result."
358
+ "Write the report directly to that path. Do not use vcm-artifact.",
359
+ "End your turn after the report is complete."
343
360
  ].join("\n");
344
361
  }
345
- function renderMemoryProposalDecisionTemplate(candidate) {
346
- if (candidate.operation === "remove") {
347
- return [
348
- `#### Candidate ${candidate.id}`,
349
- `Source: ${candidate.source}`,
350
- "Operation: remove",
351
- `Target: ${candidate.target}`,
352
- `Existing: ${candidate.existing}`,
353
- "Decision: remove|retain",
354
- "Reason: <why the proposed removal should be applied or rejected>",
355
- "Evidence checked: <current code, durable documentation, or final task evidence>",
356
- ""
357
- ];
358
- }
359
- return [
360
- `#### Candidate ${candidate.id}`,
361
- `Source: ${candidate.source}`,
362
- `Operation: ${candidate.operation}`,
363
- `Target: ${candidate.target}`,
364
- `Candidate: ${candidate.content}`,
365
- "Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
366
- "Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
367
- "Why memory is necessary: <independent reason, or why it is not necessary>",
368
- "Impact if absent: <specific impact, or why no durable impact exists>",
369
- "Durable doc disposition: memory|durable-doc|memory-reference",
370
- "Durable doc analysis: <why this destination is correct>",
371
- "Durable doc path: <none or a project-relative durable doc path>",
372
- "Evidence checked: <current code, durable documentation, or final task evidence>",
373
- "Final content: <exact one-line reviewed-memory content or none>",
374
- ""
375
- ];
376
- }
377
362
  function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
378
363
  return [
379
364
  "[VCM Harness Feedback]",
@@ -431,6 +416,7 @@ export function createHarnessFeedbackService(deps) {
431
416
  sendPendingFeedback,
432
417
  startTaskRetrospective,
433
418
  handleTaskRetrospectiveHook,
419
+ completeWaitingTaskRetrospective,
434
420
  assertHarnessEngineerAvailable
435
421
  };
436
422
  }
@@ -495,6 +481,18 @@ function assertPendingFeedbackPath(feedbackPath) {
495
481
  throw new Error(`Invalid assigned Harness Feedback path: ${feedbackPath}.`);
496
482
  }
497
483
  }
484
+ function getRetrospectiveReportErrors(content) {
485
+ const errors = [];
486
+ if (!/^# Task Harness Retrospective(?::\s*.+)?\s*$/m.test(content)) {
487
+ errors.push("Retrospective report requires '# Task Harness Retrospective: <task>'.");
488
+ }
489
+ for (const heading of ["Findings", "Feedback Dispositions", "Recommended Harness Changes", "VCM Issue Drafts"]) {
490
+ if (!new RegExp(`^## ${heading}\\s*$`, "m").test(content)) {
491
+ errors.push(`Missing required section: ${heading}.`);
492
+ }
493
+ }
494
+ return errors;
495
+ }
498
496
  function parseSimpleMetadata(content) {
499
497
  const result = {};
500
498
  for (const line of content.split(/\r?\n/).slice(0, 80)) {
@@ -25,10 +25,12 @@ import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/v
25
25
  import { renderVcmProposeMemorySkillRules } from "../templates/harness/vcm-propose-memory-skill.js";
26
26
  import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
27
27
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
28
+ import { renderAskUserTool, renderVcmAskUserSkillRules } from "../templates/harness/vcm-ask-user-skill.js";
28
29
  import { renderUpdateTaskStateTool, renderVcmTaskStateSkillRules } from "../templates/harness/vcm-task-state-skill.js";
29
30
  import { renderVcmWorkflowReviewSkillRules } from "../templates/harness/vcm-workflow-review-skill.js";
30
31
  import { renderCheckScaffoldLedgerTool } from "../templates/harness/check-scaffold-ledger.js";
31
32
  import { renderRequestArchitectRestartTool, renderRestartArchitectSkillRules } from "../templates/harness/restart-architect-skill.js";
33
+ import { renderResolveDurableDocAssignmentTool } from "../templates/harness/resolve-durable-doc-assignment.js";
32
34
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
33
35
  import { VcmError } from "../errors.js";
34
36
  import { bumpHarnessRevision, readHarnessRevisionState } from "./harness-revision.js";
@@ -138,6 +140,14 @@ const HARNESS_FILES = [
138
140
  ownership: "whole-file",
139
141
  renderRules: renderVcmRouteMessageSkillRules
140
142
  },
143
+ {
144
+ kind: "skill-vcm-ask-user",
145
+ path: ".claude/skills/vcm-ask-user/SKILL.md",
146
+ title: "VCM Ask User Skill",
147
+ frontmatter: renderSkillFrontmatter("vcm-ask-user", "Use whenever project-manager asks the user a question and must pause the workflow."),
148
+ ownership: "whole-file",
149
+ renderRules: renderVcmAskUserSkillRules
150
+ },
141
151
  {
142
152
  kind: "skill-vcm-task-state",
143
153
  path: ".claude/skills/vcm-task-state/SKILL.md",
@@ -257,6 +267,13 @@ const HARNESS_FILES = [
257
267
  ownership: "raw-file",
258
268
  renderRules: renderRequestGateReviewTool
259
269
  },
270
+ {
271
+ kind: "tool-vcm-ask-user",
272
+ path: ".ai/tools/vcm-ask-user",
273
+ title: "VCM Ask User Tool",
274
+ ownership: "raw-file",
275
+ renderRules: renderAskUserTool
276
+ },
260
277
  {
261
278
  kind: "tool-update-task-state",
262
279
  path: ".ai/tools/update-task-state",
@@ -278,6 +295,13 @@ const HARNESS_FILES = [
278
295
  ownership: "raw-file",
279
296
  renderRules: renderRequestArchitectRestartTool
280
297
  },
298
+ {
299
+ kind: "tool-resolve-durable-doc-assignment",
300
+ path: ".ai/tools/resolve-durable-doc-assignment",
301
+ title: "Resolve Durable Documentation Assignment Tool",
302
+ ownership: "raw-file",
303
+ renderRules: renderResolveDurableDocAssignmentTool
304
+ },
281
305
  {
282
306
  kind: "agent-project-manager",
283
307
  path: ".claude/agents/project-manager.md",
@@ -360,7 +384,7 @@ export function createHarnessService(deps) {
360
384
  let harnessCommit;
361
385
  if (nextContent !== currentContent) {
362
386
  await bumpHarnessRevision(deps.fs, repoRoot, now());
363
- harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update harness file")).harnessCommit;
387
+ harnessCommit = (await commitHarnessVisibleChanges(deps.git, repoRoot, "[VCM Harness] Update harness file")).harnessCommit;
364
388
  }
365
389
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
366
390
  const [analyses, codeIntelligence] = await Promise.all([
@@ -384,7 +408,7 @@ export function createHarnessService(deps) {
384
408
  if (result.changedFiles.length > 0) {
385
409
  await bumpHarnessRevision(deps.fs, repoRoot, now());
386
410
  }
387
- const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update fixed harness");
411
+ const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "[VCM Harness] Update fixed harness");
388
412
  return {
389
413
  ...result,
390
414
  changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : result.changedFiles,
@@ -408,7 +432,7 @@ export function createHarnessService(deps) {
408
432
  if (changedFiles.length > 0) {
409
433
  await bumpHarnessRevision(deps.fs, repoRoot, now());
410
434
  }
411
- const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "chore(vcm-harness): update fixed harness");
435
+ const committed = await commitHarnessVisibleChanges(deps.git, repoRoot, "[VCM Harness] Update fixed harness");
412
436
  return {
413
437
  version: VCM_HARNESS_VERSION,
414
438
  changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : changedFiles,
@@ -66,7 +66,8 @@ export function createRuntimeCoordinatorService(deps) {
66
66
  else {
67
67
  await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
68
68
  }
69
- await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
69
+ const memoryState = await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
70
+ await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState.status);
70
71
  const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
71
72
  if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
72
73
  await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
@@ -59,6 +59,7 @@ function degradedArtifactSummary(handoffDir) {
59
59
  architectDebugPath: `${handoffDir}/architect-debug.md`,
60
60
  architectureDiagnosisPath: `${handoffDir}/architecture-diagnosis.md`,
61
61
  testReportPath: `${handoffDir}/test-report.md`,
62
+ docsUpdateReportPath: `${handoffDir}/docs-update-report.md`,
62
63
  docsSyncReportPath: `${handoffDir}/docs-sync-report.md`,
63
64
  workflowProgressPath: `${handoffDir}/workflow-progress.md`,
64
65
  finalAcceptancePath: `${handoffDir}/final-acceptance.md`
@@ -214,14 +214,23 @@ export function createTranslationWorkerService(deps) {
214
214
  }
215
215
  const session = await ensureTranslatorSession(repoRoot, next.targetLanguage, next.taskSlug);
216
216
  await submitTerminalInput(deps.runtime, session.id, batch?.prompt ?? await buildQueuePrompt(repoRoot, next));
217
+ const latestQueue = await loadQueue(repoRoot);
218
+ if (latestQueue.activeItemId !== next.id) {
219
+ return;
220
+ }
221
+ const dispatchedItems = latestQueue.items.filter((item) => item.status === "dispatching" &&
222
+ (batch ? item.batchId === batch.items[0]?.batchId : item.id === next.id));
223
+ if (dispatchedItems.length === 0) {
224
+ return;
225
+ }
217
226
  const dispatchedAt = now();
218
- for (const item of batch?.items ?? [next]) {
227
+ for (const item of dispatchedItems) {
219
228
  item.status = "running";
220
229
  item.updatedAt = dispatchedAt;
221
230
  }
222
- queue.updatedAt = dispatchedAt;
223
- await saveQueue(repoRoot, queue);
224
- await Promise.all((batch?.items ?? [next]).map((item) => syncJobStatus(repoRoot, item)));
231
+ latestQueue.updatedAt = dispatchedAt;
232
+ await saveQueue(repoRoot, latestQueue);
233
+ await Promise.all(dispatchedItems.map((item) => syncJobStatus(repoRoot, item)));
225
234
  }
226
235
  catch (error) {
227
236
  const failedItems = queue.activeItemId === next.id
@@ -438,6 +447,12 @@ export function createTranslationWorkerService(deps) {
438
447
  await validateActiveQueueItem(repoRoot);
439
448
  return true;
440
449
  }
450
+ // `dispatching` covers the interval between writing the prompt and the
451
+ // Translator's UserPromptSubmit hook. The role session is legitimately idle
452
+ // during that interval, so only an actual result can reconcile the item.
453
+ if (active.status === "dispatching") {
454
+ return false;
455
+ }
441
456
  if (await translatorSessionSettled(repoRoot, active.taskSlug)) {
442
457
  await validateActiveQueueItem(repoRoot);
443
458
  return true;
@@ -10,6 +10,15 @@ const HISTORY_SEPARATOR = "| --- | --- | --- | --- | --- | --- |";
10
10
  const TARGET_ROLES = new Set(["architect", "coder", "tester"]);
11
11
  const FINAL_GATE_STATUSES = new Set(["disabled", "not_required", "skipped", "overridden"]);
12
12
  const MISSING_EVIDENCE_HASH = "<missing>";
13
+ const DOCS_ONLY_ROLE_TRANSITIONS = [
14
+ "docs-only/architect",
15
+ "docs-only/coder",
16
+ "docs-only/tester"
17
+ ];
18
+ const DOCS_ONLY_EXIT_TRANSITIONS = [
19
+ "code-change/architect",
20
+ "validation-only/tester"
21
+ ];
13
22
  export function createWorkflowControlService(deps) {
14
23
  const now = deps.now ?? (() => new Date().toISOString());
15
24
  const id = deps.id ?? (() => `wfauth_${randomUUID()}`);
@@ -35,6 +44,7 @@ export function createWorkflowControlService(deps) {
35
44
  return withLock(statePath(input), async () => {
36
45
  const state = await getState(input);
37
46
  failOnStateWarnings(state);
47
+ failWhileAwaitingUser(state);
38
48
  if (state.pendingDispatch) {
39
49
  throw workflowError("WORKFLOW_DISPATCH_PENDING", `A ${state.pendingDispatch.targetRole} dispatch is already ${state.pendingDispatch.status}.`, "Complete or recover the existing dispatch before proposing another workflow transition.");
40
50
  }
@@ -112,6 +122,7 @@ export function createWorkflowControlService(deps) {
112
122
  async function assertRouteAuthorized(input) {
113
123
  const state = await getState(input);
114
124
  failOnStateWarnings(state);
125
+ failWhileAwaitingUser(state);
115
126
  const pending = state.pendingDispatch;
116
127
  if (!pending || pending.status !== "pending") {
117
128
  throw workflowError("WORKFLOW_ROUTE_NOT_APPROVED", "Project Manager has no pending workflow approval for this route.", "Submit a valid workflow-progress.md transition before the PM route message.");
@@ -165,6 +176,7 @@ export function createWorkflowControlService(deps) {
165
176
  await withLock(statePath(input), async () => {
166
177
  const state = await getState(input);
167
178
  failOnStateWarnings(state);
179
+ failWhileAwaitingUser(state);
168
180
  const pending = state.pendingDispatch;
169
181
  if (!pending || pending.status !== "dispatching" || pending.messageId !== messageId) {
170
182
  throw workflowError("WORKFLOW_DISPATCH_CONFIRMATION_MISMATCH", `Message ${messageId} does not own the pending workflow dispatch.`, "Do not advance Workflow Progress from an unrelated UserPromptSubmit event.");
@@ -207,12 +219,50 @@ export function createWorkflowControlService(deps) {
207
219
  }
208
220
  return {
209
221
  getState,
222
+ getProgress: (input) => readProgress(deps.fs, input),
223
+ requestUserInput,
224
+ resolveUserInput,
210
225
  submitProgress,
211
226
  assertRouteAuthorized,
212
227
  claimDispatch,
213
228
  releaseDispatch,
214
229
  confirmDispatch
215
230
  };
231
+ async function requestUserInput(input, question) {
232
+ return withLock(statePath(input), async () => {
233
+ const state = await getState(input);
234
+ const normalizedQuestion = question.trim();
235
+ if (!normalizedQuestion) {
236
+ throw workflowError("WORKFLOW_USER_QUESTION_REQUIRED", "A non-empty user question is required.");
237
+ }
238
+ const timestamp = now();
239
+ const next = {
240
+ ...state,
241
+ awaitingUser: {
242
+ question: normalizedQuestion,
243
+ requestedAt: timestamp
244
+ },
245
+ pendingDispatch: null,
246
+ updatedAt: timestamp
247
+ };
248
+ await saveState(input, next);
249
+ return next;
250
+ });
251
+ }
252
+ async function resolveUserInput(input) {
253
+ return withLock(statePath(input), async () => {
254
+ const state = await getState(input);
255
+ if (!state.awaitingUser)
256
+ return state;
257
+ const next = {
258
+ ...state,
259
+ awaitingUser: null,
260
+ updatedAt: now()
261
+ };
262
+ await saveState(input, next);
263
+ return next;
264
+ });
265
+ }
216
266
  async function saveState(input, state) {
217
267
  await deps.fs.writeJsonAtomic(statePath(input), state);
218
268
  }
@@ -351,11 +401,15 @@ async function getAllowedTransitions(fs, input, state, current) {
351
401
  const flow = current.flow;
352
402
  const flowRun = resolveFlowRun(state.flowRun, current);
353
403
  if (flow === "docs-only") {
354
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
355
- return evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash)
356
- && docs.complete && (docs.value === "synced" || docs.value === "unchanged")
357
- ? []
358
- : ["docs-only/architect", "code-change/architect", "validation-only/tester"];
404
+ const docs = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
405
+ const active = state.activeDispatch;
406
+ if (!active || active.flow !== "docs-only") {
407
+ return [...DOCS_ONLY_ROLE_TRANSITIONS, ...DOCS_ONLY_EXIT_TRANSITIONS];
408
+ }
409
+ const hasFreshResult = evidenceProducedAfterActiveDispatch(state, flow, active.targetRole, "docs-update-report.md", docs.hash);
410
+ return hasFreshResult && docs.complete
411
+ ? [...DOCS_ONLY_ROLE_TRANSITIONS, ...DOCS_ONLY_EXIT_TRANSITIONS]
412
+ : [`docs-only/${active.targetRole}`, ...DOCS_ONLY_EXIT_TRANSITIONS];
359
413
  }
360
414
  if (flow === "validation-only") {
361
415
  const test = await artifactState(fs, input, "test-report.md", "test-report");
@@ -387,7 +441,7 @@ function initialTransitions() {
387
441
  "code-change/architect",
388
442
  "architect-debug/architect",
389
443
  "architecture-diagnosis/architect",
390
- "docs-only/architect",
444
+ ...DOCS_ONLY_ROLE_TRANSITIONS,
391
445
  "validation-only/tester"
392
446
  ];
393
447
  }
@@ -554,8 +608,9 @@ async function artifactState(fs, input, fileName, kind) {
554
608
  value = readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase();
555
609
  if (kind === "test-report")
556
610
  value = inline("Test Result");
557
- if (kind === "docs-sync-report" || kind === "final-acceptance")
611
+ if (kind === "docs-update-report" || kind === "docs-sync-report" || kind === "final-acceptance") {
558
612
  value = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
613
+ }
559
614
  const infrastructure = kind === "test-report"
560
615
  ? /^Status:\s*(.+?)\s*$/mi.exec(readArtifactSectionContent(content, "Test Infrastructure") ?? "")?.[1]?.trim().toLowerCase()
561
616
  : undefined;
@@ -786,10 +841,14 @@ async function validateCompletion(fs, input, state, candidate) {
786
841
  return;
787
842
  }
788
843
  if (candidate.flow === "docs-only") {
789
- const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
790
- if (!evidenceProducedAfterActiveDispatch(state, candidate.flow, "architect", "docs-sync-report.md", docs.hash)
844
+ const docs = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
845
+ const activeRole = state.activeDispatch?.flow === "docs-only"
846
+ ? state.activeDispatch.targetRole
847
+ : undefined;
848
+ if (!activeRole
849
+ || !evidenceProducedAfterActiveDispatch(state, candidate.flow, activeRole, "docs-update-report.md", docs.hash)
791
850
  || !docs.complete || (docs.value !== "synced" && docs.value !== "unchanged")) {
792
- throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs-only completion requires a complete Docs Sync Report with Decision: synced or Decision: unchanged.");
851
+ throw workflowError("WORKFLOW_COMPLETION_INVALID", "Docs-only completion requires a fresh, complete Docs Update Report from the latest assigned role with Decision: synced or Decision: unchanged.");
793
852
  }
794
853
  return;
795
854
  }
@@ -911,12 +970,20 @@ function normalizeState(value, taskSlug, timestamp) {
911
970
  const flowRun = value.flowRun === undefined || value.flowRun === null
912
971
  ? null
913
972
  : isFlowRun(value.flowRun) ? value.flowRun : undefined;
914
- if (pendingDispatch === undefined || activeDispatch === undefined || flowRun === undefined || userAuthorizations === undefined) {
973
+ const awaitingUser = value.awaitingUser === undefined || value.awaitingUser === null
974
+ ? null
975
+ : isAwaitingUser(value.awaitingUser) ? value.awaitingUser : undefined;
976
+ if (pendingDispatch === undefined
977
+ || activeDispatch === undefined
978
+ || flowRun === undefined
979
+ || userAuthorizations === undefined
980
+ || awaitingUser === undefined) {
915
981
  return { ...emptyState(taskSlug, timestamp), warnings: ["Workflow control state has an unsupported shape."] };
916
982
  }
917
983
  return {
918
984
  version: 1,
919
985
  taskSlug,
986
+ awaitingUser,
920
987
  pendingDispatch,
921
988
  activeDispatch,
922
989
  flowRun,
@@ -929,6 +996,7 @@ function emptyState(taskSlug, timestamp) {
929
996
  return {
930
997
  version: 1,
931
998
  taskSlug,
999
+ awaitingUser: null,
932
1000
  pendingDispatch: null,
933
1001
  activeDispatch: null,
934
1002
  flowRun: null,
@@ -988,11 +1056,21 @@ function failOnStateWarnings(state) {
988
1056
  if (state.warnings.length > 0)
989
1057
  throw workflowError("WORKFLOW_STATE_INVALID", state.warnings.join(" "));
990
1058
  }
1059
+ function failWhileAwaitingUser(state) {
1060
+ if (!state.awaitingUser)
1061
+ return;
1062
+ throw workflowError("WORKFLOW_AWAITING_USER", "Project Manager is waiting for the user's answer and cannot advance the workflow.", "Wait for a new direct user message. The previous workflow approval was canceled; request a fresh approval after the answer arrives.");
1063
+ }
991
1064
  function progressValidationError(errors) {
992
1065
  return workflowError("WORKFLOW_PROGRESS_INVALID", `Workflow Progress validation failed:\n${errors.map((error) => `- ${error}`).join("\n")}`);
993
1066
  }
994
1067
  function workflowError(code, message, hint) {
995
- return new VcmError({ code, message, hint, statusCode: code.includes("PENDING") ? 409 : 422 });
1068
+ return new VcmError({
1069
+ code,
1070
+ message,
1071
+ hint,
1072
+ statusCode: code.includes("PENDING") || code.includes("AWAITING_USER") ? 409 : 422
1073
+ });
996
1074
  }
997
1075
  async function writeAtomic(fs, target, content) {
998
1076
  if (fs.writeTextAtomic)
@@ -1003,6 +1081,12 @@ async function writeAtomic(fs, target, content) {
1003
1081
  function isRecord(value) {
1004
1082
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1005
1083
  }
1084
+ function isAwaitingUser(value) {
1085
+ return isRecord(value)
1086
+ && typeof value.question === "string"
1087
+ && value.question.trim().length > 0
1088
+ && typeof value.requestedAt === "string";
1089
+ }
1006
1090
  function isPendingDispatch(value) {
1007
1091
  if (!isRecord(value))
1008
1092
  return false;
@@ -1,4 +1,4 @@
1
- import { ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_SYNC_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
1
+ import { ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
2
2
  const CURRENT_HANDOFF_NOTICE = "<!-- VCM current handoff: replace this file with one complete, self-contained snapshot of the current result. Restate all still-relevant evidence; do not refer to a prior revision, route message, Session, or transcript as evidence. -->";
3
3
  export function renderArchitectureBriefTemplate(taskSlug) {
4
4
  return `# Architecture Brief: ${taskSlug}
@@ -518,7 +518,49 @@ TBD
518
518
 
519
519
  ## Decision
520
520
 
521
- ${renderArtifactOptions(DOCS_SYNC_DECISIONS)}
521
+ ${renderArtifactOptions(DOCS_REPORT_DECISIONS)}
522
+ `;
523
+ }
524
+ export function renderDocsUpdateReportTemplate(taskSlug, assignmentId = "docs-only") {
525
+ return `# Docs Update Report: ${taskSlug}
526
+
527
+ ${CURRENT_HANDOFF_NOTICE}
528
+
529
+ ## Summary
530
+
531
+ TBD
532
+
533
+ ## Assignment ID
534
+
535
+ ${assignmentId}
536
+
537
+ ## Documents Updated
538
+
539
+ TBD
540
+
541
+ ## Documents Reviewed And Left Unchanged
542
+
543
+ TBD
544
+
545
+ ## Evidence Reviewed
546
+
547
+ TBD
548
+
549
+ ## Checks Performed
550
+
551
+ TBD
552
+
553
+ ## Commit
554
+
555
+ TBD
556
+
557
+ ## Remaining Documentation Issues
558
+
559
+ TBD
560
+
561
+ ## Decision
562
+
563
+ ${renderArtifactOptions(DOCS_REPORT_DECISIONS)}
522
564
  `;
523
565
  }
524
566
  export function renderWorkflowProgressTemplate(taskSlug) {