vibe-coding-master 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/backend/server.js +4 -1
- package/dist/backend/services/artifact-service.js +14 -0
- package/dist/backend/services/claude-hook-service.js +6 -13
- package/dist/backend/services/pm-user-question.js +62 -0
- package/dist/backend/services/workflow-control-service.js +73 -33
- package/dist/backend/templates/harness/project-manager-agent.js +9 -3
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -1
- package/dist-frontend/assets/{index-C21rIHsU.js → index-ClaVB8jM.js} +33 -33
- package/dist-frontend/index.html +1 -1
- package/package.json +4 -1
- package/scripts/harness-tools/run-long-check +90 -43
package/README.md
CHANGED
|
@@ -195,7 +195,10 @@ Whenever Project Manager asks the user a question, it first uses
|
|
|
195
195
|
`vcm-ask-user`. VCM records the wait and cancels any pending role dispatch in
|
|
196
196
|
one state update. Stop Hook then ends the PM turn without route scanning. Only
|
|
197
197
|
a new direct user message clears the wait, and the next role dispatch requires
|
|
198
|
-
a fresh Workflow Review.
|
|
198
|
+
a fresh Workflow Review. PM treats text emitted while a Round continues as
|
|
199
|
+
intermediate output, not as delivered user communication. When the Round pauses
|
|
200
|
+
or completes, its Round Final Reply restates the complete user-facing result or
|
|
201
|
+
question without relying on earlier PM output.
|
|
199
202
|
|
|
200
203
|
Typical flow:
|
|
201
204
|
|
package/dist/backend/server.js
CHANGED
|
@@ -243,7 +243,10 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
243
243
|
const runtime = createNodePtyTerminalRuntime({ fs });
|
|
244
244
|
const registry = createSessionRegistry();
|
|
245
245
|
const workflowControlService = createWorkflowControlService({ fs });
|
|
246
|
-
const artifactService = createArtifactService(fs, {
|
|
246
|
+
const artifactService = createArtifactService(fs, {
|
|
247
|
+
workflowControlService,
|
|
248
|
+
isRoleMemoryTurn: (taskRepoRoot, role) => autoMemoryService.isRoleMemoryTurn(taskRepoRoot, role)
|
|
249
|
+
});
|
|
247
250
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
248
251
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
249
252
|
const taskWorkflowService = createTaskWorkflowService({ fs });
|
|
@@ -219,6 +219,20 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
219
219
|
if (validation.errors.length > 0) {
|
|
220
220
|
throw artifactRejected(input.kind, validation.errors);
|
|
221
221
|
}
|
|
222
|
+
if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
|
|
223
|
+
&& deps.workflowControlService) {
|
|
224
|
+
// Memory-owned documentation work is independent of PM's active dispatch.
|
|
225
|
+
const memoryUpdate = input.kind === "docs-update-report"
|
|
226
|
+
&& await deps.isRoleMemoryTurn?.(input.repoRoot, input.role);
|
|
227
|
+
if (!memoryUpdate) {
|
|
228
|
+
await deps.workflowControlService.assertDocsArtifactAllowed({
|
|
229
|
+
taskRepoRoot: input.repoRoot,
|
|
230
|
+
stateRoot: input.stateRoot ?? ".ai/vcm",
|
|
231
|
+
handoffDir: input.handoffDir,
|
|
232
|
+
taskSlug: input.taskSlug
|
|
233
|
+
}, input.kind, input.role);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
222
236
|
if (input.kind === "workflow-progress") {
|
|
223
237
|
if (input.mode !== "final") {
|
|
224
238
|
throw artifactRejected(input.kind, ["Workflow Progress must be submitted in final mode."]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isHarnessEngineerToolRoleName, isReviewerRoleName, isTranslatorToolRoleName, isVcmRoleName } from "../../shared/constants.js";
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { readTranscriptTurnEvidence } from "./claude-transcript-reply.js";
|
|
4
|
+
import { findPmUserQuestion } from "./pm-user-question.js";
|
|
4
5
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
5
6
|
import { matchesRoleHookSession } from "./session-service.js";
|
|
6
7
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
@@ -338,7 +339,10 @@ export function createClaudeHookService(deps) {
|
|
|
338
339
|
}
|
|
339
340
|
else {
|
|
340
341
|
const lastAssistantMessage = stringOrUndefined(input.event.last_assistant_message);
|
|
341
|
-
|
|
342
|
+
const question = options.allowBlock && lastAssistantMessage
|
|
343
|
+
? findPmUserQuestion(lastAssistantMessage)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (question) {
|
|
342
346
|
return {
|
|
343
347
|
ok: true,
|
|
344
348
|
eventName,
|
|
@@ -348,7 +352,7 @@ export function createClaudeHookService(deps) {
|
|
|
348
352
|
dispatchedCount: 0,
|
|
349
353
|
stopDecision: {
|
|
350
354
|
behavior: "block",
|
|
351
|
-
reason:
|
|
355
|
+
reason: `Possible unregistered user question: ${JSON.stringify(question.slice(0, 600))}. If you are asking the user, use vcm-ask-user with the exact question, then ask it and end the turn without advancing or routing the workflow. If this only reports or quotes another question, clarify that context and end normally; do not invent a user question or register a false wait.`
|
|
352
356
|
}
|
|
353
357
|
};
|
|
354
358
|
}
|
|
@@ -1003,14 +1007,3 @@ function isDirectUserPrompt(prompt) {
|
|
|
1003
1007
|
const normalized = prompt.trim();
|
|
1004
1008
|
return normalized.length > 0 && !/^\[VCM(?:\s|\])/i.test(normalized);
|
|
1005
1009
|
}
|
|
1006
|
-
function asksUserQuestion(message) {
|
|
1007
|
-
const withoutCodeBlocks = message.replace(/```[\s\S]*?```/g, "");
|
|
1008
|
-
const lines = withoutCodeBlocks
|
|
1009
|
-
.split(/\r?\n/)
|
|
1010
|
-
.map((line) => line.trim())
|
|
1011
|
-
.filter(Boolean);
|
|
1012
|
-
return lines.some((line) => /[??]/.test(line)
|
|
1013
|
-
|| /^(?:please\s+(?:answer|choose|confirm|decide|provide|select|tell)|(?:can|could|do|does|is|are|should|will|would)\s+you\b)/i.test(line)
|
|
1014
|
-
|| /\b(?:please\s+(?:reply|respond|choose|confirm|decide|provide|select|tell|give)|i\s+need\s+your\s+(?:decision|choice|confirmation|answer)|let\s+me\s+know|your\s+(?:decision|choice|confirmation|answer)\s+is\s+required|waiting\s+for\s+your)\b/i.test(line)
|
|
1015
|
-
|| /(?:请(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|你(?:是否|能否|要不要|可否)|是否需要你|需要你(?:选择|确认|决定|提供|告知)|需要您的(?:选择|确认|决定|回复)|等待您的(?:选择|确认|决定|回复)|告诉我|告知我)/.test(line));
|
|
1016
|
-
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import remarkGfm from "remark-gfm";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import { unified } from "unified";
|
|
4
|
+
const markdown = unified().use(remarkParse).use(remarkGfm).freeze();
|
|
5
|
+
// Match direct requests, not punctuation or requests mentioned inside a report.
|
|
6
|
+
const DIRECT_REQUESTS = [
|
|
7
|
+
/^(?:can|could|do|did|are|have|should|will|would)\s+you\b/i,
|
|
8
|
+
/^(?:should|shall|may|can|could)\s+(?:i|we)\b/i,
|
|
9
|
+
/^(?:which|what|how|where|when|who)\b[^.!?]*\b(?:you|your|(?:should|shall|can|may)\s+(?:i|we))\b/i,
|
|
10
|
+
/^(?:which|what)\s+(?:option|approach|behavior|behaviour|choice)\b[^.!?]*\bshould\s+be\b/i,
|
|
11
|
+
/^(?:please\s+)?(?:answer|reply|respond|choose|confirm|decide|provide|select|tell\s+me|let\s+me\s+know|give\s+(?:me|your))\b/i,
|
|
12
|
+
/^i\s+need\s+your\s+(?:decision|choice|confirmation|answer|approval)\b/i,
|
|
13
|
+
/^your\s+(?:decision|choice|confirmation|answer|approval)\s+is\s+required\b/i,
|
|
14
|
+
/^(?:(?:i\s+am|i'm|we\s+are|we're)\s+)?waiting\s+for\s+your\s+(?:decision|choice|confirmation|answer|reply|approval)\b/i,
|
|
15
|
+
/^(?:请(?:您|你)?(?:回答|回复|选择|确认|决定|提供|告知|给出|说明)|[你您](?:是否|能否|要不要|可否|希望|想要)|是否需要[你您]|需要[你您](?:选择|确认|决定|提供|告知)|(?:需要|等待)[你您]的(?:选择|确认|决定|回复)|告诉我|告知我)/
|
|
16
|
+
];
|
|
17
|
+
export function findPmUserQuestion(message) {
|
|
18
|
+
return findQuestion(markdown.parse(message));
|
|
19
|
+
}
|
|
20
|
+
function findQuestion(node) {
|
|
21
|
+
// Quoted evidence and code are not PM addressing the user. Headings and table
|
|
22
|
+
// cells remain eligible: a real request does not stop being one when formatted.
|
|
23
|
+
if (["blockquote", "code", "html", "definition", "footnoteDefinition", "delete"].includes(node.type)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (node.type === "paragraph" || node.type === "heading" || node.type === "tableCell") {
|
|
27
|
+
const prose = removeQuotedText(proseText(node)).replace(/\s+/g, " ");
|
|
28
|
+
for (const sentence of prose.split(/(?<=[.!?。!?;])\s*/)) {
|
|
29
|
+
const request = sentence.trim().replace(/^(?:(?:question(?:\s+for\s+you)?|your\s+decision|问题|请问)\s*[::]\s*|before\s+(?:i|we)\s+(?:continue|proceed),?\s+)/i, "");
|
|
30
|
+
if (DIRECT_REQUESTS.some((pattern) => pattern.test(request))) {
|
|
31
|
+
return sentence.trim();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if ("children" in node) {
|
|
37
|
+
for (const child of node.children) {
|
|
38
|
+
const question = findQuestion(child);
|
|
39
|
+
if (question)
|
|
40
|
+
return question;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function proseText(node) {
|
|
46
|
+
if (node.type === "text")
|
|
47
|
+
return node.value;
|
|
48
|
+
if (node.type === "break")
|
|
49
|
+
return "\n";
|
|
50
|
+
if (["inlineCode", "html", "image", "imageReference", "delete", "footnoteReference"].includes(node.type))
|
|
51
|
+
return " ";
|
|
52
|
+
if (node.type === "link") {
|
|
53
|
+
const label = node.children.map(proseText).join("");
|
|
54
|
+
return label === node.url || /^(?:https?:\/\/|www\.)/i.test(label) ? " " : label;
|
|
55
|
+
}
|
|
56
|
+
if ("children" in node)
|
|
57
|
+
return node.children.map(proseText).join("");
|
|
58
|
+
return " ";
|
|
59
|
+
}
|
|
60
|
+
function removeQuotedText(text) {
|
|
61
|
+
return text.replace(/"[^"]*"|“[^”]*”|‘[^’]*’|「[^」]*」|『[^』]*』|(?<!\w)'[^']+'(?!\w)/g, " ");
|
|
62
|
+
}
|
|
@@ -122,11 +122,11 @@ export function createWorkflowControlService(deps) {
|
|
|
122
122
|
const violatedRule = candidate.proposal.violatedRule?.trim();
|
|
123
123
|
if (!authorizationText && !violatedRule) {
|
|
124
124
|
throw workflowError("WORKFLOW_TRANSITION_DENIED", verdict.reason, verdict.allowedTransitions.length > 0
|
|
125
|
-
? `Allowed next dispatches: ${verdict.allowedTransitions.join(", ")}. Recheck the flow, or ask the user
|
|
125
|
+
? `Allowed next dispatches: ${verdict.allowedTransitions.join(", ")}. Recheck the flow, reuse an existing direct user instruction that explicitly authorizes this exact transition, or ask the user only when no such authorization exists.`
|
|
126
126
|
: verdict.blockedHint);
|
|
127
127
|
}
|
|
128
128
|
if (!authorizationText || violatedRule !== verdict.reason) {
|
|
129
|
-
throw workflowError("WORKFLOW_USER_AUTHORIZATION_INVALID", `User authorization must include the exact authorization text and exact violated rule: ${verdict.reason}`, "
|
|
129
|
+
throw workflowError("WORKFLOW_USER_AUTHORIZATION_INVALID", `User authorization must include the exact authorization text and exact violated rule: ${verdict.reason}`, "Copy an existing direct user instruction that explicitly authorizes this exact transition into Authorization Text. Ask the user only when no such authorization exists. Copy the rejection reason verbatim into Violated Rule.");
|
|
130
130
|
}
|
|
131
131
|
userAuthorization = createUserAuthorization(id(), current, candidate, effectiveFlow, baseHistoryHash, verdict.reason, authorizationText, now());
|
|
132
132
|
overrideAuthorizationId = userAuthorization.id;
|
|
@@ -318,6 +318,7 @@ export function createWorkflowControlService(deps) {
|
|
|
318
318
|
requestUserInput,
|
|
319
319
|
resolveUserInput,
|
|
320
320
|
submitProgress,
|
|
321
|
+
assertDocsArtifactAllowed,
|
|
321
322
|
assertRouteAuthorized,
|
|
322
323
|
claimDispatch,
|
|
323
324
|
releaseDispatch,
|
|
@@ -325,6 +326,37 @@ export function createWorkflowControlService(deps) {
|
|
|
325
326
|
confirmDispatch,
|
|
326
327
|
recoverTask
|
|
327
328
|
};
|
|
329
|
+
async function assertDocsArtifactAllowed(input, kind, role) {
|
|
330
|
+
const state = await getState(input);
|
|
331
|
+
failOnStateWarnings(state);
|
|
332
|
+
const progress = await readProgress(deps.fs, input);
|
|
333
|
+
const active = state.activeDispatch;
|
|
334
|
+
if (progress.status !== "active" || !active || active.targetRole !== role)
|
|
335
|
+
return;
|
|
336
|
+
let expected;
|
|
337
|
+
if (progress.flow === "docs-only") {
|
|
338
|
+
expected = "docs-update-report";
|
|
339
|
+
}
|
|
340
|
+
else if (role === "architect" && (progress.flow === "code-change"
|
|
341
|
+
|| progress.flow === "architect-debug" || progress.flow === "architecture-diagnosis")) {
|
|
342
|
+
const flow = progress.flow;
|
|
343
|
+
const run = resolveFlowRun(state.flowRun, progress);
|
|
344
|
+
const hadTester = progress.history.some((entry) => entry.sequence >= run.startedAtSequence
|
|
345
|
+
&& entry.sequence < active.sequence && entry.targetRole === "tester");
|
|
346
|
+
if (!run.activeBranch && hadTester) {
|
|
347
|
+
const source = run.resumedFromBranch ?? (flow === "code-change" ? "coder" : flow);
|
|
348
|
+
const docsTarget = `${flow}/architect`;
|
|
349
|
+
const allowed = await allowedAfterTester(deps.fs, input, state, flow, source, {
|
|
350
|
+
successTarget: docsTarget
|
|
351
|
+
});
|
|
352
|
+
if (allowed.includes(docsTarget))
|
|
353
|
+
expected = "docs-sync-report";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (expected && expected !== kind) {
|
|
357
|
+
throw workflowError("WORKFLOW_DOCS_ARTIFACT_MISMATCH", `${kind} was not written: the active ${progress.flow} dispatch #${active.sequence} requires ${path.posix.join(input.handoffDir, `${expected}.md`)}.`, `Submit --kind ${expected} through .ai/tools/vcm-artifact. The two documentation reports are not interchangeable.`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
328
360
|
async function requestUserInput(input, question) {
|
|
329
361
|
return withLock(statePath(input), async () => {
|
|
330
362
|
const state = await getState(input);
|
|
@@ -667,7 +699,7 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
|
667
699
|
const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
|
|
668
700
|
if (resumedFromBranch && coderIndex < 0) {
|
|
669
701
|
if (testerIndex < 0) {
|
|
670
|
-
return allowedPostImplementationArchitect(fs, input, state, "code-change"
|
|
702
|
+
return allowedPostImplementationArchitect(fs, input, state, "code-change");
|
|
671
703
|
}
|
|
672
704
|
return allowedAfterTester(fs, input, state, "code-change", resumedFromBranch, {
|
|
673
705
|
testerFailureFlow: "architect-debug",
|
|
@@ -676,17 +708,7 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
|
676
708
|
});
|
|
677
709
|
}
|
|
678
710
|
if (coderIndex < 0) {
|
|
679
|
-
|
|
680
|
-
if (!evidenceIsFresh(state, "code-change", "architect", "architecture-plan.md", plan.hash)
|
|
681
|
-
|| plan.value !== "complete")
|
|
682
|
-
return ["code-change/architect"];
|
|
683
|
-
const gate = await gateState(fs, input, "architecture-plan");
|
|
684
|
-
if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
|
|
685
|
-
return ["code-change/architect"];
|
|
686
|
-
}
|
|
687
|
-
return gatePassedForDispatch(state, "code-change", "architect", "architecture-plan", gate)
|
|
688
|
-
? ["code-change/coder"]
|
|
689
|
-
: [];
|
|
711
|
+
return (await allowedFreshArchitecturePlan(fs, input, state)) ?? ["code-change/architect"];
|
|
690
712
|
}
|
|
691
713
|
if (testerIndex < coderIndex) {
|
|
692
714
|
const coder = await artifactState(fs, input, "coder-completion.md", "coder-completion");
|
|
@@ -697,9 +719,8 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
|
697
719
|
return ["architect-debug/architect"];
|
|
698
720
|
return coder.value === "ready_for_review" ? ["code-change/tester"] : ["code-change/coder"];
|
|
699
721
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return allowedPostImplementationArchitect(fs, input, state, "code-change", architectsAfterTester);
|
|
722
|
+
if (segment.some((entry, index) => index > testerIndex && entry.targetRole === "architect")) {
|
|
723
|
+
return allowedPostImplementationArchitect(fs, input, state, "code-change");
|
|
703
724
|
}
|
|
704
725
|
return allowedAfterTester(fs, input, state, "code-change", "coder", {
|
|
705
726
|
testerFailureFlow: "architect-debug",
|
|
@@ -707,13 +728,15 @@ async function allowedCodeChange(fs, input, state, segment, resumedFromBranch) {
|
|
|
707
728
|
successTarget: "code-change/architect"
|
|
708
729
|
});
|
|
709
730
|
}
|
|
710
|
-
async function
|
|
731
|
+
async function allowedFreshArchitecturePlan(fs, input, state, gateMustPostdate) {
|
|
732
|
+
const baseline = matchingEvidenceBaseline(state, "code-change", "architect");
|
|
711
733
|
const plan = await artifactState(fs, input, "architecture-plan.md", "architecture-plan");
|
|
712
|
-
if (!
|
|
713
|
-
|
|
734
|
+
if (!baseline || baseline.artifactHashes["architecture-plan.md"] === plan.hash)
|
|
735
|
+
return undefined;
|
|
736
|
+
if (plan.value !== "complete")
|
|
714
737
|
return ["code-change/architect"];
|
|
715
738
|
const gate = await gateState(fs, input, "architecture-plan");
|
|
716
|
-
if (!gate ||
|
|
739
|
+
if (!gate || (gateMustPostdate && gate.updatedAt <= gateMustPostdate))
|
|
717
740
|
return [];
|
|
718
741
|
if (freshGateDecision(state, "code-change", "architect", "architecture-plan", gate) === "request_changes") {
|
|
719
742
|
return ["code-change/architect"];
|
|
@@ -726,7 +749,7 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
726
749
|
const architectIndex = findLastIndex(segment, (entry) => entry.targetRole === "architect");
|
|
727
750
|
const testerIndex = findLastIndex(segment, (entry) => entry.targetRole === "tester");
|
|
728
751
|
if (resumedFromBranch && architectIndex >= 0 && testerIndex < 0) {
|
|
729
|
-
return allowedPostImplementationArchitect(fs, input, state, source
|
|
752
|
+
return allowedPostImplementationArchitect(fs, input, state, source);
|
|
730
753
|
}
|
|
731
754
|
if (architectIndex < 0)
|
|
732
755
|
return [`${source}/architect`];
|
|
@@ -773,11 +796,13 @@ async function allowedArchitectFix(fs, input, state, segment, source, resumedFro
|
|
|
773
796
|
successTarget: parentFlow ? `${parentFlow}/architect` : `${source}/architect`
|
|
774
797
|
});
|
|
775
798
|
}
|
|
776
|
-
async function allowedPostImplementationArchitect(fs, input, state, flow
|
|
799
|
+
async function allowedPostImplementationArchitect(fs, input, state, flow) {
|
|
777
800
|
const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
|
|
778
801
|
const acceptance = await artifactState(fs, input, "final-acceptance.md", "final-acceptance");
|
|
779
|
-
if (flow === "code-change"
|
|
780
|
-
|
|
802
|
+
if (flow === "code-change") {
|
|
803
|
+
const planningTransition = await allowedFreshArchitecturePlan(fs, input, state, matchingEvidenceBaseline(state, "code-change", "architect")?.confirmedAt);
|
|
804
|
+
if (planningTransition !== undefined)
|
|
805
|
+
return planningTransition;
|
|
781
806
|
}
|
|
782
807
|
if (evidenceIsFresh(state, flow, "architect", "docs-sync-report.md", docs.hash) && docs.complete) {
|
|
783
808
|
if (docs.value === "synced" || docs.value === "unchanged") {
|
|
@@ -824,13 +849,13 @@ async function allowedAfterTester(fs, input, state, currentFlow, codeSource, opt
|
|
|
824
849
|
}
|
|
825
850
|
if (test.value === "incomplete" || test.infrastructure === "repair-required")
|
|
826
851
|
return [`${currentFlow}/tester`];
|
|
827
|
-
if (test.value === "fail" && test.infrastructure !== "repair-required") {
|
|
828
|
-
return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
|
|
829
|
-
}
|
|
830
852
|
const validation = await gateState(fs, input, "validation-adequacy");
|
|
831
853
|
if (freshGateDecision(state, currentFlow, "tester", "validation-adequacy", validation) === "request_changes") {
|
|
832
854
|
return [`${currentFlow}/tester`];
|
|
833
855
|
}
|
|
856
|
+
if (test.value === "fail" && !test.hasUserApprovedCoverageGap) {
|
|
857
|
+
return options.testerFailureFlow ? [`${options.testerFailureFlow}/architect`] : [];
|
|
858
|
+
}
|
|
834
859
|
if (!gatePassedForDispatch(state, currentFlow, "tester", "validation-adequacy", validation))
|
|
835
860
|
return [];
|
|
836
861
|
const codeDiff = await gateState(fs, input, "code-diff");
|
|
@@ -873,6 +898,10 @@ async function artifactState(fs, input, fileName, kind) {
|
|
|
873
898
|
const infrastructure = kind === "test-report"
|
|
874
899
|
? /^Status:\s*(.+?)\s*$/mi.exec(readArtifactSectionContent(content, "Test Infrastructure") ?? "")?.[1]?.trim().toLowerCase()
|
|
875
900
|
: undefined;
|
|
901
|
+
const hasUserApprovedCoverageGap = kind === "test-report"
|
|
902
|
+
&& check.status === "ok"
|
|
903
|
+
&& hasSubstantiveArtifactSection(readArtifactSectionContent(content, "Coverage Gaps"))
|
|
904
|
+
&& hasSubstantiveArtifactSection(readArtifactSectionContent(content, "User Approval Evidence"));
|
|
876
905
|
const disposition = kind === "architect-debug" || kind === "architecture-diagnosis"
|
|
877
906
|
? readArtifactSectionContent(content, "Final Disposition")?.trim().toLowerCase()
|
|
878
907
|
: undefined;
|
|
@@ -884,12 +913,17 @@ async function artifactState(fs, input, fileName, kind) {
|
|
|
884
913
|
hash: await artifactFingerprint(fs, absolute, content),
|
|
885
914
|
value,
|
|
886
915
|
infrastructure,
|
|
916
|
+
hasUserApprovedCoverageGap,
|
|
887
917
|
disposition,
|
|
888
918
|
correctionOwner: correctionOwner === "none" || asTargetRole(correctionOwner)
|
|
889
919
|
? correctionOwner
|
|
890
920
|
: undefined
|
|
891
921
|
};
|
|
892
922
|
}
|
|
923
|
+
function hasSubstantiveArtifactSection(value) {
|
|
924
|
+
const normalized = value?.trim();
|
|
925
|
+
return Boolean(normalized && !/^(none|tbd)\.?$/i.test(normalized));
|
|
926
|
+
}
|
|
893
927
|
async function gateState(fs, input, gate) {
|
|
894
928
|
const target = resolveRepoPath(input.taskRepoRoot, path.posix.join(".ai/vcm/gate-reviews", "index.json"));
|
|
895
929
|
if (!(await fs.pathExists(target)))
|
|
@@ -1138,11 +1172,17 @@ async function validateCompleteDeliveryEvidence(fs, input, state, flow) {
|
|
|
1138
1172
|
throw workflowError("WORKFLOW_COMPLETION_INVALID", `The latest ${flow} dispatch is to ${baseline.targetRole}. Complete every required downstream validation and Gate step, then route Architect for docs sync before completing the flow.`);
|
|
1139
1173
|
}
|
|
1140
1174
|
const docs = await artifactState(fs, input, "docs-sync-report.md", "docs-sync-report");
|
|
1141
|
-
if (!docs.complete) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1175
|
+
if (!docs.complete || baseline.artifactHashes["docs-sync-report.md"] === docs.hash) {
|
|
1176
|
+
const update = await artifactState(fs, input, "docs-update-report.md", "docs-update-report");
|
|
1177
|
+
const wrongReport = update.hash !== MISSING_EVIDENCE_HASH
|
|
1178
|
+
&& baseline.artifactHashes["docs-update-report.md"] !== update.hash;
|
|
1179
|
+
const reason = !docs.complete
|
|
1180
|
+
? "Docs Sync Report is missing or malformed for this complete delivery flow."
|
|
1181
|
+
: "Docs Sync Report was not produced after the final Architect dispatch.";
|
|
1182
|
+
throw workflowError("WORKFLOW_COMPLETION_INVALID", `${reason} Required file: ${path.posix.join(input.handoffDir, "docs-sync-report.md")}, produced after dispatch #${baseline.sequence} (${baseline.confirmedAt}).`
|
|
1183
|
+
+ (wrongReport
|
|
1184
|
+
? ` ${path.posix.join(input.handoffDir, "docs-update-report.md")} was updated after this dispatch, but cannot replace docs-sync-report.md.`
|
|
1185
|
+
: ""), "Ask Architect to submit --kind docs-sync-report through .ai/tools/vcm-artifact with Decision: synced or unchanged and Correction Owner: none. Report blocked with its correction owner when synchronization is incomplete.");
|
|
1146
1186
|
}
|
|
1147
1187
|
if (docs.value !== "synced" && docs.value !== "unchanged") {
|
|
1148
1188
|
throw workflowError("WORKFLOW_COMPLETION_INVALID", `Docs Sync Report Decision must be synced or unchanged before completion; found ${docs.value ?? "missing"}.`);
|
|
@@ -18,6 +18,10 @@ ${renderRoleMemoryRules("project-manager")}
|
|
|
18
18
|
|
|
19
19
|
### User Communication
|
|
20
20
|
|
|
21
|
+
- Assistant text emitted before a tool call or role dispatch while the current Round continues is not user communication. Do not use intermediate text to deliver status, findings, decisions, questions, or required actions to the user.
|
|
22
|
+
- The user is informed only by PM's Round Final Reply when the Round pauses for user input or reaches a terminal result.
|
|
23
|
+
- The Round Final Reply must be self-contained. Restate every result, blocker, decision, risk, and required user action needed to understand the current state. Never rely on earlier PM text in the same Round.
|
|
24
|
+
- When asking the user a question, register it through \`vcm-ask-user\`, make the complete question and its necessary context the final reply, and end the turn.
|
|
21
25
|
- Explain task status, blockers, role results, and decisions in user-facing language.
|
|
22
26
|
- Prefer plain logic over code-level detail: describe what changed, why it matters, what risk remains, and what decision is needed.
|
|
23
27
|
- Exclude irrelevant implementation detail, but retain the technical facts needed to explain the cause, evidence, impact, and unresolved state. Plain language means translating technical detail, not deleting it.
|
|
@@ -77,7 +81,7 @@ PM owns task flow selection. Every user request that asks VCM to perform deliver
|
|
|
77
81
|
|
|
78
82
|
- After PM selects a flow, only its defined main path and explicitly allowed branches may be used.
|
|
79
83
|
- The active flow may change only through a transition explicitly defined by that flow or an explicit user instruction.
|
|
80
|
-
- Only the user may authorize any other flow change.
|
|
84
|
+
- Only the user may authorize any other flow change. A direct user instruction that explicitly authorizes the exact change is the authorization. Reuse that instruction verbatim and do not ask the user to confirm it again.
|
|
81
85
|
- A role request, role result, tool suggestion, requested next action, workload, task size, context size, difficulty, predicted risk, or PM discretion must not create an unlisted branch or change the active flow.
|
|
82
86
|
- If a role requests an unlisted branch or role handoff, keep the current flow active and return the incomplete work to the current responsible role.
|
|
83
87
|
- An incomplete, unrecognized, or non-standard role result returns to the same role for a valid result.
|
|
@@ -90,6 +94,8 @@ The main flow is:
|
|
|
90
94
|
|
|
91
95
|
\`Architect Interview and planning -> architecture-plan Gate -> Coder implementation -> Tester validation -> validation-adequacy Gate -> code-diff Gate -> Architect docs sync -> Final Acceptance -> completed\`
|
|
92
96
|
|
|
97
|
+
Require \`.ai/vcm/handoffs/docs-sync-report.md\` from the current Architect docs-sync dispatch. \`docs-update-report.md\` cannot replace it.
|
|
98
|
+
|
|
93
99
|
PM may leave this path only through the allowed branches below.
|
|
94
100
|
|
|
95
101
|
#### Allowed Branches
|
|
@@ -171,7 +177,7 @@ The shared path is:
|
|
|
171
177
|
|
|
172
178
|
#### Successful Exit
|
|
173
179
|
|
|
174
|
-
- For Architect Debug Flow, code-diff approval continues to \`Architect docs sync -> Final Acceptance\`.
|
|
180
|
+
- For Architect Debug Flow, code-diff approval continues to \`Architect docs sync -> Final Acceptance\`. Require \`.ai/vcm/handoffs/docs-sync-report.md\` from this docs-sync dispatch with \`Decision: synced\` or \`unchanged\` and \`Correction Owner: none\`; handle \`blocked\` through Docs Sync Correction.
|
|
175
181
|
- For Architect Debug Branch, code-diff approval returns to the recorded parent-flow resume point after the parent flow's validation and code-diff milestones. The branch does not run its own docs sync or Final Acceptance.
|
|
176
182
|
|
|
177
183
|
Architect Debug Flow or Branch never routes implementation to Coder. Architect executes Architect Debug Mode; PM owns whether the current context is a Flow or Branch and where it continues afterward.
|
|
@@ -207,7 +213,7 @@ Architecture Diagnosis Mode must run before another Debug Mode fix or Coder disp
|
|
|
207
213
|
|
|
208
214
|
- An analysis-only Architecture Diagnosis Flow completes from the diagnosis result.
|
|
209
215
|
- An analysis-only Architecture Diagnosis Branch returns to the recorded parent-flow resume point.
|
|
210
|
-
- A code-producing Architecture Diagnosis Flow continues after code-diff approval to \`Architect docs sync -> Final Acceptance\`.
|
|
216
|
+
- A code-producing Architecture Diagnosis Flow continues after code-diff approval to \`Architect docs sync -> Final Acceptance\`. Require \`.ai/vcm/handoffs/docs-sync-report.md\` from this docs-sync dispatch with \`Decision: synced\` or \`unchanged\` and \`Correction Owner: none\`; handle \`blocked\` through Docs Sync Correction.
|
|
211
217
|
- A code-producing Architecture Diagnosis Branch returns after code-diff approval to the recorded parent-flow resume point after the parent flow's validation and code-diff milestones. It does not run its own docs sync or Final Acceptance.
|
|
212
218
|
|
|
213
219
|
After Tester Failure, PM should summarize:
|
|
@@ -48,7 +48,13 @@ If VCM rejects the transition, remain in the current PM turn and choose a legal
|
|
|
48
48
|
|
|
49
49
|
## User Authorization
|
|
50
50
|
|
|
51
|
-
Only the user's explicit instruction may authorize a rejected transition.
|
|
51
|
+
Only the user's explicit instruction may authorize a rejected transition.
|
|
52
|
+
|
|
53
|
+
If the current user instruction already explicitly authorizes the exact rejected transition, reuse that instruction verbatim as \`Authorization Text\`. Do not ask the user to confirm it again.
|
|
54
|
+
|
|
55
|
+
If no such instruction exists, use \`vcm-ask-user\` with the exact authorization question, ask it, and wait.
|
|
56
|
+
|
|
57
|
+
Resubmit the unchanged transition with:
|
|
52
58
|
|
|
53
59
|
\`\`\`text
|
|
54
60
|
Authorization Text: <user's exact authorization>
|