@structor-dev/cli 0.1.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/bin/structor.mjs +576 -0
  4. package/docs/INIT.md +109 -0
  5. package/docs/adr/0001-default-generated-repo-name.md +9 -0
  6. package/docs/issues/0001-structor-doctor.md +39 -0
  7. package/examples/frontend-backend/harness.config.json +35 -0
  8. package/examples/openai-and-anthropic/harness.config.json +28 -0
  9. package/examples/single-repo/harness.config.json +26 -0
  10. package/harness.config.example.json +38 -0
  11. package/package.json +58 -0
  12. package/schemas/contract-manifest.schema.json +18 -0
  13. package/schemas/harness-config.schema.json +85 -0
  14. package/schemas/task-brief.schema.json +37 -0
  15. package/scripts/check-config.mjs +76 -0
  16. package/scripts/check-contract-manifests.mjs +85 -0
  17. package/scripts/check-model-overlays.mjs +30 -0
  18. package/scripts/check-placeholders.mjs +48 -0
  19. package/scripts/check-task-template.mjs +53 -0
  20. package/scripts/check-template-files.mjs +110 -0
  21. package/scripts/init-harness.mjs +270 -0
  22. package/scripts/lib.mjs +190 -0
  23. package/scripts/smoke-template.mjs +309 -0
  24. package/scripts/validate-governance.mjs +3 -0
  25. package/scripts/validate-template.mjs +16 -0
  26. package/template/.claude/CLAUDE.md.tpl +12 -0
  27. package/template/.claude/rules/harness-client-surfaces.md.tpl +20 -0
  28. package/template/.claude/settings.json.tpl +10 -0
  29. package/template/.codex/hooks.json.tpl +77 -0
  30. package/template/AGENTS.md.tpl +22 -0
  31. package/template/CLAUDE.md.tpl +16 -0
  32. package/template/README.md.tpl +109 -0
  33. package/template/ai/AGENT-GARBAGE-COLLECTION.md.tpl +18 -0
  34. package/template/ai/AGENTS.md.tpl +36 -0
  35. package/template/ai/ARCHITECTURE.md.tpl +35 -0
  36. package/template/ai/CODEX-HOOKS.md.tpl +23 -0
  37. package/template/ai/DECISIONS.md.tpl +22 -0
  38. package/template/ai/DESIGN.md.tpl +22 -0
  39. package/template/ai/HARNESS-ENGINEERING.md.tpl +107 -0
  40. package/template/ai/HARNESS.md.tpl +53 -0
  41. package/template/ai/HUB.md.tpl +53 -0
  42. package/template/ai/PRODUCT-SUMMARY.md.tpl +28 -0
  43. package/template/ai/PRODUCT.md.tpl +32 -0
  44. package/template/ai/QUALITY.md.tpl +37 -0
  45. package/template/ai/READINESS.md.tpl +39 -0
  46. package/template/ai/RUNNER-READINESS.md.tpl +14 -0
  47. package/template/ai/RUNNER-SAFETY.md.tpl +21 -0
  48. package/template/ai/VERSIONING.md.tpl +16 -0
  49. package/template/ai/WORKFLOW.md.tpl +42 -0
  50. package/template/ai/context.md.tpl +17 -0
  51. package/template/ai/contracts/README.md.tpl +23 -0
  52. package/template/ai/contracts/api-boundary.contract.json.tpl +11 -0
  53. package/template/ai/contracts/api-boundary.md.tpl +17 -0
  54. package/template/ai/contracts/app-legibility.contract.json.tpl +11 -0
  55. package/template/ai/contracts/app-legibility.md.tpl +24 -0
  56. package/template/ai/contracts/codex-hooks.contract.json.tpl +15 -0
  57. package/template/ai/contracts/codex-hooks.md.tpl +18 -0
  58. package/template/ai/contracts/github-safety.contract.json.tpl +11 -0
  59. package/template/ai/contracts/github-safety.md.tpl +15 -0
  60. package/template/ai/contracts/release-flow.contract.json.tpl +12 -0
  61. package/template/ai/contracts/release-flow.md.tpl +15 -0
  62. package/template/ai/contracts/repo-boundaries.contract.json.tpl +12 -0
  63. package/template/ai/contracts/repo-boundaries.md.tpl +18 -0
  64. package/template/ai/contracts/security-boundary.contract.json.tpl +11 -0
  65. package/template/ai/contracts/security-boundary.md.tpl +19 -0
  66. package/template/ai/knowledge-manifest.json.tpl +149 -0
  67. package/template/ai/model-overlays/anthropic/CLAUDE.md.tpl +14 -0
  68. package/template/ai/model-overlays/openai/AGENTS.md.tpl +13 -0
  69. package/template/ai/plans/README.md.tpl +10 -0
  70. package/template/ai/plans/tech-debt.md.tpl +7 -0
  71. package/template/ai/skills/README.md.tpl +15 -0
  72. package/template/ai/skills/review-architecture.md.tpl +41 -0
  73. package/template/ai/skills/review-contract-drift.md.tpl +41 -0
  74. package/template/ai/skills/review-governance-drift.md.tpl +42 -0
  75. package/template/ai/skills/review-security.md.tpl +40 -0
  76. package/template/ai/specs/README.md.tpl +14 -0
  77. package/template/ai/templates/README.md.tpl +13 -0
  78. package/template/ai/templates/fixtures/issues/invalid-placeholder.md.tpl +20 -0
  79. package/template/ai/templates/fixtures/issues/invalid-protected-surface.md.tpl +21 -0
  80. package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +105 -0
  81. package/template/ai/templates/issue-template.md.tpl +107 -0
  82. package/template/ai/templates/task-brief-template.md.tpl +185 -0
  83. package/template/ai/workspace/LOCAL-STACK.md.tpl +21 -0
  84. package/template/ai/workspace/REPOS.md.tpl +19 -0
  85. package/template/ai/workspace/SESSION-BOOTSTRAP.md.tpl +27 -0
  86. package/template/ai/workspace/SYSTEM-MAP.md.tpl +19 -0
  87. package/template/ai/workspace/TEST-STRATEGY.md.tpl +22 -0
  88. package/template/consumer/.claude/CLAUDE.md.tpl +14 -0
  89. package/template/consumer/AGENTS.md.tpl +23 -0
  90. package/template/consumer/CLAUDE.md.tpl +15 -0
  91. package/template/scripts/bootstrap-codex-worktree.mjs.tpl +52 -0
  92. package/template/scripts/bootstrap-workspace.mjs.tpl +100 -0
  93. package/template/scripts/check-claude-compatibility.mjs.tpl +120 -0
  94. package/template/scripts/check-codex-hooks.mjs.tpl +190 -0
  95. package/template/scripts/check-contract-manifests.mjs.tpl +81 -0
  96. package/template/scripts/check-garbage-collection.mjs.tpl +25 -0
  97. package/template/scripts/check-html-views.mjs.tpl +60 -0
  98. package/template/scripts/check-issue-template.mjs.tpl +167 -0
  99. package/template/scripts/check-knowledge-manifest.mjs.tpl +82 -0
  100. package/template/scripts/check-overlay-drift.mjs.tpl +49 -0
  101. package/template/scripts/check-plans.mjs.tpl +70 -0
  102. package/template/scripts/check-readiness.mjs.tpl +130 -0
  103. package/template/scripts/check-review-skills.mjs.tpl +48 -0
  104. package/template/scripts/check-task-template.mjs.tpl +63 -0
  105. package/template/scripts/check-template-governance.mjs.tpl +161 -0
  106. package/template/scripts/check-workspace.mjs.tpl +212 -0
  107. package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +122 -0
  108. package/template/scripts/check-worktrees.mjs.tpl +69 -0
  109. package/template/scripts/fixtures/worktrees/README.md.tpl +4 -0
  110. package/template/scripts/generate-html-views.mjs.tpl +189 -0
  111. package/template/scripts/hooks/codex-hook.mjs.tpl +21 -0
  112. package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +114 -0
  113. package/template/scripts/lib/worktree-bootstrap.mjs.tpl +388 -0
  114. package/template/scripts/validate-governance.mjs.tpl +78 -0
  115. package/template/workspace/.claude/CLAUDE.md.tpl +9 -0
  116. package/template/workspace/.claude/rules/harness-client-surfaces.md.tpl +15 -0
  117. package/template/workspace/.claude/settings.json.tpl +10 -0
  118. package/template/workspace/AGENTS.md.tpl +17 -0
  119. package/template/workspace/CLAUDE.md.tpl +18 -0
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, readdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const issueTemplatePath = "ai/templates/issue-template.md";
9
+ const issueFixtureDirectory = "ai/templates/fixtures/issues";
10
+ const issueFixtureFileExtension = ".md";
11
+ const invalidFixturePrefix = "invalid-";
12
+ const validStatuses = new Set(["Backlog", "Ready for Agent", "Running", "Needs Fix", "Report Ready", "PR Ready", "Human Review", "Blocked", "Done"]);
13
+ const validRisk = new Set(["low", "medium", "high"]);
14
+ const validAutonomy = new Set(["report_only", "pr_ready", "auto_merge"]);
15
+ const validModelPolicy = new Set(["cheap", "standard", "reasoning", "frontier", "review_only"]);
16
+ const canonicalRepos = new Set(["{{HARNESS_REPO_NAME}}", ...{{CONSUMER_REPO_NAMES_JSON}}]);
17
+ const requiredKeys = ["id", "status", "risk", "autonomy", "model_policy", "repos", "allowed_paths", "forbidden_paths", "requires_human_approval"];
18
+ const requiredSections = [
19
+ "Summary",
20
+ "Context",
21
+ "Goals",
22
+ "Non-Goals",
23
+ "Scope",
24
+ "Path Contract",
25
+ "Requirements",
26
+ "Bootstrap Requirements",
27
+ "Proposed Approach",
28
+ "Agent Execution Protocol",
29
+ "Success Criteria",
30
+ "Validation",
31
+ "Validation Evidence Required",
32
+ "Risk and Autonomy",
33
+ "Review Routing",
34
+ "Dependencies",
35
+ "Rollback / Recovery",
36
+ "Open Questions",
37
+ "Notes for the Agent",
38
+ ];
39
+ const placeholderPattern = /<[^>]+>/;
40
+ const sectionHeaderPatternMetaChars = /[.*+?^${}()|[\]\\]/g;
41
+ const protectedSurfacePattern = /\b(auth|authentication|authorization|billing|subscription|payment|secret|environment variable|infrastructure|deployment|database migration|production data|tenant|quota|rate limit|shared contract)\b/i;
42
+ const protectedSurfaceMentionPattern = /protected surface|protected surfaces/i;
43
+ const autoMergeFutureFacingPattern = /auto_merge.*future-facing|future-facing.*auto_merge/i;
44
+ const validationSectionHeader = "## Validation";
45
+ const validationCommandPattern = /`[^`]+`/;
46
+
47
+ async function read(relativePath) {
48
+ return readFile(path.join(repoRoot, relativePath), "utf8");
49
+ }
50
+
51
+ function parseScalar(value) {
52
+ if (value === "true") return true;
53
+ if (value === "false") return false;
54
+ return value;
55
+ }
56
+
57
+ function parseFrontMatter(content) {
58
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
59
+ if (!match) return null;
60
+ const result = {};
61
+ let currentKey = null;
62
+ for (const rawLine of match[1].split(/\r?\n/)) {
63
+ const line = rawLine.trimEnd();
64
+ const keyValue = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
65
+ if (keyValue) {
66
+ currentKey = keyValue[1];
67
+ result[currentKey] = keyValue[2] === "" ? [] : parseScalar(keyValue[2]);
68
+ continue;
69
+ }
70
+ const listItem = line.match(/^\s*-\s+(.+)$/);
71
+ if (listItem && currentKey) {
72
+ if (!Array.isArray(result[currentKey])) result[currentKey] = [];
73
+ result[currentKey].push(parseScalar(listItem[1]));
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+
79
+ function hasSection(content, section) {
80
+ return new RegExp(`^## ${section.replace(sectionHeaderPatternMetaChars, "\\$&")}\\s*$`, "m").test(content);
81
+ }
82
+
83
+ function valuesFor(value) {
84
+ return Array.isArray(value) ? value : [value];
85
+ }
86
+
87
+ function validateIssueFile(relativePath, content, { allowPlaceholders }) {
88
+ const errors = [];
89
+ const frontMatter = parseFrontMatter(content);
90
+ if (!frontMatter) return [`${relativePath} must start with YAML front matter.`];
91
+
92
+ for (const key of requiredKeys) {
93
+ if (!(key in frontMatter)) errors.push(`${relativePath} is missing front matter key '${key}'.`);
94
+ }
95
+ if (!allowPlaceholders && placeholderPattern.test(content)) {
96
+ errors.push(`${relativePath} contains placeholder text.`);
97
+ }
98
+ if (!placeholderPattern.test(String(frontMatter.status)) && !validStatuses.has(frontMatter.status)) {
99
+ errors.push(`${relativePath} has invalid status '${frontMatter.status}'.`);
100
+ }
101
+ if (!placeholderPattern.test(String(frontMatter.risk)) && !validRisk.has(frontMatter.risk)) {
102
+ errors.push(`${relativePath} has invalid risk '${frontMatter.risk}'.`);
103
+ }
104
+ if (!placeholderPattern.test(String(frontMatter.autonomy)) && !validAutonomy.has(frontMatter.autonomy)) {
105
+ errors.push(`${relativePath} has invalid autonomy '${frontMatter.autonomy}'.`);
106
+ }
107
+ if (!placeholderPattern.test(String(frontMatter.model_policy)) && !validModelPolicy.has(frontMatter.model_policy)) {
108
+ errors.push(`${relativePath} has invalid model_policy '${frontMatter.model_policy}'.`);
109
+ }
110
+ for (const repo of frontMatter.repos ?? []) {
111
+ if (!placeholderPattern.test(String(repo)) && !canonicalRepos.has(repo)) {
112
+ errors.push(`${relativePath} references non-canonical repo '${repo}'.`);
113
+ }
114
+ }
115
+ for (const key of ["allowed_paths", "forbidden_paths"]) {
116
+ if (!Array.isArray(frontMatter[key]) || frontMatter[key].length === 0) {
117
+ errors.push(`${relativePath} front matter key '${key}' must contain at least one path.`);
118
+ }
119
+ }
120
+ if (frontMatter.risk === "high" && frontMatter.autonomy === "auto_merge") {
121
+ errors.push(`${relativePath} has risk: high with autonomy: auto_merge.`);
122
+ }
123
+ if ("requires_human_approval" in frontMatter && typeof frontMatter.requires_human_approval !== "boolean") {
124
+ errors.push(`${relativePath} requires_human_approval must be boolean.`);
125
+ }
126
+ for (const section of requiredSections) {
127
+ if (!hasSection(content, section)) errors.push(`${relativePath} is missing required section '${section}'.`);
128
+ }
129
+ if (!protectedSurfaceMentionPattern.test(content)) {
130
+ errors.push(`${relativePath} must mention protected surfaces.`);
131
+ }
132
+ if (!autoMergeFutureFacingPattern.test(content)) {
133
+ errors.push(`${relativePath} must state that auto_merge is future-facing metadata.`);
134
+ }
135
+ const validationSection = content.match(new RegExp(`^${validationSectionHeader}\\s*\\n([\\s\\S]*?)(?=^## |$)`, "m"))?.[1] ?? "";
136
+ if (!allowPlaceholders && !validationCommandPattern.test(validationSection)) {
137
+ errors.push(`${relativePath} Validation section must include a concrete command in backticks.`);
138
+ }
139
+ if (!allowPlaceholders && protectedSurfacePattern.test(content) && frontMatter.requires_human_approval !== true) {
140
+ errors.push(`${relativePath} touches protected surfaces but requires_human_approval is not true.`);
141
+ }
142
+
143
+ return errors;
144
+ }
145
+
146
+ async function fixtureFiles() {
147
+ const dir = path.join(repoRoot, issueFixtureDirectory);
148
+ const entries = await readdir(dir, { withFileTypes: true });
149
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(issueFixtureFileExtension)).map((entry) => `${issueFixtureDirectory}/${entry.name}`);
150
+ }
151
+
152
+ const errors = [];
153
+ errors.push(...validateIssueFile(issueTemplatePath, await read(issueTemplatePath), { allowPlaceholders: true }));
154
+ for (const fixture of await fixtureFiles()) {
155
+ const fixtureErrors = validateIssueFile(fixture, await read(fixture), { allowPlaceholders: false });
156
+ const shouldFail = path.basename(fixture).startsWith(invalidFixturePrefix);
157
+ if (shouldFail && fixtureErrors.length === 0) errors.push(`${fixture} was expected to fail but passed.`);
158
+ if (!shouldFail && fixtureErrors.length > 0) errors.push(`${fixture} was expected to pass but failed: ${fixtureErrors.join("; ")}`);
159
+ }
160
+
161
+ if (errors.length > 0) {
162
+ console.error("Issue template check failed.");
163
+ for (const error of errors) console.error(`- ${error}`);
164
+ process.exit(1);
165
+ }
166
+
167
+ console.log("Issue template check passed.");
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, readFile, readdir } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const manifestPath = "ai/knowledge-manifest.json";
10
+ const ignoredAiDocDirectories = new Set(["model-overlays", "templates", "contracts", "skills", "specs", "plans"]);
11
+ const archiveOrGeneratedPattern = /archive|archived|historical|generated/i;
12
+
13
+ async function exists(relativePath) {
14
+ try {
15
+ await access(path.join(repoRoot, relativePath), fsConstants.F_OK);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async function read(relativePath) {
23
+ return readFile(path.join(repoRoot, relativePath), "utf8");
24
+ }
25
+
26
+ async function markdownFiles(baseRelativePath) {
27
+ const files = [];
28
+ async function walk(currentPath) {
29
+ const entries = await readdir(currentPath, { withFileTypes: true });
30
+ for (const entry of entries) {
31
+ const absolute = path.join(currentPath, entry.name);
32
+ const relative = path.relative(repoRoot, absolute).replaceAll(path.sep, "/");
33
+ if (entry.isDirectory()) {
34
+ if (ignoredAiDocDirectories.has(entry.name)) continue;
35
+ await walk(absolute);
36
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
37
+ files.push(relative);
38
+ }
39
+ }
40
+ }
41
+ await walk(path.join(repoRoot, baseRelativePath));
42
+ return files.sort();
43
+ }
44
+
45
+ const errors = [];
46
+ const manifest = JSON.parse(await read(manifestPath));
47
+ const docs = manifest.canonicalDocs ?? [];
48
+ const listed = new Set(docs.map((doc) => doc.path));
49
+
50
+ for (const doc of docs) {
51
+ if (!(await exists(doc.path))) {
52
+ errors.push(`${doc.path} is listed in ${manifestPath} but does not exist.`);
53
+ continue;
54
+ }
55
+ if (doc.status === "active" && !doc.purpose?.trim()) {
56
+ errors.push(`${doc.path} is active but has no purpose.`);
57
+ }
58
+ for (const linkedFrom of doc.linkedFrom ?? []) {
59
+ if (!(await exists(linkedFrom))) {
60
+ errors.push(`${doc.path} expects a routing link from ${linkedFrom}, but it does not exist.`);
61
+ continue;
62
+ }
63
+ const source = await read(linkedFrom);
64
+ if (!source.includes(doc.path) && !source.includes(path.basename(doc.path))) {
65
+ errors.push(`${doc.path} is not linked from ${linkedFrom}.`);
66
+ }
67
+ }
68
+ }
69
+
70
+ for (const relativePath of await markdownFiles("ai")) {
71
+ if (!listed.has(relativePath) && !archiveOrGeneratedPattern.test((await read(relativePath)).slice(0, 400))) {
72
+ errors.push(`${relativePath} is an active ai/*.md doc but is not listed in ${manifestPath}.`);
73
+ }
74
+ }
75
+
76
+ if (errors.length > 0) {
77
+ console.error("Knowledge manifest check failed.");
78
+ for (const error of errors) console.error(`- ${error}`);
79
+ process.exit(1);
80
+ }
81
+
82
+ console.log("Knowledge manifest check passed.");
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const models = {
9
+ openai: {{MODEL_OPENAI_ENABLED}},
10
+ anthropic: {{MODEL_ANTHROPIC_ENABLED}},
11
+ };
12
+
13
+ async function read(relativePath) {
14
+ return readFile(path.join(repoRoot, relativePath), "utf8");
15
+ }
16
+
17
+ const errors = [];
18
+ const canonical = await read("ai/AGENTS.md");
19
+ if (!canonical.includes("model-neutral")) {
20
+ errors.push("ai/AGENTS.md must keep shared guidance model-neutral.");
21
+ }
22
+
23
+ if (models.openai) {
24
+ const openai = await read("ai/model-overlays/openai/AGENTS.md");
25
+ if (!/OpenAI|Codex/i.test(openai)) {
26
+ errors.push("ai/model-overlays/openai/AGENTS.md must identify the OpenAI/Codex surface.");
27
+ }
28
+ if (!openai.includes("../AGENTS.md") && !openai.includes("../../AGENTS.md")) {
29
+ errors.push("ai/model-overlays/openai/AGENTS.md must route back to canonical ai/* guidance.");
30
+ }
31
+ }
32
+
33
+ if (models.anthropic) {
34
+ const claude = await read("ai/model-overlays/anthropic/CLAUDE.md");
35
+ if (!/Anthropic|Claude/i.test(claude)) {
36
+ errors.push("ai/model-overlays/anthropic/CLAUDE.md must identify the Anthropic/Claude surface.");
37
+ }
38
+ if (!claude.includes("../AGENTS.md") && !claude.includes("../../CLAUDE.md")) {
39
+ errors.push("ai/model-overlays/anthropic/CLAUDE.md must route back to canonical ai/* guidance.");
40
+ }
41
+ }
42
+
43
+ if (errors.length > 0) {
44
+ console.error("Overlay drift check failed.");
45
+ for (const error of errors) console.error(`- ${error}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ console.log("Overlay drift check passed.");
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, readFile, readdir } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const plansDir = "ai/plans/active";
10
+ const headingRules = ["Status", "Goal", "Next Step", "Validation Plan"];
11
+ const runtimeTokens = ["app-server.jsonl", "run-status.json", "events.jsonl", ".agent-runs", ".agent-workspaces", "scripts/orchestrator/"];
12
+ const readmePath = "ai/plans/README.md";
13
+ const rulesHeading = "## Rules";
14
+ const techDebtPath = "ai/plans/tech-debt.md";
15
+ const openItemsHeading = "## Open Items";
16
+
17
+ async function exists(relativePath) {
18
+ try {
19
+ await access(path.join(repoRoot, relativePath), fsConstants.F_OK);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ async function read(relativePath) {
27
+ return readFile(path.join(repoRoot, relativePath), "utf8");
28
+ }
29
+
30
+ async function markdownFiles(relativeDir) {
31
+ if (!(await exists(relativeDir))) return [];
32
+ const entries = await readdir(path.join(repoRoot, relativeDir), { withFileTypes: true });
33
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => `${relativeDir}/${entry.name}`);
34
+ }
35
+
36
+ function sectionHasContent(content, heading) {
37
+ const match = content.match(new RegExp(`^## ${heading}\\s*\\n([\\s\\S]*?)(?=^## |$)`, "m"));
38
+ return Boolean(match?.[1]?.replace(/[-*\\s:]/g, "").trim());
39
+ }
40
+
41
+ const errors = [];
42
+ for (const relativePath of await markdownFiles(plansDir)) {
43
+ const content = await read(relativePath);
44
+ for (const heading of headingRules) {
45
+ if (!sectionHasContent(content, heading)) errors.push(`${relativePath} has no ${heading} content.`);
46
+ }
47
+ for (const token of runtimeTokens) {
48
+ if (content.includes(token)) errors.push(`${relativePath} references runtime state token '${token}'.`);
49
+ }
50
+ }
51
+
52
+ const readme = await read(readmePath);
53
+ const rulesRegex = new RegExp(`^${rulesHeading}\\s*$`, "m");
54
+ if (!rulesRegex.test(readme)) {
55
+ errors.push("ai/plans/README.md must include a Rules section.");
56
+ }
57
+
58
+ const techDebt = await read(techDebtPath);
59
+ const openItemsRegex = new RegExp(`^${openItemsHeading}\\s*$`, "m");
60
+ if (!openItemsRegex.test(techDebt)) {
61
+ errors.push("ai/plans/tech-debt.md must include Open Items.");
62
+ }
63
+
64
+ if (errors.length > 0) {
65
+ console.error("Plan check failed.");
66
+ for (const error of errors) console.error(`- ${error}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ console.log("Plan check passed.");
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const models = {
9
+ openai: {{MODEL_OPENAI_ENABLED}},
10
+ anthropic: {{MODEL_ANTHROPIC_ENABLED}},
11
+ };
12
+ const clientSupport = {
13
+ codexHooks: {{CLIENT_CODEX_HOOKS_ENABLED}},
14
+ };
15
+ const requiredVerdicts = ["PASS", "FAIL", "MANUAL REVIEW REQUIRED"];
16
+ const readinessCommands = {
17
+ validateGovernance: "node scripts/validate-governance.mjs",
18
+ bootstrapWorkspaceDryRun: "node scripts/bootstrap-workspace.mjs --dry-run",
19
+ bootstrapWorkspace: "node scripts/bootstrap-workspace.mjs",
20
+ checkWorkspace: "node scripts/check-workspace.mjs",
21
+ checkOverlayDrift: "node scripts/check-overlay-drift.mjs",
22
+ checkCodexHooks: "node scripts/check-codex-hooks.mjs",
23
+ checkClaudeCompatibility: "node scripts/check-claude-compatibility.mjs",
24
+ };
25
+ const qualityTableHeader = "| Domain | Grade | Evidence | Enforced by | Blocking gaps |";
26
+ const gradePattern = /^(A|B|C|D|F|Manual|N\/A)$/;
27
+ const placeholderPattern = /^(todo|tbd|none)$/i;
28
+
29
+ async function read(relativePath) {
30
+ return readFile(path.join(repoRoot, relativePath), "utf8");
31
+ }
32
+
33
+ function tableRows(markdown, headerPrefix) {
34
+ const lines = markdown.split(/\r?\n/);
35
+ const start = lines.findIndex((line) => line.startsWith(headerPrefix));
36
+ if (start === -1) return [];
37
+ const rows = [];
38
+ for (const line of lines.slice(start + 2)) {
39
+ if (!line.startsWith("|")) break;
40
+ rows.push(line);
41
+ }
42
+ return rows;
43
+ }
44
+
45
+ function cells(row) {
46
+ return row
47
+ .split("|")
48
+ .slice(1, -1)
49
+ .map((cell) => cell.trim());
50
+ }
51
+
52
+ const readiness = await read("ai/READINESS.md");
53
+ const quality = await read("ai/QUALITY.md");
54
+ const errors = [];
55
+
56
+ for (const verdict of requiredVerdicts) {
57
+ if (!readiness.includes(verdict)) {
58
+ errors.push(`ai/READINESS.md must define the ${verdict} verdict.`);
59
+ }
60
+ }
61
+
62
+ const requiredCommands = [
63
+ readinessCommands.validateGovernance,
64
+ readinessCommands.bootstrapWorkspaceDryRun,
65
+ readinessCommands.bootstrapWorkspace,
66
+ readinessCommands.checkWorkspace,
67
+ ];
68
+ if (models.openai || models.anthropic) requiredCommands.push(readinessCommands.checkOverlayDrift);
69
+ if (clientSupport.codexHooks) requiredCommands.push(readinessCommands.checkCodexHooks);
70
+ if (models.anthropic) requiredCommands.push(readinessCommands.checkClaudeCompatibility);
71
+
72
+ for (const command of requiredCommands) {
73
+ if (!readiness.includes(command)) {
74
+ errors.push(`ai/READINESS.md must list required command: ${command}`);
75
+ }
76
+ }
77
+
78
+ const scorePatterns = [
79
+ /\b\d{1,3}\s*%/,
80
+ /\b\d+\s*\/\s*100\b/,
81
+ /\breadiness score\s*[:=]\s*\d+/i,
82
+ ];
83
+ for (const [relativePath, content] of [
84
+ ["ai/READINESS.md", readiness],
85
+ ["ai/QUALITY.md", quality],
86
+ ]) {
87
+ for (const pattern of scorePatterns) {
88
+ if (pattern.test(content)) {
89
+ errors.push(`${relativePath} must not use a numeric readiness score.`);
90
+ break;
91
+ }
92
+ }
93
+ }
94
+
95
+ const qualityRows = tableRows(quality, qualityTableHeader);
96
+ if (qualityRows.length === 0) {
97
+ errors.push("ai/QUALITY.md must include the readiness scorecard table.");
98
+ }
99
+
100
+ for (const row of qualityRows) {
101
+ const [domain, grade, evidence, enforcedBy, blockingGaps] = cells(row);
102
+ if (![domain, grade, evidence, enforcedBy, blockingGaps].every(Boolean)) {
103
+ errors.push(`ai/QUALITY.md has an incomplete scorecard row: ${row}`);
104
+ continue;
105
+ }
106
+ if (!gradePattern.test(grade)) {
107
+ errors.push(`ai/QUALITY.md has unsupported grade "${grade}" for ${domain}.`);
108
+ }
109
+ for (const [label, value] of [
110
+ ["Evidence", evidence],
111
+ ["Enforced by", enforcedBy],
112
+ ["Blocking gaps", blockingGaps],
113
+ ]) {
114
+ if (placeholderPattern.test(value)) {
115
+ errors.push(`ai/QUALITY.md ${label} for ${domain} must be evidence-based, not "${value}".`);
116
+ }
117
+ }
118
+ }
119
+
120
+ if (!quality.includes("ai/READINESS.md") || !quality.includes("scripts/check-readiness.mjs")) {
121
+ errors.push("ai/QUALITY.md must link readiness evidence to ai/READINESS.md and scripts/check-readiness.mjs.");
122
+ }
123
+
124
+ if (errors.length > 0) {
125
+ console.error("Readiness check failed.");
126
+ for (const error of errors) console.error(`- ${error}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ console.log("Readiness check passed.");
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, readdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const skillsReadmePath = "ai/skills/README.md";
9
+ const skillsDir = "ai/skills";
10
+ const skillFileExtension = ".md";
11
+ const requiredSections = [
12
+ "Purpose",
13
+ "When to Use",
14
+ "Required Inputs",
15
+ "Blocking Findings",
16
+ "Non-Blocking Observations",
17
+ "Output Format",
18
+ "Escalation Rules",
19
+ "Validation Or Evidence",
20
+ ];
21
+
22
+ async function read(relativePath) {
23
+ return readFile(path.join(repoRoot, relativePath), "utf8");
24
+ }
25
+
26
+ function hasHeading(content, heading) {
27
+ return new RegExp(`^## ${heading}\\s*$`, "m").test(content);
28
+ }
29
+
30
+ const errors = [];
31
+ const readme = await read(skillsReadmePath);
32
+ const entries = await readdir(path.join(repoRoot, skillsDir), { withFileTypes: true });
33
+ for (const entry of entries.filter((item) => item.isFile() && item.name.endsWith(skillFileExtension) && item.name !== "README.md")) {
34
+ const relativePath = `ai/skills/${entry.name}`;
35
+ const content = await read(relativePath);
36
+ if (!readme.includes(entry.name)) errors.push(`${relativePath} is not linked from ai/skills/README.md.`);
37
+ for (const section of requiredSections) {
38
+ if (!hasHeading(content, section)) errors.push(`${relativePath} is missing section '${section}'.`);
39
+ }
40
+ }
41
+
42
+ if (errors.length > 0) {
43
+ console.error("Review skill check failed.");
44
+ for (const error of errors) console.error(`- ${error}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ console.log("Review skill check passed.");
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const taskTemplatePath = "ai/templates/task-brief-template.md";
9
+ const content = await readFile(path.join(repoRoot, taskTemplatePath), "utf8");
10
+ const providerModelPattern = /\b(?:gpt-|claude|opus|sonnet|haiku)/i;
11
+
12
+ const requiredSections = [
13
+ "## Summary",
14
+ "## Context",
15
+ "## Goals",
16
+ "## Non-Goals",
17
+ "## Scope",
18
+ "## Path Contract",
19
+ "## Requirements",
20
+ "## Bootstrap Requirements",
21
+ "## Proposed Approach",
22
+ "## Agent Execution Protocol",
23
+ "## Success Criteria",
24
+ "## Validation",
25
+ "## Validation Evidence Required",
26
+ "## Risk and Autonomy",
27
+ "## Review Routing",
28
+ "## Dependencies",
29
+ "## Rollback / Recovery",
30
+ "## Open Questions",
31
+ "## Notes for the Agent",
32
+ ];
33
+
34
+ const requiredFrontmatter = [
35
+ "id:",
36
+ "status:",
37
+ "risk:",
38
+ "autonomy:",
39
+ "model_policy:",
40
+ "model:",
41
+ "repos:",
42
+ "allowed_paths:",
43
+ "forbidden_paths:",
44
+ "requires_human_approval:",
45
+ ];
46
+
47
+ const errors = [];
48
+ for (const token of [...requiredSections, ...requiredFrontmatter]) {
49
+ if (!content.includes(token)) errors.push(`${taskTemplatePath} is missing ${token}`);
50
+ }
51
+
52
+ const modelLine = content.split("\n").find((line) => line.startsWith("model:"));
53
+ if (modelLine && providerModelPattern.test(modelLine)) {
54
+ errors.push(`${taskTemplatePath} must use a runtime-neutral model selector, not a concrete provider model.`);
55
+ }
56
+
57
+ if (errors.length > 0) {
58
+ console.error("Task template check failed.");
59
+ for (const error of errors) console.error(`- ${error}`);
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log("Task template check passed.");