vibe-coding-master 0.7.42 → 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 (50) hide show
  1. package/README.md +49 -17
  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 +32 -2
  5. package/dist/backend/api/workflow-control-routes.js +7 -26
  6. package/dist/backend/cli/install-vcm-harness.js +61 -6
  7. package/dist/backend/role-tool-policy.js +1 -1
  8. package/dist/backend/server.js +14 -5
  9. package/dist/backend/services/artifact-service.js +10 -32
  10. package/dist/backend/services/auto-memory-service.js +642 -14
  11. package/dist/backend/services/claude-hook-service.js +183 -6
  12. package/dist/backend/services/gate-review-service.js +173 -74
  13. package/dist/backend/services/harness-feedback-service.js +180 -80
  14. package/dist/backend/services/harness-service.js +65 -9
  15. package/dist/backend/services/memory-review-paths.js +13 -0
  16. package/dist/backend/services/role-stall-detector-service.js +322 -0
  17. package/dist/backend/services/round-service.js +25 -0
  18. package/dist/backend/services/runtime-coordinator-service.js +12 -1
  19. package/dist/backend/services/session-service.js +70 -3
  20. package/dist/backend/services/status-service.js +1 -0
  21. package/dist/backend/services/translation-worker-service.js +19 -4
  22. package/dist/backend/services/workflow-control-service.js +524 -204
  23. package/dist/backend/templates/handoff.js +46 -5
  24. package/dist/backend/templates/harness/architect-agent.js +12 -6
  25. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
  26. package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
  27. package/dist/backend/templates/harness/claude-root.js +3 -2
  28. package/dist/backend/templates/harness/coder-agent.js +12 -5
  29. package/dist/backend/templates/harness/gate-review.js +150 -57
  30. package/dist/backend/templates/harness/harness-engineer-agent.js +38 -13
  31. package/dist/backend/templates/harness/project-manager-agent.js +18 -12
  32. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
  33. package/dist/backend/templates/harness/tester-agent.js +13 -0
  34. package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
  35. package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -6
  36. package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
  37. package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
  38. package/dist/shared/types/role-stall.js +1 -0
  39. package/dist/shared/types/workflow.js +15 -0
  40. package/dist/shared/validation/artifact-check.js +3 -3
  41. package/dist/shared/validation/artifact-contract.js +1 -1
  42. package/dist/shared/validation/artifact-registry.js +17 -1
  43. package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
  44. package/dist-frontend/assets/index-BvCmrFlN.js +97 -0
  45. package/dist-frontend/index.html +2 -2
  46. package/package.json +1 -1
  47. package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
  48. package/scripts/harness-tools/vcm-artifact +1 -2
  49. package/scripts/harness-tools/vcm-bash-guard +204 -14
  50. package/dist-frontend/assets/index-Bocc2DWF.js +0 -97
