@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.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/bin/structor.mjs +576 -0
- package/docs/INIT.md +109 -0
- package/docs/adr/0001-default-generated-repo-name.md +9 -0
- package/docs/issues/0001-structor-doctor.md +39 -0
- package/examples/frontend-backend/harness.config.json +35 -0
- package/examples/openai-and-anthropic/harness.config.json +28 -0
- package/examples/single-repo/harness.config.json +26 -0
- package/harness.config.example.json +38 -0
- package/package.json +58 -0
- package/schemas/contract-manifest.schema.json +18 -0
- package/schemas/harness-config.schema.json +85 -0
- package/schemas/task-brief.schema.json +37 -0
- package/scripts/check-config.mjs +76 -0
- package/scripts/check-contract-manifests.mjs +85 -0
- package/scripts/check-model-overlays.mjs +30 -0
- package/scripts/check-placeholders.mjs +48 -0
- package/scripts/check-task-template.mjs +53 -0
- package/scripts/check-template-files.mjs +110 -0
- package/scripts/init-harness.mjs +270 -0
- package/scripts/lib.mjs +190 -0
- package/scripts/smoke-template.mjs +309 -0
- package/scripts/validate-governance.mjs +3 -0
- package/scripts/validate-template.mjs +16 -0
- package/template/.claude/CLAUDE.md.tpl +12 -0
- package/template/.claude/rules/harness-client-surfaces.md.tpl +20 -0
- package/template/.claude/settings.json.tpl +10 -0
- package/template/.codex/hooks.json.tpl +77 -0
- package/template/AGENTS.md.tpl +22 -0
- package/template/CLAUDE.md.tpl +16 -0
- package/template/README.md.tpl +109 -0
- package/template/ai/AGENT-GARBAGE-COLLECTION.md.tpl +18 -0
- package/template/ai/AGENTS.md.tpl +36 -0
- package/template/ai/ARCHITECTURE.md.tpl +35 -0
- package/template/ai/CODEX-HOOKS.md.tpl +23 -0
- package/template/ai/DECISIONS.md.tpl +22 -0
- package/template/ai/DESIGN.md.tpl +22 -0
- package/template/ai/HARNESS-ENGINEERING.md.tpl +107 -0
- package/template/ai/HARNESS.md.tpl +53 -0
- package/template/ai/HUB.md.tpl +53 -0
- package/template/ai/PRODUCT-SUMMARY.md.tpl +28 -0
- package/template/ai/PRODUCT.md.tpl +32 -0
- package/template/ai/QUALITY.md.tpl +37 -0
- package/template/ai/READINESS.md.tpl +39 -0
- package/template/ai/RUNNER-READINESS.md.tpl +14 -0
- package/template/ai/RUNNER-SAFETY.md.tpl +21 -0
- package/template/ai/VERSIONING.md.tpl +16 -0
- package/template/ai/WORKFLOW.md.tpl +42 -0
- package/template/ai/context.md.tpl +17 -0
- package/template/ai/contracts/README.md.tpl +23 -0
- package/template/ai/contracts/api-boundary.contract.json.tpl +11 -0
- package/template/ai/contracts/api-boundary.md.tpl +17 -0
- package/template/ai/contracts/app-legibility.contract.json.tpl +11 -0
- package/template/ai/contracts/app-legibility.md.tpl +24 -0
- package/template/ai/contracts/codex-hooks.contract.json.tpl +15 -0
- package/template/ai/contracts/codex-hooks.md.tpl +18 -0
- package/template/ai/contracts/github-safety.contract.json.tpl +11 -0
- package/template/ai/contracts/github-safety.md.tpl +15 -0
- package/template/ai/contracts/release-flow.contract.json.tpl +12 -0
- package/template/ai/contracts/release-flow.md.tpl +15 -0
- package/template/ai/contracts/repo-boundaries.contract.json.tpl +12 -0
- package/template/ai/contracts/repo-boundaries.md.tpl +18 -0
- package/template/ai/contracts/security-boundary.contract.json.tpl +11 -0
- package/template/ai/contracts/security-boundary.md.tpl +19 -0
- package/template/ai/knowledge-manifest.json.tpl +149 -0
- package/template/ai/model-overlays/anthropic/CLAUDE.md.tpl +14 -0
- package/template/ai/model-overlays/openai/AGENTS.md.tpl +13 -0
- package/template/ai/plans/README.md.tpl +10 -0
- package/template/ai/plans/tech-debt.md.tpl +7 -0
- package/template/ai/skills/README.md.tpl +15 -0
- package/template/ai/skills/review-architecture.md.tpl +41 -0
- package/template/ai/skills/review-contract-drift.md.tpl +41 -0
- package/template/ai/skills/review-governance-drift.md.tpl +42 -0
- package/template/ai/skills/review-security.md.tpl +40 -0
- package/template/ai/specs/README.md.tpl +14 -0
- package/template/ai/templates/README.md.tpl +13 -0
- package/template/ai/templates/fixtures/issues/invalid-placeholder.md.tpl +20 -0
- package/template/ai/templates/fixtures/issues/invalid-protected-surface.md.tpl +21 -0
- package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +105 -0
- package/template/ai/templates/issue-template.md.tpl +107 -0
- package/template/ai/templates/task-brief-template.md.tpl +185 -0
- package/template/ai/workspace/LOCAL-STACK.md.tpl +21 -0
- package/template/ai/workspace/REPOS.md.tpl +19 -0
- package/template/ai/workspace/SESSION-BOOTSTRAP.md.tpl +27 -0
- package/template/ai/workspace/SYSTEM-MAP.md.tpl +19 -0
- package/template/ai/workspace/TEST-STRATEGY.md.tpl +22 -0
- package/template/consumer/.claude/CLAUDE.md.tpl +14 -0
- package/template/consumer/AGENTS.md.tpl +23 -0
- package/template/consumer/CLAUDE.md.tpl +15 -0
- package/template/scripts/bootstrap-codex-worktree.mjs.tpl +52 -0
- package/template/scripts/bootstrap-workspace.mjs.tpl +100 -0
- package/template/scripts/check-claude-compatibility.mjs.tpl +120 -0
- package/template/scripts/check-codex-hooks.mjs.tpl +190 -0
- package/template/scripts/check-contract-manifests.mjs.tpl +81 -0
- package/template/scripts/check-garbage-collection.mjs.tpl +25 -0
- package/template/scripts/check-html-views.mjs.tpl +60 -0
- package/template/scripts/check-issue-template.mjs.tpl +167 -0
- package/template/scripts/check-knowledge-manifest.mjs.tpl +82 -0
- package/template/scripts/check-overlay-drift.mjs.tpl +49 -0
- package/template/scripts/check-plans.mjs.tpl +70 -0
- package/template/scripts/check-readiness.mjs.tpl +130 -0
- package/template/scripts/check-review-skills.mjs.tpl +48 -0
- package/template/scripts/check-task-template.mjs.tpl +63 -0
- package/template/scripts/check-template-governance.mjs.tpl +161 -0
- package/template/scripts/check-workspace.mjs.tpl +212 -0
- package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +122 -0
- package/template/scripts/check-worktrees.mjs.tpl +69 -0
- package/template/scripts/fixtures/worktrees/README.md.tpl +4 -0
- package/template/scripts/generate-html-views.mjs.tpl +189 -0
- package/template/scripts/hooks/codex-hook.mjs.tpl +21 -0
- package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +114 -0
- package/template/scripts/lib/worktree-bootstrap.mjs.tpl +388 -0
- package/template/scripts/validate-governance.mjs.tpl +78 -0
- package/template/workspace/.claude/CLAUDE.md.tpl +9 -0
- package/template/workspace/.claude/rules/harness-client-surfaces.md.tpl +15 -0
- package/template/workspace/.claude/settings.json.tpl +10 -0
- package/template/workspace/AGENTS.md.tpl +17 -0
- 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,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,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`.
|