vibe-coding-master 0.8.12 → 0.9.0

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 +7 -3
  2. package/dist/backend/cli/install-vcm-harness.js +57 -18
  3. package/dist/backend/gateway/gateway-service.js +5 -3
  4. package/dist/backend/runtime/terminal-submit.js +1 -0
  5. package/dist/backend/server.js +14 -4
  6. package/dist/backend/services/app-settings-service.js +3 -2
  7. package/dist/backend/services/auto-memory-service.js +6 -4
  8. package/dist/backend/services/claude-hook-service.js +6 -0
  9. package/dist/backend/services/claude-transcript-service.js +2 -13
  10. package/dist/backend/services/claude-version-check.js +46 -0
  11. package/dist/backend/services/gate-review-service.js +47 -8
  12. package/dist/backend/services/harness-service.js +41 -20
  13. package/dist/backend/services/message-service.js +30 -6
  14. package/dist/backend/services/pm-user-question.js +2 -1
  15. package/dist/backend/services/project-service.js +8 -2
  16. package/dist/backend/services/runtime-recovery-service.js +36 -9
  17. package/dist/backend/services/session-service.js +10 -19
  18. package/dist/backend/services/task-close-service.js +1 -0
  19. package/dist/backend/services/terminal-process-exit-service.js +4 -1
  20. package/dist/backend/services/workflow-control-service.js +55 -11
  21. package/dist/backend/templates/harness/architect-agent.js +2 -1
  22. package/dist/backend/templates/harness/architect-evidence-worker-agent.js +2 -1
  23. package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +8 -2
  24. package/dist/backend/templates/harness/architect-validation-worker-agent.js +2 -1
  25. package/dist/backend/templates/harness/architect-worker-frontmatter.js +33 -0
  26. package/dist/backend/templates/harness/check-scaffold-ledger.js +480 -511
  27. package/dist/backend/templates/harness/coder-worker-agent.js +1 -0
  28. package/dist/backend/templates/harness/gate-review.js +676 -643
  29. package/dist/backend/templates/harness/node-tool-runtime.js +102 -0
  30. package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +49 -55
  31. package/dist/backend/templates/harness/restart-architect-skill.js +49 -58
  32. package/dist/backend/templates/harness/vcm-ask-user-skill.js +57 -59
  33. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +4 -0
  34. package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +6 -2
  35. package/dist/backend/templates/harness/vcm-task-state-skill.js +66 -72
  36. package/dist/shared/types/app-settings.js +31 -0
  37. package/dist-frontend/assets/index-CShYYQQa.css +32 -0
  38. package/dist-frontend/assets/{index-DqANomt1.js → index-DOvvS76Q.js} +41 -41
  39. package/dist-frontend/index.html +2 -2
  40. package/package.json +1 -1
  41. package/scripts/harness-tools/check-durable-docs +332 -298
  42. package/scripts/harness-tools/generate-module-index +1047 -512
  43. package/scripts/harness-tools/generate-public-surface +1231 -1069
  44. package/scripts/harness-tools/package.json +3 -0
  45. package/scripts/harness-tools/run-long-check +608 -531
  46. package/scripts/harness-tools/vcm-artifact +181 -113
  47. package/scripts/harness-tools/vcm-bash-guard +839 -605
  48. package/scripts/harness-tools/vcm-subagent-guard +67 -63
  49. package/scripts/harness-tools/watch-job +313 -238
  50. package/dist-frontend/assets/index-xgiQeUD5.css +0 -32