@@ -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`;
@@ -76,6 +75,7 @@ export function createHarnessFeedbackService(deps) {
76
75
  });
77
76
  }
78
77
  const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
78
+ const pendingFeedback = await listPendingFeedback(repoRoot);
79
79
  const timestamp = now();
80
80
  const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
81
81
  const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
@@ -87,11 +87,11 @@ export function createHarnessFeedbackService(deps) {
87
87
  status: "running",
88
88
  analysisPath,
89
89
  finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
90
+ pendingFeedbackPaths: pendingFeedback.map((item) => item.path),
90
91
  ...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
91
92
  createdAt: timestamp,
92
93
  updatedAt: timestamp
93
94
  };
94
- const pendingFeedback = await listPendingFeedback(repoRoot);
95
95
  try {
96
96
  await persistTaskRetrospectiveMarker(repoRoot, marker);
97
97
  await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
@@ -135,9 +135,14 @@ export function createHarnessFeedbackService(deps) {
135
135
  const reportContent = await deps.fs.pathExists(reportPath)
136
136
  ? await deps.fs.readText(reportPath)
137
137
  : "";
138
- const reportErrors = reportContent.trim() ? getRetrospectiveReportErrors(reportContent) : ["Report is empty."];
138
+ const reportErrors = reportContent.trim()
139
+ ? [
140
+ ...getRetrospectiveReportErrors(reportContent),
141
+ ...getFeedbackDispositionErrors(repoRoot, marker.pendingFeedbackPaths ?? [], reportContent)
142
+ ]
143
+ : ["Report is empty."];
139
144
  const reportReady = reportErrors.length === 0;
140
- if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
145
+ if (!reportReady || (marker.memoryRunId && input.memoryReviewStatus === "failed")) {
141
146
  await persistTaskRetrospectiveMarker(repoRoot, {
142
147
  ...marker,
143
148
  status: "failed",
@@ -149,13 +154,60 @@ export function createHarnessFeedbackService(deps) {
149
154
  });
150
155
  return true;
151
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();
192
+ try {
193
+ await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
194
+ }
195
+ catch (error) {
196
+ await persistTaskRetrospectiveMarker(repoRoot, {
197
+ ...marker,
198
+ status: "failed",
199
+ failedAt: timestamp,
200
+ updatedAt: timestamp,
201
+ error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
202
+ });
203
+ return;
204
+ }
152
205
  await persistTaskRetrospectiveMarker(repoRoot, {
153
206
  ...marker,
154
207
  status: "completed",
155
208
  completedAt: timestamp,
156
209
  updatedAt: timestamp
157
210
  });
158
- return true;
159
211
  }
160
212
  async function assertHarnessEngineerAvailable(_repoRoot) {
161
213
  return undefined;
@@ -245,7 +297,15 @@ export function createHarnessFeedbackService(deps) {
245
297
  ...(pendingFeedbackPaths.length > 0
246
298
  ? [
247
299
  "",
248
- "Process every listed feedback inside this retrospective. Record every disposition in the retrospective report, then delete the processed feedback files before ending the turn."
300
+ "Process every listed feedback inside this retrospective. Record every disposition in the retrospective report using this exact block for each assigned path:",
301
+ "",
302
+ "### Feedback: <exact assigned absolute path>",
303
+ "Decision: confirmed|rejected|duplicate|already-covered",
304
+ "Evidence: <concise evidence>",
305
+ "Impact: <impact>",
306
+ "Required action: <action or none>",
307
+ "",
308
+ "Do not edit or delete pending feedback files. VCM removes the assigned files after validating every disposition in the accepted report."
249
309
  ]
250
310
  : []),
251
311
  "",
@@ -258,6 +318,16 @@ export function createHarnessFeedbackService(deps) {
258
318
  `Current memory snapshot: ${memoryReview.currentMemoryPath}`,
259
319
  "Active memory files:",
260
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"]),
261
331
  ...(memoryReview.planningCandidatePath
262
332
  ? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
263
333
  : []),
@@ -265,88 +335,30 @@ export function createHarnessFeedbackService(deps) {
265
335
  "Review every memory candidate against final task evidence while performing this retrospective.",
266
336
  "The snapshot files contain only the matching pre-review <VCM-memory> block content.",
267
337
  "Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
268
- "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.",
269
- "Complete this full existing-memory review even when every proposal says no-change.",
270
- "Then evaluate every proposal item independently. Do not accept or reject a whole role draft as one decision.",
271
- "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.",
272
- "Do not copy the proposer rationale as the review. Verify it against current code, durable documentation, and final task evidence.",
273
- "Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
274
- "Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
275
- "Final content must be one exact line written to the selected active <VCM-memory> block. Use none when the decision does not keep memory.",
276
- "For keep-in-memory use Durable doc disposition: memory. For keep-memory-reference use memory-reference. For move-to-durable-doc use durable-doc.",
277
- "Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
278
- "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.",
279
- "Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
280
- "Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
281
- "Apply the reviewed result directly to the <VCM-memory> blocks in the listed active memory files. Do not change any content outside those blocks.",
282
- "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.",
283
- "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:",
284
349
  "",
285
350
  "## Memory Review",
286
- "Existing memory reviewed: complete",
287
- "",
288
- "### Proposal Decisions",
289
- ...(memoryReview.proposalCandidates.length > 0
290
- ? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
291
- : ["none"]),
292
- "",
293
- "### Existing Memory Decisions",
294
- "#### Item 1",
295
- "Target: shared",
296
- "Existing: <exact existing memory entry>",
297
- "Decision: retain",
298
- "Reason: <why this decision is correct>",
299
- "Impact if removed: <specific future role or task failure>",
300
- "Durable doc disposition: memory",
301
- "Durable doc path: none",
302
- "Evidence: <current code, durable documentation, or final task evidence>",
303
- "",
304
- "### Existing Memory Changes",
305
- "- retained: <summary or none>",
306
- "- updated: <summary or none>",
307
- "- removed: <summary or none>",
308
- "",
309
- "Reviewed memory set: complete"
351
+ "Memory commit: <full commit or none>",
352
+ `Review result: ${memoryReview.reviewResultPath}`,
353
+ "Durable document assignments: <count>"
310
354
  ]
311
355
  : []),
312
356
  "",
313
357
  `Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
