vibe-coding-master 0.9.2 → 0.9.3
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 +3 -2
- package/dist/backend/adapters/command-runner.js +1 -0
- package/dist/backend/adapters/filesystem.js +5 -2
- package/dist/backend/api/artifact-routes.js +0 -3
- package/dist/backend/api/task-routes.js +0 -1
- package/dist/backend/cli/install-vcm-harness.js +6 -0
- package/dist/backend/server.js +2 -0
- package/dist/backend/services/artifact-service.js +1 -16
- package/dist/backend/services/docs-sync-check.js +32 -0
- package/dist/backend/services/gate-review-service.js +10 -2
- package/dist/backend/services/lsp-plugin.js +6 -2
- package/dist/backend/services/managed-artifact-validation.js +2 -1
- package/dist/backend/services/session-service.js +1 -1
- package/dist/backend/services/status-service.js +0 -1
- package/dist/backend/services/translation-worker-service.js +44 -6
- package/dist/backend/services/workflow-control-service.js +103 -165
- package/dist/backend/templates/handoff.js +2 -48
- package/dist/backend/templates/harness/architect-agent.js +42 -23
- package/dist/backend/templates/harness/claude-root.js +9 -9
- package/dist/backend/templates/harness/coder-agent.js +0 -1
- package/dist/backend/templates/harness/gate-review.js +55 -12
- package/dist/backend/templates/harness/project-manager-agent.js +33 -27
- package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +13 -14
- package/dist/backend/templates/harness/vcm-long-running-validation-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +2 -2
- package/dist/shared/types/gate-review.js +4 -0
- package/dist/shared/types/workflow.js +0 -1
- package/dist/shared/validation/artifact-check.js +1 -27
- package/dist/shared/validation/artifact-contract.js +0 -2
- package/dist/shared/validation/artifact-registry.js +1 -18
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs +5 -1
- package/scripts/harness-tools/check-docs-sync +246 -0
- package/scripts/harness-tools/check-durable-docs +24 -0
- package/scripts/harness-tools/run-long-check +3 -3
- package/scripts/verify-package.mjs +1 -0
|
@@ -3,9 +3,9 @@ export function renderVcmFinalAcceptanceSkillRules() {
|
|
|
3
3
|
|
|
4
4
|
Use this skill only when project-manager is ready to close a complete VCM code-delivery flow, including Architect Debug Flow or an Architecture Diagnosis Flow that produced code changes.
|
|
5
5
|
|
|
6
|
-
Do not use it for Docs-Only Flow, Validation-Only Flow, Communication-Only Flow, PR-Preparation Flow, analysis-only Diagnosis
|
|
6
|
+
Do not use it for Docs-Only Flow, Validation-Only Flow, Communication-Only Flow, PR-Preparation Flow, or analysis-only Diagnosis. After an Architect Debug Branch or Architecture Diagnosis Branch passes its code-diff Gate, use it for the parent flow the branch returned to, auditing the whole task; never for a branch that has not passed its code-diff Gate.
|
|
7
7
|
|
|
8
|
-
This skill is a final evidence audit. It does not replace architect
|
|
8
|
+
This skill is a final evidence audit. It does not replace architect documentation ownership, tester validation acceptance, coder implementation responsibility, or user approval for high-risk decisions.
|
|
9
9
|
|
|
10
10
|
Project-manager must not use this skill to perform technical design review, implementation review, source-code analysis, or test adequacy analysis. Missing or conflicting evidence must be routed to the responsible role.
|
|
11
11
|
|
|
@@ -17,11 +17,10 @@ Read the relevant task evidence before deciding:
|
|
|
17
17
|
- \`.ai/vcm/handoffs/architecture-plan.md\` when the task required architect planning
|
|
18
18
|
- \`.ai/vcm/handoffs/architecture-diagnosis.md\` when the flow used Architecture Diagnosis Mode
|
|
19
19
|
- \`.ai/vcm/handoffs/test-report.md\` when tester validation was required
|
|
20
|
-
- \`.ai/vcm/handoffs/docs-sync-report.md\` when durable docs could be affected
|
|
21
20
|
- \`.ai/vcm/handoffs/known-issues.md\` when unresolved findings were recorded
|
|
22
21
|
- \`.ai/vcm/gate-reviews/index.json\` and referenced Gate Review reports when Gate Reviews were required, skipped, or overridden
|
|
23
22
|
- current \`git status\` and changed file list
|
|
24
|
-
-
|
|
23
|
+
- the architecture plan's Docs Impact and the code-diff Gate's Documentation Fit result when durable docs could be affected
|
|
25
24
|
|
|
26
25
|
## Evidence Audit
|
|
27
26
|
|
|
@@ -29,7 +28,7 @@ Check whether the required role evidence exists, is current, and gives a clear r
|
|
|
29
28
|
|
|
30
29
|
Acceptable evidence must show:
|
|
31
30
|
|
|
32
|
-
- architect plan
|
|
31
|
+
- architect plan or architecture diagnosis when required by the completed flow
|
|
33
32
|
- tester terminal \`Test Result: pass|fail\` and validation evidence when code, behavior, tests, or generated context changed; \`incomplete\` is not acceptance evidence
|
|
34
33
|
- required Gate Review decisions, skip reasons, or override reasons when Gate Reviews were enabled
|
|
35
34
|
- known-issues disposition when unresolved findings were recorded
|
|
@@ -43,7 +42,7 @@ Review the changed file list only, then classify files:
|
|
|
43
42
|
|
|
44
43
|
- expected files: directly named by the user request, route message, durable plan, architecture plan, or architecture diagnosis
|
|
45
44
|
- supporting files: tests, fixtures, generated context, docs, or wiring needed for expected files
|
|
46
|
-
- approved deviations: files explained by Replan, tester follow-up, docs
|
|
45
|
+
- approved deviations: files explained by Replan, a Debug or Diagnosis fix, tester follow-up, a post-validation docs correction, or explicit user approval
|
|
47
46
|
- unexplained files: files with no traceable reason in the task evidence
|
|
48
47
|
- high-risk unexpected files: auth, permissions, payment, billing, schema, migrations, data deletion, secrets, dependencies, lockfiles, broad generated artifacts, or broad formatting churn
|
|
49
48
|
|
|
@@ -61,9 +60,10 @@ Check:
|
|
|
61
60
|
- tester report records terminal \`Test Result: pass|fail\`, validation commands, results, and skipped checks with reasons; do not accept \`Test Result: incomplete\`
|
|
62
61
|
- required Gate Reviews are approved, or skipped/overridden through a VCM-recorded user action
|
|
63
62
|
- Gate Review enable state is confirmed authoritatively: do not infer that no Gate Reviews were required from an absent or empty \`.ai/vcm/gate-reviews/index.json\`. When Gate Review is enabled, a missing index or a required gate without a recorded decision means the gate was skipped — run the matching command from the \`vcm-gate-review\` skill, including the code source for \`code-diff\`, and do not accept until each required gate returns \`approve\`/\`already_approved\`, \`disabled\`/\`not_required\`, or a VCM-recorded user skip/override
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
63
|
+
- the code-diff Gate approval covers the current HEAD commit, so every doc correction and follow-up commit was reviewed; a commit made after the approval requires rerunning the code-diff Gate before acceptance
|
|
64
|
+
- \`.ai/tools/check-durable-docs\` passes when durable docs changed
|
|
65
|
+
- any cross-document inconsistency was resolved by the owning role
|
|
66
|
+
- known issues are either resolved, promoted to durable docs by architect, or explicitly accepted by the user; when \`docs/known-issues.md\` still needs a promotion or removal, decide \`needs-architect-follow-up\`
|
|
67
67
|
- temporary task state is ready to clean after durable facts are promoted
|
|
68
68
|
|
|
69
69
|
## Decisions
|
|
@@ -74,10 +74,9 @@ Choose exactly one:
|
|
|
74
74
|
- accepted-with-known-risks
|
|
75
75
|
- needs-coder-follow-up
|
|
76
76
|
- needs-architect-follow-up
|
|
77
|
-
- needs-docs-sync
|
|
78
77
|
- blocked-by-user-decision
|
|
79
78
|
|
|
80
|
-
Do not accept when required role evidence is missing, required Gate Review evidence is missing, tester findings are unresolved,
|
|
79
|
+
Do not accept when required role evidence is missing, required Gate Review evidence is missing, tester findings are unresolved, the code-diff approval does not cover the current HEAD, the durable-doc audit failed or is missing after durable-doc changes, known-issues disposition is missing, or unexplained high-risk files remain.
|
|
81
80
|
|
|
82
81
|
## Output
|
|
83
82
|
|
|
@@ -94,7 +93,7 @@ Use this structure:
|
|
|
94
93
|
|
|
95
94
|
## Decision
|
|
96
95
|
|
|
97
|
-
accepted|accepted-with-known-risks|needs-coder-follow-up|needs-architect-follow-up|
|
|
96
|
+
accepted|accepted-with-known-risks|needs-coder-follow-up|needs-architect-follow-up|blocked-by-user-decision
|
|
98
97
|
|
|
99
98
|
## Evidence Reviewed
|
|
100
99
|
|
|
@@ -112,7 +111,7 @@ accepted|accepted-with-known-risks|needs-coder-follow-up|needs-architect-follow-
|
|
|
112
111
|
|
|
113
112
|
## Validation Summary
|
|
114
113
|
|
|
115
|
-
## Review And
|
|
114
|
+
## Review And Documentation
|
|
116
115
|
|
|
117
116
|
## Known Issues Disposition
|
|
118
117
|
|
|
@@ -123,7 +122,7 @@ accepted|accepted-with-known-risks|needs-coder-follow-up|needs-architect-follow-
|
|
|
123
122
|
## Final User Summary
|
|
124
123
|
\`\`\`
|
|
125
124
|
|
|
126
|
-
Each rewrite must be a complete, self-contained snapshot of the current acceptance decision and supporting evidence. Restate every still-relevant evidence result, file classification, validation result, Gate decision,
|
|
125
|
+
Each rewrite must be a complete, self-contained snapshot of the current acceptance decision and supporting evidence. Restate every still-relevant evidence result, file classification, validation result, Gate decision, documentation result, known-issues disposition, cleanup result, risk, and next action; do not rely on a prior acceptance revision, route message, Session, or transcript.
|
|
127
126
|
|
|
128
127
|
The final user summary should be concise and include files changed, validation, docs updates, open risks, and next action.
|
|
129
128
|
`;
|
|
@@ -9,11 +9,11 @@ The only sanctioned long-running mechanism is \`.ai/tools/run-long-check\` plus
|
|
|
9
9
|
|
|
10
10
|
Each run-long-check or watch-job invocation must be the only top-level command in its Bash tool call. Do not pipe it, append or prepend another command, place it in a conditional chain or subshell, capture it with command substitution, or invoke it through a shell command string. Ordinary redirection is allowed. A directly passed script such as \`-- bash /tmp/check.sh\` is allowed; \`-- bash -c '...'\` is not.
|
|
11
11
|
|
|
12
|
-
The hard ceiling is
|
|
12
|
+
The hard ceiling is 100 minutes per job, enforced by the job worker itself. No approval can raise this ceiling; split larger operations into jobs that each fit within it.
|
|
13
13
|
|
|
14
14
|
## Protocol
|
|
15
15
|
|
|
16
|
-
1. Start the command with an explicit ceiling: \`.ai/tools/run-long-check --timeout <duration> -- <command>\`. Pass the validation executable and its arguments directly. Do not use a shell command-string wrapper, pipeline its output, or append another command: run-long-check already captures stdout/stderr, and shell wrappers can hide the validation exit code. Pick the ceiling from \`docs/TESTING.md\` guidance or a realistic estimate, never above
|
|
16
|
+
1. Start the command with an explicit ceiling: \`.ai/tools/run-long-check --timeout <duration> -- <command>\`. Pass the validation executable and its arguments directly. Do not use a shell command-string wrapper, pipeline its output, or append another command: run-long-check already captures stdout/stderr, and shell wrappers can hide the validation exit code. Pick the ceiling from \`docs/TESTING.md\` guidance or a realistic estimate, never above 100m. The tool prints the job id and creates job state under \`.ai/vcm/jobs/<job-id>/\`.
|
|
17
17
|
2. In the same turn, run \`.ai/tools/watch-job <job-id>\` as its own Bash tool call. The default watch window is 8 minutes.
|
|
18
18
|
3. If watch-job exits 125, the job is still running and a bounded handoff is active for the next watcher: run \`.ai/tools/watch-job <job-id>\` again immediately. Do not end the turn between windows.
|
|
19
19
|
4. Repeat until watch-job reports a terminal result.
|
|
@@ -65,7 +65,7 @@ VCM binds that authorization to this exact task state, flow, target role, eviden
|
|
|
65
65
|
|
|
66
66
|
## User-Approved Post-Validation Work
|
|
67
67
|
|
|
68
|
-
When Tester has returned \`pass\`, validation-adequacy and code-diff are
|
|
68
|
+
When Tester has returned \`pass\`, the validation-adequacy and code-diff Gates are successful, Final Acceptance has not routed a follow-up, and the user explicitly approves additional Tester-owned work in the current task, propose Tester again with:
|
|
69
69
|
|
|
70
70
|
\`\`\`text
|
|
71
71
|
Authorization Text: none
|
|
@@ -73,7 +73,7 @@ Violated Rule: none
|
|
|
73
73
|
Approval Text: <user's exact approval>
|
|
74
74
|
\`\`\`
|
|
75
75
|
|
|
76
|
-
This is a normal one-time follow-up approval, not a Workflow Override. Do not use it before
|
|
76
|
+
This is a normal one-time follow-up approval, not a Workflow Override. Do not use it before those Gates are successful, for required unresolved coverage, for another owner, or without the user's exact approval. After Tester completes the approved work, rerun Tester validation and every invalidated Gate.
|
|
77
77
|
|
|
78
78
|
## Completion
|
|
79
79
|
|
|
@@ -5,6 +5,7 @@ export const GATE_REVIEW_GATES = [
|
|
|
5
5
|
];
|
|
6
6
|
export const CODE_DIFF_FINDING_SCOPES = [
|
|
7
7
|
"test-only",
|
|
8
|
+
"docs-only",
|
|
8
9
|
"implementation"
|
|
9
10
|
];
|
|
10
11
|
export const CODE_DIFF_SOURCES = [
|
|
@@ -12,3 +13,6 @@ export const CODE_DIFF_SOURCES = [
|
|
|
12
13
|
"architect-debug",
|
|
13
14
|
"architect-diagnosis"
|
|
14
15
|
];
|
|
16
|
+
// Prefixes the code-diff error recorded when the Docs Impact coverage check
|
|
17
|
+
// refuses to start the Gate; Workflow Control routes it to Architect.
|
|
18
|
+
export const CODE_DIFF_DOCS_COVERAGE_ERROR_PREFIX = "Docs coverage check failed:";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE,
|
|
1
|
+
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
2
2
|
import { getArtifactDefinition } from "./artifact-registry.js";
|
|
3
3
|
const PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|Not run yet\.?|status:\s*draft)\s*(\n|$)/i;
|
|
4
4
|
export function checkMarkdownArtifact(kind, artifactPath, content, options = {}) {
|
|
@@ -260,32 +260,6 @@ function validateArtifactFields(kind, content, mode) {
|
|
|
260
260
|
}
|
|
261
261
|
return invalidFields;
|
|
262
262
|
}
|
|
263
|
-
if (kind === "docs-sync-report") {
|
|
264
|
-
const invalidFields = validateDecision(content, DOCS_REPORT_DECISIONS);
|
|
265
|
-
const decision = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
|
|
266
|
-
const owner = readArtifactSectionContent(content, "Correction Owner")?.trim().toLowerCase();
|
|
267
|
-
const evidence = readArtifactSectionContent(content, "Correction Evidence");
|
|
268
|
-
if (!isAllowedValue(owner, DOCS_SYNC_CORRECTION_OWNERS)) {
|
|
269
|
-
invalidFields.push(renderExactSectionError("Correction Owner", DOCS_SYNC_CORRECTION_OWNERS.join("|"), owner));
|
|
270
|
-
}
|
|
271
|
-
if (decision === "synced" || decision === "unchanged") {
|
|
272
|
-
if (owner !== "none") {
|
|
273
|
-
invalidFields.push(renderExactSectionError("Correction Owner", "none", owner, `when Decision is ${decision}`));
|
|
274
|
-
}
|
|
275
|
-
if (!isExactNone(evidence)) {
|
|
276
|
-
invalidFields.push(renderExactSectionError("Correction Evidence", STRICT_NONE_VALUE, evidence, `when Decision is ${decision}`));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (decision === "blocked") {
|
|
280
|
-
if (!owner || owner === "none") {
|
|
281
|
-
invalidFields.push("Correction Owner must be architect, coder, or tester when Decision is blocked.");
|
|
282
|
-
}
|
|
283
|
-
if (!hasSubstantiveSectionValue(evidence)) {
|
|
284
|
-
invalidFields.push("Correction Evidence must identify the unresolved documentation correction when Decision is blocked.");
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return invalidFields;
|
|
288
|
-
}
|
|
289
263
|
if (kind === "final-acceptance") {
|
|
290
264
|
return validateDecision(content, FINAL_ACCEPTANCE_DECISIONS);
|
|
291
265
|
}
|
|
@@ -32,13 +32,11 @@ export const L3_ACTIONS = ["run-existing", "updated", "added"];
|
|
|
32
32
|
export const DOCS_REPORT_DECISIONS = ["synced", "unchanged", "blocked"];
|
|
33
33
|
export const DOCS_UPDATE_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/;
|
|
34
34
|
export const DOCS_UPDATE_COMMIT_RULE = "When Decision is synced, Commit must contain only one bare lowercase hexadecimal commit hash (7-40 characters, for example abc1234) on a single line. No backticks, bullets, links, subjects, or extra lines; put explanations in Evidence Reviewed. An unchanged result does not require a new commit.";
|
|
35
|
-
export const DOCS_SYNC_CORRECTION_OWNERS = ["none", "architect", "coder", "tester"];
|
|
36
35
|
export const FINAL_ACCEPTANCE_DECISIONS = [
|
|
37
36
|
"accepted",
|
|
38
37
|
"accepted-with-known-risks",
|
|
39
38
|
"needs-coder-follow-up",
|
|
40
39
|
"needs-architect-follow-up",
|
|
41
|
-
"needs-docs-sync",
|
|
42
40
|
"blocked-by-user-decision"
|
|
43
41
|
];
|
|
44
42
|
export function renderArtifactOptions(values) {
|
|
@@ -172,23 +172,6 @@ export const ARTIFACT_DEFINITIONS = [
|
|
|
172
172
|
"Decision"
|
|
173
173
|
]
|
|
174
174
|
},
|
|
175
|
-
{
|
|
176
|
-
kind: "docs-sync-report",
|
|
177
|
-
fileName: "docs-sync-report.md",
|
|
178
|
-
owner: "architect",
|
|
179
|
-
requiredHeadings: [
|
|
180
|
-
"Summary",
|
|
181
|
-
"Architecture Drift Check",
|
|
182
|
-
"Docs Updated",
|
|
183
|
-
"Docs Reviewed And Left Unchanged",
|
|
184
|
-
"Public Contract / Module Boundary Notes",
|
|
185
|
-
"Remaining Documentation Risks",
|
|
186
|
-
"Known Issues Disposition",
|
|
187
|
-
"Correction Owner",
|
|
188
|
-
"Correction Evidence",
|
|
189
|
-
"Decision"
|
|
190
|
-
]
|
|
191
|
-
},
|
|
192
175
|
{
|
|
193
176
|
kind: "workflow-progress",
|
|
194
177
|
fileName: "workflow-progress.md",
|
|
@@ -209,7 +192,7 @@ export const ARTIFACT_DEFINITIONS = [
|
|
|
209
192
|
"Evidence Reviewed",
|
|
210
193
|
"Scope Traceability",
|
|
211
194
|
"Validation Summary",
|
|
212
|
-
"Review And
|
|
195
|
+
"Review And Documentation",
|
|
213
196
|
"Known Issues Disposition",
|
|
214
197
|
"Gate Review Gates",
|
|
215
198
|
"Cleanup Readiness",
|
package/package.json
CHANGED
|
@@ -265,5 +265,9 @@ if (separator >= 0 && process.argv[1] && realpathSync(process.argv[1]) === fileU
|
|
|
265
265
|
process.stderr.write("usage: vcm-lsp-proxy.mjs -- <server-command> [args...]\n");
|
|
266
266
|
process.exit(2);
|
|
267
267
|
}
|
|
268
|
-
startProxy({ command, args, input: process.stdin, output: process.stdout, errorOutput: process.stderr });
|
|
268
|
+
const { server } = startProxy({ command, args, input: process.stdin, output: process.stdout, errorOutput: process.stderr });
|
|
269
|
+
// Exit with the server so Claude Code sees the crash and restarts it; an open
|
|
270
|
+
// stdin would otherwise keep the proxy alive with no server behind it.
|
|
271
|
+
server.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
272
|
+
server.on("error", () => process.exit(1));
|
|
269
273
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Check that the architecture plan's Docs Impact covers every module the task
|
|
4
|
+
// touched. Without .ai/generated/module-index.json the repository is one module
|
|
5
|
+
// named "." whose owning doc is docs/ARCHITECTURE.md (or its docs/architecture/
|
|
6
|
+
// area docs).
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// .ai/tools/check-docs-sync [--base <commit>] [--json]
|
|
10
|
+
//
|
|
11
|
+
// The check compares the task's commits (base..HEAD, excluding [VCM Harness]
|
|
12
|
+
// commits) with .ai/generated/module-index.json and the Docs Impact table of
|
|
13
|
+
// .ai/vcm/handoffs/architecture-plan.md:
|
|
14
|
+
// - every module touched by a code change had its owning architecture doc
|
|
15
|
+
// changed in the task or has a Docs Impact row for that doc;
|
|
16
|
+
// - a Docs Impact row that records no change gives its reason;
|
|
17
|
+
// - every document Docs Impact records as changed was changed.
|
|
18
|
+
// A task without a complete architecture plan (an Architect Debug Flow or an
|
|
19
|
+
// Architecture Diagnosis Flow) is skipped.
|
|
20
|
+
// The base defaults to the merge-base of HEAD with the base repository's HEAD.
|
|
21
|
+
|
|
22
|
+
const childProcess = require("node:child_process");
|
|
23
|
+
const fs = require("node:fs");
|
|
24
|
+
const path = require("node:path");
|
|
25
|
+
|
|
26
|
+
const HARNESS_COMMIT_PREFIX = "[VCM Harness] ";
|
|
27
|
+
const PLAN_PATH = ".ai/vcm/handoffs/architecture-plan.md";
|
|
28
|
+
const MODULE_INDEX_PATH = ".ai/generated/module-index.json";
|
|
29
|
+
const PROJECT_DOC = "docs/ARCHITECTURE.md";
|
|
30
|
+
const AREA_DOC_DIR = "docs/architecture/";
|
|
31
|
+
const USAGE = "usage: check-docs-sync [-h] [--base BASE] [--json]";
|
|
32
|
+
|
|
33
|
+
function usageError(message) {
|
|
34
|
+
process.stderr.write(USAGE + "\ncheck-docs-sync: error: " + message + "\n");
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const args = { base: undefined, json: false };
|
|
40
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
41
|
+
const argument = argv[index];
|
|
42
|
+
if (argument === "-h" || argument === "--help") {
|
|
43
|
+
process.stdout.write(USAGE + "\n\nCheck that the architecture plan's Docs Impact covers every touched module.\n");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
} else if (argument === "--json") {
|
|
46
|
+
args.json = true;
|
|
47
|
+
} else if (argument === "--base") {
|
|
48
|
+
if (index + 1 >= argv.length) {
|
|
49
|
+
usageError("argument " + argument + ": expected one argument");
|
|
50
|
+
}
|
|
51
|
+
index += 1;
|
|
52
|
+
args.base = argv[index];
|
|
53
|
+
} else {
|
|
54
|
+
usageError("unrecognized arguments: " + argument);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function git(root, args) {
|
|
61
|
+
const result = childProcess.spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
62
|
+
return result.status === 0 ? result.stdout.trim() : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function repositoryRoot() {
|
|
66
|
+
return git(process.cwd(), ["rev-parse", "--show-toplevel"]) || process.cwd();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The task branch forks from the base repository's checked-out branch, which
|
|
70
|
+
// is the main worktree sharing this repository's git directory.
|
|
71
|
+
function resolveBase(root, explicit) {
|
|
72
|
+
if (explicit) {
|
|
73
|
+
return git(root, ["rev-parse", "--verify", explicit + "^{commit}"]) || undefined;
|
|
74
|
+
}
|
|
75
|
+
const fromEnv = (process.env.VCM_BASE_COMMIT || "").trim();
|
|
76
|
+
if (fromEnv && git(root, ["rev-parse", "--verify", fromEnv + "^{commit}"])) {
|
|
77
|
+
return fromEnv;
|
|
78
|
+
}
|
|
79
|
+
const commonDir = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
80
|
+
const baseHead = commonDir ? git(root, ["--git-dir", commonDir, "rev-parse", "HEAD"]) : "";
|
|
81
|
+
const head = git(root, ["rev-parse", "HEAD"]);
|
|
82
|
+
if (baseHead && baseHead !== head) {
|
|
83
|
+
return git(root, ["merge-base", "HEAD", baseHead]) || undefined;
|
|
84
|
+
}
|
|
85
|
+
const upstream = git(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
86
|
+
return upstream ? git(root, ["merge-base", "HEAD", upstream]) || undefined : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function taskCommits(root, base) {
|
|
90
|
+
const lines = git(root, ["log", "--format=%H%x09%s", base + "..HEAD"]).split("\n").filter(Boolean);
|
|
91
|
+
return lines
|
|
92
|
+
.map((line) => {
|
|
93
|
+
const separator = line.indexOf("\t");
|
|
94
|
+
return { sha: line.slice(0, separator), subject: line.slice(separator + 1) };
|
|
95
|
+
})
|
|
96
|
+
.filter((commit) => !commit.subject.startsWith(HARNESS_COMMIT_PREFIX))
|
|
97
|
+
.map((commit) => ({
|
|
98
|
+
...commit,
|
|
99
|
+
files: git(root, ["show", "--format=", "--name-only", "--no-renames", commit.sha]).split("\n").filter(Boolean)
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isArchitectureDoc(file) {
|
|
104
|
+
return file === PROJECT_DOC || file.startsWith(AREA_DOC_DIR) || path.posix.basename(file) === "ARCHITECTURE.md";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Files that describe or derive from code rather than change it.
|
|
108
|
+
function isDocumentationFile(file) {
|
|
109
|
+
return isArchitectureDoc(file) || file.startsWith("docs/") || file.startsWith(".ai/") || /\.md$/i.test(file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The repository as one module, for projects without a module index (the
|
|
113
|
+
// generator supports Cargo and npm workspaces only).
|
|
114
|
+
const ROOT_MODULE = { name: ".", path: ".", doc: PROJECT_DOC };
|
|
115
|
+
|
|
116
|
+
function loadModules(root) {
|
|
117
|
+
const target = path.join(root, MODULE_INDEX_PATH);
|
|
118
|
+
if (!fs.existsSync(target)) {
|
|
119
|
+
return [ROOT_MODULE];
|
|
120
|
+
}
|
|
121
|
+
const index = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
122
|
+
return (index.layers || []).flatMap((layer) => layer.modules || []).map((module) => ({
|
|
123
|
+
name: module.name,
|
|
124
|
+
path: module.path || ".",
|
|
125
|
+
doc: module.path && module.path !== "." ? module.architectureDoc || module.path + "/ARCHITECTURE.md" : PROJECT_DOC
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function owningModule(modules, file) {
|
|
130
|
+
let best;
|
|
131
|
+
for (const module of modules) {
|
|
132
|
+
const prefix = module.path === "." ? "" : module.path + "/";
|
|
133
|
+
if ((prefix === "" || file.startsWith(prefix)) && (!best || prefix.length > best.prefix.length)) {
|
|
134
|
+
best = { module, prefix };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return best && best.module;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sectionContent(content, heading) {
|
|
141
|
+
const match = new RegExp("^#{1,6}\\s+" + heading.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&") + "\\s*$", "im").exec(content);
|
|
142
|
+
if (!match) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const after = content.slice(match.index + match[0].length);
|
|
146
|
+
const next = /\n#{1,6}\s+\S/.exec(after);
|
|
147
|
+
return (next ? after.slice(0, next.index) : after).replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function tableRows(section) {
|
|
151
|
+
const lines = (section || "").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("|"));
|
|
152
|
+
return lines.slice(2).map((line) => line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim().replace(/^`|`$/g, "")));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function finding(code, subject, message) {
|
|
156
|
+
return { code, subject, message };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function planIsComplete(content) {
|
|
160
|
+
return /^Planning Result:\s*complete\s*$/im.test(content);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A module's doc is the project doc or one of its area docs for the root module.
|
|
164
|
+
function documentsModule(module, document) {
|
|
165
|
+
return module.doc === PROJECT_DOC
|
|
166
|
+
? document === PROJECT_DOC || document.startsWith(AREA_DOC_DIR)
|
|
167
|
+
: document === module.doc;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function check(root, planContent, base) {
|
|
171
|
+
const findings = [];
|
|
172
|
+
const modules = loadModules(root);
|
|
173
|
+
const commits = taskCommits(root, base);
|
|
174
|
+
const changed = new Set(commits.flatMap((commit) => commit.files));
|
|
175
|
+
|
|
176
|
+
const touched = new Map();
|
|
177
|
+
for (const file of changed) {
|
|
178
|
+
if (isDocumentationFile(file)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const module = owningModule(modules, file);
|
|
182
|
+
if (module) {
|
|
183
|
+
touched.set(module.path, module);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rows = tableRows(sectionContent(planContent, "Docs Impact")).map(([documentCell, change, reason]) => ({
|
|
188
|
+
document: (documentCell || "").split(/\s+/)[0],
|
|
189
|
+
change: change || "",
|
|
190
|
+
reason: reason || ""
|
|
191
|
+
}));
|
|
192
|
+
const docChanged = (module) => [...changed].some((file) => documentsModule(module, file));
|
|
193
|
+
|
|
194
|
+
for (const module of touched.values()) {
|
|
195
|
+
if (!docChanged(module) && !rows.some((row) => documentsModule(module, row.document))) {
|
|
196
|
+
findings.push(finding("DS_MODULE_NOT_COVERED", module.path,
|
|
197
|
+
"Module " + module.name + " (" + module.path + ") changed in the task, but neither its owning doc " + module.doc
|
|
198
|
+
+ " changed nor does Docs Impact list it with the reason no owned fact changed."));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const row of rows) {
|
|
203
|
+
if (!/\.md$/i.test(row.document)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const unchanged = !row.change || /^(none|unchanged|no change)\b/i.test(row.change);
|
|
207
|
+
if (unchanged && !/\w/.test(row.reason.replace(/^(none|tbd)\.?$/i, ""))) {
|
|
208
|
+
findings.push(finding("DS_UNCHANGED_WITHOUT_REASON", row.document,
|
|
209
|
+
"Docs Impact records no change to " + row.document + " without the reason no owned fact changed."));
|
|
210
|
+
} else if (!unchanged && !changed.has(row.document)) {
|
|
211
|
+
findings.push(finding("DS_PLANNED_DOC_NOT_CHANGED", row.document,
|
|
212
|
+
"Docs Impact records a change to " + row.document + ", but the task did not change it."));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return findings;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function main() {
|
|
219
|
+
const args = parseArgs(process.argv.slice(2));
|
|
220
|
+
const root = repositoryRoot();
|
|
221
|
+
const planPath = path.join(root, PLAN_PATH);
|
|
222
|
+
const plan = fs.existsSync(planPath) ? fs.readFileSync(planPath, "utf8") : "";
|
|
223
|
+
if (!planIsComplete(plan)) {
|
|
224
|
+
const reason = "No complete " + PLAN_PATH + "; the task has no Docs Impact to check.";
|
|
225
|
+
process.stdout.write(args.json ? JSON.stringify({ skipped: reason, findings: [] }, null, 2) + "\n" : reason + "\n");
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
const base = resolveBase(root, args.base);
|
|
229
|
+
if (!base) {
|
|
230
|
+
process.stderr.write("check-docs-sync: cannot determine the task base commit; pass --base <commit>.\n");
|
|
231
|
+
return 2;
|
|
232
|
+
}
|
|
233
|
+
const findings = check(root, plan, base);
|
|
234
|
+
if (args.json) {
|
|
235
|
+
process.stdout.write(JSON.stringify({ base, findings }, null, 2) + "\n");
|
|
236
|
+
} else if (findings.length === 0) {
|
|
237
|
+
process.stdout.write("Docs sync check passed.\n");
|
|
238
|
+
} else {
|
|
239
|
+
for (const item of findings) {
|
|
240
|
+
process.stdout.write(item.code + " " + item.subject + ": " + item.message + "\n");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return findings.length === 0 ? 0 : 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
process.exitCode = main();
|
|
@@ -303,6 +303,29 @@ function checkGeneratedArchitectureFacts(root, findings) {
|
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// Area docs under docs/architecture/ split the project doc's detailed design,
|
|
307
|
+
// so docs/ARCHITECTURE.md must link each one.
|
|
308
|
+
function checkAreaDocLinks(root, findings) {
|
|
309
|
+
const areaDir = path.join(root, "docs/architecture");
|
|
310
|
+
let entries;
|
|
311
|
+
try {
|
|
312
|
+
entries = fs.readdirSync(areaDir, { withFileTypes: true });
|
|
313
|
+
} catch {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const projectDoc = path.join(root, "docs/ARCHITECTURE.md");
|
|
317
|
+
const projectText = isFile(projectDoc) ? fs.readFileSync(projectDoc, "utf8") : "";
|
|
318
|
+
for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : 1))) {
|
|
319
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (!projectText.includes("architecture/" + entry.name)) {
|
|
323
|
+
findings.push(finding("DD_AREA_DOC_UNLINKED", "docs/architecture/" + entry.name, 1,
|
|
324
|
+
"Area architecture doc is not linked from docs/ARCHITECTURE.md."));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
306
329
|
function main(argv) {
|
|
307
330
|
const args = parseArgs(argv);
|
|
308
331
|
const root = repositoryRoot();
|
|
@@ -312,6 +335,7 @@ function main(argv) {
|
|
|
312
335
|
checkArchitectureHistory(root, findings);
|
|
313
336
|
checkTestingHistory(root, findings);
|
|
314
337
|
checkGeneratedArchitectureFacts(root, findings);
|
|
338
|
+
checkAreaDocLinks(root, findings);
|
|
315
339
|
findings.sort((left, right) =>
|
|
316
340
|
left.path < right.path ? -1 : left.path > right.path ? 1
|
|
317
341
|
: left.line !== right.line ? left.line - right.line
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// wrappers are rejected because they can hide the validation command's exit code.
|
|
10
10
|
//
|
|
11
11
|
// The command runs in a detached worker process group. The worker itself
|
|
12
|
-
// enforces the job ceiling (--timeout, max
|
|
12
|
+
// enforces the job ceiling (--timeout, max 100m) and a supervision lease: when
|
|
13
13
|
// no foreground watcher renews .ai/vcm/jobs/<job-id>/lease, the worker kills
|
|
14
14
|
// the command process group and records the job as orphaned. Watch the job
|
|
15
15
|
// with .ai/tools/watch-job in the same turn.
|
|
@@ -22,7 +22,7 @@ const fs = require("node:fs");
|
|
|
22
22
|
const os = require("node:os");
|
|
23
23
|
const path = require("node:path");
|
|
24
24
|
|
|
25
|
-
const MAX_TIMEOUT_SECONDS =
|
|
25
|
+
const MAX_TIMEOUT_SECONDS = 100 * 60;
|
|
26
26
|
const DEFAULT_TIMEOUT_SECONDS = MAX_TIMEOUT_SECONDS;
|
|
27
27
|
const QUEUED_STALE_SECONDS = 60;
|
|
28
28
|
const ACTIVE_STATUSES = new Set(["queued", "starting", "running"]);
|
|
@@ -584,7 +584,7 @@ async function main(argv) {
|
|
|
584
584
|
return 2;
|
|
585
585
|
}
|
|
586
586
|
if (timeoutSeconds > MAX_TIMEOUT_SECONDS) {
|
|
587
|
-
process.stderr.write("error: timeout exceeds maximum
|
|
587
|
+
process.stderr.write("error: timeout exceeds maximum 100m; split the work or get user approval\n");
|
|
588
588
|
return 2;
|
|
589
589
|
}
|
|
590
590
|
|
|
@@ -9,6 +9,7 @@ const requiredFiles = [
|
|
|
9
9
|
"scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json",
|
|
10
10
|
"scripts/claude-plugins/vcm-lsp-bridge/vcm-lsp-proxy.mjs",
|
|
11
11
|
"scripts/harness-tools/check-durable-docs",
|
|
12
|
+
"scripts/harness-tools/check-docs-sync",
|
|
12
13
|
"scripts/harness-tools/generate-module-index",
|
|
13
14
|
"scripts/harness-tools/generate-public-surface",
|
|
14
15
|
"scripts/harness-tools/run-long-check",
|