@pieai/doc-gov 0.3.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.
package/dist/cli.js ADDED
@@ -0,0 +1,2387 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/approve.ts
4
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5
+ import { join as join4 } from "node:path";
6
+
7
+ // src/core/frontmatter.ts
8
+ import { readFileSync } from "node:fs";
9
+ function readFrontmatterFile(path) {
10
+ const content = readFileSync(path, "utf8");
11
+ if (!content.startsWith("---\n")) return null;
12
+ const closing = content.indexOf("\n---", 4);
13
+ if (closing === -1) return null;
14
+ const raw = content.slice(4, closing).trimEnd();
15
+ const body = content.slice(closing + 4).replace(/^\n/, "");
16
+ return { path, body, raw, data: parseFrontmatter(raw) };
17
+ }
18
+ function parseFrontmatter(raw) {
19
+ const data = {};
20
+ let currentArrayKey = "";
21
+ for (const line of raw.split("\n")) {
22
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
23
+ const arrayItem = line.match(/^\s+-\s+(.+)$/);
24
+ if (arrayItem && currentArrayKey) {
25
+ const value2 = data[currentArrayKey];
26
+ if (Array.isArray(value2)) value2.push(parseScalar(arrayItem[1] ?? ""));
27
+ continue;
28
+ }
29
+ const keyValue = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
30
+ if (!keyValue) {
31
+ currentArrayKey = "";
32
+ continue;
33
+ }
34
+ const key = keyValue[1] ?? "";
35
+ const value = keyValue[2] ?? "";
36
+ if (!value) {
37
+ data[key] = [];
38
+ currentArrayKey = key;
39
+ continue;
40
+ }
41
+ data[key] = parseScalar(value);
42
+ currentArrayKey = "";
43
+ }
44
+ return data;
45
+ }
46
+ function stringValue(value) {
47
+ return typeof value === "string" ? value : "";
48
+ }
49
+ function booleanValue(value) {
50
+ if (typeof value === "boolean") return value;
51
+ if (value === "true") return true;
52
+ if (value === "false") return false;
53
+ return null;
54
+ }
55
+ function stringArrayValue(value) {
56
+ return Array.isArray(value) ? value : [];
57
+ }
58
+ function parseScalar(value) {
59
+ const trimmed = value.trim().replace(/^['"]|['"]$/g, "");
60
+ if (trimmed === "true") return true;
61
+ if (trimmed === "false") return false;
62
+ if (trimmed === "null") return null;
63
+ if (trimmed === "[]") return [];
64
+ return trimmed;
65
+ }
66
+
67
+ // src/core/files.ts
68
+ import { existsSync, readdirSync, statSync } from "node:fs";
69
+ import { join, relative } from "node:path";
70
+ function listGovernedMarkdownFiles(rootDir) {
71
+ const roots = [join(rootDir, "docs")];
72
+ const files = [];
73
+ for (const root of roots) {
74
+ if (!existsSync(root)) continue;
75
+ files.push(...walk(rootDir, root));
76
+ }
77
+ return files.sort();
78
+ }
79
+ function walk(rootDir, dir) {
80
+ const entries = readdirSync(dir, { withFileTypes: true });
81
+ const files = [];
82
+ for (const entry of entries) {
83
+ const path = join(dir, entry.name);
84
+ const rel = toRepoPath(rootDir, path);
85
+ if (shouldSkip(rel)) continue;
86
+ if (entry.isSymbolicLink()) continue;
87
+ if (entry.isDirectory()) files.push(...walk(rootDir, path));
88
+ if (entry.isFile() && entry.name.endsWith(".md")) files.push(path);
89
+ }
90
+ return files;
91
+ }
92
+ function shouldSkip(repoPath) {
93
+ return repoPath.startsWith("docs/governance/templates/") || repoPath === "docs/governance/MANIFEST.yml";
94
+ }
95
+ function toRepoPath(rootDir, absolutePath) {
96
+ return relative(rootDir, absolutePath).split(/\\/g).join("/");
97
+ }
98
+
99
+ // src/core/lifecycle.ts
100
+ var normalStatuses = [
101
+ "draft",
102
+ "active",
103
+ "completed",
104
+ "stable",
105
+ "superseded",
106
+ "archived"
107
+ ];
108
+ var decisionStatuses = ["proposed", "accepted", "rejected", "superseded"];
109
+
110
+ // src/core/schema.ts
111
+ var docTypes = [
112
+ "policy",
113
+ "decision",
114
+ "spec",
115
+ "plan",
116
+ "canon",
117
+ "reference",
118
+ "archive"
119
+ ];
120
+ function validateFrontmatter(rootDir, file) {
121
+ const path = toRepoPath(rootDir, file.path);
122
+ const issues = [];
123
+ const data = file.data;
124
+ const id = stringValue(data.id);
125
+ const title = stringValue(data.title);
126
+ const type = stringValue(data.type);
127
+ const status = stringValue(data.status);
128
+ const canonical = booleanValue(data.canonical);
129
+ const owner = stringValue(data.owner);
130
+ const created = stringValue(data.created);
131
+ const lastReviewed = stringValue(data.last_reviewed);
132
+ const domain = stringValue(data.domain);
133
+ const tags = stringArrayValue(data.tags);
134
+ const related = stringArrayValue(data.related);
135
+ const pinned = booleanValue(data.pinned) ?? false;
136
+ const supersedes = stringArrayValue(data.supersedes);
137
+ const supersededByRaw = stringValue(data.superseded_by);
138
+ const supersededBy = supersededByRaw ? supersededByRaw : void 0;
139
+ const archiveReasonRaw = stringValue(data.archive_reason);
140
+ const archiveReason = archiveReasonRaw ? archiveReasonRaw : void 0;
141
+ for (const [field, value] of Object.entries({
142
+ id,
143
+ title,
144
+ type,
145
+ status,
146
+ owner,
147
+ created,
148
+ last_reviewed: lastReviewed
149
+ })) {
150
+ if (!value)
151
+ issues.push({
152
+ file: path,
153
+ code: "missing-field",
154
+ message: `Missing required frontmatter field: ${field}`
155
+ });
156
+ }
157
+ if (canonical === null)
158
+ issues.push({
159
+ file: path,
160
+ code: "invalid-canonical",
161
+ message: "canonical must be true or false."
162
+ });
163
+ if (!docTypes.includes(type)) {
164
+ issues.push({ file: path, code: "invalid-type", message: `Invalid type: ${type}` });
165
+ }
166
+ const allowedStatuses = type === "decision" ? decisionStatuses : normalStatuses;
167
+ if (!allowedStatuses.includes(status)) {
168
+ issues.push({
169
+ file: path,
170
+ code: "invalid-status",
171
+ message: `Invalid status for ${type}: ${status}`
172
+ });
173
+ }
174
+ if ((status === "archived" || status === "superseded" || status === "rejected") && canonical !== false) {
175
+ issues.push({
176
+ file: path,
177
+ code: "terminal-canonical",
178
+ message: `${status} documents must have canonical: false.`
179
+ });
180
+ }
181
+ if (!isPathAllowedForType(path, type)) {
182
+ issues.push({
183
+ file: path,
184
+ code: "path-type-mismatch",
185
+ message: `Path does not match type ${type}.`
186
+ });
187
+ }
188
+ if (tags.length === 0) {
189
+ issues.push({
190
+ file: path,
191
+ code: "missing-tags",
192
+ message: "tags must contain at least one item."
193
+ });
194
+ }
195
+ if (created && lastReviewed && lastReviewed < created) {
196
+ issues.push({
197
+ file: path,
198
+ code: "bad-review-date",
199
+ message: "last_reviewed cannot be earlier than created."
200
+ });
201
+ }
202
+ if (status === "superseded" && !supersededBy) {
203
+ issues.push({
204
+ file: path,
205
+ code: "missing-superseded-by",
206
+ message: "status=superseded requires superseded_by to point at the successor document id."
207
+ });
208
+ }
209
+ if (issues.length > 0) return { issues };
210
+ return {
211
+ issues,
212
+ record: {
213
+ id,
214
+ title,
215
+ type,
216
+ status,
217
+ canonical: canonical ?? false,
218
+ owner,
219
+ created,
220
+ lastReviewed,
221
+ domain,
222
+ tags,
223
+ pinned,
224
+ related,
225
+ supersedes,
226
+ supersededBy,
227
+ archiveReason,
228
+ path
229
+ }
230
+ };
231
+ }
232
+ function isPathAllowedForType(path, type) {
233
+ if (path.startsWith("docs/governance/") && !path.startsWith("docs/governance/templates/")) {
234
+ return type === "policy" || type === "reference";
235
+ }
236
+ if (type === "policy") return path.startsWith("docs/policy/");
237
+ if (type === "decision") return path.startsWith("docs/decisions/");
238
+ if (type === "spec") return path.startsWith("docs/specs/");
239
+ if (type === "plan") return path.startsWith("docs/plans/");
240
+ if (type === "canon") return path.startsWith("docs/canon/");
241
+ if (type === "reference") return path.startsWith("docs/reference/");
242
+ if (type === "archive") return path.startsWith("docs/archive/");
243
+ return false;
244
+ }
245
+
246
+ // src/core/checker.ts
247
+ function checkDocs(rootDir = process.cwd()) {
248
+ const issues = [];
249
+ const records = [];
250
+ for (const path of listGovernedMarkdownFiles(rootDir)) {
251
+ const repoPath = pathToRepo(rootDir, path);
252
+ issues.push(...validateGovernedPath(repoPath));
253
+ const file = readFrontmatterFile(path);
254
+ if (!file) {
255
+ issues.push({
256
+ file: repoPath,
257
+ code: "missing-frontmatter",
258
+ message: "Markdown file must start with YAML frontmatter."
259
+ });
260
+ continue;
261
+ }
262
+ const result = validateFrontmatter(rootDir, file);
263
+ issues.push(...result.issues);
264
+ if (result.record) records.push(result.record);
265
+ }
266
+ issues.push(...validateGlobalIntegrity(records));
267
+ return {
268
+ ok: issues.length === 0,
269
+ records: records.sort((a, b) => a.path.localeCompare(b.path)),
270
+ issues
271
+ };
272
+ }
273
+ function validateGlobalIntegrity(records) {
274
+ const issues = [];
275
+ const byId = /* @__PURE__ */ new Map();
276
+ for (const record of records) {
277
+ byId.set(record.id, [...byId.get(record.id) ?? [], record]);
278
+ }
279
+ for (const [id, matches] of byId.entries()) {
280
+ if (matches.length > 1) {
281
+ for (const match of matches) {
282
+ issues.push({
283
+ file: match.path,
284
+ code: "duplicate-id",
285
+ message: `Duplicate document id: ${id}`
286
+ });
287
+ }
288
+ }
289
+ }
290
+ const ids = new Set(records.map((record) => record.id));
291
+ for (const record of records) {
292
+ for (const relatedId of record.related) {
293
+ if (!ids.has(relatedId)) {
294
+ issues.push({
295
+ file: record.path,
296
+ code: "missing-related",
297
+ message: `related id does not exist: ${relatedId}`
298
+ });
299
+ }
300
+ }
301
+ for (const supersededId of record.supersedes) {
302
+ if (!ids.has(supersededId)) {
303
+ issues.push({
304
+ file: record.path,
305
+ code: "missing-supersedes",
306
+ message: `supersedes id does not exist: ${supersededId}`
307
+ });
308
+ }
309
+ }
310
+ if (record.supersededBy && !ids.has(record.supersededBy)) {
311
+ issues.push({
312
+ file: record.path,
313
+ code: "missing-superseded-by-target",
314
+ message: `superseded_by id does not exist: ${record.supersededBy}`
315
+ });
316
+ }
317
+ }
318
+ return issues;
319
+ }
320
+ function validateGovernedPath(path) {
321
+ const issues = [];
322
+ const segments = path.split("/");
323
+ const fileName = segments[segments.length - 1] ?? "";
324
+ const baseName = fileName.replace(/\.md$/i, "").toLowerCase();
325
+ if (fileName.toLowerCase() === "readme.md") {
326
+ issues.push({
327
+ file: path,
328
+ code: "non-root-readme",
329
+ message: "Governed docs must use purpose-based names, not README.md. Keep README.md as the root human introduction only."
330
+ });
331
+ }
332
+ const forbiddenFileNames = /* @__PURE__ */ new Set([
333
+ "temp",
334
+ "tmp",
335
+ "scratch",
336
+ "notes",
337
+ "untitled",
338
+ "new",
339
+ "copy",
340
+ "final",
341
+ "final-final",
342
+ "final-v2",
343
+ "latest",
344
+ "wip",
345
+ "todo"
346
+ ]);
347
+ if (forbiddenFileNames.has(baseName)) {
348
+ issues.push({
349
+ file: path,
350
+ code: "forbidden-doc-name",
351
+ message: "Governed docs must use content-based names, not temp/latest/final/todo-style names."
352
+ });
353
+ }
354
+ const aiNameSegment = segments.find(
355
+ (segment) => /^(opus|codex|copilot|claude|gemini|cursor)(?:[-_ .]?\d.*)?$/i.test(segment)
356
+ );
357
+ if (aiNameSegment) {
358
+ issues.push({
359
+ file: path,
360
+ code: "ai-name-directory",
361
+ message: `Do not classify governed docs by AI/tool name (${aiNameSegment}); classify by type and domain.`
362
+ });
363
+ }
364
+ return issues;
365
+ }
366
+ function pathToRepo(rootDir, path) {
367
+ return path.startsWith(rootDir) ? path.slice(rootDir.length + 1).split(/\\/g).join("/") : path;
368
+ }
369
+
370
+ // src/core/paths.ts
371
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
372
+ import { join as join2 } from "node:path";
373
+ function planPath(rootDir, type, slugInput) {
374
+ const cleanSlug = slugInput.replace(/^\/+|\/+$/g, "");
375
+ if (!cleanSlug) throw new Error("Slug is required.");
376
+ const parts = cleanSlug.split("/").filter(Boolean);
377
+ const baseSlug = parts[parts.length - 1] ?? "";
378
+ const subdir = parts.slice(0, -1).join("/");
379
+ if (!isKebabCase(baseSlug)) {
380
+ throw new Error(`Slug "${baseSlug}" must be kebab-case (lowercase, digits, hyphens).`);
381
+ }
382
+ if (type === "decision") {
383
+ const n = nextSerial(rootDir, "docs/decisions", /^ADR-(\d{4})-/);
384
+ return {
385
+ filePath: `docs/decisions/ADR-${n}-${baseSlug}.md`,
386
+ id: `ADR-${n}`,
387
+ slug: baseSlug,
388
+ subdir: ""
389
+ };
390
+ }
391
+ if (type === "spec") {
392
+ const n = nextSerial(rootDir, "docs/specs", /^SPEC-(\d{4})-/);
393
+ return {
394
+ filePath: `docs/specs/active/SPEC-${n}-${baseSlug}.md`,
395
+ id: `SPEC-${n}`,
396
+ slug: baseSlug,
397
+ subdir: ""
398
+ };
399
+ }
400
+ if (type === "plan") {
401
+ const n = nextSerial(rootDir, "docs/plans", /^PLAN-(\d{4})-/);
402
+ return {
403
+ filePath: `docs/plans/active/PLAN-${n}-${baseSlug}.md`,
404
+ id: `PLAN-${n}`,
405
+ slug: baseSlug,
406
+ subdir: ""
407
+ };
408
+ }
409
+ if (type === "canon") {
410
+ const dir = subdir ? `docs/canon/${subdir}` : "docs/canon";
411
+ return {
412
+ filePath: `${dir}/${baseSlug}.md`,
413
+ id: kebabToUpper(baseSlug),
414
+ slug: baseSlug,
415
+ subdir
416
+ };
417
+ }
418
+ if (type === "policy") {
419
+ return {
420
+ filePath: `docs/policy/${baseSlug}.md`,
421
+ id: kebabToUpper(baseSlug),
422
+ slug: baseSlug,
423
+ subdir: ""
424
+ };
425
+ }
426
+ if (type === "reference") {
427
+ const dir = subdir ? `docs/reference/${subdir}` : "docs/reference";
428
+ return {
429
+ filePath: `${dir}/${baseSlug}.md`,
430
+ id: `REF-${kebabToUpper(baseSlug)}`,
431
+ slug: baseSlug,
432
+ subdir
433
+ };
434
+ }
435
+ if (type === "archive") {
436
+ throw new Error('Use "doc-gov archive <id>" instead of "doc-gov new archive ...".');
437
+ }
438
+ throw new Error(`Unknown type: ${type}`);
439
+ }
440
+ function nextSerial(rootDir, scanDir, regex) {
441
+ const root = join2(rootDir, scanDir);
442
+ if (!existsSync2(root)) return "0001";
443
+ let max = 0;
444
+ walkSerial(root, regex, (n) => {
445
+ if (n > max) max = n;
446
+ });
447
+ return String(max + 1).padStart(4, "0");
448
+ }
449
+ function walkSerial(dir, regex, onMatch) {
450
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
451
+ const path = join2(dir, entry.name);
452
+ if (entry.isDirectory()) walkSerial(path, regex, onMatch);
453
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
454
+ const m = entry.name.match(regex);
455
+ if (m && m[1]) onMatch(parseInt(m[1], 10));
456
+ }
457
+ }
458
+ }
459
+ function isKebabCase(slug) {
460
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
461
+ }
462
+ function kebabToUpper(slug) {
463
+ return slug.toUpperCase();
464
+ }
465
+ function quarterTag(date = /* @__PURE__ */ new Date()) {
466
+ const y = date.getUTCFullYear();
467
+ const q = Math.floor(date.getUTCMonth() / 3) + 1;
468
+ return `${y}-q${q}`;
469
+ }
470
+ function todayIso() {
471
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
472
+ }
473
+
474
+ // src/core/manifest.ts
475
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
476
+ import { dirname, join as join3 } from "node:path";
477
+ function buildManifest(rootDir = process.cwd()) {
478
+ const result = checkDocs(rootDir);
479
+ if (!result.ok) {
480
+ const error = result.issues.map((issue) => `${issue.file}: ${issue.code}: ${issue.message}`).join("\n");
481
+ throw new Error(`Cannot build manifest while doc-gov check fails:
482
+ ${error}`);
483
+ }
484
+ return renderManifest(result.records);
485
+ }
486
+ function writeManifest(rootDir = process.cwd()) {
487
+ const manifest = buildManifest(rootDir);
488
+ const path = join3(rootDir, "docs/governance/MANIFEST.yml");
489
+ mkdirSync(dirname(path), { recursive: true });
490
+ writeFileSync(path, manifest);
491
+ }
492
+ function manifestInSync(rootDir = process.cwd()) {
493
+ const path = join3(rootDir, "docs/governance/MANIFEST.yml");
494
+ if (!existsSync3(path)) return false;
495
+ return normalizeManifest(readFileSync2(path, "utf8")) === normalizeManifest(buildManifest(rootDir));
496
+ }
497
+ function normalizeManifest(value) {
498
+ return value.replace(/^generated_at: .*$/m, "generated_at: <ignored>").replace(/^generator_version: .*$/m, "generator_version: <ignored>");
499
+ }
500
+ function renderManifest(records) {
501
+ const lines = [
502
+ "# docs/governance/MANIFEST.yml \u2014 auto-generated, DO NOT EDIT MANUALLY",
503
+ "# Regenerate: pnpm doc-gov scan",
504
+ `generated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
505
+ "generator_version: doc-gov@0.3.0",
506
+ `docs_count: ${records.length}`,
507
+ "docs:"
508
+ ];
509
+ for (const record of records) {
510
+ lines.push(` - id: ${record.id}`);
511
+ lines.push(` path: ${record.path}`);
512
+ lines.push(` type: ${record.type}`);
513
+ lines.push(` status: ${record.status}`);
514
+ lines.push(` canonical: ${record.canonical}`);
515
+ lines.push(` last_reviewed: ${record.lastReviewed}`);
516
+ lines.push(` pinned: ${record.pinned}`);
517
+ }
518
+ lines.push("");
519
+ return lines.join("\n");
520
+ }
521
+
522
+ // src/commands/approve.ts
523
+ function runApprove(args2) {
524
+ const id = args2[0];
525
+ if (!id) {
526
+ console.error("Usage: pnpm doc-gov approve <id>");
527
+ return 1;
528
+ }
529
+ const root = process.cwd();
530
+ const result = checkDocs(root);
531
+ if (!result.ok) {
532
+ console.error("doc-gov check currently fails; fix issues before approving.");
533
+ return 1;
534
+ }
535
+ const record = result.records.find((r) => r.id === id);
536
+ if (!record) {
537
+ console.error(`No doc found with id: ${id}`);
538
+ return 1;
539
+ }
540
+ const fromStatus = record.status;
541
+ let toStatus;
542
+ if (fromStatus === "draft") toStatus = "active";
543
+ else if (fromStatus === "proposed") toStatus = "accepted";
544
+ else {
545
+ console.error(
546
+ `Cannot approve doc with status=${fromStatus}. Only draft or proposed can be approved.`
547
+ );
548
+ return 1;
549
+ }
550
+ const filePath = join4(root, record.path);
551
+ const content = readFileSync3(filePath, "utf8");
552
+ let next = content;
553
+ next = updateFrontmatterField(next, "status", toStatus);
554
+ next = updateFrontmatterField(next, "canonical", "true");
555
+ next = updateFrontmatterField(next, "last_reviewed", todayIso());
556
+ writeFileSync2(filePath, next);
557
+ console.log(`Approved ${id}: ${fromStatus} \u2192 ${toStatus}, canonical=true.`);
558
+ console.log(`
559
+ IMPORTANT: when committing this change, include in the commit message:`);
560
+ console.log(` Approves: ${id}`);
561
+ console.log(`(Lefthook commit-msg hook checks for this line on draft\u2192active transitions.)`);
562
+ try {
563
+ writeManifest(root);
564
+ } catch (err) {
565
+ console.error(err.message);
566
+ return 1;
567
+ }
568
+ return 0;
569
+ }
570
+ function updateFrontmatterField(content, key, value) {
571
+ if (!content.startsWith("---\n")) {
572
+ throw new Error("File has no frontmatter.");
573
+ }
574
+ const closing = content.indexOf("\n---", 4);
575
+ if (closing === -1) throw new Error("Frontmatter is unterminated.");
576
+ const head = content.slice(4, closing);
577
+ const tail = content.slice(closing);
578
+ const lines = head.split("\n");
579
+ const re = new RegExp(`^${key}:`);
580
+ let replaced = false;
581
+ const newLines = lines.map((line) => {
582
+ if (re.test(line) && !replaced) {
583
+ replaced = true;
584
+ return `${key}: ${value}`;
585
+ }
586
+ return line;
587
+ });
588
+ if (!replaced) {
589
+ newLines.push(`${key}: ${value}`);
590
+ }
591
+ return `---
592
+ ${newLines.join("\n")}${tail}`;
593
+ }
594
+
595
+ // src/commands/archive.ts
596
+ import { execSync } from "node:child_process";
597
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync, writeFileSync as writeFileSync3 } from "node:fs";
598
+ import { basename, dirname as dirname2, join as join5 } from "node:path";
599
+ function runArchive(args2) {
600
+ const positional = args2.filter((a) => !a.startsWith("--"));
601
+ const id = positional[0];
602
+ const reasonIdx = args2.indexOf("--reason");
603
+ const reason = reasonIdx !== -1 ? args2[reasonIdx + 1] ?? "" : "";
604
+ if (!id) {
605
+ console.error("Usage: pnpm doc-gov archive <id> [--reason <text>]");
606
+ return 1;
607
+ }
608
+ if (!reason) {
609
+ console.error("--reason is required (one short sentence describing why this doc is retired).");
610
+ return 1;
611
+ }
612
+ const root = process.cwd();
613
+ const result = checkDocs(root);
614
+ if (!result.ok) {
615
+ console.error("doc-gov check currently fails; fix issues before archiving.");
616
+ return 1;
617
+ }
618
+ const record = result.records.find((r) => r.id === id);
619
+ if (!record) {
620
+ console.error(`No doc with id: ${id}`);
621
+ return 1;
622
+ }
623
+ if (record.pinned) {
624
+ console.error(
625
+ `${id} is pinned. A human must commit the archival with "Pinned-Override: ${id}".`
626
+ );
627
+ return 1;
628
+ }
629
+ if (record.type === "archive") {
630
+ console.error(`${id} is already archived.`);
631
+ return 1;
632
+ }
633
+ const oldPath = record.path;
634
+ const fileName = basename(oldPath);
635
+ const archiveDir = `docs/archive/${quarterTag()}-${record.type}`;
636
+ const newPath = `${archiveDir}/${fileName}`;
637
+ const absNew = join5(root, newPath);
638
+ const absOld = join5(root, oldPath);
639
+ let content = readFileSync4(absOld, "utf8");
640
+ content = updateFrontmatterField(content, "type", "archive");
641
+ content = updateFrontmatterField(content, "status", "archived");
642
+ content = updateFrontmatterField(content, "canonical", "false");
643
+ content = updateFrontmatterField(content, "last_reviewed", todayIso());
644
+ content = updateFrontmatterField(content, "archive_reason", reason);
645
+ mkdirSync2(dirname2(absNew), { recursive: true });
646
+ let movedByGit = false;
647
+ try {
648
+ execSync(`git mv "${oldPath}" "${newPath}"`, { cwd: root, stdio: "ignore" });
649
+ movedByGit = true;
650
+ } catch {
651
+ renameSync(absOld, absNew);
652
+ }
653
+ writeFileSync3(absNew, content);
654
+ console.log(
655
+ `Archived ${id}: ${oldPath} \u2192 ${newPath}${movedByGit ? " (git mv)" : " (fs rename)"}`
656
+ );
657
+ try {
658
+ writeManifest(root);
659
+ console.log("MANIFEST.yml regenerated.");
660
+ } catch (err) {
661
+ console.error(err.message);
662
+ return 1;
663
+ }
664
+ return 0;
665
+ }
666
+
667
+ // src/commands/audit.ts
668
+ import { existsSync as existsSync5, readdirSync as readdirSync4 } from "node:fs";
669
+ import { join as join7 } from "node:path";
670
+
671
+ // src/core/link-checker.ts
672
+ import { existsSync as existsSync4, lstatSync, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
673
+ import { dirname as dirname3, extname, join as join6, resolve } from "node:path";
674
+ var CURRENT_MARKDOWN_ROOTS = ["AGENTS.md", "README.md", "docs"];
675
+ var CURRENT_DOC_DIR_PREFIXES = [
676
+ "docs/canon/",
677
+ "docs/reference/",
678
+ "docs/plans/",
679
+ "docs/specs/",
680
+ "docs/decisions/",
681
+ "docs/policy/"
682
+ ];
683
+ var LINK_PATTERN = /!??\[[^\]\n]*\]\(([^)\n]+)\)/g;
684
+ function checkCurrentMarkdownLinks(rootDir = process.cwd()) {
685
+ const files = listCurrentMarkdownFiles(rootDir);
686
+ const issues = [];
687
+ let checkedLinks = 0;
688
+ for (const filePath of files) {
689
+ const content = readText(filePath);
690
+ let match;
691
+ while (match = LINK_PATTERN.exec(content)) {
692
+ const rawTarget = match[1]?.trim();
693
+ if (!rawTarget) continue;
694
+ const target = parseMarkdownLinkTarget(rawTarget);
695
+ if (!target || shouldIgnoreTarget(target)) continue;
696
+ checkedLinks += 1;
697
+ if (!localTargetExists(rootDir, filePath, target)) {
698
+ const file = toRepoPath(rootDir, filePath);
699
+ const line = lineNumberAt(content, match.index);
700
+ issues.push({
701
+ file,
702
+ line,
703
+ target,
704
+ message: `Broken current doc link: ${file}:${line} -> ${target}`
705
+ });
706
+ }
707
+ }
708
+ }
709
+ return {
710
+ ok: issues.length === 0,
711
+ checkedFiles: files.length,
712
+ checkedLinks,
713
+ issues
714
+ };
715
+ }
716
+ function listCurrentMarkdownFiles(rootDir) {
717
+ const files = [];
718
+ for (const root of CURRENT_MARKDOWN_ROOTS) {
719
+ const fullPath = join6(rootDir, root);
720
+ if (!existsSync4(fullPath)) continue;
721
+ const stat = lstatSync(fullPath);
722
+ if (stat.isSymbolicLink()) continue;
723
+ if (stat.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
724
+ else if (stat.isFile() && fullPath.endsWith(".md")) files.push(fullPath);
725
+ }
726
+ return Array.from(new Set(files)).sort();
727
+ }
728
+ function walkMarkdown(rootDir, dir) {
729
+ const files = [];
730
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
731
+ const fullPath = join6(dir, entry.name);
732
+ const repoPath = toRepoPath(rootDir, fullPath);
733
+ if (shouldSkipTree(repoPath)) continue;
734
+ if (entry.isSymbolicLink()) continue;
735
+ if (entry.isDirectory()) files.push(...walkMarkdown(rootDir, fullPath));
736
+ if (entry.isFile() && entry.name.endsWith(".md") && shouldIncludeSource(repoPath)) {
737
+ files.push(fullPath);
738
+ }
739
+ }
740
+ return files;
741
+ }
742
+ function shouldSkipTree(repoPath) {
743
+ if (repoPath.startsWith("docs/archive/")) return true;
744
+ if (repoPath.startsWith("docs/governance/templates/")) return true;
745
+ if (repoPath === "docs/governance/MANIFEST.yml") return true;
746
+ return false;
747
+ }
748
+ function shouldIncludeSource(repoPath) {
749
+ if (repoPath.startsWith("docs/")) {
750
+ if (repoPath.startsWith("docs/governance/")) return true;
751
+ return CURRENT_DOC_DIR_PREFIXES.some((prefix) => repoPath.startsWith(prefix));
752
+ }
753
+ return false;
754
+ }
755
+ function parseMarkdownLinkTarget(rawTarget) {
756
+ const trimmed = rawTarget.trim();
757
+ if (trimmed.startsWith("<")) {
758
+ const closeIndex = trimmed.indexOf(">");
759
+ if (closeIndex > 0) return trimmed.slice(1, closeIndex);
760
+ }
761
+ return trimmed.split(/\s+/)[0] ?? "";
762
+ }
763
+ function shouldIgnoreTarget(target) {
764
+ if (target.startsWith("#")) return true;
765
+ if (target.startsWith("//")) return true;
766
+ return /^[a-z][a-z0-9+.-]*:/i.test(target);
767
+ }
768
+ function localTargetExists(rootDir, sourcePath, target) {
769
+ const pathPart = decodeTarget(target).split("#")[0]?.split("?")[0] ?? "";
770
+ if (!pathPart) return true;
771
+ const resolved = pathPart.startsWith("/") ? resolve(rootDir, `.${pathPart}`) : resolve(dirname3(sourcePath), pathPart);
772
+ const candidates = [resolved];
773
+ if (!extname(resolved)) {
774
+ candidates.push(`${resolved}.md`);
775
+ }
776
+ return candidates.some((candidate) => existsSync4(candidate));
777
+ }
778
+ function decodeTarget(target) {
779
+ try {
780
+ return decodeURIComponent(target);
781
+ } catch {
782
+ return target;
783
+ }
784
+ }
785
+ function lineNumberAt(content, index) {
786
+ let line = 1;
787
+ for (let i = 0; i < index; i += 1) {
788
+ if (content.charCodeAt(i) === 10) line += 1;
789
+ }
790
+ return line;
791
+ }
792
+ function readText(filePath) {
793
+ return readFileSync5(filePath, "utf8");
794
+ }
795
+
796
+ // src/commands/audit.ts
797
+ function runAudit() {
798
+ const root = process.cwd();
799
+ const result = checkDocs(root);
800
+ let warnings = 0;
801
+ if (!result.ok) {
802
+ for (const issue of result.issues) {
803
+ console.error(`${issue.file}: ${issue.code}: ${issue.message}`);
804
+ }
805
+ return 1;
806
+ }
807
+ const migrationSource = join7(root, "Docs-for trans");
808
+ if (existsSync5(migrationSource)) {
809
+ const count = countFiles(migrationSource);
810
+ warnings += 1;
811
+ console.log(
812
+ `Migration source still exists: Docs-for trans (${count} files). This was a one-time migration shell; it should not be reintroduced.`
813
+ );
814
+ }
815
+ if (existsSync5(join7(root, "DocSystemStarter.md"))) {
816
+ warnings += 1;
817
+ console.log(
818
+ "Stray root-level DocSystemStarter.md exists. The original draft has been archived; remove or re-archive."
819
+ );
820
+ }
821
+ const rootEntries = new Set(readdirSync4(root));
822
+ if (rootEntries.has("Docs")) {
823
+ console.error(
824
+ "Old Docs/ directory still exists. Move remaining files into docs/ or archive them."
825
+ );
826
+ return 1;
827
+ }
828
+ const linkResult = checkCurrentMarkdownLinks(root);
829
+ if (!linkResult.ok) {
830
+ for (const issue of linkResult.issues) {
831
+ console.error(issue.message);
832
+ }
833
+ return 1;
834
+ }
835
+ console.log(`doc-gov audit completed with ${warnings} warning(s).`);
836
+ return 0;
837
+ }
838
+ function countFiles(dir) {
839
+ let count = 0;
840
+ for (const entry of readdirSync4(dir, { withFileTypes: true })) {
841
+ const path = join7(dir, entry.name);
842
+ if (entry.isDirectory()) count += countFiles(path);
843
+ if (entry.isFile() || entry.isSymbolicLink()) count += 1;
844
+ }
845
+ return count;
846
+ }
847
+
848
+ // src/commands/check.ts
849
+ function runCheck() {
850
+ const result = checkDocs(process.cwd());
851
+ if (!result.ok) {
852
+ for (const issue of result.issues) {
853
+ console.error(`${issue.file}: ${issue.code}: ${issue.message}`);
854
+ }
855
+ return 1;
856
+ }
857
+ console.log(`doc-gov check passed (${result.records.length} docs).`);
858
+ return 0;
859
+ }
860
+
861
+ // src/commands/doctor.ts
862
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
863
+ import { join as join9 } from "node:path";
864
+
865
+ // src/core/router-integrity.ts
866
+ import { existsSync as existsSync6, readdirSync as readdirSync5, readFileSync as readFileSync6 } from "node:fs";
867
+ import { join as join8, relative as relative2 } from "node:path";
868
+ var CENTRAL_REQUIRED_FILES = [
869
+ "AGENTS.md",
870
+ "README.md",
871
+ "docs/governance/boundary.md",
872
+ "docs/governance/ssot-v0.9.md",
873
+ "docs/governance/templates/adr.md",
874
+ "docs/governance/templates/spec.md",
875
+ "docs/governance/templates/plan.md",
876
+ "docs/governance/templates/canon-entry.md",
877
+ "docs/governance/templates/reference.md",
878
+ "docs/governance/templates/policy.md",
879
+ "docs/governance/templates/archive.md",
880
+ "docs/governance/agents-routing/engineering-runtime-v0.9.md",
881
+ "docs/governance/agents-routing/doc-only-v0.9.md",
882
+ "integrations/superpowers.md",
883
+ "integrations/directed-development.md",
884
+ "profiles/engineering-runtime/profile.md",
885
+ "profiles/engineering-runtime/manifest.yml",
886
+ "profiles/doc-only/profile.md",
887
+ "profiles/doc-only/manifest.yml",
888
+ "starter/AGENTS.template.md",
889
+ "starter/CLAUDE.template.md",
890
+ "starter/lefthook.template.yml",
891
+ "starter/.github/workflows/docs-check.yml",
892
+ "starter/docs/reference/execution/current-work.md",
893
+ "starter/docs/policy/best-practice-for-this-project.md",
894
+ "starter/docs/governance/boundary.md",
895
+ "starter/docs/governance/ssot-v0.9.md",
896
+ "starter/docs/governance/agents-routing/engineering-runtime-v0.9.md",
897
+ "starter/docs/governance/agents-routing/doc-only-v0.9.md",
898
+ "starter/docs/governance/doc-agent-rules.md",
899
+ "starter/docs/governance/doc-types.md",
900
+ "starter/docs/governance/templates/adr.md",
901
+ "starter/docs/governance/templates/spec.md",
902
+ "starter/docs/governance/templates/plan.md",
903
+ "starter/docs/governance/templates/canon-entry.md",
904
+ "starter/docs/governance/templates/reference.md",
905
+ "starter/docs/governance/templates/policy.md",
906
+ "starter/docs/governance/templates/archive.md"
907
+ ];
908
+ var PROJECT_REQUIRED_FILES = [
909
+ "AGENTS.md",
910
+ "CLAUDE.md",
911
+ "docs/governance/boundary.md",
912
+ "docs/governance/ssot-v0.9.md",
913
+ "docs/governance/doc-agent-rules.md",
914
+ "docs/governance/doc-types.md",
915
+ "docs/reference/execution/current-work.md"
916
+ ];
917
+ var PROJECT_AGENTS_ROUTING_FILES = [
918
+ "docs/governance/agents-routing/engineering-runtime-v0.9.md",
919
+ "docs/governance/agents-routing/doc-only-v0.9.md"
920
+ ];
921
+ var FORBIDDEN_LEGACY_PATHS = [
922
+ "governance",
923
+ "docs-governance",
924
+ "routing",
925
+ "starter/governance",
926
+ "starter/docs-governance"
927
+ ];
928
+ var FORBIDDEN_PROJECT_PATHS = [
929
+ "docs/policy/shared-rules/ssot.md",
930
+ "docs/policy/shared-rules/task-routing.md"
931
+ ];
932
+ var FORBIDDEN_PROJECT_ROOTS = ["integrations"];
933
+ var FORBIDDEN_CENTRAL_ROOTS = ["shared-rules"];
934
+ var ROUTER_BLOCK_BEGIN = "<!-- PGS-ROUTER:BEGIN v0.9 -->";
935
+ var ROUTER_BLOCK_END = "<!-- PGS-ROUTER:END -->";
936
+ var EXTERNAL_ROUTER_MARKERS = [
937
+ "## gstack",
938
+ "## GStack",
939
+ "## Skill routing",
940
+ "## Superpowers"
941
+ ];
942
+ var CENTRAL_REQUIRED_NEEDLES = [
943
+ {
944
+ file: "AGENTS.md",
945
+ needle: ROUTER_BLOCK_BEGIN,
946
+ message: "AGENTS.md must include the PGS router block."
947
+ },
948
+ {
949
+ file: "AGENTS.md",
950
+ needle: "docs/governance/boundary.md",
951
+ message: "AGENTS.md must mention docs/governance/boundary.md."
952
+ },
953
+ {
954
+ file: "AGENTS.md",
955
+ needle: "docs/governance/ssot-v0.9.md",
956
+ message: "AGENTS.md must mention docs/governance/ssot-v0.9.md."
957
+ },
958
+ {
959
+ file: "AGENTS.md",
960
+ needle: "starter/docs/governance/doc-agent-rules.md",
961
+ message: "AGENTS.md must mention starter/docs/governance/doc-agent-rules.md."
962
+ },
963
+ {
964
+ file: "AGENTS.md",
965
+ needle: "starter/docs/governance/doc-types.md",
966
+ message: "AGENTS.md must mention starter/docs/governance/doc-types.md."
967
+ },
968
+ {
969
+ file: "AGENTS.md",
970
+ needle: "docs/governance/agents-routing/engineering-runtime-v0.9.md",
971
+ message: "AGENTS.md must mention docs/governance/agents-routing/engineering-runtime-v0.9.md."
972
+ },
973
+ {
974
+ file: "AGENTS.md",
975
+ needle: "docs/governance/agents-routing/doc-only-v0.9.md",
976
+ message: "AGENTS.md must mention docs/governance/agents-routing/doc-only-v0.9.md."
977
+ },
978
+ {
979
+ file: "AGENTS.md",
980
+ needle: "integrations/superpowers.md",
981
+ message: "AGENTS.md must mention integrations/superpowers.md."
982
+ },
983
+ {
984
+ file: "AGENTS.md",
985
+ needle: "integrations/directed-development.md",
986
+ message: "AGENTS.md must mention integrations/directed-development.md."
987
+ },
988
+ {
989
+ file: "AGENTS.md",
990
+ needle: "profiles/engineering-runtime/",
991
+ message: "AGENTS.md must mention profiles/engineering-runtime/."
992
+ },
993
+ {
994
+ file: "AGENTS.md",
995
+ needle: "profiles/doc-only/",
996
+ message: "AGENTS.md must mention profiles/doc-only/."
997
+ },
998
+ {
999
+ file: "README.md",
1000
+ needle: "docs/governance/agents-routing/",
1001
+ message: "README.md must point readers to agents-routing under docs/governance/."
1002
+ },
1003
+ {
1004
+ file: "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1005
+ needle: "Use matching Superpowers workflow if applicable",
1006
+ message: "Engineering routing must delegate Superpowers only inside the selected lane."
1007
+ },
1008
+ {
1009
+ file: "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1010
+ needle: "current-work.md",
1011
+ message: "Engineering routing must separate routing from current-work.md."
1012
+ },
1013
+ {
1014
+ file: "docs/governance/agents-routing/doc-only-v0.9.md",
1015
+ needle: "does not use Superpowers TDD or Directed Development by default",
1016
+ message: "Doc-only routing must exclude engineering Superpowers/DD by default."
1017
+ },
1018
+ {
1019
+ file: "integrations/superpowers.md",
1020
+ needle: "Agents routing classifies first",
1021
+ message: "Superpowers integration must state that agents routing classifies first."
1022
+ },
1023
+ {
1024
+ file: "integrations/superpowers.md",
1025
+ needle: "Superpowers executes inside the selected lane",
1026
+ message: "Superpowers integration must state that Superpowers executes inside the lane."
1027
+ },
1028
+ {
1029
+ file: "integrations/superpowers.md",
1030
+ needle: "does not vendor",
1031
+ message: "Superpowers integration must preserve the external-plugin boundary."
1032
+ },
1033
+ {
1034
+ file: "integrations/directed-development.md",
1035
+ needle: "optional workflow",
1036
+ message: "Directed Development integration must stay optional."
1037
+ },
1038
+ {
1039
+ file: "profiles/engineering-runtime/manifest.yml",
1040
+ needle: "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1041
+ message: "Engineering profile manifest must point to engineering routing."
1042
+ },
1043
+ {
1044
+ file: "profiles/engineering-runtime/manifest.yml",
1045
+ needle: "integrations/superpowers.md",
1046
+ message: "Engineering profile manifest must include the Superpowers integration."
1047
+ },
1048
+ {
1049
+ file: "profiles/doc-only/manifest.yml",
1050
+ needle: "docs/governance/agents-routing/doc-only-v0.9.md",
1051
+ message: "Doc-only profile manifest must point to doc-only routing."
1052
+ },
1053
+ {
1054
+ file: "profiles/doc-only/manifest.yml",
1055
+ needle: "superpowers: false",
1056
+ message: "Doc-only profile manifest must keep Superpowers disabled by default."
1057
+ },
1058
+ {
1059
+ file: "starter/AGENTS.template.md",
1060
+ needle: ROUTER_BLOCK_BEGIN,
1061
+ message: "Starter AGENTS template must include the PGS router block."
1062
+ },
1063
+ {
1064
+ file: "starter/AGENTS.template.md",
1065
+ needle: "adopted profile",
1066
+ message: "Starter AGENTS template must make projects name their adopted profile."
1067
+ },
1068
+ {
1069
+ file: "starter/AGENTS.template.md",
1070
+ needle: "chosen agents-routing file",
1071
+ message: "Starter AGENTS template must make projects name their chosen agents-routing file."
1072
+ },
1073
+ {
1074
+ file: "starter/AGENTS.template.md",
1075
+ needle: "docs/governance/boundary.md",
1076
+ message: "Starter AGENTS template must point agents to docs/governance/boundary.md."
1077
+ },
1078
+ {
1079
+ file: "starter/AGENTS.template.md",
1080
+ needle: "docs/governance/ssot-v0.9.md",
1081
+ message: "Starter AGENTS template must point agents to docs/governance/ssot-v0.9.md."
1082
+ },
1083
+ {
1084
+ file: "starter/AGENTS.template.md",
1085
+ needle: "docs/governance/agents-routing/",
1086
+ message: "Starter AGENTS template must point agents to docs/governance/agents-routing/."
1087
+ },
1088
+ {
1089
+ file: "starter/AGENTS.template.md",
1090
+ needle: "docs/governance/doc-agent-rules.md",
1091
+ message: "Starter AGENTS template must point agents to docs/governance/doc-agent-rules.md."
1092
+ },
1093
+ {
1094
+ file: "starter/AGENTS.template.md",
1095
+ needle: "docs/governance/doc-types.md",
1096
+ message: "Starter AGENTS template must point agents to docs/governance/doc-types.md."
1097
+ },
1098
+ {
1099
+ file: "starter/AGENTS.template.md",
1100
+ needle: "docs/policy/",
1101
+ message: "Starter AGENTS template must keep project AI development policy in docs/policy/."
1102
+ },
1103
+ {
1104
+ file: "starter/lefthook.template.yml",
1105
+ needle: "pnpm doc-gov router-check",
1106
+ message: "Starter lefthook template must run doc-gov router-check."
1107
+ },
1108
+ {
1109
+ file: "starter/lefthook.template.yml",
1110
+ needle: "pnpm doc-gov links",
1111
+ message: "Starter lefthook template must run doc-gov links."
1112
+ },
1113
+ {
1114
+ file: "starter/.github/workflows/docs-check.yml",
1115
+ needle: "pnpm doc-gov router-check",
1116
+ message: "Starter docs-check workflow must run doc-gov router-check."
1117
+ },
1118
+ {
1119
+ file: "starter/.github/workflows/docs-check.yml",
1120
+ needle: "pnpm doc-gov links",
1121
+ message: "Starter docs-check workflow must run doc-gov links."
1122
+ }
1123
+ ];
1124
+ var PROJECT_REQUIRED_NEEDLES = [
1125
+ {
1126
+ file: "AGENTS.md",
1127
+ needle: ROUTER_BLOCK_BEGIN,
1128
+ message: "AGENTS.md must include the PGS router block."
1129
+ },
1130
+ {
1131
+ file: "AGENTS.md",
1132
+ needle: "README.md",
1133
+ message: "AGENTS.md must state how README.md is used."
1134
+ },
1135
+ {
1136
+ file: "AGENTS.md",
1137
+ needle: "docs/policy/",
1138
+ message: "AGENTS.md must point agents to docs/policy/."
1139
+ },
1140
+ {
1141
+ file: "AGENTS.md",
1142
+ needle: "docs/governance/boundary.md",
1143
+ message: "AGENTS.md must mention docs/governance/boundary.md."
1144
+ },
1145
+ {
1146
+ file: "AGENTS.md",
1147
+ needle: "docs/governance/ssot-v0.9.md",
1148
+ message: "AGENTS.md must mention docs/governance/ssot-v0.9.md."
1149
+ },
1150
+ {
1151
+ file: "AGENTS.md",
1152
+ needle: "docs/governance/doc-agent-rules.md",
1153
+ message: "AGENTS.md must mention docs/governance/doc-agent-rules.md."
1154
+ },
1155
+ {
1156
+ file: "AGENTS.md",
1157
+ needle: "docs/governance/doc-types.md",
1158
+ message: "AGENTS.md must mention docs/governance/doc-types.md."
1159
+ },
1160
+ {
1161
+ file: "AGENTS.md",
1162
+ needle: "docs/governance/agents-routing/",
1163
+ message: "AGENTS.md must point agents to docs/governance/agents-routing/."
1164
+ },
1165
+ {
1166
+ file: "AGENTS.md",
1167
+ needle: "docs/reference/execution/current-work.md",
1168
+ message: "AGENTS.md must mention docs/reference/execution/current-work.md."
1169
+ },
1170
+ {
1171
+ file: "docs/governance/boundary.md",
1172
+ needle: "Product artifacts outside governed docs",
1173
+ message: "Governance boundary must preserve the product artifact boundary."
1174
+ },
1175
+ {
1176
+ file: "docs/governance/ssot-v0.9.md",
1177
+ needle: "automatically govern every Markdown file",
1178
+ message: "SSOT rule must state that not every Markdown file is governed."
1179
+ },
1180
+ {
1181
+ file: "docs/governance/doc-agent-rules.md",
1182
+ needle: "Doc-gov governs `docs/**` by default",
1183
+ message: "Doc agent rules must keep the docs-only governed scope."
1184
+ },
1185
+ {
1186
+ file: "docs/governance/doc-types.md",
1187
+ needle: "Markdown outside `docs/**` is not a governed doc by default",
1188
+ message: "Doc types must keep Markdown outside docs/** out of governance by default."
1189
+ }
1190
+ ];
1191
+ function checkRouterIntegrity(rootDir = process.cwd()) {
1192
+ const issues = [];
1193
+ const isCentral = isCentralRepository(rootDir);
1194
+ const requiredFiles = isCentral ? CENTRAL_REQUIRED_FILES : PROJECT_REQUIRED_FILES;
1195
+ const requiredNeedles = isCentral ? CENTRAL_REQUIRED_NEEDLES : PROJECT_REQUIRED_NEEDLES;
1196
+ for (const file of requiredFiles) {
1197
+ if (!existsSync6(join8(rootDir, file))) {
1198
+ issues.push({
1199
+ file,
1200
+ code: "missing-router-file",
1201
+ message: `Required router/integration file is missing: ${file}`
1202
+ });
1203
+ }
1204
+ }
1205
+ if (!isCentral) {
1206
+ issues.push(...validateProjectAgentsRouting(rootDir));
1207
+ }
1208
+ for (const file of FORBIDDEN_LEGACY_PATHS) {
1209
+ if (existsSync6(join8(rootDir, file))) {
1210
+ issues.push({
1211
+ file,
1212
+ code: "legacy-governance-path",
1213
+ message: `Legacy governance path must not exist: ${file}`
1214
+ });
1215
+ }
1216
+ }
1217
+ if (!isCentral) {
1218
+ for (const file of FORBIDDEN_PROJECT_PATHS) {
1219
+ if (existsSync6(join8(rootDir, file))) {
1220
+ issues.push({
1221
+ file,
1222
+ code: "legacy-project-policy-path",
1223
+ message: `Legacy project policy path must not exist: ${file}`
1224
+ });
1225
+ }
1226
+ }
1227
+ for (const file of FORBIDDEN_PROJECT_ROOTS) {
1228
+ if (existsSync6(join8(rootDir, file))) {
1229
+ issues.push({
1230
+ file,
1231
+ code: "project-root-integration-path",
1232
+ message: `Project-level root integrations path must not exist: ${file}. Keep upstream integration docs in project-governance-system; use AGENTS.md or docs/reference/integrations/ only when project-specific guidance is needed.`
1233
+ });
1234
+ }
1235
+ }
1236
+ } else {
1237
+ for (const file of FORBIDDEN_CENTRAL_ROOTS) {
1238
+ if (existsSync6(join8(rootDir, file))) {
1239
+ issues.push({
1240
+ file,
1241
+ code: "central-external-shared-rule-copy",
1242
+ message: `Central repository must not keep external shared-rule copies at root path: ${file}. Link external shared rules from target projects under docs/policy/shared-rules/ instead.`
1243
+ });
1244
+ }
1245
+ }
1246
+ }
1247
+ for (const file of findGovernedReadmes(rootDir)) {
1248
+ issues.push({
1249
+ file,
1250
+ code: "non-root-readme",
1251
+ message: `Governed README.md must not exist outside the repository root: ${file}`
1252
+ });
1253
+ }
1254
+ for (const requirement of requiredNeedles) {
1255
+ const path = join8(rootDir, requirement.file);
1256
+ if (!existsSync6(path)) continue;
1257
+ const content = readFileSync6(path, "utf8");
1258
+ if (!content.includes(requirement.needle)) {
1259
+ issues.push({
1260
+ file: requirement.file,
1261
+ code: "missing-router-reference",
1262
+ message: requirement.message
1263
+ });
1264
+ }
1265
+ }
1266
+ const routerBlockFiles = isCentral ? ["AGENTS.md", "starter/AGENTS.template.md"] : ["AGENTS.md"];
1267
+ for (const file of routerBlockFiles) {
1268
+ issues.push(...validateRouterBlock(rootDir, file));
1269
+ }
1270
+ issues.push(...validateBacktickedLocalPaths(rootDir, "AGENTS.md"));
1271
+ issues.push(...validateBacktickedLocalPaths(rootDir, "CLAUDE.md"));
1272
+ issues.push(...validateBacktickedLocalPaths(rootDir, "README.md"));
1273
+ if (isCentral) {
1274
+ issues.push(...validateBacktickedLocalPaths(rootDir, "starter/AGENTS.template.md", "starter"));
1275
+ }
1276
+ const portableRouterFiles = isCentral ? [
1277
+ "AGENTS.md",
1278
+ "README.md",
1279
+ "starter/AGENTS.template.md",
1280
+ "starter/CLAUDE.template.md",
1281
+ "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1282
+ "docs/governance/agents-routing/doc-only-v0.9.md"
1283
+ ] : [
1284
+ "AGENTS.md",
1285
+ "CLAUDE.md",
1286
+ "README.md",
1287
+ "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1288
+ "docs/governance/agents-routing/doc-only-v0.9.md"
1289
+ ];
1290
+ for (const file of portableRouterFiles) {
1291
+ issues.push(...validatePortableRouterText(rootDir, file));
1292
+ }
1293
+ return {
1294
+ ok: issues.length === 0,
1295
+ issues
1296
+ };
1297
+ }
1298
+ function isCentralRepository(rootDir) {
1299
+ const packageJsonPath = join8(rootDir, "package.json");
1300
+ if (!existsSync6(packageJsonPath)) return false;
1301
+ const packageJson = readFileSync6(packageJsonPath, "utf8");
1302
+ return packageJson.includes('"name": "project-governance-system"') && existsSync6(join8(rootDir, "profiles")) && existsSync6(join8(rootDir, "starter")) && existsSync6(join8(rootDir, "integrations"));
1303
+ }
1304
+ function validateProjectAgentsRouting(rootDir) {
1305
+ const issues = [];
1306
+ const existingRoutes = PROJECT_AGENTS_ROUTING_FILES.filter(
1307
+ (file) => existsSync6(join8(rootDir, file))
1308
+ );
1309
+ if (existingRoutes.length === 0) {
1310
+ issues.push({
1311
+ file: "docs/governance/agents-routing",
1312
+ code: "missing-project-agents-routing",
1313
+ message: "Project must install one agents-routing file under docs/governance/agents-routing/."
1314
+ });
1315
+ return issues;
1316
+ }
1317
+ const agentsPath = join8(rootDir, "AGENTS.md");
1318
+ if (!existsSync6(agentsPath)) return issues;
1319
+ const agents = readFileSync6(agentsPath, "utf8");
1320
+ if (!existingRoutes.some((file) => agents.includes(file))) {
1321
+ issues.push({
1322
+ file: "AGENTS.md",
1323
+ code: "missing-selected-agents-routing",
1324
+ message: "AGENTS.md must name the selected agents-routing file under docs/governance/agents-routing/."
1325
+ });
1326
+ }
1327
+ return issues;
1328
+ }
1329
+ function validateRouterBlock(rootDir, file) {
1330
+ const path = join8(rootDir, file);
1331
+ if (!existsSync6(path)) return [];
1332
+ const content = readFileSync6(path, "utf8");
1333
+ const begin = content.indexOf(ROUTER_BLOCK_BEGIN);
1334
+ const end = content.indexOf(ROUTER_BLOCK_END);
1335
+ const issues = [];
1336
+ if (begin === -1 || end === -1 || end < begin) {
1337
+ issues.push({
1338
+ file,
1339
+ code: "invalid-router-block",
1340
+ message: `${file} must contain a valid ${ROUTER_BLOCK_BEGIN} block before external workflow routing.`
1341
+ });
1342
+ return issues;
1343
+ }
1344
+ for (const marker of EXTERNAL_ROUTER_MARKERS) {
1345
+ const markerIndex = content.indexOf(marker);
1346
+ if (markerIndex !== -1 && markerIndex < end) {
1347
+ issues.push({
1348
+ file,
1349
+ code: "external-routing-before-pgs",
1350
+ message: `${file} must place ${marker} after the PGS router block.`
1351
+ });
1352
+ }
1353
+ }
1354
+ return issues;
1355
+ }
1356
+ function validateBacktickedLocalPaths(rootDir, file, pathRoot = "") {
1357
+ const path = join8(rootDir, file);
1358
+ if (!existsSync6(path)) return [];
1359
+ const content = readFileSync6(path, "utf8");
1360
+ const issues = [];
1361
+ const seen = /* @__PURE__ */ new Set();
1362
+ const matches = content.matchAll(/`([^`]+)`/g);
1363
+ for (const match of matches) {
1364
+ const value = match[1]?.trim();
1365
+ if (!value || seen.has(value)) continue;
1366
+ seen.add(value);
1367
+ if (!isLocalPathReference(value)) continue;
1368
+ const normalized = value.endsWith("/") ? value.slice(0, -1) : value;
1369
+ if (existsSync6(join8(rootDir, pathRoot, normalized)) || existsSync6(join8(rootDir, normalized))) {
1370
+ continue;
1371
+ }
1372
+ issues.push({
1373
+ file,
1374
+ code: "missing-backticked-path",
1375
+ message: `${file} references a local path that does not exist: ${value}`
1376
+ });
1377
+ }
1378
+ return issues;
1379
+ }
1380
+ function isLocalPathReference(value) {
1381
+ if (/\s/.test(value)) return false;
1382
+ if (/^[a-z]+:\/\//i.test(value)) return false;
1383
+ if (value.includes("*")) return false;
1384
+ if (value.startsWith("<") || value.endsWith(">")) return false;
1385
+ return value === "README.md" || value.endsWith(".md") || value.endsWith("/") || value.startsWith("docs/") || value.startsWith("starter/") || value.startsWith("profiles/") || value.startsWith("integrations/");
1386
+ }
1387
+ function validatePortableRouterText(rootDir, file) {
1388
+ const path = join8(rootDir, file);
1389
+ if (!existsSync6(path)) return [];
1390
+ const content = readFileSync6(path, "utf8");
1391
+ if (!hasNonPortablePath(content)) return [];
1392
+ return [
1393
+ {
1394
+ file,
1395
+ code: "non-portable-router-path",
1396
+ message: `${file} must not contain machine-local or parent-escape paths. Use repository-relative paths or profile/starter references instead.`
1397
+ }
1398
+ ];
1399
+ }
1400
+ function hasNonPortablePath(content) {
1401
+ return /(^|[\s`'"])(\/Users\/|~\/|\$HOME(?:\/|\b)|%USERPROFILE%|[A-Za-z]:\\|\.\.\/)/.test(
1402
+ content
1403
+ ) || /\b(OneDrive|CloudStorage)\b/.test(content);
1404
+ }
1405
+ function findGovernedReadmes(rootDir) {
1406
+ const matches = [];
1407
+ for (const relRoot of ["docs", "starter/docs"]) {
1408
+ const absRoot = join8(rootDir, relRoot);
1409
+ if (existsSync6(absRoot)) walk2(absRoot);
1410
+ }
1411
+ return matches.sort();
1412
+ function walk2(dir) {
1413
+ for (const entry of readdirSync5(dir, { withFileTypes: true })) {
1414
+ if (entry.isSymbolicLink()) continue;
1415
+ if (entry.isDirectory()) {
1416
+ walk2(join8(dir, entry.name));
1417
+ continue;
1418
+ }
1419
+ if (!entry.isFile()) continue;
1420
+ if (entry.name.toLowerCase() !== "readme.md") continue;
1421
+ const repoPath = relative2(rootDir, join8(dir, entry.name)).split(/\\/g).join("/");
1422
+ if (repoPath !== "README.md") matches.push(repoPath);
1423
+ }
1424
+ }
1425
+ }
1426
+
1427
+ // src/commands/doctor.ts
1428
+ function runDoctor(_args) {
1429
+ const root = process.cwd();
1430
+ const issues = collectDoctorIssues(root);
1431
+ const errors = issues.filter((issue) => issue.severity === "error");
1432
+ const warnings = issues.filter((issue) => issue.severity === "warning");
1433
+ for (const issue of errors) {
1434
+ console.error(`error ${issue.code}: ${issue.message}`);
1435
+ }
1436
+ for (const issue of warnings) {
1437
+ console.log(`warning ${issue.code}: ${issue.message}`);
1438
+ }
1439
+ if (errors.length > 0) {
1440
+ console.error(
1441
+ `doc-gov doctor failed with ${errors.length} error(s) and ${warnings.length} warning(s).`
1442
+ );
1443
+ return 1;
1444
+ }
1445
+ console.log(`doc-gov doctor passed with ${warnings.length} warning(s).`);
1446
+ return 0;
1447
+ }
1448
+ function collectDoctorIssues(rootDir = process.cwd()) {
1449
+ const issues = [];
1450
+ const router = checkRouterIntegrity(rootDir);
1451
+ if (!router.ok) {
1452
+ for (const issue of router.issues) {
1453
+ issues.push({
1454
+ severity: "error",
1455
+ code: `router:${issue.code}`,
1456
+ message: `${issue.file}: ${issue.message}`
1457
+ });
1458
+ }
1459
+ }
1460
+ const docs = checkDocs(rootDir);
1461
+ if (!docs.ok) {
1462
+ for (const issue of docs.issues) {
1463
+ issues.push({
1464
+ severity: "error",
1465
+ code: `docs:${issue.code}`,
1466
+ message: `${issue.file}: ${issue.message}`
1467
+ });
1468
+ }
1469
+ }
1470
+ if (!manifestInSync(rootDir)) {
1471
+ issues.push({
1472
+ severity: "error",
1473
+ code: "manifest:out-of-sync",
1474
+ message: "docs/governance/MANIFEST.yml is stale. Run pnpm doc-gov scan."
1475
+ });
1476
+ }
1477
+ const links = checkCurrentMarkdownLinks(rootDir);
1478
+ if (!links.ok) {
1479
+ for (const issue of links.issues) {
1480
+ issues.push({
1481
+ severity: "error",
1482
+ code: "links:broken-local-link",
1483
+ message: issue.message
1484
+ });
1485
+ }
1486
+ }
1487
+ issues.push(...checkLefthook(rootDir));
1488
+ issues.push(...checkDocsCheckWorkflow(rootDir));
1489
+ return issues;
1490
+ }
1491
+ function checkLefthook(rootDir) {
1492
+ const path = join9(rootDir, "lefthook.yml");
1493
+ if (!existsSync7(path)) {
1494
+ return [
1495
+ {
1496
+ severity: "warning",
1497
+ code: "guardrail:missing-lefthook",
1498
+ message: "lefthook.yml is missing; local commits do not have the standard doc-gov gate."
1499
+ }
1500
+ ];
1501
+ }
1502
+ const content = readFileSync7(path, "utf8");
1503
+ const issues = [];
1504
+ for (const command2 of [
1505
+ "pnpm doc-gov router-check",
1506
+ "pnpm doc-gov check",
1507
+ "pnpm doc-gov scan --check",
1508
+ "pnpm doc-gov links",
1509
+ "pnpm doc-gov audit",
1510
+ "pnpm doc-gov verify-commit-msg"
1511
+ ]) {
1512
+ if (!content.includes(command2)) {
1513
+ issues.push({
1514
+ severity: "error",
1515
+ code: "guardrail:incomplete-lefthook",
1516
+ message: `lefthook.yml must include: ${command2}`
1517
+ });
1518
+ }
1519
+ }
1520
+ const preCommit = join9(rootDir, ".git/hooks/pre-commit");
1521
+ const commitMsg = join9(rootDir, ".git/hooks/commit-msg");
1522
+ if (!hookCallsLefthook(preCommit)) {
1523
+ issues.push({
1524
+ severity: "error",
1525
+ code: "guardrail:lefthook-not-installed",
1526
+ message: "lefthook.yml exists, but .git/hooks/pre-commit is not installed for lefthook."
1527
+ });
1528
+ }
1529
+ if (!hookCallsLefthook(commitMsg)) {
1530
+ issues.push({
1531
+ severity: "error",
1532
+ code: "guardrail:lefthook-not-installed",
1533
+ message: "lefthook.yml exists, but .git/hooks/commit-msg is not installed for lefthook."
1534
+ });
1535
+ }
1536
+ return issues;
1537
+ }
1538
+ function checkDocsCheckWorkflow(rootDir) {
1539
+ const path = join9(rootDir, ".github/workflows/docs-check.yml");
1540
+ if (!existsSync7(path)) {
1541
+ return [
1542
+ {
1543
+ severity: "warning",
1544
+ code: "guardrail:missing-docs-check-workflow",
1545
+ message: ".github/workflows/docs-check.yml is missing; CI does not have the standard doc-gov gate."
1546
+ }
1547
+ ];
1548
+ }
1549
+ const content = readFileSync7(path, "utf8");
1550
+ const issues = [];
1551
+ for (const command2 of [
1552
+ "pnpm doc-gov router-check",
1553
+ "pnpm doc-gov check",
1554
+ "pnpm doc-gov scan --check",
1555
+ "pnpm doc-gov links",
1556
+ "pnpm doc-gov audit"
1557
+ ]) {
1558
+ if (!content.includes(command2)) {
1559
+ issues.push({
1560
+ severity: "error",
1561
+ code: "guardrail:incomplete-docs-check-workflow",
1562
+ message: `.github/workflows/docs-check.yml must include: ${command2}`
1563
+ });
1564
+ }
1565
+ }
1566
+ return issues;
1567
+ }
1568
+ function hookCallsLefthook(path) {
1569
+ return existsSync7(path) && readFileSync7(path, "utf8").includes("lefthook");
1570
+ }
1571
+
1572
+ // src/commands/find.ts
1573
+ function runFind(args2) {
1574
+ const query = args2.join(" ").trim().toLowerCase();
1575
+ if (!query) {
1576
+ console.error("Usage: pnpm doc-gov find <topic>");
1577
+ return 1;
1578
+ }
1579
+ const result = checkDocs(process.cwd());
1580
+ if (!result.ok) {
1581
+ console.error("doc-gov check currently fails; fix docs before relying on find.");
1582
+ return 1;
1583
+ }
1584
+ const matches = result.records.filter((record) => {
1585
+ const haystack = [
1586
+ record.id,
1587
+ record.title,
1588
+ record.type,
1589
+ record.status,
1590
+ record.domain,
1591
+ record.tags.join(" "),
1592
+ record.path
1593
+ ].join(" ").toLowerCase();
1594
+ return haystack.includes(query);
1595
+ });
1596
+ if (matches.length === 0) {
1597
+ console.log(`No docs found for: ${query}`);
1598
+ return 0;
1599
+ }
1600
+ for (const record of matches) {
1601
+ console.log(`${record.id} ${record.type} ${record.status} ${record.path} ${record.title}`);
1602
+ }
1603
+ return 0;
1604
+ }
1605
+
1606
+ // src/commands/init.ts
1607
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
1608
+ import { join as join11 } from "node:path";
1609
+
1610
+ // src/core/templates.ts
1611
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
1612
+ import { join as join10 } from "node:path";
1613
+ var TEMPLATE_FILES = {
1614
+ decision: "adr.md",
1615
+ spec: "spec.md",
1616
+ plan: "plan.md",
1617
+ canon: "canon-entry.md",
1618
+ reference: "reference.md",
1619
+ policy: "policy.md",
1620
+ archive: "archive.md"
1621
+ };
1622
+ var DEFAULT_TEMPLATES = {
1623
+ "adr.md": [
1624
+ "---",
1625
+ "id: REPLACE-ME",
1626
+ "title: Replace Me",
1627
+ "type: decision",
1628
+ "status: proposed",
1629
+ "canonical: true",
1630
+ "owner: human",
1631
+ "created: YYYY-MM-DD",
1632
+ "last_reviewed: YYYY-MM-DD",
1633
+ "domain: meta",
1634
+ "tags:",
1635
+ " - replace-me",
1636
+ "pinned: false",
1637
+ "related: []",
1638
+ "---",
1639
+ "",
1640
+ "# REPLACE-ME: Replace Me",
1641
+ "",
1642
+ "## Context",
1643
+ "",
1644
+ "## Decision",
1645
+ "",
1646
+ "## Consequences",
1647
+ ""
1648
+ ].join("\n"),
1649
+ "spec.md": [
1650
+ "---",
1651
+ "id: REPLACE-ME",
1652
+ "title: Replace Me",
1653
+ "type: spec",
1654
+ "status: draft",
1655
+ "canonical: false",
1656
+ "owner: human",
1657
+ "created: YYYY-MM-DD",
1658
+ "last_reviewed: YYYY-MM-DD",
1659
+ "domain: meta",
1660
+ "tags:",
1661
+ " - replace-me",
1662
+ "pinned: false",
1663
+ "related: []",
1664
+ "---",
1665
+ "",
1666
+ "# REPLACE-ME: Replace Me",
1667
+ "",
1668
+ "## Problem",
1669
+ "",
1670
+ "## Requirements",
1671
+ "",
1672
+ "## Acceptance",
1673
+ ""
1674
+ ].join("\n"),
1675
+ "plan.md": [
1676
+ "---",
1677
+ "id: REPLACE-ME",
1678
+ "title: Replace Me",
1679
+ "type: plan",
1680
+ "status: draft",
1681
+ "canonical: false",
1682
+ "owner: ai-assisted",
1683
+ "created: YYYY-MM-DD",
1684
+ "last_reviewed: YYYY-MM-DD",
1685
+ "domain: meta",
1686
+ "tags:",
1687
+ " - replace-me",
1688
+ "pinned: false",
1689
+ "related: []",
1690
+ "---",
1691
+ "",
1692
+ "# REPLACE-ME: Replace Me",
1693
+ "",
1694
+ "## Goal",
1695
+ "",
1696
+ "## Scope",
1697
+ "",
1698
+ "## Steps",
1699
+ "",
1700
+ "- [ ] Step 1",
1701
+ "",
1702
+ "## Acceptance",
1703
+ "",
1704
+ "- [ ] Verification completed",
1705
+ "",
1706
+ "## Closeout",
1707
+ "",
1708
+ "When complete, move this plan to `docs/plans/completed/` and set `status: completed`.",
1709
+ ""
1710
+ ].join("\n"),
1711
+ "canon-entry.md": [
1712
+ "---",
1713
+ "id: REPLACE-ME",
1714
+ "title: Replace Me",
1715
+ "type: canon",
1716
+ "status: draft",
1717
+ "canonical: false",
1718
+ "owner: human",
1719
+ "created: YYYY-MM-DD",
1720
+ "last_reviewed: YYYY-MM-DD",
1721
+ "domain: canon",
1722
+ "tags:",
1723
+ " - replace-me",
1724
+ "pinned: false",
1725
+ "related: []",
1726
+ "---",
1727
+ "",
1728
+ "# REPLACE-ME: Replace Me",
1729
+ "",
1730
+ "## Current Truth",
1731
+ "",
1732
+ "## Source / Provenance",
1733
+ "",
1734
+ "## Open Questions",
1735
+ ""
1736
+ ].join("\n"),
1737
+ "reference.md": [
1738
+ "---",
1739
+ "id: REPLACE-ME",
1740
+ "title: Replace Me",
1741
+ "type: reference",
1742
+ "status: draft",
1743
+ "canonical: false",
1744
+ "owner: human",
1745
+ "created: YYYY-MM-DD",
1746
+ "last_reviewed: YYYY-MM-DD",
1747
+ "domain: reference",
1748
+ "tags:",
1749
+ " - replace-me",
1750
+ "pinned: false",
1751
+ "related: []",
1752
+ "---",
1753
+ "",
1754
+ "# REPLACE-ME: Replace Me",
1755
+ "",
1756
+ "## Purpose",
1757
+ "",
1758
+ "## Details",
1759
+ "",
1760
+ "## Related Commands / Files",
1761
+ ""
1762
+ ].join("\n"),
1763
+ "policy.md": [
1764
+ "---",
1765
+ "id: REPLACE-ME",
1766
+ "title: Replace Me",
1767
+ "type: policy",
1768
+ "status: draft",
1769
+ "canonical: false",
1770
+ "owner: human",
1771
+ "created: YYYY-MM-DD",
1772
+ "last_reviewed: YYYY-MM-DD",
1773
+ "domain: policy",
1774
+ "tags:",
1775
+ " - replace-me",
1776
+ "pinned: false",
1777
+ "related: []",
1778
+ "---",
1779
+ "",
1780
+ "# REPLACE-ME: Replace Me",
1781
+ "",
1782
+ "## Rule",
1783
+ "",
1784
+ "## Rationale",
1785
+ "",
1786
+ "## Examples",
1787
+ ""
1788
+ ].join("\n"),
1789
+ "archive.md": [
1790
+ "---",
1791
+ "id: REPLACE-ME",
1792
+ "title: Replace Me",
1793
+ "type: archive",
1794
+ "status: archived",
1795
+ "canonical: false",
1796
+ "owner: human",
1797
+ "created: YYYY-MM-DD",
1798
+ "last_reviewed: YYYY-MM-DD",
1799
+ "domain: archive",
1800
+ "tags:",
1801
+ " - replace-me",
1802
+ "pinned: false",
1803
+ "related: []",
1804
+ "archive_reason: Replace me",
1805
+ "---",
1806
+ "",
1807
+ "# REPLACE-ME: Replace Me (archived)",
1808
+ "",
1809
+ "## Archived Reason",
1810
+ "",
1811
+ "## Historical Notes",
1812
+ ""
1813
+ ].join("\n")
1814
+ };
1815
+ function loadTemplate(rootDir, type) {
1816
+ const file = TEMPLATE_FILES[type];
1817
+ if (!file) throw new Error(`No template file mapped for type: ${type}`);
1818
+ const path = join10(rootDir, "docs/governance/templates", file);
1819
+ if (existsSync8(path)) return readFileSync8(path, "utf8");
1820
+ const fallback = DEFAULT_TEMPLATES[file];
1821
+ if (fallback) return fallback;
1822
+ throw new Error(`Template file is missing: docs/governance/templates/${file}`);
1823
+ }
1824
+ function ensureDefaultTemplates(rootDir) {
1825
+ const templatesDir = join10(rootDir, "docs/governance/templates");
1826
+ mkdirSync3(templatesDir, { recursive: true });
1827
+ let created = 0;
1828
+ for (const [file, content] of Object.entries(DEFAULT_TEMPLATES)) {
1829
+ const path = join10(templatesDir, file);
1830
+ if (existsSync8(path)) continue;
1831
+ writeFileSync4(path, content);
1832
+ created++;
1833
+ }
1834
+ return created;
1835
+ }
1836
+ function renderTemplate(template, values) {
1837
+ const closing = template.indexOf("\n---", 4);
1838
+ if (closing === -1) throw new Error("Template is missing closing frontmatter marker.");
1839
+ const body = template.slice(closing + 4).replace(/^\n/, "");
1840
+ const tagsBlock = values.tags.length > 0 ? values.tags.map((t) => ` - ${t}`).join("\n") : " - replace-me";
1841
+ const frontmatter = [
1842
+ "---",
1843
+ `id: ${values.id}`,
1844
+ `title: ${values.title}`,
1845
+ `type: ${values.type}`,
1846
+ `status: ${values.status}`,
1847
+ `canonical: ${values.canonical}`,
1848
+ `owner: ${values.owner}`,
1849
+ `created: ${values.created}`,
1850
+ `last_reviewed: ${values.lastReviewed}`,
1851
+ `domain: ${values.domain}`,
1852
+ "tags:",
1853
+ tagsBlock,
1854
+ `pinned: ${values.pinned}`,
1855
+ "related: []",
1856
+ "---",
1857
+ ""
1858
+ ].join("\n");
1859
+ const replacedBody = body.replace(/^# REPLACE-ME: Replace Me/m, `# ${values.id}: ${values.title}`).replace(/^# Replace Me$/m, `# ${values.title}`).replace(/^# Replace Me \(archived\)$/m, `# ${values.title} (archived)`);
1860
+ return frontmatter + replacedBody;
1861
+ }
1862
+
1863
+ // src/commands/init.ts
1864
+ function runInit(args2) {
1865
+ const force = args2.includes("--force");
1866
+ const root = process.cwd();
1867
+ const dirs = [
1868
+ "docs",
1869
+ "docs/governance",
1870
+ "docs/governance/agents-routing",
1871
+ "docs/governance/templates",
1872
+ "docs/policy",
1873
+ "docs/decisions",
1874
+ "docs/specs/active",
1875
+ "docs/specs/completed",
1876
+ "docs/plans/active",
1877
+ "docs/plans/completed",
1878
+ "docs/canon",
1879
+ "docs/reference",
1880
+ "docs/reference/execution",
1881
+ "docs/archive"
1882
+ ];
1883
+ let created = 0;
1884
+ for (const dir of dirs) {
1885
+ const abs = join11(root, dir);
1886
+ if (!existsSync9(abs)) {
1887
+ mkdirSync4(abs, { recursive: true });
1888
+ created++;
1889
+ } else if (!force) {
1890
+ }
1891
+ }
1892
+ for (const dir of ["docs/specs/completed", "docs/plans/completed", "docs/archive"]) {
1893
+ const keep = join11(root, dir, ".gitkeep");
1894
+ if (!existsSync9(keep)) writeFileSync5(keep, "");
1895
+ }
1896
+ const templatesCreated = ensureDefaultTemplates(root);
1897
+ console.log(
1898
+ `doc-gov init: ${created} directories created, ${templatesCreated} templates created (existing files left untouched).`
1899
+ );
1900
+ console.log(`
1901
+ Next steps for a brand-new project:`);
1902
+ console.log(` 1. Copy starter/AGENTS.template.md to AGENTS.md and fill the project name.`);
1903
+ console.log(` Copy starter/CLAUDE.template.md to CLAUDE.md as the Claude adapter.`);
1904
+ console.log(` 2. Copy starter/docs/governance/boundary.md, ssot-v0.9.md,`);
1905
+ console.log(` doc-agent-rules.md, doc-types.md, and one agents-routing file.`);
1906
+ console.log(` 3. Copy starter/docs/policy/best-practice-for-this-project.md and replace`);
1907
+ console.log(` placeholder policy with this project's real AI/development rules.`);
1908
+ console.log(` 4. Copy starter/docs/reference/documentation-map.md and`);
1909
+ console.log(` starter/docs/reference/execution/current-work.md.`);
1910
+ console.log(` Current work is required, but it can stay very lightweight.`);
1911
+ console.log(` 5. Pick a profile: profiles/engineering-runtime or profiles/doc-only.`);
1912
+ console.log(` 6. Install the CLI: pnpm add -D @pieai/doc-gov`);
1913
+ console.log(` or run this built CLI directly during local development.`);
1914
+ console.log(` 7. Optional hard guardrails: copy starter/lefthook.template.yml to`);
1915
+ console.log(` lefthook.yml, and starter/.github/workflows/docs-check.yml to`);
1916
+ console.log(` .github/workflows/docs-check.yml when the project is ready for gates.`);
1917
+ console.log(` 8. Add the target project's doc-gov script or package bin wiring.`);
1918
+ console.log(` 9. Validate the router: doc-gov router-check`);
1919
+ console.log(` Full health check: doc-gov doctor`);
1920
+ console.log(` 10. Write your first ADR: doc-gov new decision adopt-doc-gov`);
1921
+ return 0;
1922
+ }
1923
+
1924
+ // src/commands/links.ts
1925
+ function runLinks() {
1926
+ const result = checkCurrentMarkdownLinks(process.cwd());
1927
+ if (!result.ok) {
1928
+ for (const issue of result.issues) {
1929
+ console.error(issue.message);
1930
+ }
1931
+ return 1;
1932
+ }
1933
+ console.log(
1934
+ `doc-gov links passed (${result.checkedFiles} current files, ${result.checkedLinks} local links).`
1935
+ );
1936
+ return 0;
1937
+ }
1938
+
1939
+ // src/commands/migrate.ts
1940
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
1941
+ import { join as join12 } from "node:path";
1942
+ var PROFILE_ROUTES = {
1943
+ "engineering-runtime": "docs/governance/agents-routing/engineering-runtime-v0.9.md",
1944
+ "doc-only": "docs/governance/agents-routing/doc-only-v0.9.md"
1945
+ };
1946
+ function runMigrate(args2) {
1947
+ const checkOnly = args2.includes("--check");
1948
+ const apply = args2.includes("--apply");
1949
+ const profile = parseProfile(args2);
1950
+ if (!profile) {
1951
+ console.error("Usage: doc-gov migrate --profile <engineering-runtime|doc-only> --check");
1952
+ return 1;
1953
+ }
1954
+ if (apply) {
1955
+ console.error("doc-gov migrate --apply is not implemented yet. Run --check first.");
1956
+ return 1;
1957
+ }
1958
+ if (!checkOnly) {
1959
+ console.error("doc-gov migrate currently supports --check only.");
1960
+ return 1;
1961
+ }
1962
+ const issues = checkMigrationReadiness(process.cwd(), profile);
1963
+ if (issues.length > 0) {
1964
+ for (const issue of issues) console.error(issue);
1965
+ console.error(`doc-gov migrate --check failed for profile: ${profile}`);
1966
+ return 1;
1967
+ }
1968
+ console.log(`doc-gov migrate --check passed for profile: ${profile}`);
1969
+ return 0;
1970
+ }
1971
+ function checkMigrationReadiness(rootDir, profile) {
1972
+ const issues = [];
1973
+ const router = checkRouterIntegrity(rootDir);
1974
+ for (const issue of router.issues) {
1975
+ issues.push(`${issue.file}: ${issue.code}: ${issue.message}`);
1976
+ }
1977
+ const route = PROFILE_ROUTES[profile];
1978
+ if (!existsSync10(join12(rootDir, route))) {
1979
+ issues.push(`missing selected profile route: ${route}`);
1980
+ }
1981
+ const agentsPath = join12(rootDir, "AGENTS.md");
1982
+ if (existsSync10(agentsPath) && !readFileSync9(agentsPath, "utf8").includes(route)) {
1983
+ issues.push(`AGENTS.md must name selected profile route: ${route}`);
1984
+ }
1985
+ return issues;
1986
+ }
1987
+ function parseProfile(args2) {
1988
+ const index = args2.indexOf("--profile");
1989
+ const value = index >= 0 ? args2[index + 1] : void 0;
1990
+ if (value === "engineering-runtime" || value === "doc-only") return value;
1991
+ return void 0;
1992
+ }
1993
+
1994
+ // src/commands/list.ts
1995
+ function runList(args2) {
1996
+ const type = readFlag(args2, "--type");
1997
+ const status = readFlag(args2, "--status");
1998
+ const pinnedOnly = args2.includes("--pinned");
1999
+ const result = checkDocs(process.cwd());
2000
+ if (!result.ok) {
2001
+ console.error("doc-gov check currently fails; run `pnpm doc-gov check` to see issues.");
2002
+ return 1;
2003
+ }
2004
+ let records = result.records;
2005
+ if (type) records = records.filter((r) => r.type === type);
2006
+ if (status) records = records.filter((r) => r.status === status);
2007
+ if (pinnedOnly) records = records.filter((r) => r.pinned);
2008
+ if (records.length === 0) {
2009
+ console.log("No matching docs.");
2010
+ return 0;
2011
+ }
2012
+ console.log("ID TYPE STATUS CANONICAL PINNED PATH TITLE");
2013
+ for (const r of records) {
2014
+ console.log(
2015
+ `${r.id} ${r.type} ${r.status} ${r.canonical} ${r.pinned} ${r.path} ${r.title}`
2016
+ );
2017
+ }
2018
+ console.log(`
2019
+ ${records.length} doc(s).`);
2020
+ return 0;
2021
+ }
2022
+ function readFlag(args2, name) {
2023
+ const idx = args2.indexOf(name);
2024
+ if (idx === -1) return void 0;
2025
+ return args2[idx + 1];
2026
+ }
2027
+
2028
+ // src/commands/new.ts
2029
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
2030
+ import { dirname as dirname4, join as join13 } from "node:path";
2031
+ function runNew(args2) {
2032
+ const positional = args2.filter((a) => !a.startsWith("--"));
2033
+ const owner = readFlag2(args2, "--owner") ?? "human";
2034
+ const force = args2.includes("--force");
2035
+ const titleOverride = readFlag2(args2, "--title");
2036
+ const domainOverride = readFlag2(args2, "--domain") ?? "meta";
2037
+ if (positional.length < 2) {
2038
+ console.error(
2039
+ "Usage: pnpm doc-gov new <type> <slug> [--owner <h|ai-assisted|team>] [--title <text>] [--domain <slug>] [--force]"
2040
+ );
2041
+ return 1;
2042
+ }
2043
+ const [type, slug] = positional;
2044
+ if (!docTypes.includes(type)) {
2045
+ console.error(`Invalid type: ${type}. Allowed: ${docTypes.join(", ")}`);
2046
+ return 1;
2047
+ }
2048
+ const root = process.cwd();
2049
+ let plan;
2050
+ try {
2051
+ plan = planPath(root, type, slug);
2052
+ } catch (err) {
2053
+ console.error(err.message);
2054
+ return 1;
2055
+ }
2056
+ const absPath = join13(root, plan.filePath);
2057
+ if (existsSync11(absPath) && !force) {
2058
+ console.error(`File already exists: ${plan.filePath}. Use --force to overwrite.`);
2059
+ return 1;
2060
+ }
2061
+ const template = loadTemplate(root, type);
2062
+ const status = type === "decision" ? "proposed" : "draft";
2063
+ const canonical = type === "decision";
2064
+ const today = todayIso();
2065
+ const title = titleOverride ?? slugToTitle(plan.slug);
2066
+ const rendered = renderTemplate(template, {
2067
+ id: plan.id,
2068
+ title,
2069
+ type,
2070
+ status,
2071
+ canonical,
2072
+ owner,
2073
+ created: today,
2074
+ lastReviewed: today,
2075
+ domain: domainOverride,
2076
+ tags: [plan.slug.split("-")[0] ?? "replace-me"],
2077
+ pinned: false
2078
+ });
2079
+ mkdirSync5(dirname4(absPath), { recursive: true });
2080
+ writeFileSync6(absPath, rendered);
2081
+ console.log(`Created ${plan.filePath} with id ${plan.id}.`);
2082
+ try {
2083
+ writeManifest(root);
2084
+ console.log("MANIFEST.yml regenerated.");
2085
+ } catch (err) {
2086
+ console.error(
2087
+ "Created file but MANIFEST regeneration failed (probably check errors). Fix the new file then run `pnpm doc-gov scan`."
2088
+ );
2089
+ console.error(err.message);
2090
+ return 0;
2091
+ }
2092
+ console.log(`
2093
+ Next steps:`);
2094
+ console.log(` 1. Edit ${plan.filePath} (replace placeholder content).`);
2095
+ console.log(` 2. Run: pnpm doc-gov check`);
2096
+ if (status === "draft") {
2097
+ console.log(` 3. When ready: pnpm doc-gov approve ${plan.id}`);
2098
+ }
2099
+ return 0;
2100
+ }
2101
+ function readFlag2(args2, name) {
2102
+ const idx = args2.indexOf(name);
2103
+ if (idx === -1) return void 0;
2104
+ return args2[idx + 1];
2105
+ }
2106
+ function slugToTitle(slug) {
2107
+ return slug.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2108
+ }
2109
+
2110
+ // src/commands/router-check.ts
2111
+ function runRouterCheck() {
2112
+ const result = checkRouterIntegrity(process.cwd());
2113
+ if (!result.ok) {
2114
+ for (const issue of result.issues) {
2115
+ console.error(`${issue.file}: ${issue.code}: ${issue.message}`);
2116
+ }
2117
+ return 1;
2118
+ }
2119
+ console.log("doc-gov router-check passed.");
2120
+ return 0;
2121
+ }
2122
+
2123
+ // src/commands/scan.ts
2124
+ function runScan(args2) {
2125
+ const checkOnly = args2.includes("--check");
2126
+ if (checkOnly) {
2127
+ if (!manifestInSync(process.cwd())) {
2128
+ console.error("docs/governance/MANIFEST.yml is out of sync. Run: pnpm doc-gov scan");
2129
+ return 1;
2130
+ }
2131
+ console.log("doc-gov scan --check passed.");
2132
+ return 0;
2133
+ }
2134
+ writeManifest(process.cwd());
2135
+ console.log("docs/governance/MANIFEST.yml regenerated.");
2136
+ return 0;
2137
+ }
2138
+
2139
+ // src/commands/supersede.ts
2140
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
2141
+ import { join as join14 } from "node:path";
2142
+ function runSupersede(args2) {
2143
+ const [oldId, newId] = args2;
2144
+ if (!oldId || !newId) {
2145
+ console.error("Usage: pnpm doc-gov supersede <old-id> <new-id>");
2146
+ return 1;
2147
+ }
2148
+ const root = process.cwd();
2149
+ const result = checkDocs(root);
2150
+ if (!result.ok) {
2151
+ console.error("doc-gov check currently fails; fix issues before superseding.");
2152
+ return 1;
2153
+ }
2154
+ const oldRec = result.records.find((r) => r.id === oldId);
2155
+ const newRec = result.records.find((r) => r.id === newId);
2156
+ if (!oldRec) {
2157
+ console.error(`No doc with id: ${oldId}`);
2158
+ return 1;
2159
+ }
2160
+ if (!newRec) {
2161
+ console.error(`No doc with id: ${newId}`);
2162
+ return 1;
2163
+ }
2164
+ if (oldRec.type !== newRec.type) {
2165
+ console.error(
2166
+ `Cannot supersede across types: ${oldId}=${oldRec.type} vs ${newId}=${newRec.type}.`
2167
+ );
2168
+ return 1;
2169
+ }
2170
+ if (oldRec.pinned) {
2171
+ console.error(
2172
+ `${oldId} is pinned. Use commit message "Pinned-Override: ${oldId}" via human commit.`
2173
+ );
2174
+ return 1;
2175
+ }
2176
+ const today = todayIso();
2177
+ const oldPath = join14(root, oldRec.path);
2178
+ let oldContent = readFileSync10(oldPath, "utf8");
2179
+ oldContent = updateFrontmatterField(oldContent, "status", "superseded");
2180
+ oldContent = updateFrontmatterField(oldContent, "canonical", "false");
2181
+ oldContent = updateFrontmatterField(oldContent, "superseded_by", newId);
2182
+ oldContent = updateFrontmatterField(oldContent, "last_reviewed", today);
2183
+ writeFileSync7(oldPath, oldContent);
2184
+ const newPath = join14(root, newRec.path);
2185
+ let newContent = readFileSync10(newPath, "utf8");
2186
+ newContent = appendToFrontmatterList(newContent, "supersedes", newId === oldId ? "" : oldId);
2187
+ newContent = updateFrontmatterField(newContent, "last_reviewed", today);
2188
+ writeFileSync7(newPath, newContent);
2189
+ console.log(`Superseded ${oldId} (${oldRec.path}) by ${newId} (${newRec.path}).`);
2190
+ console.log(` ${oldId}: status=superseded, canonical=false, superseded_by=${newId}`);
2191
+ console.log(` ${newId}: supersedes += [${oldId}]`);
2192
+ try {
2193
+ writeManifest(root);
2194
+ console.log("MANIFEST.yml regenerated.");
2195
+ } catch (err) {
2196
+ console.error(err.message);
2197
+ return 1;
2198
+ }
2199
+ return 0;
2200
+ }
2201
+ function appendToFrontmatterList(content, key, value) {
2202
+ if (!value) return content;
2203
+ if (!content.startsWith("---\n")) throw new Error("File has no frontmatter.");
2204
+ const closing = content.indexOf("\n---", 4);
2205
+ if (closing === -1) throw new Error("Frontmatter is unterminated.");
2206
+ const head = content.slice(4, closing);
2207
+ const tail = content.slice(closing);
2208
+ const lines = head.split("\n");
2209
+ const keyRe = new RegExp(`^${key}:\\s*(.*)$`);
2210
+ const idx = lines.findIndex((l) => keyRe.test(l));
2211
+ if (idx === -1) {
2212
+ lines.push(`${key}:`);
2213
+ lines.push(` - ${value}`);
2214
+ return `---
2215
+ ${lines.join("\n")}${tail}`;
2216
+ }
2217
+ const inlineMatch = lines[idx]?.match(keyRe);
2218
+ const existing = inlineMatch?.[1]?.trim() ?? "";
2219
+ if (existing === "[]" || existing === "") {
2220
+ lines[idx] = `${key}:`;
2221
+ lines.splice(idx + 1, 0, ` - ${value}`);
2222
+ return `---
2223
+ ${lines.join("\n")}${tail}`;
2224
+ }
2225
+ let insertAt = idx + 1;
2226
+ while (insertAt < lines.length && /^\s+-\s+/.test(lines[insertAt] ?? "")) insertAt++;
2227
+ lines.splice(insertAt, 0, ` - ${value}`);
2228
+ return `---
2229
+ ${lines.join("\n")}${tail}`;
2230
+ }
2231
+
2232
+ // src/commands/verify-commit-msg.ts
2233
+ import { execSync as execSync2 } from "node:child_process";
2234
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "node:fs";
2235
+ function runVerifyCommitMsg(args2) {
2236
+ const msgFile = args2[0];
2237
+ if (!msgFile || !existsSync12(msgFile)) {
2238
+ return 0;
2239
+ }
2240
+ const message = readFileSync11(msgFile, "utf8");
2241
+ if (/^Merge\b/m.test(message) || /^Revert\b/m.test(message)) return 0;
2242
+ let stagedFiles = [];
2243
+ try {
2244
+ stagedFiles = execSync2("git diff --cached --name-only --diff-filter=ACM", {
2245
+ encoding: "utf8"
2246
+ }).split("\n").filter(isGovernedMarkdownPath);
2247
+ } catch {
2248
+ return 0;
2249
+ }
2250
+ const failures = [];
2251
+ for (const file of stagedFiles) {
2252
+ if (!existsSync12(file)) continue;
2253
+ const fm = readFrontmatterFile(file);
2254
+ if (!fm) continue;
2255
+ const id = stringValue(fm.data.id);
2256
+ if (!id) continue;
2257
+ const pinned = booleanValue(fm.data.pinned) === true;
2258
+ if (pinned) {
2259
+ const re = new RegExp(`^Pinned-Override:\\s*${escapeRegex(id)}\\b`, "m");
2260
+ if (!re.test(message)) {
2261
+ failures.push(
2262
+ `Pinned doc ${id} (${file}) modified without "Pinned-Override: ${id}" in commit message.`
2263
+ );
2264
+ }
2265
+ }
2266
+ const previous = readHeadFrontmatter(file);
2267
+ if (!previous) continue;
2268
+ const oldStatus = stringValue(previous.status);
2269
+ const newStatus = stringValue(fm.data.status);
2270
+ const requiresApproval = oldStatus === "draft" && newStatus === "active" || oldStatus === "proposed" && newStatus === "accepted";
2271
+ if (requiresApproval) {
2272
+ const re = new RegExp(`^Approves:\\s*${escapeRegex(id)}\\b`, "m");
2273
+ if (!re.test(message)) {
2274
+ failures.push(
2275
+ `Doc ${id} (${file}) changed status ${oldStatus}\u2192${newStatus} without "Approves: ${id}" in commit message.`
2276
+ );
2277
+ }
2278
+ }
2279
+ }
2280
+ if (failures.length > 0) {
2281
+ console.error("doc-gov verify-commit-msg failed:");
2282
+ for (const f of failures) console.error(" - " + f);
2283
+ console.error("\nAdd the required line(s) to your commit message and try again.");
2284
+ return 1;
2285
+ }
2286
+ return 0;
2287
+ }
2288
+ function escapeRegex(s) {
2289
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2290
+ }
2291
+ function isGovernedMarkdownPath(path) {
2292
+ if (!/\.md$/.test(path)) return false;
2293
+ if (path.startsWith("docs/governance/templates/")) return false;
2294
+ if (path === "docs/governance/MANIFEST.yml") return false;
2295
+ if (path.startsWith("docs/")) return true;
2296
+ return false;
2297
+ }
2298
+ function readHeadFrontmatter(file) {
2299
+ try {
2300
+ const content = execSync2(`git show HEAD:${shellQuote(file)}`, {
2301
+ encoding: "utf8",
2302
+ stdio: ["ignore", "pipe", "ignore"]
2303
+ });
2304
+ if (!content.startsWith("---\n")) return null;
2305
+ const closing = content.indexOf("\n---", 4);
2306
+ if (closing === -1) return null;
2307
+ return parseFrontmatter(content.slice(4, closing).trimEnd());
2308
+ } catch {
2309
+ return null;
2310
+ }
2311
+ }
2312
+ function shellQuote(value) {
2313
+ return `'${value.replace(/'/g, "'\\''")}'`;
2314
+ }
2315
+
2316
+ // src/cli.ts
2317
+ var COMMANDS = [
2318
+ "check",
2319
+ "scan",
2320
+ "audit",
2321
+ "links",
2322
+ "router-check",
2323
+ "doctor",
2324
+ "migrate",
2325
+ "find",
2326
+ "list",
2327
+ "new",
2328
+ "approve",
2329
+ "supersede",
2330
+ "archive",
2331
+ "init",
2332
+ "verify-commit-msg"
2333
+ ];
2334
+ var [command, ...args] = process.argv.slice(2);
2335
+ var exitCode = 0;
2336
+ if (command === "check") exitCode = runCheck();
2337
+ else if (command === "scan") exitCode = runScan(args);
2338
+ else if (command === "audit") exitCode = runAudit();
2339
+ else if (command === "links") exitCode = runLinks();
2340
+ else if (command === "router-check") exitCode = runRouterCheck();
2341
+ else if (command === "doctor") exitCode = runDoctor(args);
2342
+ else if (command === "migrate") exitCode = runMigrate(args);
2343
+ else if (command === "find") exitCode = runFind(args);
2344
+ else if (command === "list") exitCode = runList(args);
2345
+ else if (command === "new") exitCode = runNew(args);
2346
+ else if (command === "approve") exitCode = runApprove(args);
2347
+ else if (command === "supersede") exitCode = runSupersede(args);
2348
+ else if (command === "archive") exitCode = runArchive(args);
2349
+ else if (command === "init") exitCode = runInit(args);
2350
+ else if (command === "verify-commit-msg") exitCode = runVerifyCommitMsg(args);
2351
+ else if (!command || command === "--help" || command === "-h") {
2352
+ printHelp();
2353
+ exitCode = command ? 0 : 1;
2354
+ } else {
2355
+ console.error(`Unknown command: ${command}`);
2356
+ printHelp();
2357
+ exitCode = 1;
2358
+ }
2359
+ process.exitCode = exitCode;
2360
+ function printHelp() {
2361
+ console.log("doc-gov \u2014 cross-project AI-collaboration documentation governance");
2362
+ console.log("");
2363
+ console.log("Usage: pnpm doc-gov <command> [args...]");
2364
+ console.log("");
2365
+ console.log("Commands:");
2366
+ console.log(" check Validate frontmatter, status, canonical, integrity");
2367
+ console.log(" scan [--check] Regenerate (or verify) docs/governance/MANIFEST.yml");
2368
+ console.log(" audit Advisory health report");
2369
+ console.log(" links Validate current-layer local Markdown links");
2370
+ console.log(" router-check Validate router/profile/Superpowers wiring");
2371
+ console.log(" doctor Run the full governance health and guardrail check");
2372
+ console.log(" migrate --profile X --check Check project readiness for a selected profile");
2373
+ console.log(" find <topic> Search canonical docs by id/title/tag/path");
2374
+ console.log(" list [--type X] [--status Y] [--pinned] List docs");
2375
+ console.log(
2376
+ " new <type> <slug> Create a doc from template (decision/spec/plan/canon/policy/reference)"
2377
+ );
2378
+ console.log(" approve <id> draft\u2192active (or proposed\u2192accepted)");
2379
+ console.log(" supersede <oldId> <newId> Mark old as superseded by new (bidirectional link)");
2380
+ console.log(" archive <id> --reason TEXT Move doc to docs/archive/<quarter>-<type>/");
2381
+ console.log(" init [--force] Create directory skeleton in current project");
2382
+ console.log(
2383
+ " verify-commit-msg <file> Hook helper: enforce Pinned-Override / Approves markers"
2384
+ );
2385
+ console.log("");
2386
+ console.log(`Available commands: ${COMMANDS.join(", ")}`);
2387
+ }