314
- `Submit with: .ai/tools/vcm-artifact retrospective-report --file <candidate> --path ${analysisPath} --mode final`,
315
- "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."
316
360
  ].join("\n");
317
361
  }
318
- function renderMemoryProposalDecisionTemplate(candidate) {
319
- if (candidate.operation === "remove") {
320
- return [
321
- `#### Candidate ${candidate.id}`,
322
- `Source: ${candidate.source}`,
323
- "Operation: remove",
324
- `Target: ${candidate.target}`,
325
- `Existing: ${candidate.existing}`,
326
- "Decision: remove|retain",
327
- "Reason: <why the proposed removal should be applied or rejected>",
328
- "Evidence checked: <current code, durable documentation, or final task evidence>",
329
- ""
330
- ];
331
- }
332
- return [
333
- `#### Candidate ${candidate.id}`,
334
- `Source: ${candidate.source}`,
335
- `Operation: ${candidate.operation}`,
336
- `Target: ${candidate.target}`,
337
- `Candidate: ${candidate.content}`,
338
- "Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
339
- "Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
340
- "Why memory is necessary: <independent reason, or why it is not necessary>",
341
- "Impact if absent: <specific impact, or why no durable impact exists>",
342
- "Durable doc disposition: memory|durable-doc|memory-reference",
343
- "Durable doc analysis: <why this destination is correct>",
344
- "Durable doc path: <none or a project-relative durable doc path>",
345
- "Evidence checked: <current code, durable documentation, or final task evidence>",
346
- "Final content: <exact one-line reviewed-memory content or none>",
347
- ""
348
- ];
349
- }
350
362
  function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
351
363
  return [
352
364
  "[VCM Harness Feedback]",
@@ -375,6 +387,20 @@ export function createHarnessFeedbackService(deps) {
375
387
  await deps.fs.removePath?.(statePath, { force: true });
376
388
  }
377
389
  }
390
+ async function removeProcessedFeedback(repoRoot, feedbackPaths) {
391
+ if (feedbackPaths.length === 0) {
392
+ return;
393
+ }
394
+ if (!deps.fs.removePath) {
395
+ throw new Error("The filesystem adapter does not support feedback removal.");
396
+ }
397
+ for (const feedbackPath of feedbackPaths) {
398
+ assertPendingFeedbackPath(feedbackPath);
399
+ }
400
+ for (const feedbackPath of feedbackPaths) {
401
+ await deps.fs.removePath(resolveRepoPath(repoRoot, feedbackPath), { force: true });
402
+ }
403
+ }
378
404
  async function readOptionalText(repoRoot, relativePath) {
379
405
  const absolutePath = resolveRepoPath(repoRoot, relativePath);
380
406
  return readAbsoluteOptionalText(absolutePath);
@@ -390,9 +416,83 @@ export function createHarnessFeedbackService(deps) {
390
416
  sendPendingFeedback,
391
417
  startTaskRetrospective,
392
418
  handleTaskRetrospectiveHook,
419
+ completeWaitingTaskRetrospective,
393
420
  assertHarnessEngineerAvailable
394
421
  };
395
422
  }