@@ -1,516 +1,485 @@
1
+ import { NODE_TOOL_ARGUMENT_PARSER, renderNodeToolHeader } from "./node-tool-runtime.js";
1
2
  export function renderCheckScaffoldLedgerTool() {
2
- return `#!/usr/bin/env python3
3
- """Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.
4
-
5
- The architecture plan's Scaffold Manifest is an item ledger: one entry per implementation
6
- item, one unique ID per entry, and exactly one \`VCM:CODE <ID>\` marker in the tree per
7
- \`create\`/\`change\`/\`delete\` entry during scaffolding. Coder then removes successful
8
- markers and preserves failed markers. The required \`--mode\` selects that lifecycle:
9
-
10
- - \`scaffold\`: require the ledger and tree markers to form an exact bijection;
11
- - \`completion\`: reconcile the ledger, Coder's Scaffold Completion table, and the
12
- final tree marker state.
13
-
14
- The tool checks:
15
-
16
- 1. the ledger header follows the mandated column order (\`ID | Action | File | ...\`),
17
- every data-row ID matches the documented grammar as a complete cell and is unique,
18
- classified by its whole-cell action column
19
- (create/change/delete), and bound to a declared file path in the file column;
20
- 2. the ledger ID set equals the tree marker ID set, each ID exactly once on each side;
21
- 3. every marker sits in its entry's declared file;
22
- 4. the manifest contains no open-ended coverage language ("as work proceeds",
23
- "replicate", "etc.", "and others").
24
-
25
- Markers are scanned in git-tracked source files only (\`.md\` files and \`.ai/\` are excluded:
26
- prose may quote markers legitimately). Pure read; findings go to stderr; exit 0 clean,
27
- 1 on findings. \`--plan <path>\` and \`--completion <path>\` override the default artifact
28
- locations. No plan file at all means there is nothing to check (exit 0) — a docs-only or
29
- planning-free task.
30
- """
31
- import argparse
32
- import re
33
- import subprocess
34
- import sys
35
- from pathlib import Path
36
-
37
- PLAN = ".ai/vcm/handoffs/architecture-plan.md"
38
- COMPLETION = ".ai/vcm/handoffs/coder-completion.md"
39
- MANIFEST_HEADING = re.compile(r"^##\\s+Scaffold Manifest\\s*$")
40
- COMPLETION_HEADING = re.compile(r"^##\\s+Scaffold Completion\\s*$")
41
- SECTION_HEADING = re.compile(r"^##\\s+\\S")
42
- ID_PATTERN = re.compile(r"[A-Z]{2,6}-\\d{1,4}")
43
- EMPTY_MANIFEST = "No scaffold items."
44
- TABLE_SEPARATOR_CELL = re.compile(r":?-{3,}:?")
45
- # Mandated column order: ID | action | file | symbol/site | work | freedom | proof.
46
- # The action is read from its own cell as a whole-cell verb — never sniffed from the
47
- # row text, so paths or prose containing action words cannot flip an item's class.
48
- ACTIONS = frozenset({"create", "change", "delete"})
49
- RESULTS = frozenset({"done", "failed"})
50
- MARKER_STATES = frozenset({"removed", "present"})
51
- PATH_TOKEN = re.compile(r"\`([^\`\\s]+/[^\`\\s]+|[^\`\\s]+\\.[A-Za-z0-9]{1,8})\`")
52
- MARKER_ANY = re.compile(r"VCM:CODE(?![A-Za-z0-9_])")
53
- FORBIDDEN = [
54
- re.compile(r"as work proceeds", re.IGNORECASE),
55
- re.compile(r"\\breplicate\\b", re.IGNORECASE),
56
- re.compile(r"\\betc\\.", re.IGNORECASE),
57
- re.compile(r"\\band others\\b", re.IGNORECASE),
58
- ]
59
-
60
-
61
- def manifest_section(plan_text: str) -> tuple[int, list[str]] | None:
62
- """(start line number, section lines) of the Scaffold Manifest section, or None."""
63
- lines = plan_text.splitlines()
64
- start = None
65
- for index, line in enumerate(lines):
66
- if start is None:
67
- if MANIFEST_HEADING.match(line):
68
- start = index + 1
69
- elif SECTION_HEADING.match(line):
70
- return (start, lines[start : index])
71
- return None if start is None else (start, lines[start:])
72
-
73
-
74
- def completion_section(completion_text: str) -> tuple[int, list[str]] | None:
75
- """(start line number, section lines) of the Scaffold Completion section, or None."""
76
- lines = completion_text.splitlines()
77
- start = None
78
- for index, line in enumerate(lines):
79
- if start is None:
80
- if COMPLETION_HEADING.match(line):
81
- start = index + 1
82
- elif SECTION_HEADING.match(line):
83
- return (start, lines[start : index])
84
- return None if start is None else (start, lines[start:])
85
-
86
-
87
- def is_separator_row(cells: list[str]) -> bool:
88
- return bool(cells) and all(TABLE_SEPARATOR_CELL.fullmatch(cell.replace(" ", "")) for cell in cells)
89
-
90
-
91
- def parse_ledger(section_start: int, section: list[str], plan: str) -> tuple[dict, list[str], bool]:
92
- """{id: {"path", "action", "line"}}, parse findings, and explicit-empty state."""
93
- entries: dict[str, dict] = {}
94
- findings: list[str] = []
95
- header_line = None
96
- separator_seen = False
97
- explicit_empty_line = None
98
- for offset, line in enumerate(section):
99
- line_no = section_start + offset + 1
100
- stripped = line.strip()
101
- if stripped == EMPTY_MANIFEST:
102
- if explicit_empty_line is not None:
103
- findings.append(
104
- f"{plan}:{line_no} [ledger] duplicate \`{EMPTY_MANIFEST}\` declaration "
105
- f"(first at line {explicit_empty_line})"
106
- )
107
- else:
108
- explicit_empty_line = line_no
109
- continue
110
- if not stripped.startswith("|"):
111
- continue
112
- cells = [cell.strip() for cell in stripped.strip("|").split("|")]
113
-
114
- if header_line is None:
115
- header_line = line_no
116
- head = [cell.lower() for cell in cells[:3]]
117
- if head != ["id", "action", "file"]:
118
- findings.append(
119
- f"{plan}:{line_no} [ledger] header columns must begin exactly "
120
- f"\`ID | Action | File | ...\` -> fix the ledger column order"
121
- )
122
- continue
123
-
124
- if not separator_seen:
125
- if is_separator_row(cells):
126
- separator_seen = True
127
- continue
128
- findings.append(
129
- f"{plan}:{line_no} [ledger] missing Markdown separator row after "
130
- f"the Scaffold Manifest header"
131
- )
132
- separator_seen = True
133
- elif is_separator_row(cells):
134
- findings.append(
135
- f"{plan}:{line_no} [ledger] unexpected separator row inside Scaffold Manifest data"
136
- )
137
- continue
138
-
139
- id_cell = cells[0] if cells else ""
140
- if not ID_PATTERN.fullmatch(id_cell):
141
- findings.append(
142
- f"{plan}:{line_no} [ledger] ID cell \`{id_cell}\` must match "
143
- f"\`[A-Z]{{2,6}}-[0-9]{{1,4}}\` exactly"
144
- )
145
- continue
146
- entry_id = id_cell
147
- if entry_id in entries:
148
- findings.append(
149
- f"{plan}:{line_no} [ledger] duplicate ledger ID {entry_id} "
150
- f"(first at line {entries[entry_id]['line']}) -> one entry per item"
151
- )
152
- continue
153
- action_cell = cells[1].lower() if len(cells) > 1 else ""
154
- action = action_cell if action_cell in ACTIONS else None
155
- declared = None
156
- if len(cells) > 2:
157
- path_match = PATH_TOKEN.search(cells[2])
158
- declared = path_match.group(1) if path_match else None
159
- if action is None:
160
- findings.append(
161
- f"{plan}:{line_no} [ledger] {entry_id} action column is not exactly "
162
- f"one of create/change/delete -> classify the item in column 2"
163
- )
164
- if declared is None:
165
- findings.append(
166
- f"{plan}:{line_no} [ledger] {entry_id} has no declared file path in "
167
- f"column 3 -> bind the item to its exact file"
168
- )
169
- entries[entry_id] = {"path": declared, "action": action, "line": line_no}
170
- if explicit_empty_line is not None:
171
- if header_line is not None:
172
- findings.append(
173
- f"{plan}:{explicit_empty_line} [ledger] \`{EMPTY_MANIFEST}\` cannot be "
174
- f"combined with a Scaffold Manifest table"
175
- )
176
- return entries, findings, True
177
-
178
- if header_line is None:
179
- findings.append(
180
- f"{plan}:{section_start + 1} [ledger] Scaffold Manifest must contain the "
181
- f"required table or the exact line \`{EMPTY_MANIFEST}\`"
182
- )
183
- return entries, findings, False
184
- if not separator_seen:
185
- findings.append(
186
- f"{plan}:{header_line} [ledger] Scaffold Manifest table is missing its separator row"
187
- )
188
- if not entries:
189
- findings.append(
190
- f"{plan}:{header_line} [ledger] Scaffold Manifest contains no valid entries; "
191
- f"use the exact line \`{EMPTY_MANIFEST}\` only when no scaffold item exists"
192
- )
193
- return entries, findings, False
194
-
195
-
196
- def parse_completion(
197
- section_start: int,
198
- section: list[str],
199
- completion: str,
200
- ) -> tuple[dict, list[str]]:
201
- """{id: {action, result, marker_state, line}} and completion-table findings."""
202
- entries: dict[str, dict] = {}
203
- findings: list[str] = []
204
- header_line = None
205
- separator_seen = False
206
- for offset, line in enumerate(section):
207
- line_no = section_start + offset + 1
208
- stripped = line.strip()
209
- if not stripped.startswith("|"):
210
- continue
211
- cells = [cell.strip() for cell in stripped.strip("|").split("|")]
212
-
213
- if header_line is None:
214
- header_line = line_no
215
- head = [cell.lower() for cell in cells]
216
- if head != ["id", "action", "result", "marker state", "proof evidence"]:
217
- findings.append(
218
- f"{completion}:{line_no} [completion] header columns must be exactly "
219
- f"\`ID | Action | Result | Marker State | Proof Evidence\`"
220
- )
221
- continue
222
-
223
- if not separator_seen:
224
- if is_separator_row(cells):
225
- separator_seen = True
226
- continue
227
- findings.append(
228
- f"{completion}:{line_no} [completion] missing Markdown separator row after "
229
- f"the Scaffold Completion header"
230
- )
231
- separator_seen = True
232
- elif is_separator_row(cells):
233
- findings.append(
234
- f"{completion}:{line_no} [completion] unexpected separator row inside "
235
- f"Scaffold Completion data"
236
- )
237
- continue
238
-
239
- entry_id = cells[0] if cells else ""
240
- if not ID_PATTERN.fullmatch(entry_id):
241
- findings.append(
242
- f"{completion}:{line_no} [completion] ID cell \`{entry_id}\` must match "
243
- f"\`[A-Z]{{2,6}}-[0-9]{{1,4}}\` exactly"
244
- )
245
- continue
246
- if entry_id in entries:
247
- findings.append(
248
- f"{completion}:{line_no} [completion] duplicate ID {entry_id} "
249
- f"(first at line {entries[entry_id]['line']})"
250
- )
251
- continue
252
-
253
- action = cells[1].lower() if len(cells) > 1 else ""
254
- result = cells[2].lower() if len(cells) > 2 else ""
255
- marker_state = cells[3].lower() if len(cells) > 3 else ""
256
- if action not in ACTIONS:
257
- findings.append(
258
- f"{completion}:{line_no} [completion] {entry_id} action must be exactly "
259
- f"create/change/delete"
260
- )
261
- if result not in RESULTS:
262
- findings.append(
263
- f"{completion}:{line_no} [completion] {entry_id} result must be exactly "
264
- f"done/failed"
265
- )
266
- if marker_state not in MARKER_STATES:
267
- findings.append(
268
- f"{completion}:{line_no} [completion] {entry_id} Marker State must be "
269
- f"exactly removed/present"
270
- )
271
- entries[entry_id] = {
272
- "action": action,
273
- "result": result,
274
- "marker_state": marker_state,
275
- "line": line_no,
3
+ return String.raw `${renderNodeToolHeader("Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.")}${NODE_TOOL_ARGUMENT_PARSER}
4
+ // Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.
5
+ //
6
+ // The architecture plan's Scaffold Manifest is an item ledger: one entry per implementation
7
+ // item, one unique ID per entry, and exactly one "VCM:CODE <ID>" marker in the tree per
8
+ // create/change/delete entry during scaffolding. Coder then removes successful
9
+ // markers and preserves failed markers. The required --mode selects that lifecycle:
10
+ //
11
+ // - scaffold: require the ledger and tree markers to form an exact bijection;
12
+ // - completion: reconcile the ledger, Coder's Scaffold Completion table, and the
13
+ // final tree marker state.
14
+ //
15
+ // The tool checks:
16
+ //
17
+ // 1. the ledger header follows the mandated column order (ID | Action | File | ...),
18
+ // every data-row ID matches the documented grammar as a complete cell and is unique,
19
+ // classified by its whole-cell action column
20
+ // (create/change/delete), and bound to a declared file path in the file column;
21
+ // 2. the ledger ID set equals the tree marker ID set, each ID exactly once on each side;
22
+ // 3. every marker sits in its entry's declared file;
23
+ // 4. the manifest contains no open-ended coverage language ("as work proceeds",
24
+ // "replicate", "etc.", "and others").
25
+ //
26
+ // Markers are scanned in git-tracked source files only (.md files and .ai/ are excluded:
27
+ // prose may quote markers legitimately). Pure read; findings go to stderr; exit 0 clean,
28
+ // 1 on findings. --plan <path> and --completion <path> override the default artifact
29
+ // locations. No plan file at all means there is nothing to check (exit 0) - a docs-only or
30
+ // planning-free task.
31
+
32
+ const childProcess = require("node:child_process");
33
+ const fs = require("node:fs");
34
+ const path = require("node:path");
35
+
36
+ const B = "\x60";
37
+ const PLAN = ".ai/vcm/handoffs/architecture-plan.md";
38
+ const COMPLETION = ".ai/vcm/handoffs/coder-completion.md";
39
+ const MANIFEST_HEADING = /^##\s+Scaffold Manifest\s*$/;
40
+ const COMPLETION_HEADING = /^##\s+Scaffold Completion\s*$/;
41
+ const SECTION_HEADING = /^##\s+\S/;
42
+ const ID_PATTERN = /^[A-Z]{2,6}-\d{1,4}$/;
43
+ const EMPTY_MANIFEST = "No scaffold items.";
44
+ const TABLE_SEPARATOR_CELL = /^:?-{3,}:?$/;
45
+ // Mandated column order: ID | action | file | symbol/site | work | freedom | proof.
46
+ // The action is read from its own cell as a whole-cell verb - never sniffed from the
47
+ // row text, so paths or prose containing action words cannot flip an item's class.
48
+ const ACTIONS = new Set(["create", "change", "delete"]);
49
+ const RESULTS = new Set(["done", "failed"]);
50
+ const MARKER_STATES = new Set(["removed", "present"]);
51
+ const PATH_TOKEN = /\x60([^\x60\s]+\/[^\x60\s]+|[^\x60\s]+\.[A-Za-z0-9]{1,8})\x60/;
52
+ const MARKER_ANY = /VCM:CODE(?![A-Za-z0-9_])/g;
53
+ const FORBIDDEN = [
54
+ [/as work proceeds/i, "as work proceeds"],
55
+ [/\breplicate\b/i, "\\breplicate\\b"],
56
+ [/\betc\./i, "\\betc\\."],
57
+ [/\band others\b/i, "\\band others\\b"]
58
+ ];
59
+ const ID_GRAMMAR = B + "[A-Z]{2,6}-[0-9]{1,4}" + B;
60
+
61
+ function splitLines(text) {
62
+ return text.split(/\r\n|\n|\r/).filter((line, index, all) => index < all.length - 1 || line !== "");
63
+ }
64
+
65
+ function section(text, heading) {
66
+ const lines = splitLines(text);
67
+ let start = null;
68
+ for (let index = 0; index < lines.length; index += 1) {
69
+ if (start === null) {
70
+ if (heading.test(lines[index])) {
71
+ start = index + 1;
72
+ }
73
+ } else if (SECTION_HEADING.test(lines[index])) {
74
+ return { start, lines: lines.slice(start, index) };
75
+ }
76
+ }
77
+ return start === null ? null : { start, lines: lines.slice(start) };
78
+ }
79
+
80
+ function tableCells(stripped) {
81
+ return stripped.replace(/^\|+|\|+$/g, "").split("|").map((cell) => cell.trim());
82
+ }
83
+
84
+ function isSeparatorRow(cells) {
85
+ return cells.length > 0 && cells.every((cell) => TABLE_SEPARATOR_CELL.test(cell.replaceAll(" ", "")));
86
+ }
87
+
88
+ function parseLedger(sectionStart, lines, plan) {
89
+ const entries = new Map();
90
+ const findings = [];
91
+ let headerLine = null;
92
+ let separatorSeen = false;
93
+ let explicitEmptyLine = null;
94
+ lines.forEach((line, offset) => {
95
+ const lineNo = sectionStart + offset + 1;
96
+ const stripped = line.trim();
97
+ if (stripped === EMPTY_MANIFEST) {
98
+ if (explicitEmptyLine !== null) {
99
+ findings.push(plan + ":" + lineNo + " [ledger] duplicate " + B + EMPTY_MANIFEST + B + " declaration "
100
+ + "(first at line " + explicitEmptyLine + ")");
101
+ } else {
102
+ explicitEmptyLine = lineNo;
103
+ }
104
+ return;
105
+ }
106
+ if (!stripped.startsWith("|")) {
107
+ return;
108
+ }
109
+ const cells = tableCells(stripped);
110
+
111
+ if (headerLine === null) {
112
+ headerLine = lineNo;
113
+ const head = cells.slice(0, 3).map((cell) => cell.toLowerCase());
114
+ if (head.join("|") !== "id|action|file" || head.length !== 3) {
115
+ findings.push(plan + ":" + lineNo + " [ledger] header columns must begin exactly "
116
+ + B + "ID | Action | File | ..." + B + " -> fix the ledger column order");
117
+ }
118
+ return;
119
+ }
120
+
121
+ if (!separatorSeen) {
122
+ if (isSeparatorRow(cells)) {
123
+ separatorSeen = true;
124
+ return;
125
+ }
126
+ findings.push(plan + ":" + lineNo + " [ledger] missing Markdown separator row after "
127
+ + "the Scaffold Manifest header");
128
+ separatorSeen = true;
129
+ } else if (isSeparatorRow(cells)) {
130
+ findings.push(plan + ":" + lineNo + " [ledger] unexpected separator row inside Scaffold Manifest data");
131
+ return;
132
+ }
133
+
134
+ const idCell = cells.length > 0 ? cells[0] : "";
135
+ if (!ID_PATTERN.test(idCell)) {
136
+ findings.push(plan + ":" + lineNo + " [ledger] ID cell " + B + idCell + B + " must match " + ID_GRAMMAR + " exactly");
137
+ return;
138
+ }
139
+ if (entries.has(idCell)) {
140
+ findings.push(plan + ":" + lineNo + " [ledger] duplicate ledger ID " + idCell + " "
141
+ + "(first at line " + entries.get(idCell).line + ") -> one entry per item");
142
+ return;
143
+ }
144
+ const actionCell = cells.length > 1 ? cells[1].toLowerCase() : "";
145
+ const action = ACTIONS.has(actionCell) ? actionCell : null;
146
+ let declared = null;
147
+ if (cells.length > 2) {
148
+ const pathMatch = PATH_TOKEN.exec(cells[2]);
149
+ declared = pathMatch ? pathMatch[1] : null;
150
+ }
151
+ if (action === null) {
152
+ findings.push(plan + ":" + lineNo + " [ledger] " + idCell + " action column is not exactly "
153
+ + "one of create/change/delete -> classify the item in column 2");
154
+ }
155
+ if (declared === null) {
156
+ findings.push(plan + ":" + lineNo + " [ledger] " + idCell + " has no declared file path in "
157
+ + "column 3 -> bind the item to its exact file");
158
+ }
159
+ entries.set(idCell, { path: declared, action, line: lineNo });
160
+ });
161
+ if (explicitEmptyLine !== null) {
162
+ if (headerLine !== null) {
163
+ findings.push(plan + ":" + explicitEmptyLine + " [ledger] " + B + EMPTY_MANIFEST + B + " cannot be "
164
+ + "combined with a Scaffold Manifest table");
165
+ }
166
+ return { entries, findings, explicitEmpty: true };
167
+ }
168
+ if (headerLine === null) {
169
+ findings.push(plan + ":" + (sectionStart + 1) + " [ledger] Scaffold Manifest must contain the "
170
+ + "required table or the exact line " + B + EMPTY_MANIFEST + B);
171
+ return { entries, findings, explicitEmpty: false };
172
+ }
173
+ if (!separatorSeen) {
174
+ findings.push(plan + ":" + headerLine + " [ledger] Scaffold Manifest table is missing its separator row");
175
+ }
176
+ if (entries.size === 0) {
177
+ findings.push(plan + ":" + headerLine + " [ledger] Scaffold Manifest contains no valid entries; "
178
+ + "use the exact line " + B + EMPTY_MANIFEST + B + " only when no scaffold item exists");
179
+ }
180
+ return { entries, findings, explicitEmpty: false };
181
+ }
182
+
183
+ function parseCompletion(sectionStart, lines, completion) {
184
+ const entries = new Map();
185
+ const findings = [];
186
+ let headerLine = null;
187
+ let separatorSeen = false;
188
+ lines.forEach((line, offset) => {
189
+ const lineNo = sectionStart + offset + 1;
190
+ const stripped = line.trim();
191
+ if (!stripped.startsWith("|")) {
192
+ return;
193
+ }
194
+ const cells = tableCells(stripped);
195
+
196
+ if (headerLine === null) {
197
+ headerLine = lineNo;
198
+ if (cells.map((cell) => cell.toLowerCase()).join("|") !== "id|action|result|marker state|proof evidence") {
199
+ findings.push(completion + ":" + lineNo + " [completion] header columns must be exactly "
200
+ + B + "ID | Action | Result | Marker State | Proof Evidence" + B);
201
+ }
202
+ return;
203
+ }
204
+
205
+ if (!separatorSeen) {
206
+ if (isSeparatorRow(cells)) {
207
+ separatorSeen = true;
208
+ return;
209
+ }
210
+ findings.push(completion + ":" + lineNo + " [completion] missing Markdown separator row after "
211
+ + "the Scaffold Completion header");
212
+ separatorSeen = true;
213
+ } else if (isSeparatorRow(cells)) {
214
+ findings.push(completion + ":" + lineNo + " [completion] unexpected separator row inside "
215
+ + "Scaffold Completion data");
216
+ return;
217
+ }
218
+
219
+ const entryId = cells.length > 0 ? cells[0] : "";
220
+ if (!ID_PATTERN.test(entryId)) {
221
+ findings.push(completion + ":" + lineNo + " [completion] ID cell " + B + entryId + B + " must match " + ID_GRAMMAR + " exactly");
222
+ return;
223
+ }
224
+ if (entries.has(entryId)) {
225
+ findings.push(completion + ":" + lineNo + " [completion] duplicate ID " + entryId + " "
226
+ + "(first at line " + entries.get(entryId).line + ")");
227
+ return;
228
+ }
229
+
230
+ const action = cells.length > 1 ? cells[1].toLowerCase() : "";
231
+ const result = cells.length > 2 ? cells[2].toLowerCase() : "";
232
+ const markerState = cells.length > 3 ? cells[3].toLowerCase() : "";
233
+ if (!ACTIONS.has(action)) {
234
+ findings.push(completion + ":" + lineNo + " [completion] " + entryId + " action must be exactly create/change/delete");
235
+ }
236
+ if (!RESULTS.has(result)) {
237
+ findings.push(completion + ":" + lineNo + " [completion] " + entryId + " result must be exactly done/failed");
238
+ }
239
+ if (!MARKER_STATES.has(markerState)) {
240
+ findings.push(completion + ":" + lineNo + " [completion] " + entryId + " Marker State must be exactly removed/present");
241
+ }
242
+ entries.set(entryId, { action, result, markerState, line: lineNo });
243
+ });
244
+
245
+ if (headerLine === null) {
246
+ findings.push(completion + ":" + (sectionStart + 1) + " [completion] Scaffold Completion must "
247
+ + "contain the required table");
248
+ } else if (!separatorSeen) {
249
+ findings.push(completion + ":" + headerLine + " [completion] Scaffold Completion table is "
250
+ + "missing its separator row");
251
+ }
252
+ if (entries.size === 0) {
253
+ findings.push(completion + ":" + (headerLine || sectionStart + 1) + " [completion] Scaffold "
254
+ + "Completion contains no valid entries");
255
+ }
256
+ return { entries, findings };
257
+ }
258
+
259
+ function completionDecision(text) {
260
+ const match = /^Decision:\s*(\S+)\s*$/m.exec(text);
261
+ return match ? match[1] : null;
262
+ }
263
+
264
+ function forbiddenLanguage(sectionStart, lines, plan) {
265
+ const findings = [];
266
+ lines.forEach((line, offset) => {
267
+ for (const [pattern, source] of FORBIDDEN) {
268
+ if (pattern.test(line)) {
269
+ findings.push(plan + ":" + (sectionStart + offset + 1) + " [ledger] open-ended coverage "
270
+ + "language (" + source + ") -> enumerate every item explicitly");
271
+ }
272
+ }
273
+ });
274
+ return findings;
275
+ }
276
+
277
+ function treeMarkers(root) {
278
+ const result = childProcess.spawnSync("git", ["grep", "-In", "VCM:CODE", "--", ".", ":!*.md", ":!.ai"], {
279
+ cwd: root,
280
+ encoding: "utf8",
281
+ maxBuffer: 64 * 1024 * 1024
282
+ });
283
+ const markers = new Map();
284
+ const findings = [];
285
+ for (const raw of splitLines(result.stdout || "")) {
286
+ const first = raw.indexOf(":");
287
+ const second = first >= 0 ? raw.indexOf(":", first + 1) : -1;
288
+ if (first < 0 || second < 0) {
289
+ continue;
290
+ }
291
+ const file = raw.slice(0, first);
292
+ const lineNo = Number.parseInt(raw.slice(first + 1, second), 10);
293
+ const content = raw.slice(second + 1);
294
+ for (const occurrence of content.matchAll(MARKER_ANY)) {
295
+ const tokenMatch = /^\s+(\S+)/.exec(content.slice(occurrence.index + occurrence[0].length));
296
+ if (tokenMatch === null) {
297
+ findings.push(file + ":" + lineNo + " [ledger] marker without an ID -> use " + B + "VCM:CODE <ID>" + B);
298
+ continue;
299
+ }
300
+ const markerId = tokenMatch[1];
301
+ if (!ID_PATTERN.test(markerId)) {
302
+ findings.push(file + ":" + lineNo + " [ledger] marker ID " + B + markerId + B + " must match " + ID_GRAMMAR + " exactly");
303
+ continue;
304
+ }
305
+ if (!markers.has(markerId)) {
306
+ markers.set(markerId, []);
307
+ }
308
+ markers.get(markerId).push([file, lineNo]);
309
+ }
310
+ }
311
+ return { markers, findings };
312
+ }
313
+
314
+ function sortedIds(ids) {
315
+ return [...ids].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
316
+ }
317
+
318
+ // Mirrors str(pathlib.Path(value)): drops "." segments and repeated slashes.
319
+ function displayPath(value) {
320
+ const absolute = value.startsWith("/");
321
+ const parts = value.split("/").filter((part) => part !== "" && part !== ".");
322
+ return (absolute ? "/" : "") + parts.join("/") || ".";
323
+ }
324
+
325
+ function isFile(target) {
326
+ try {
327
+ return fs.statSync(target).isFile();
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ function main() {
334
+ const args = parseToolArguments(process.argv.slice(2), {
335
+ description: "Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation.",
336
+ options: {
337
+ "--mode": { dest: "mode", required: true, choices: ["scaffold", "completion"] },
338
+ "--plan": { dest: "plan" },
339
+ "--completion": { dest: "completion" }
340
+ }
341
+ });
342
+
343
+ const root = path.resolve(path.dirname(fs.realpathSync(__filename)), "..", "..");
344
+ const planPath = args.plan ? displayPath(args.plan) : path.join(root, PLAN);
345
+ const plan = planPath;
346
+ if (!isFile(planPath)) {
347
+ process.stdout.write("no architecture plan at " + planPath + "; nothing to check\n");
348
+ return 0;
349
+ }
350
+
351
+ let findings = [];
352
+ const manifest = section(fs.readFileSync(planPath, "utf8"), MANIFEST_HEADING);
353
+ if (manifest === null) {
354
+ process.stderr.write(planPath + ":1 [ledger] no " + B + "## Scaffold Manifest" + B + " section\n");
355
+ return 1;
356
+ }
357
+ const ledger = parseLedger(manifest.start, manifest.lines, plan);
358
+ const entries = ledger.entries;
359
+ findings = findings.concat(ledger.findings, forbiddenLanguage(manifest.start, manifest.lines, plan));
360
+
361
+ const tree = treeMarkers(root);
362
+ const markers = tree.markers;
363
+ findings = findings.concat(tree.findings);
364
+
365
+ for (const entryId of sortedIds(markers.keys())) {
366
+ const sites = markers.get(entryId);
367
+ if (sites.length > 1) {
368
+ const where = sites.map(([file, lineNo]) => file + ":" + lineNo).join(", ");
369
+ findings.push("[ledger] " + entryId + " has " + sites.length + " markers (" + where + ") -> exactly one per item");
370
+ }
371
+ }
372
+
373
+ const ledgerIds = new Set(entries.keys());
374
+ const treeIds = new Set(markers.keys());
375
+ for (const entryId of sortedIds([...treeIds].filter((id) => !ledgerIds.has(id)))) {
376
+ const [file, lineNo] = markers.get(entryId)[0];
377
+ findings.push(file + ":" + lineNo + " [ledger] marker " + entryId + " has no ledger entry "
378
+ + "-> every item must be manifested");
379
+ }
380
+ for (const entryId of sortedIds([...ledgerIds].filter((id) => treeIds.has(id)))) {
381
+ const declared = entries.get(entryId).path;
382
+ if (declared) {
383
+ for (const [file, lineNo] of markers.get(entryId)) {
384
+ if (!(file === declared || file.endsWith("/" + declared) || declared.endsWith("/" + file))) {
385
+ findings.push(file + ":" + lineNo + " [ledger] marker " + entryId + " is outside its "
386
+ + "declared file " + B + declared + B);
276
387
  }
388
+ }
389
+ }
390
+ }
391
+
392
+ let completed = 0;
393
+ let failed = 0;
394
+ if (args.mode === "scaffold") {
395
+ for (const entryId of sortedIds([...ledgerIds].filter((id) => !treeIds.has(id)))) {
396
+ findings.push(plan + ":" + entries.get(entryId).line + " [ledger] " + entryId + " has no marker "
397
+ + "in the tree -> pre-place " + B + "VCM:CODE " + entryId + B + " in its declared file");
398
+ }
399
+ } else if (!ledger.explicitEmpty) {
400
+ const completionPath = args.completion ? displayPath(args.completion) : path.join(root, COMPLETION);
401
+ if (!isFile(completionPath)) {
402
+ findings.push(completionPath + ":1 [completion] completion mode requires coder-completion.md");
403
+ } else {
404
+ const completionText = fs.readFileSync(completionPath, "utf8");
405
+ const completion = completionPath;
406
+ const completedSection = section(completionText, COMPLETION_HEADING);
407
+ let completionEntries = new Map();
408
+ if (completedSection === null) {
409
+ findings.push(completionPath + ":1 [completion] no " + B + "## Scaffold Completion" + B + " section");
410
+ } else {
411
+ const parsed = parseCompletion(completedSection.start, completedSection.lines, completion);
412
+ completionEntries = parsed.entries;
413
+ findings = findings.concat(parsed.findings);
414
+ }
415
+
416
+ const completionIds = new Set(completionEntries.keys());
417
+ for (const entryId of sortedIds([...ledgerIds].filter((id) => !completionIds.has(id)))) {
418
+ findings.push(plan + ":" + entries.get(entryId).line + " [completion] " + entryId + " is missing "
419
+ + "from Scaffold Completion");
420
+ }
421
+ for (const entryId of sortedIds([...completionIds].filter((id) => !ledgerIds.has(id)))) {
422
+ findings.push(completion + ":" + completionEntries.get(entryId).line + " [completion] "
423
+ + entryId + " has no Scaffold Manifest entry");
424
+ }
425
+
426
+ for (const entryId of sortedIds([...ledgerIds].filter((id) => completionIds.has(id)))) {
427
+ const ledgerEntry = entries.get(entryId);
428
+ const completionEntry = completionEntries.get(entryId);
429
+ const markerCount = (markers.get(entryId) || []).length;
430
+ const where = completion + ":" + completionEntry.line + " [completion] " + entryId;
431
+ if (completionEntry.action !== ledgerEntry.action) {
432
+ findings.push(where + " action " + B + completionEntry.action + B + " does not match Scaffold "
433
+ + "Manifest action " + B + (ledgerEntry.action === null ? "None" : ledgerEntry.action) + B);
434
+ }
435
+ if (completionEntry.result === "done") {
436
+ completed += 1;
437
+ if (completionEntry.markerState !== "removed") {
438
+ findings.push(where + " done requires Marker State " + B + "removed" + B);
439
+ }
440
+ if (markerCount !== 0) {
441
+ findings.push(where + " is done but " + markerCount + " marker(s) remain -> remove the completed marker");
442
+ }
443
+ } else if (completionEntry.result === "failed") {
444
+ failed += 1;
445
+ if (completionEntry.markerState !== "present") {
446
+ findings.push(where + " failed requires Marker State " + B + "present" + B);
447
+ }
448
+ if (markerCount !== 1) {
449
+ findings.push(where + " failed requires exactly one preserved marker; found " + markerCount);
450
+ }
451
+ }
452
+ }
453
+
454
+ const decision = completionDecision(completionText);
455
+ const expectedDecision = failed ? "failed" : "ready_for_review";
456
+ if (decision !== expectedDecision) {
457
+ findings.push(completionPath + ":1 [completion] Decision must be "
458
+ + B + expectedDecision + B + " for the recorded item results; found "
459
+ + B + (decision || "missing") + B);
460
+ }
461
+ }
462
+ }
463
+
464
+ for (const item of findings) {
465
+ process.stderr.write(item + "\n");
466
+ }
467
+ if (findings.length > 0) {
468
+ process.stderr.write("ledger reconciliation failed with " + findings.length + " finding(s)\n");
469
+ return 1;
470
+ }
471
+ if (ledger.explicitEmpty) {
472
+ process.stdout.write("ledger explicitly empty: 0 ledger item(s), 0 marker(s)\n");
473
+ } else if (args.mode === "completion") {
474
+ process.stdout.write("ledger completion clean: " + ledgerIds.size + " ledger item(s), "
475
+ + completed + " done, " + failed + " failed, " + treeIds.size + " marker(s) remain\n");
476
+ } else {
477
+ process.stdout.write("ledger reconciliation clean: " + ledgerIds.size + " ledger item(s), "
478
+ + treeIds.size + " marker(s)\n");
479
+ }
480
+ return 0;
481
+ }
277
482
 
278
- if header_line is None:
279
- findings.append(
280
- f"{completion}:{section_start + 1} [completion] Scaffold Completion must "
281
- f"contain the required table"
282
- )
283
- elif not separator_seen:
284
- findings.append(
285
- f"{completion}:{header_line} [completion] Scaffold Completion table is "
286
- f"missing its separator row"
287
- )
288
- if not entries:
289
- findings.append(
290
- f"{completion}:{header_line or section_start + 1} [completion] Scaffold "
291
- f"Completion contains no valid entries"
292
- )
293
- return entries, findings
294
-
295
-
296
- def completion_decision(completion_text: str) -> str | None:
297
- match = re.search(r"^Decision:\\s*(\\S+)\\s*$", completion_text, re.MULTILINE)
298
- return match.group(1) if match else None
299
-
300
-
301
- def forbidden_language(section_start: int, section: list[str], plan: str) -> list[str]:
302
- findings = []
303
- for offset, line in enumerate(section):
304
- for pattern in FORBIDDEN:
305
- if pattern.search(line):
306
- findings.append(
307
- f"{plan}:{section_start + offset + 1} [ledger] open-ended coverage "
308
- f"language ({pattern.pattern}) -> enumerate every item explicitly"
309
- )
310
- return findings
311
-
312
-
313
- def tree_markers(root: Path) -> tuple[dict[str, list[tuple[str, int]]], list[str]]:
314
- """{id: [(path, line)]} for tracked source markers, plus malformed-marker findings."""
315
- result = subprocess.run(
316
- ["git", "grep", "-In", "VCM:CODE", "--", ".", ":!*.md", ":!.ai"],
317
- capture_output=True,
318
- text=True,
319
- cwd=root,
320
- )
321
- markers: dict[str, list[tuple[str, int]]] = {}
322
- findings: list[str] = []
323
- for raw in result.stdout.splitlines():
324
- parts = raw.split(":", 2)
325
- if len(parts) < 3:
326
- continue
327
- path, line_no, content = parts[0], int(parts[1]), parts[2]
328
- for occurrence in MARKER_ANY.finditer(content):
329
- token_match = re.match(r"\\s+(\\S+)", content[occurrence.end():])
330
- if token_match is None:
331
- findings.append(
332
- f"{path}:{line_no} [ledger] marker without an ID -> "
333
- f"use \`VCM:CODE <ID>\`"
334
- )
335
- continue
336
- marker_id = token_match.group(1)
337
- if not ID_PATTERN.fullmatch(marker_id):
338
- findings.append(
339
- f"{path}:{line_no} [ledger] marker ID \`{marker_id}\` must match "
340
- f"\`[A-Z]{{2,6}}-[0-9]{{1,4}}\` exactly"
341
- )
342
- continue
343
- markers.setdefault(marker_id, []).append((path, line_no))
344
- return markers, findings
345
-
346
-
347
- def main() -> int:
348
- parser = argparse.ArgumentParser(
349
- description="Lifecycle-aware Scaffold Manifest and VCM:CODE reconciliation."
350
- )
351
- parser.add_argument("--mode", required=True, choices=("scaffold", "completion"))
352
- parser.add_argument("--plan", default=None, help=f"plan path (default {PLAN})")
353
- parser.add_argument(
354
- "--completion",
355
- default=None,
356
- help=f"Coder completion path for completion mode (default {COMPLETION})",
357
- )
358
- args = parser.parse_args()
359
-
360
- root = Path(__file__).resolve().parents[2]
361
- plan_path = Path(args.plan) if args.plan else root / PLAN
362
- plan = str(plan_path)
363
- if not plan_path.is_file():
364
- print(f"no architecture plan at {plan_path}; nothing to check")
365
- return 0
366
-
367
- findings: list[str] = []
368
- section = manifest_section(plan_path.read_text(errors="replace"))
369
- if section is None:
370
- sys.stderr.write(f"{plan_path}:1 [ledger] no \`## Scaffold Manifest\` section\\n")
371
- return 1
372
- entries, parse_findings, explicit_empty = parse_ledger(*section, str(plan_path))
373
- findings += parse_findings
374
- findings += forbidden_language(*section, str(plan_path))
375
-
376
- markers, marker_findings = tree_markers(root)
377
- findings += marker_findings
378
-
379
- for entry_id, sites in sorted(markers.items()):
380
- if len(sites) > 1:
381
- where = ", ".join(f"{p}:{n}" for p, n in sites)
382
- findings.append(
383
- f"[ledger] {entry_id} has {len(sites)} markers ({where}) -> exactly one per item"
384
- )
385
-
386
- ledger_ids = set(entries)
387
- tree_ids = set(markers)
388
- for entry_id in sorted(tree_ids - ledger_ids):
389
- path, line_no = markers[entry_id][0]
390
- findings.append(
391
- f"{path}:{line_no} [ledger] marker {entry_id} has no ledger entry "
392
- f"-> every item must be manifested"
393
- )
394
- for entry_id in sorted(ledger_ids & tree_ids):
395
- entry = entries[entry_id]
396
- if entry["path"]:
397
- declared = entry["path"]
398
- for path, line_no in markers[entry_id]:
399
- if not (path == declared or path.endswith("/" + declared) or declared.endswith("/" + path)):
400
- findings.append(
401
- f"{path}:{line_no} [ledger] marker {entry_id} is outside its "
402
- f"declared file \`{declared}\`"
403
- )
404
-
405
- completed = 0
406
- failed = 0
407
- if args.mode == "scaffold":
408
- for entry_id in sorted(ledger_ids - tree_ids):
409
- findings.append(
410
- f"{plan}:{entries[entry_id]['line']} [ledger] {entry_id} has no marker "
411
- f"in the tree -> pre-place \`VCM:CODE {entry_id}\` in its declared file"
412
- )
413
- elif not explicit_empty:
414
- completion_path = Path(args.completion) if args.completion else root / COMPLETION
415
- if not completion_path.is_file():
416
- findings.append(
417
- f"{completion_path}:1 [completion] completion mode requires coder-completion.md"
418
- )
419
- else:
420
- completion_text = completion_path.read_text(errors="replace")
421
- completion = str(completion_path)
422
- completed_section = completion_section(completion_text)
423
- if completed_section is None:
424
- findings.append(
425
- f"{completion_path}:1 [completion] no \`## Scaffold Completion\` section"
426
- )
427
- completion_entries = {}
428
- else:
429
- completion_entries, completion_findings = parse_completion(
430
- *completed_section,
431
- completion,
432
- )
433
- findings += completion_findings
434
-
435
- completion_ids = set(completion_entries)
436
- for entry_id in sorted(ledger_ids - completion_ids):
437
- findings.append(
438
- f"{plan}:{entries[entry_id]['line']} [completion] {entry_id} is missing "
439
- f"from Scaffold Completion"
440
- )
441
- for entry_id in sorted(completion_ids - ledger_ids):
442
- findings.append(
443
- f"{completion}:{completion_entries[entry_id]['line']} [completion] "
444
- f"{entry_id} has no Scaffold Manifest entry"
445
- )
446
-
447
- for entry_id in sorted(ledger_ids & completion_ids):
448
- ledger_entry = entries[entry_id]
449
- completion_entry = completion_entries[entry_id]
450
- result = completion_entry["result"]
451
- marker_state = completion_entry["marker_state"]
452
- marker_count = len(markers.get(entry_id, []))
453
- if completion_entry["action"] != ledger_entry["action"]:
454
- findings.append(
455
- f"{completion}:{completion_entry['line']} [completion] {entry_id} "
456
- f"action \`{completion_entry['action']}\` does not match Scaffold "
457
- f"Manifest action \`{ledger_entry['action']}\`"
458
- )
459
- if result == "done":
460
- completed += 1
461
- if marker_state != "removed":
462
- findings.append(
463
- f"{completion}:{completion_entry['line']} [completion] {entry_id} "
464
- f"done requires Marker State \`removed\`"
465
- )
466
- if marker_count != 0:
467
- findings.append(
468
- f"{completion}:{completion_entry['line']} [completion] {entry_id} "
469
- f"is done but {marker_count} marker(s) remain -> remove the completed marker"
470
- )
471
- elif result == "failed":
472
- failed += 1
473
- if marker_state != "present":
474
- findings.append(
475
- f"{completion}:{completion_entry['line']} [completion] {entry_id} "
476
- f"failed requires Marker State \`present\`"
477
- )
478
- if marker_count != 1:
479
- findings.append(
480
- f"{completion}:{completion_entry['line']} [completion] {entry_id} "
481
- f"failed requires exactly one preserved marker; found {marker_count}"
482
- )
483
-
484
- decision = completion_decision(completion_text)
485
- expected_decision = "failed" if failed else "ready_for_review"
486
- if decision != expected_decision:
487
- findings.append(
488
- f"{completion_path}:1 [completion] Decision must be "
489
- f"\`{expected_decision}\` for the recorded item results; found "
490
- f"\`{decision or 'missing'}\`"
491
- )
492
-
493
- for finding in findings:
494
- sys.stderr.write(finding + "\\n")
495
- if findings:
496
- sys.stderr.write(f"ledger reconciliation failed with {len(findings)} finding(s)\\n")
497
- return 1
498
- if explicit_empty:
499
- print("ledger explicitly empty: 0 ledger item(s), 0 marker(s)")
500
- elif args.mode == "completion":
501
- print(
502
- f"ledger completion clean: {len(ledger_ids)} ledger item(s), "
503
- f"{completed} done, {failed} failed, {len(tree_ids)} marker(s) remain"
504
- )
505
- else:
506
- print(
507
- f"ledger reconciliation clean: {len(ledger_ids)} ledger item(s), "
508
- f"{len(tree_ids)} marker(s)"
509
- )
510
- return 0
511
-
512
-
513
- if __name__ == "__main__":
514
- raise SystemExit(main())
483
+ process.exitCode = main();
515
484
  `;
516
485
  }