@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,309 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { repoRoot } from "./lib.mjs";
9
+
10
+ const cases = [
11
+ {
12
+ name: "openai-only",
13
+ models: { openai: true, anthropic: false },
14
+ consumers: [{ name: "product-app", purpose: "Application repository" }],
15
+ },
16
+ {
17
+ name: "anthropic-only",
18
+ models: { openai: false, anthropic: true },
19
+ consumers: [{ name: "product-app", purpose: "Application repository" }],
20
+ },
21
+ {
22
+ name: "both-models",
23
+ models: { openai: true, anthropic: true },
24
+ consumers: [
25
+ { name: "product-frontend", purpose: "Frontend application repository" },
26
+ { name: "product-backend", purpose: "Backend API repository" },
27
+ ],
28
+ },
29
+ ];
30
+ const smokePrefix = "smoke-";
31
+ const tempRootPrefix = "structor-";
32
+ const harnessConfigFileName = "harness.config.json";
33
+ const harnessSchemaPath = "schemas/harness-config.schema.json";
34
+ const initHarnessScript = "scripts/init-harness.mjs";
35
+ const nodeCommand = "node";
36
+ const lintCommand = "npm run lint";
37
+ const testCommand = "npm test";
38
+ const openaiRootEntrypoint = "AGENTS.md";
39
+ const openaiCodexConfig = ".codex/hooks.json";
40
+ const claudeRootEntrypoint = "CLAUDE.md";
41
+ const claudeMemoryEntrypoint = ".claude/CLAUDE.md";
42
+ const claudeRulesEntrypoint = ".claude/rules/harness-client-surfaces.md";
43
+
44
+ function run(command, args, cwd) {
45
+ execFileSync(command, args, { cwd, stdio: "pipe" });
46
+ }
47
+
48
+ function runResult(command, args, cwd) {
49
+ return spawnSync(command, args, { cwd, encoding: "utf8" });
50
+ }
51
+
52
+ function assertFails(command, args, cwd, label, expectedMessage) {
53
+ const result = runResult(command, args, cwd);
54
+ if (result.status === 0) {
55
+ throw new Error(`${label} should have failed.`);
56
+ }
57
+ const output = `${result.stdout}\n${result.stderr}`;
58
+ if (expectedMessage && !output.includes(expectedMessage)) {
59
+ throw new Error(`${label} failed without expected message ${JSON.stringify(expectedMessage)}. Output: ${output}`);
60
+ }
61
+ }
62
+
63
+ function assertExists(filePath, label) {
64
+ if (!existsSync(filePath)) {
65
+ throw new Error(`${label} was not generated: ${filePath}`);
66
+ }
67
+ }
68
+
69
+ function assertMissing(filePath, label) {
70
+ if (existsSync(filePath)) {
71
+ throw new Error(`${label} should not have been generated: ${filePath}`);
72
+ }
73
+ }
74
+
75
+ async function writeConfig(workspaceRoot, smokeCase, overrides = {}) {
76
+ for (const consumer of smokeCase.consumers) {
77
+ await mkdir(path.join(workspaceRoot, consumer.name), { recursive: true });
78
+ await writeFile(path.join(workspaceRoot, consumer.name, "README.md"), `# ${consumer.name}\n`);
79
+ }
80
+
81
+ const config = {
82
+ $schema: path.relative(workspaceRoot, path.join(repoRoot, harnessSchemaPath)),
83
+ project: {
84
+ name: `Smoke ${smokeCase.name}`,
85
+ slug: `smoke-${smokeCase.name}`,
86
+ harnessRepoName: `smoke-${smokeCase.name}-structor`,
87
+ },
88
+ output: overrides.output ?? {
89
+ path: `./smoke-${smokeCase.name}-structor`,
90
+ },
91
+ models: overrides.models ?? smokeCase.models,
92
+ clientSupport: {
93
+ codex: { hooks: smokeCase.models.openai },
94
+ claude: { rules: smokeCase.models.anthropic, hooks: false, skills: false },
95
+ },
96
+ consumers: overrides.consumers ?? smokeCase.consumers.map((consumer) => ({
97
+ name: consumer.name,
98
+ path: `./${consumer.name}`,
99
+ purpose: consumer.purpose,
100
+ validation: {
101
+ lint: lintCommand,
102
+ test: testCommand,
103
+ },
104
+ })),
105
+ };
106
+ if (overrides.removeProject) delete config.project;
107
+
108
+ const configPath = path.join(workspaceRoot, harnessConfigFileName);
109
+ await writeFile(configPath, overrides.rawJson ?? `${JSON.stringify(config, null, 2)}\n`);
110
+ return configPath;
111
+ }
112
+
113
+ async function validateCase(smokeCase) {
114
+ const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}${smokeCase.name}-`));
115
+ const configPath = await writeConfig(workspaceRoot, smokeCase);
116
+ const harnessRoot = path.join(workspaceRoot, `${smokePrefix}${smokeCase.name}-structor`);
117
+
118
+ run(nodeCommand, [path.join(repoRoot, initHarnessScript), "--config", configPath, "--dry-run"], repoRoot);
119
+ run(
120
+ nodeCommand,
121
+ [path.join(repoRoot, initHarnessScript), "--config", configPath, "--install-consumer-entrypoints"],
122
+ repoRoot,
123
+ );
124
+ run(nodeCommand, ["scripts/validate-governance.mjs"], harnessRoot);
125
+ assertExists(path.join(harnessRoot, "ai/views/index.html"), `${smokeCase.name} generated HTML view`);
126
+ run(nodeCommand, ["scripts/bootstrap-workspace.mjs", "--dry-run"], harnessRoot);
127
+ run(nodeCommand, ["scripts/bootstrap-workspace.mjs"], harnessRoot);
128
+ run(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot);
129
+
130
+ if (smokeCase.models.openai) {
131
+ assertExists(path.join(harnessRoot, "workspace/AGENTS.md"), `${smokeCase.name} generated workspace AGENTS`);
132
+ assertExists(path.join(workspaceRoot, "AGENTS.md"), `${smokeCase.name} workspace AGENTS`);
133
+ } else {
134
+ assertMissing(path.join(harnessRoot, "workspace/AGENTS.md"), `${smokeCase.name} generated workspace AGENTS`);
135
+ assertMissing(path.join(workspaceRoot, "AGENTS.md"), `${smokeCase.name} workspace AGENTS`);
136
+ }
137
+ if (smokeCase.models.anthropic) {
138
+ assertExists(path.join(harnessRoot, "workspace/CLAUDE.md"), `${smokeCase.name} generated workspace CLAUDE`);
139
+ assertExists(path.join(harnessRoot, "workspace/.claude/CLAUDE.md"), `${smokeCase.name} generated workspace Claude memory`);
140
+ assertExists(path.join(harnessRoot, "workspace/.claude/settings.json"), `${smokeCase.name} generated workspace Claude settings`);
141
+ assertExists(path.join(workspaceRoot, "CLAUDE.md"), `${smokeCase.name} workspace CLAUDE`);
142
+ assertExists(path.join(workspaceRoot, ".claude/CLAUDE.md"), `${smokeCase.name} workspace Claude memory`);
143
+ assertExists(path.join(workspaceRoot, ".claude/settings.json"), `${smokeCase.name} workspace Claude settings`);
144
+ } else {
145
+ assertMissing(path.join(harnessRoot, "workspace/CLAUDE.md"), `${smokeCase.name} generated workspace CLAUDE`);
146
+ assertMissing(path.join(harnessRoot, "workspace/.claude/CLAUDE.md"), `${smokeCase.name} generated workspace Claude memory`);
147
+ assertMissing(path.join(harnessRoot, "workspace/.claude/settings.json"), `${smokeCase.name} generated workspace Claude settings`);
148
+ assertMissing(path.join(workspaceRoot, "CLAUDE.md"), `${smokeCase.name} workspace CLAUDE`);
149
+ assertMissing(path.join(workspaceRoot, ".claude/CLAUDE.md"), `${smokeCase.name} workspace Claude memory`);
150
+ assertMissing(path.join(workspaceRoot, ".claude/settings.json"), `${smokeCase.name} workspace Claude settings`);
151
+ }
152
+
153
+ if (smokeCase.models.openai) {
154
+ assertExists(path.join(harnessRoot, openaiRootEntrypoint), `${smokeCase.name} OpenAI root entrypoint`);
155
+ assertExists(path.join(harnessRoot, openaiCodexConfig), `${smokeCase.name} Codex hook config`);
156
+ assertExists(path.join(harnessRoot, "scripts/check-codex-hooks.mjs"), `${smokeCase.name} Codex hook validator`);
157
+ assertExists(path.join(harnessRoot, "scripts/hooks/codex-hook.mjs"), `${smokeCase.name} Codex hook script`);
158
+ assertExists(
159
+ path.join(harnessRoot, "ai/model-overlays/openai/AGENTS.md"),
160
+ `${smokeCase.name} OpenAI overlay`,
161
+ );
162
+ } else {
163
+ assertMissing(path.join(harnessRoot, openaiRootEntrypoint), `${smokeCase.name} OpenAI root entrypoint`);
164
+ assertMissing(path.join(harnessRoot, ".codex/hooks.json"), `${smokeCase.name} Codex hook config`);
165
+ }
166
+
167
+ if (smokeCase.models.anthropic) {
168
+ assertExists(path.join(harnessRoot, claudeRootEntrypoint), `${smokeCase.name} Claude root entrypoint`);
169
+ assertExists(path.join(harnessRoot, claudeMemoryEntrypoint), `${smokeCase.name} Claude memory`);
170
+ assertExists(path.join(harnessRoot, claudeRulesEntrypoint), `${smokeCase.name} Claude rule`);
171
+ assertExists(
172
+ path.join(harnessRoot, "scripts/check-claude-compatibility.mjs"),
173
+ `${smokeCase.name} Claude compatibility validator`,
174
+ );
175
+ assertExists(
176
+ path.join(harnessRoot, "ai/model-overlays/anthropic/CLAUDE.md"),
177
+ `${smokeCase.name} Claude overlay`,
178
+ );
179
+ } else {
180
+ assertMissing(path.join(harnessRoot, claudeRootEntrypoint), `${smokeCase.name} Claude root entrypoint`);
181
+ assertMissing(path.join(harnessRoot, claudeRulesEntrypoint), `${smokeCase.name} Claude rule`);
182
+ }
183
+
184
+ for (const consumer of smokeCase.consumers) {
185
+ const consumerRoot = path.join(workspaceRoot, consumer.name);
186
+ if (smokeCase.models.openai) assertExists(path.join(consumerRoot, "AGENTS.md"), `${consumer.name} AGENTS.md`);
187
+ if (smokeCase.models.anthropic) {
188
+ assertExists(path.join(consumerRoot, claudeRootEntrypoint), `${consumer.name} CLAUDE.md`);
189
+ assertExists(path.join(consumerRoot, claudeMemoryEntrypoint), `${consumer.name} .claude/CLAUDE.md`);
190
+ }
191
+ }
192
+
193
+ const readme = await readFile(path.join(harnessRoot, "README.md"), "utf8");
194
+ if (!readme.includes("workspace")) {
195
+ throw new Error(`${smokeCase.name} generated README does not include workspace bootstrap guidance.`);
196
+ }
197
+
198
+ const firstConsumerRoot = path.join(workspaceRoot, smokeCase.consumers[0].name);
199
+ if (smokeCase.models.openai) {
200
+ const agentsPath = path.join(firstConsumerRoot, openaiRootEntrypoint);
201
+ await writeFile(agentsPath, `This mentions ${path.basename(harnessRoot)} but has no usable path.\n`);
202
+ assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} substring-only pointer`, "does not contain a resolvable");
203
+ await writeFile(agentsPath, `Read /tmp/${path.basename(harnessRoot)}/AGENTS.md before editing.\n`);
204
+ assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} stale pointer`, "instead of");
205
+ }
206
+ if (smokeCase.models.anthropic && !smokeCase.models.openai) {
207
+ const claudePath = path.join(firstConsumerRoot, claudeRootEntrypoint);
208
+ const claudeMemoryPath = path.join(firstConsumerRoot, claudeMemoryEntrypoint);
209
+ await writeFile(claudePath, `This mentions ${path.basename(harnessRoot)} but has no usable path.\n`);
210
+ assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} substring-only Claude pointer`, "does not contain a resolvable");
211
+ await writeFile(claudePath, `Read /tmp/${path.basename(harnessRoot)}/CLAUDE.md before editing.\n`);
212
+ assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} stale Claude pointer`, "instead of");
213
+ await writeFile(
214
+ claudePath,
215
+ `Read ${path.join(harnessRoot, "CLAUDE.md")} before editing.\nRead ${path.join(harnessRoot, "ai/AGENTS.md")} before editing.\nRead ${path.join(harnessRoot, "ai/HUB.md")} before editing.\nRead ${path.join(harnessRoot, "ai/context.md")} before editing.\n`,
216
+ );
217
+ await writeFile(claudeMemoryPath, `@../CLAUDE.md\nRead ${path.join(harnessRoot, "AGENTS.md")} before editing.\n`);
218
+ assertFails(
219
+ nodeCommand,
220
+ ["scripts/check-workspace.mjs"],
221
+ harnessRoot,
222
+ `${smokeCase.name} Claude memory stale ref`,
223
+ );
224
+ }
225
+ }
226
+
227
+ for (const smokeCase of cases) {
228
+ await validateCase(smokeCase);
229
+ }
230
+
231
+ async function validateNegativeConfigCase({ name, overrides, args = [], expectedMessage }) {
232
+ const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}${name}-`));
233
+ const smokeCase = { name, models: { openai: true, anthropic: false }, consumers: [{ name: "product-app", purpose: "Application repository" }] };
234
+ const configPath = await writeConfig(workspaceRoot, smokeCase, overrides);
235
+ assertFails(
236
+ nodeCommand,
237
+ [path.join(repoRoot, initHarnessScript), "--config", configPath, "--dry-run", ...args],
238
+ repoRoot,
239
+ name,
240
+ expectedMessage,
241
+ );
242
+ }
243
+
244
+ await validateNegativeConfigCase({
245
+ name: "no-models",
246
+ overrides: { models: { openai: false, anthropic: false } },
247
+ expectedMessage: "Invalid harness config: at least one model provider must be enabled.",
248
+ });
249
+ await validateNegativeConfigCase({
250
+ name: "malformed-json",
251
+ overrides: { rawJson: "{not json" },
252
+ });
253
+ await validateNegativeConfigCase({
254
+ name: "missing-project",
255
+ overrides: { removeProject: true },
256
+ expectedMessage: "project is required",
257
+ });
258
+ await validateNegativeConfigCase({
259
+ name: "absolute-output",
260
+ overrides: { output: { path: path.join(os.tmpdir(), "absolute-harness-output") } },
261
+ expectedMessage: "absolute output paths require --allow-absolute-output",
262
+ });
263
+ await validateNegativeConfigCase({
264
+ name: "template-root-output",
265
+ overrides: { output: { path: repoRoot } },
266
+ args: ["--allow-absolute-output"],
267
+ expectedMessage: "template repo",
268
+ });
269
+ await validateNegativeConfigCase({
270
+ name: "inside-template-output",
271
+ overrides: { output: { path: path.join(repoRoot, "generated-harness") } },
272
+ args: ["--allow-absolute-output"],
273
+ expectedMessage: "template repo",
274
+ });
275
+ await validateNegativeConfigCase({
276
+ name: "consumer-root-output",
277
+ overrides: { output: { path: "./product-app" } },
278
+ expectedMessage: "configured consumer repo",
279
+ });
280
+ await validateNegativeConfigCase({
281
+ name: "inside-consumer-output",
282
+ overrides: { output: { path: "./product-app/generated-harness" } },
283
+ expectedMessage: "configured consumer repo",
284
+ });
285
+ await validateNegativeConfigCase({
286
+ name: "workspace-root-output",
287
+ overrides: { output: { path: "." } },
288
+ expectedMessage: "workspace root",
289
+ });
290
+ await validateNegativeConfigCase({
291
+ name: "git-segment-output",
292
+ overrides: { output: { path: "./generated/.git/harness" } },
293
+ expectedMessage: ".git path segment",
294
+ });
295
+
296
+ {
297
+ const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}check-config-`));
298
+ const smokeCase = { name: "check-config", models: { openai: true, anthropic: false }, consumers: [{ name: "product-app", purpose: "Application repository" }] };
299
+ const configPath = await writeConfig(workspaceRoot, smokeCase, { output: { path: "." } });
300
+ assertFails(
301
+ nodeCommand,
302
+ [path.join(repoRoot, "scripts/check-config.mjs"), "--config", configPath],
303
+ repoRoot,
304
+ "check-config workspace-root-output",
305
+ "workspace root",
306
+ );
307
+ }
308
+
309
+ console.log("Template smoke check passed.");
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./validate-template.mjs";
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
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
+
9
+ for (const command of [["npm", ["run", "check:ci"]], ["npm", ["run", "check:smoke"]]]) {
10
+ execFileSync(command[0], command[1], {
11
+ cwd: repoRoot,
12
+ stdio: "inherit",
13
+ });
14
+ }
15
+
16
+ console.log("Template validation passed.");
@@ -0,0 +1,12 @@
1
+ # {{PROJECT_NAME}} Claude Project Memory
2
+
3
+ Use this alongside the root `CLAUDE.md` entrypoint.
4
+
5
+ @../CLAUDE.md
6
+
7
+ - Canonical policy lives in `../ai/*`.
8
+ - Canonical harness policy lives in `../ai/*`.
9
+ - Keep Claude-specific guidance thin and route back into shared docs.
10
+ - Use `.claude/settings.json` only for local tool permission settings.
11
+ - Use `.claude/rules/**` for concise project-surface rules, not copied policy.
12
+ - Claude hooks are disabled by default; add them only with a validator.
@@ -0,0 +1,20 @@
1
+ ---
2
+ paths:
3
+ - "AGENTS.md"
4
+ - "CLAUDE.md"
5
+ - ".claude/**"
6
+ - ".codex/**"
7
+ - "ai/model-overlays/**"
8
+ - "scripts/check-*-compatibility.mjs"
9
+ - "scripts/check-overlay-drift.mjs"
10
+ ---
11
+
12
+ # Harness Client Surface Rules
13
+
14
+ - Keep `ai/*` as the canonical governance source of truth.
15
+ - Keep `AGENTS.md` and `.codex/**` Codex-native.
16
+ - Keep `CLAUDE.md` and `.claude/**` Claude Code-native.
17
+ - Do not make Codex depend on `CLAUDE.md`.
18
+ - Do not make Claude Code depend on `AGENTS.md`.
19
+ - Do not copy broad canonical docs into `.claude/`; route back to `ai/*`.
20
+ - Do not add Claude Code hooks until a fixture-backed validator is added.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Read(./.agent.env)",
5
+ "Read(./.env)",
6
+ "Read(./.env.*)",
7
+ "Read(./secrets/**)"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "*",
7
+ "timeoutMs": 2000,
8
+ "hooks": [
9
+ {
10
+ "type": "command",
11
+ "command": "node scripts/hooks/codex-hook.mjs SessionStart --json"
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "UserPromptSubmit": [
17
+ {
18
+ "matcher": "*",
19
+ "timeoutMs": 2000,
20
+ "hooks": [
21
+ {
22
+ "type": "command",
23
+ "command": "node scripts/hooks/codex-hook.mjs UserPromptSubmit --json"
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "PreToolUse": [
29
+ {
30
+ "matcher": "*",
31
+ "timeoutMs": 2000,
32
+ "hooks": [
33
+ {
34
+ "type": "command",
35
+ "command": "node scripts/hooks/codex-hook.mjs PreToolUse --json"
36
+ }
37
+ ]
38
+ }
39
+ ],
40
+ "PermissionRequest": [
41
+ {
42
+ "matcher": "*",
43
+ "timeoutMs": 2000,
44
+ "hooks": [
45
+ {
46
+ "type": "command",
47
+ "command": "node scripts/hooks/codex-hook.mjs PermissionRequest --json"
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ "PostToolUse": [
53
+ {
54
+ "matcher": "*",
55
+ "timeoutMs": 2000,
56
+ "hooks": [
57
+ {
58
+ "type": "command",
59
+ "command": "node scripts/hooks/codex-hook.mjs PostToolUse --json"
60
+ }
61
+ ]
62
+ }
63
+ ],
64
+ "Stop": [
65
+ {
66
+ "matcher": "*",
67
+ "timeoutMs": 2000,
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "node scripts/hooks/codex-hook.mjs Stop --json"
72
+ }
73
+ ]
74
+ }
75
+ ]
76
+ }
77
+ }
@@ -0,0 +1,22 @@
1
+ # {{PROJECT_NAME}} Engineering Harness Repo Guide
2
+
3
+ This repository is the canonical AI engineering harness for {{PROJECT_NAME}}.
4
+
5
+ ## Read Order
6
+
7
+ 1. `./README.md`
8
+ 2. `./ai/AGENTS.md`
9
+ 3. `./ai/HUB.md`
10
+ 4. `./ai/context.md`
11
+ 5. Topical docs selected by `./ai/HUB.md`
12
+
13
+ ## Rules
14
+
15
+ - Treat `ai/*` as canonical harness policy.
16
+ - Keep model-specific files thin.
17
+ - Keep consumer repo implementation details in consumer repos.
18
+ - Do not add runner, polling, PR automation, dashboards, or external writes to
19
+ this harness.
20
+ - Validate harness changes with `node scripts/validate-governance.mjs`.
21
+ - Use `node scripts/bootstrap-workspace.mjs --dry-run` before installing or
22
+ refreshing workspace-level or consumer repo entrypoints.
@@ -0,0 +1,16 @@
1
+ # {{PROJECT_NAME}} Engineering Harness
2
+
3
+ This is the Claude-compatible entrypoint for the {{PROJECT_NAME}} engineering
4
+ harness.
5
+
6
+ Canonical policy lives in `ai/*`. Read:
7
+
8
+ 1. `./ai/AGENTS.md`
9
+ 2. `./ai/HUB.md`
10
+ 3. `./ai/context.md`
11
+
12
+ @ai/context.md
13
+
14
+ Claude-specific notes must stay thin and route back into canonical policy.
15
+ Use `.claude/CLAUDE.md` for Claude Code project memory and keep it aligned with
16
+ this file.
@@ -0,0 +1,109 @@
1
+ # {{PROJECT_NAME}} Engineering Harness
2
+
3
+ This repository contains the AI engineering harness for {{PROJECT_NAME}}.
4
+
5
+ The harness defines policy, contracts, context routing, task templates, review
6
+ rules, quality tracking, and validation. It does not implement product behavior
7
+ and it is not a runner or orchestration runtime.
8
+
9
+ ## Client Support
10
+
11
+ This harness includes the client support selected during generation:
12
+
13
+ - OpenAI/Codex enabled: `{{MODEL_OPENAI_ENABLED}}`
14
+ - Anthropic/Claude Code enabled: `{{MODEL_ANTHROPIC_ENABLED}}`
15
+ - Codex hooks enabled: `{{CLIENT_CODEX_HOOKS_ENABLED}}`
16
+ - Claude project rules enabled: `{{CLIENT_CLAUDE_RULES_ENABLED}}`
17
+ - Claude hooks enabled: `{{CLIENT_CLAUDE_HOOKS_ENABLED}}`
18
+ - Claude skills enabled: `{{CLIENT_CLAUDE_SKILLS_ENABLED}}`
19
+
20
+ Canonical policy lives in `ai/*`. Client-specific files are thin startup,
21
+ project-rule, or local guardrail surfaces that route back to that canonical
22
+ policy. They should not become independent policy sources.
23
+
24
+ Codex hook support, when enabled, is intentionally conservative: deterministic,
25
+ local, bounded by short timeouts, and validated to avoid network calls, external
26
+ writes, and runtime-state mutation. It is a harness guardrail, not a runner or a
27
+ complete security boundary. Hooks catch common high-risk operations and provide
28
+ contextual reminders, but they do not replace sandboxing, permission controls,
29
+ code review, CI policy, or secret management.
30
+
31
+ ## Expected Workspace Layout
32
+
33
+ This harness is intended to live as a sibling of the consumer repositories it
34
+ governs:
35
+
36
+ ```text
37
+ workspace/
38
+ {{HARNESS_REPO_NAME}}/
39
+ <consumer-repo>/
40
+ <optional-second-consumer-repo>/
41
+ ```
42
+
43
+ The workspace bootstrap and check scripts resolve consumer repositories from
44
+ that shared parent folder.
45
+
46
+ ## Consumer Repositories
47
+
48
+ {{CONSUMER_REPOS_LIST}}
49
+
50
+ ## First Run
51
+
52
+ Validate the harness:
53
+
54
+ ```sh
55
+ node scripts/validate-governance.mjs
56
+ ```
57
+
58
+ `validate-governance.mjs` also runs client-support checks when the matching
59
+ surfaces are enabled:
60
+
61
+ - `scripts/check-codex-hooks.mjs`
62
+ - `scripts/check-claude-compatibility.mjs`
63
+ - `scripts/check-overlay-drift.mjs`
64
+
65
+ Preview workspace-level pointer files:
66
+
67
+ ```sh
68
+ node scripts/bootstrap-workspace.mjs --dry-run
69
+ ```
70
+
71
+ If the preview is safe, install or refresh workspace-level pointers and verify
72
+ the full layout:
73
+
74
+ ```sh
75
+ node scripts/bootstrap-workspace.mjs
76
+ node scripts/check-workspace.mjs
77
+ ```
78
+
79
+ `bootstrap-workspace.mjs` installs workspace-level `AGENTS.md`, `CLAUDE.md`,
80
+ and `.claude/*` files when the selected model support requires them. It skips
81
+ existing files unless `--force` is passed.
82
+
83
+ ## Daily Use
84
+
85
+ Agents should start from the workspace, this harness repo, or a bootstrapped
86
+ consumer repo, then follow the local `AGENTS.md` or `CLAUDE.md` entrypoint into
87
+ `ai/HUB.md`.
88
+
89
+ Consumer repos own implementation, runtime behavior, local validation, and
90
+ deployment checks. The harness owns shared policy, contracts, task templates,
91
+ review guidance, and validation evidence expectations.
92
+
93
+ ## Validation
94
+
95
+ ```sh
96
+ node scripts/validate-governance.mjs
97
+ node scripts/check-workspace.mjs
98
+ ```
99
+
100
+ If workspace entrypoints are missing or stale:
101
+
102
+ ```sh
103
+ node scripts/bootstrap-workspace.mjs --dry-run
104
+ node scripts/bootstrap-workspace.mjs
105
+ ```
106
+
107
+ Consumer repos should expose local install, lint, test, build, and health
108
+ commands. The harness documents expected contracts and validation evidence, but
109
+ consumer repos own implementation and runtime checks.
@@ -0,0 +1,18 @@
1
+ # Agent Garbage Collection
2
+
3
+ Use this file to convert repeated agent mistakes into durable fixes.
4
+
5
+ Archive stale instructions instead of letting them compete with active policy.
6
+ Generated files should be regenerated or validated before being treated as
7
+ evidence.
8
+
9
+ ## Entry Format
10
+
11
+ - Pattern:
12
+ - Impact:
13
+ - Durable fix:
14
+ - Validation:
15
+
16
+ ## Entries
17
+
18
+ No repeated patterns recorded yet.
@@ -0,0 +1,36 @@
1
+ # {{PROJECT_NAME}} Shared AI Guide
2
+
3
+ This folder holds canonical shared guidance for the {{PROJECT_NAME}} engineering
4
+ harness.
5
+
6
+ ## Scope
7
+
8
+ - shared policy for AI-assisted development
9
+ - context routing and task shape
10
+ - product, architecture, and design context ready to fill from consumer repos
11
+ - contracts and boundary rules
12
+ - review skills and validation policy
13
+ - quality tracking and repeated-mistake capture
14
+
15
+ ## Rules
16
+
17
+ - Keep shared docs model-neutral.
18
+ - Keep model overlays thin and synchronized.
19
+ - Keep consumer implementation details out of this layer.
20
+ - Use `ai/HUB.md` to route tasks before loading topical docs.
21
+ - Use `node scripts/validate-governance.mjs` for harness validation.
22
+ - Use `node scripts/check-workspace.mjs` when changing workspace bootstrap or
23
+ consumer entrypoint behavior.
24
+
25
+ ## Coding Conventions
26
+
27
+ - Prefer deep modules that hide decisions behind clear APIs.
28
+ - Keep coordination logic close to the module that owns the decision.
29
+ - Use precise names that make the domain model obvious.
30
+ - Reuse existing types, helpers, and abstractions before adding new ones.
31
+ - Keep diffs minimal and avoid unrelated rewrites.
32
+ - Comments should explain non-obvious intent, invariants, constraints, and
33
+ trade-offs; do not narrate syntax.
34
+ - In UI or structured modules, use section markers when they improve scanning:
35
+ `// PROPS`, `// STATE`, `// RQ`, `// RHF`, `// HOOKS`, `// EFFECTS`,
36
+ `// METHODS`, `// VARS`, `// ARGS`, and `// PARAMS`.