423
+ function getFeedbackDispositionErrors(repoRoot, feedbackPaths, reportContent) {
424
+ if (feedbackPaths.length === 0) {
425
+ return [];
426
+ }
427
+ const section = readLevelTwoSection(reportContent, "Feedback Dispositions");
428
+ if (!section) {
429
+ return ["Feedback Dispositions is empty."];
430
+ }
431
+ const errors = [];
432
+ const lines = section.split(/\r?\n/);
433
+ for (const feedbackPath of feedbackPaths) {
434
+ try {
435
+ assertPendingFeedbackPath(feedbackPath);
436
+ }
437
+ catch (error) {
438
+ errors.push(errorMessage(error));
439
+ continue;
440
+ }
441
+ const absolutePath = resolveRepoPath(repoRoot, feedbackPath);
442
+ const heading = `### Feedback: ${absolutePath}`;
443
+ const start = lines.findIndex((line) => line.trim() === heading);
444
+ if (start < 0) {
445
+ errors.push(`Missing disposition for assigned feedback: ${absolutePath}.`);
446
+ continue;
447
+ }
448
+ const endOffset = lines.slice(start + 1).findIndex((line) => /^###\s+/.test(line.trim()));
449
+ const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
450
+ const block = lines.slice(start + 1, end);
451
+ const decision = readDispositionField(block, "Decision");
452
+ if (!decision || !["confirmed", "rejected", "duplicate", "already-covered"].includes(decision)) {
453
+ errors.push(`Feedback disposition Decision for ${absolutePath} must be confirmed|rejected|duplicate|already-covered.`);
454
+ }
455
+ for (const field of ["Evidence", "Impact", "Required action"]) {
456
+ if (!readDispositionField(block, field)) {
457
+ errors.push(`Feedback disposition ${field} is required for ${absolutePath}.`);
458
+ }
459
+ }
460
+ }
461
+ return errors;
462
+ }
463
+ function readLevelTwoSection(content, heading) {
464
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
465
+ const match = new RegExp(`^##\\s+${escapedHeading}\\s*$`, "im").exec(content);
466
+ if (!match || match.index === undefined) {
467
+ return undefined;
468
+ }
469
+ const afterHeading = content.slice(match.index + match[0].length);
470
+ const nextHeading = /\n##\s+\S/.exec(afterHeading);
471
+ return (nextHeading ? afterHeading.slice(0, nextHeading.index) : afterHeading).trim();
472
+ }
473
+ function readDispositionField(lines, field) {
474
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
475
+ return lines
476
+ .map((line) => new RegExp(`^${escapedField}:\\s*(.+)$`, "i").exec(line.trim())?.[1]?.trim())
477
+ .find((value) => Boolean(value));
478
+ }
479
+ function assertPendingFeedbackPath(feedbackPath) {
480
+ if (!/^\.ai\/vcm\/harness-feedback\/pending\/[A-Za-z0-9._-]+\.md$/.test(feedbackPath)) {
481
+ throw new Error(`Invalid assigned Harness Feedback path: ${feedbackPath}.`);
482
+ }
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
+ }
396
496
  function parseSimpleMetadata(content) {
397
497
  const result = {};
398
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";
@@ -57,8 +59,15 @@ const VCM_HOOK_DEFINITIONS = [
57
59
  { eventName: "PreToolUse", matcher: "Bash", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
58
60
  { eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
59
61
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
62
+ { eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
63
+ { eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
64
+ { eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
65
+ { eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
66
+ { eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
67
+ { eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
60
68
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
61
69
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
70
+ { eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
62
71
  { eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
63
72
  { eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
64
73
  ];
@@ -119,7 +128,7 @@ const HARNESS_FILES = [
119
128
  kind: "skill-vcm-code-navigation",
120
129
  path: ".claude/skills/vcm-code-navigation/SKILL.md",
121
130
  title: "VCM Code Navigation Skill",
122
- frontmatter: renderSkillFrontmatter("vcm-code-navigation", "Use when Architect, Coder, or Reviewer must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths."),
131
+ frontmatter: renderSkillFrontmatter("vcm-code-navigation", "Use when Architect must resolve code symbols, references, implementations, call hierarchies, or bounded dependency paths."),
123
132
  ownership: "whole-file",
124
133
  renderRules: renderVcmCodeNavigationSkillRules
125
134
  },
@@ -131,6 +140,14 @@ const HARNESS_FILES = [
131
140
  ownership: "whole-file",
132
141
  renderRules: renderVcmRouteMessageSkillRules
133
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
+ },
134
151
  {
135
152
  kind: "skill-vcm-task-state",
136
153
  path: ".claude/skills/vcm-task-state/SKILL.md",
@@ -209,8 +226,8 @@ const HARNESS_FILES = [
209
226
  title: "Reviewer Agent",
210
227
  memoryBlock: true,
211
228
  requiredDisallowedTools: REVIEWER_DISALLOWED_TOOLS,
212
- requiredSkills: ["vcm-code-navigation"],
213
- frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", "), skills: ["vcm-code-navigation"] }),
229
+ removedSkills: ["vcm-code-navigation"],
230
+ frontmatter: renderAgentFrontmatter("reviewer", "VCM independent gate review role for architecture plans, validation adequacy, and code diffs.", { disallowedTools: REVIEWER_DISALLOWED_TOOLS.join(", ") }),
214
231
  renderRules: renderReviewerAgentRules
215
232
  },
216
233
  {
@@ -250,6 +267,13 @@ const HARNESS_FILES = [
250
267
  ownership: "raw-file",
251
268
  renderRules: renderRequestGateReviewTool
252
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
+ },
253
277
  {
254
278
  kind: "tool-update-task-state",
255
279
  path: ".ai/tools/update-task-state",
@@ -271,6 +295,13 @@ const HARNESS_FILES = [
271
295
  ownership: "raw-file",
272
296
  renderRules: renderRequestArchitectRestartTool
273
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
+ },
274
305
  {
275
306
  kind: "agent-project-manager",
276
307
  path: ".claude/agents/project-manager.md",
@@ -297,8 +328,8 @@ const HARNESS_FILES = [
297
328
  title: "Coder Agent",
298
329
  memoryBlock: true,
299
330
  requiredDisallowedTools: CODE_ROLE_DISALLOWED_TOOLS,
300
- requiredSkills: ["vcm-code-navigation"],
301
- frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", "), skills: ["vcm-code-navigation"] }),
331
+ removedSkills: ["vcm-code-navigation"],
332
+ frontmatter: renderAgentFrontmatter("coder", "VCM implementation role for scoped code changes and focused tests.", { disallowedTools: CODE_ROLE_DISALLOWED_TOOLS.join(", ") }),
302
333
  renderRules: renderCoderHarnessRules
303
334
  },
304
335
  {
@@ -353,7 +384,7 @@ export function createHarnessService(deps) {
353
384
  let harnessCommit;
354
385
  if (nextContent !== currentContent) {
355
386
  await bumpHarnessRevision(deps.fs, repoRoot, now());
356
- 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;
357
388
  }
358
389
  const file = await readHarnessFileContent(deps.fs, repoRoot, definition.path);
359
390
  const [analyses, codeIntelligence] = await Promise.all([
@@ -377,7 +408,7 @@ export function createHarnessService(deps) {
377
408
  if (result.changedFiles.length > 0) {
378
409
  await bumpHarnessRevision(deps.fs, repoRoot, now());
379
410
  }
380
- 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");
381
412
  return {
382
413
  ...result,
383
414
  changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : result.changedFiles,
@@ -401,7 +432,7 @@ export function createHarnessService(deps) {
401
432
  if (changedFiles.length > 0) {
402
433
  await bumpHarnessRevision(deps.fs, repoRoot, now());
403
434
  }
404
- 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");
405
436
  return {
406
437
  version: VCM_HARNESS_VERSION,
407
438
  changedFiles: committed.changedFiles.length > 0 ? committed.changedFiles : changedFiles,
@@ -1444,7 +1475,8 @@ function ensureAgentAllowedTool(content, requiredTool) {
1444
1475
  function normalizeAgentFrontmatter(content, definition) {
1445
1476
  const allowedToolsUpdated = (definition.requiredTools ?? []).reduce(ensureAgentAllowedTool, content);
1446
1477
  const disallowedToolsUpdated = ensureAgentDisallowedTools(allowedToolsUpdated, definition.requiredDisallowedTools);
1447
- return (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
1478
+ const skillsUpdated = (definition.requiredSkills ?? []).reduce(ensureAgentSkill, disallowedToolsUpdated);
1479
+ return (definition.removedSkills ?? []).reduce(removeAgentSkill, skillsUpdated);
1448
1480
  }
1449
1481
  function ensureAgentSkill(content, requiredSkill) {
1450
1482
  const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
@@ -1466,6 +1498,30 @@ function ensureAgentSkill(content, requiredSkill) {
1466
1498
  const nextSkills = `${skillsMatch[0].trimEnd()}\n - ${requiredSkill}`;
1467
1499
  return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], nextSkills));
1468
1500
  }
1501
+ function removeAgentSkill(content, removedSkill) {
1502
+ const frontmatterMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---/);
1503
+ if (!frontmatterMatch) {
1504
+ return content;
1505
+ }
1506
+ const frontmatter = frontmatterMatch[0];
1507
+ const skillsMatch = frontmatter.match(/^skills:[ \t]*(?:\r?\n((?:\s+-\s+[^\r\n]+\r?\n?)*))?/m);
1508
+ if (!skillsMatch) {
1509
+ return content;
1510
+ }
1511
+ const listedSkills = (skillsMatch[1] ?? "")
1512
+ .split(/\r?\n/)
1513
+ .map((line) => line.match(/^\s+-\s+(.+)$/)?.[1]?.trim())
1514
+ .filter((skill) => Boolean(skill));
1515
+ const remainingSkills = listedSkills.filter((skill) => skill !== removedSkill);
1516
+ if (remainingSkills.length === listedSkills.length) {
1517
+ return content;
1518
+ }
1519
+ const lineEnding = skillsMatch[0].includes("\r\n") ? "\r\n" : "\n";
1520
+ const replacement = remainingSkills.length > 0
1521
+ ? `skills:${lineEnding}${remainingSkills.map((skill) => ` - ${skill}`).join(lineEnding)}${lineEnding}`
1522
+ : "";
1523
+ return content.replace(frontmatter, frontmatter.replace(skillsMatch[0], replacement));
1524
+ }
1469
1525
  function migrateLegacyHarnessFile(definition, currentContent, block) {
1470
1526
  const legacyContent = definition.legacyWholeFile?.trimEnd();
1471
1527
  if (!legacyContent) {
@@ -2,6 +2,19 @@ export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
2
2
  export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
3
3
  export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
4
4
  export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
5
+ const MEMORY_REVIEW_ROLE_DRAFT_PATTERN = new RegExp(`^${escapeRegExp(MEMORY_REVIEW_RUNS_ROOT)}/[A-Za-z0-9._-]+/drafts/([A-Za-z0-9._-]+)\\.md$`);
6
+ export function memoryReviewRoleDraftPath(runId, role) {
7
+ return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`;
8
+ }
9
+ export function isMemoryProposalSubmissionPath(artifactPath, role) {
10
+ if (artifactPath === ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH) {
11
+ return role === "architect";
12
+ }
13
+ return MEMORY_REVIEW_ROLE_DRAFT_PATTERN.exec(artifactPath)?.[1] === role;
14
+ }
5
15
  export function architectPlanningCandidateSnapshotPath(runId) {
6
16
  return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
7
17
  }
18
+ function escapeRegExp(value) {
19
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ }