@structor-dev/cli 0.1.0 → 0.2.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/CHANGELOG.md +56 -0
- package/README.md +131 -21
- package/ROADMAP.md +38 -0
- package/SECURITY.md +33 -0
- package/bin/structor.mjs +553 -29
- package/contrib/self-harness/files/README.md +32 -0
- package/contrib/self-harness/files/ai/AGENTS.md +35 -0
- package/contrib/self-harness/files/ai/ARCHITECTURE.md +38 -0
- package/contrib/self-harness/files/ai/HUB.md +59 -0
- package/contrib/self-harness/files/ai/PRODUCT.md +36 -0
- package/contrib/self-harness/files/ai/QUALITY.md +31 -0
- package/contrib/self-harness/files/ai/context.md +38 -0
- package/contrib/self-harness/files/scripts/check-workspace.mjs +72 -0
- package/contrib/self-harness/harness.config.json +37 -0
- package/docs/CONTRIBUTOR-SETUP.md +45 -0
- package/docs/INIT.md +55 -2
- package/docs/public-launch.md +150 -0
- package/examples/anthropic-only/harness.config.json +26 -0
- package/examples/frontend-backend/harness.config.json +8 -8
- package/examples/generated-harness-tree.md +432 -0
- package/examples/openai-and-anthropic/harness.config.json +7 -7
- package/examples/single-repo/harness.config.json +7 -7
- package/harness.config.example.json +1 -1
- package/package.json +12 -4
- package/schemas/contract-manifest.schema.json +0 -1
- package/schemas/harness-config.schema.json +5 -2
- package/scripts/check-config.mjs +20 -31
- package/scripts/check-examples.mjs +146 -0
- package/scripts/check-public-hygiene.mjs +249 -0
- package/scripts/check-schemas.mjs +42 -0
- package/scripts/check-template-files.mjs +15 -98
- package/scripts/generated-harness-contract.mjs +416 -0
- package/scripts/init-harness.mjs +227 -139
- package/scripts/lib.mjs +462 -12
- package/scripts/rendered-config.mjs +109 -0
- package/scripts/setup-contributor.mjs +125 -0
- package/scripts/smoke-template.mjs +260 -73
- package/template/AGENTS.md.tpl +4 -2
- package/template/README.md.tpl +5 -0
- package/template/ai/CODEX-HOOKS.md.tpl +1 -1
- package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
- package/template/ai/HARNESS.md.tpl +4 -1
- package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
- package/template/ai/contracts/codex-hooks.md.tpl +6 -0
- package/template/ai/contracts/release-flow.md.tpl +1 -1
- package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
- package/template/ai/templates/issue-template.md.tpl +3 -1
- package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
- package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
- package/template/consumer/AGENTS.md.tpl +4 -4
- package/template/consumer/CLAUDE.md.tpl +4 -4
- package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
- package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
- package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
- package/template/scripts/check-template-governance.mjs.tpl +2 -114
- package/template/scripts/check-workspace.mjs.tpl +27 -103
- package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
- package/template/scripts/generate-html-views.mjs.tpl +357 -56
- package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
- package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
- package/template/scripts/lib/path-safety.mjs.tpl +87 -0
- package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
- package/template/scripts/validate-governance.mjs.tpl +52 -36
- package/schemas/task-brief.schema.json +0 -37
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
generateHarness,
|
|
9
|
+
installConsumerEntrypoints,
|
|
10
|
+
} from "./init-harness.mjs";
|
|
11
|
+
import {
|
|
12
|
+
assertSafeWriteTarget,
|
|
13
|
+
exists,
|
|
14
|
+
} from "./lib.mjs";
|
|
15
|
+
|
|
16
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const workspaceRoot = path.dirname(repoRoot);
|
|
18
|
+
const presetRoot = path.join(repoRoot, "contrib/self-harness");
|
|
19
|
+
const presetConfigPath = path.join(presetRoot, "harness.config.json");
|
|
20
|
+
const overlayRoot = path.join(presetRoot, "files");
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
return {
|
|
24
|
+
dryRun: argv.includes("--dry-run"),
|
|
25
|
+
force: argv.includes("--force"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function collectOverlayFiles() {
|
|
30
|
+
const files = [];
|
|
31
|
+
|
|
32
|
+
async function walk(currentPath) {
|
|
33
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
await walk(absolutePath);
|
|
38
|
+
} else if (entry.isFile()) {
|
|
39
|
+
files.push(path.relative(overlayRoot, absolutePath).replaceAll(path.sep, "/"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await walk(overlayRoot);
|
|
45
|
+
return files.sort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function overlaySelfHarnessFiles(outputRoot, options) {
|
|
49
|
+
for (const relativePath of await collectOverlayFiles()) {
|
|
50
|
+
const sourcePath = path.join(overlayRoot, relativePath);
|
|
51
|
+
const targetPath = path.join(outputRoot, relativePath);
|
|
52
|
+
const existed = await exists(targetPath);
|
|
53
|
+
|
|
54
|
+
if (options.dryRun) {
|
|
55
|
+
console.log(`would ${existed ? "overwrite" : "create"} self-harness overlay ${targetPath}`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await assertSafeWriteTarget({
|
|
60
|
+
targetPath,
|
|
61
|
+
rootPath: outputRoot,
|
|
62
|
+
label: `Self-harness overlay ${relativePath}`,
|
|
63
|
+
});
|
|
64
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
65
|
+
await writeFile(targetPath, await readFile(sourcePath, "utf8"));
|
|
66
|
+
console.log(`${existed ? "wrote" : "created"} self-harness overlay ${targetPath}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const options = parseArgs(process.argv.slice(2));
|
|
72
|
+
const config = JSON.parse(await readFile(presetConfigPath, "utf8"));
|
|
73
|
+
|
|
74
|
+
console.log("Structor contributor setup");
|
|
75
|
+
console.log(`Workspace: ${workspaceRoot}`);
|
|
76
|
+
console.log(`Preset: ${presetConfigPath}`);
|
|
77
|
+
|
|
78
|
+
const resolvedConfig = await generateHarness(config, {
|
|
79
|
+
configPath: presetConfigPath,
|
|
80
|
+
configDir: workspaceRoot,
|
|
81
|
+
dryRun: options.dryRun,
|
|
82
|
+
force: true,
|
|
83
|
+
allowTemplateRepoConsumer: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await overlaySelfHarnessFiles(resolvedConfig.outputRoot, options);
|
|
87
|
+
|
|
88
|
+
const bootstrapArgs = ["scripts/bootstrap-workspace.mjs"];
|
|
89
|
+
if (options.force) bootstrapArgs.push("--force");
|
|
90
|
+
|
|
91
|
+
if (options.dryRun) {
|
|
92
|
+
console.log(`would refresh self-harness HTML views in ${resolvedConfig.outputRoot}`);
|
|
93
|
+
console.log(`would run workspace bootstrap dry-run in ${resolvedConfig.outputRoot}: ${process.execPath} ${bootstrapArgs.join(" ")}`);
|
|
94
|
+
} else {
|
|
95
|
+
execFileSync(process.execPath, ["scripts/generate-html-views.mjs"], {
|
|
96
|
+
cwd: resolvedConfig.outputRoot,
|
|
97
|
+
stdio: "inherit",
|
|
98
|
+
});
|
|
99
|
+
execFileSync(process.execPath, bootstrapArgs, {
|
|
100
|
+
cwd: resolvedConfig.outputRoot,
|
|
101
|
+
stdio: "inherit",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log("Source entrypoint preview");
|
|
106
|
+
await installConsumerEntrypoints(resolvedConfig, {
|
|
107
|
+
dryRun: true,
|
|
108
|
+
force: options.force,
|
|
109
|
+
config: presetConfigPath,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!options.dryRun) {
|
|
113
|
+
console.log("Source entrypoint apply");
|
|
114
|
+
await installConsumerEntrypoints(resolvedConfig, {
|
|
115
|
+
dryRun: false,
|
|
116
|
+
force: options.force,
|
|
117
|
+
config: presetConfigPath,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((error) => {
|
|
123
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, symlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { repoRoot } from "./lib.mjs";
|
|
9
|
+
import {
|
|
10
|
+
artifactEnabled,
|
|
11
|
+
artifactTargetPath,
|
|
12
|
+
consumerEntrypointsForSettings,
|
|
13
|
+
generatedHarnessArtifacts,
|
|
14
|
+
requiredHarnessRepoFilesForWorkspaceCheck,
|
|
15
|
+
requiredWorkspaceFilesForWorkspaceCheck,
|
|
16
|
+
validationPlanForSettings,
|
|
17
|
+
} from "./generated-harness-contract.mjs";
|
|
9
18
|
|
|
10
19
|
const cases = [
|
|
11
20
|
{
|
|
@@ -35,11 +44,6 @@ const initHarnessScript = "scripts/init-harness.mjs";
|
|
|
35
44
|
const nodeCommand = "node";
|
|
36
45
|
const lintCommand = "npm run lint";
|
|
37
46
|
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
47
|
|
|
44
48
|
function run(command, args, cwd) {
|
|
45
49
|
execFileSync(command, args, { cwd, stdio: "pipe" });
|
|
@@ -76,6 +80,7 @@ async function writeConfig(workspaceRoot, smokeCase, overrides = {}) {
|
|
|
76
80
|
for (const consumer of smokeCase.consumers) {
|
|
77
81
|
await mkdir(path.join(workspaceRoot, consumer.name), { recursive: true });
|
|
78
82
|
await writeFile(path.join(workspaceRoot, consumer.name, "README.md"), `# ${consumer.name}\n`);
|
|
83
|
+
await writeFile(path.join(workspaceRoot, consumer.name, "package.json"), `${JSON.stringify({ name: consumer.name })}\n`);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
const config = {
|
|
@@ -110,10 +115,59 @@ async function writeConfig(workspaceRoot, smokeCase, overrides = {}) {
|
|
|
110
115
|
return configPath;
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
function settingsForSmokeCase(smokeCase) {
|
|
119
|
+
return {
|
|
120
|
+
models: smokeCase.models,
|
|
121
|
+
clientSupport: {
|
|
122
|
+
codexHooks: smokeCase.models.openai,
|
|
123
|
+
claudeRules: smokeCase.models.anthropic,
|
|
124
|
+
claudeHooks: false,
|
|
125
|
+
claudeSkills: false,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findEntrypoint(entrypoints, predicate, label) {
|
|
131
|
+
const entrypoint = entrypoints.find(predicate);
|
|
132
|
+
if (!entrypoint) throw new Error(`Generated harness contract is missing ${label}.`);
|
|
133
|
+
return entrypoint;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertContractSurfaces({ smokeCase, workspaceRoot, harnessRoot }) {
|
|
137
|
+
const settings = settingsForSmokeCase(smokeCase);
|
|
138
|
+
for (const relativePath of requiredHarnessRepoFilesForWorkspaceCheck(settings)) {
|
|
139
|
+
assertExists(path.join(harnessRoot, relativePath), `${smokeCase.name} contract repo file ${relativePath}`);
|
|
140
|
+
}
|
|
141
|
+
for (const relativePath of requiredWorkspaceFilesForWorkspaceCheck(settings)) {
|
|
142
|
+
assertExists(path.join(workspaceRoot, relativePath), `${smokeCase.name} contract workspace file ${relativePath}`);
|
|
143
|
+
}
|
|
144
|
+
for (const artifact of generatedHarnessArtifacts.filter((item) => item.generated && !artifactEnabled(item, settings))) {
|
|
145
|
+
assertMissing(path.join(harnessRoot, artifactTargetPath(artifact)), `${smokeCase.name} disabled contract artifact ${artifactTargetPath(artifact)}`);
|
|
146
|
+
}
|
|
147
|
+
for (const consumer of smokeCase.consumers) {
|
|
148
|
+
const consumerRoot = path.join(workspaceRoot, consumer.name);
|
|
149
|
+
for (const entrypoint of consumerEntrypointsForSettings(settings)) {
|
|
150
|
+
assertExists(path.join(consumerRoot, entrypoint.path), `${consumer.name} contract entrypoint ${entrypoint.path}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const plan = validationPlanForSettings(settings);
|
|
155
|
+
if (settings.clientSupport.codexHooks) {
|
|
156
|
+
const codexDependencies = plan.checkDependencies["scripts/check-codex-hooks.mjs"] ?? [];
|
|
157
|
+
for (const dependency of ["scripts/hooks/codex-hook.mjs", "scripts/hooks/lib/codex-hooks-core.mjs"]) {
|
|
158
|
+
if (!codexDependencies.includes(dependency)) {
|
|
159
|
+
throw new Error(`Codex hook validation must trust generated dependency ${dependency}.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
113
165
|
async function validateCase(smokeCase) {
|
|
114
166
|
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}${smokeCase.name}-`));
|
|
115
167
|
const configPath = await writeConfig(workspaceRoot, smokeCase);
|
|
116
168
|
const harnessRoot = path.join(workspaceRoot, `${smokePrefix}${smokeCase.name}-structor`);
|
|
169
|
+
const settings = settingsForSmokeCase(smokeCase);
|
|
170
|
+
const consumerEntrypoints = consumerEntrypointsForSettings(settings);
|
|
117
171
|
|
|
118
172
|
run(nodeCommand, [path.join(repoRoot, initHarnessScript), "--config", configPath, "--dry-run"], repoRoot);
|
|
119
173
|
run(
|
|
@@ -126,69 +180,7 @@ async function validateCase(smokeCase) {
|
|
|
126
180
|
run(nodeCommand, ["scripts/bootstrap-workspace.mjs", "--dry-run"], harnessRoot);
|
|
127
181
|
run(nodeCommand, ["scripts/bootstrap-workspace.mjs"], harnessRoot);
|
|
128
182
|
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
|
-
}
|
|
183
|
+
assertContractSurfaces({ smokeCase, workspaceRoot, harnessRoot });
|
|
192
184
|
|
|
193
185
|
const readme = await readFile(path.join(harnessRoot, "README.md"), "utf8");
|
|
194
186
|
if (!readme.includes("workspace")) {
|
|
@@ -197,15 +189,32 @@ async function validateCase(smokeCase) {
|
|
|
197
189
|
|
|
198
190
|
const firstConsumerRoot = path.join(workspaceRoot, smokeCase.consumers[0].name);
|
|
199
191
|
if (smokeCase.models.openai) {
|
|
200
|
-
const agentsPath = path.join(
|
|
192
|
+
const agentsPath = path.join(
|
|
193
|
+
firstConsumerRoot,
|
|
194
|
+
findEntrypoint(consumerEntrypoints, (entrypoint) => entrypoint.model === "openai", "OpenAI consumer entrypoint").path,
|
|
195
|
+
);
|
|
201
196
|
await writeFile(agentsPath, `This mentions ${path.basename(harnessRoot)} but has no usable path.\n`);
|
|
202
197
|
assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} substring-only pointer`, "does not contain a resolvable");
|
|
203
198
|
await writeFile(agentsPath, `Read /tmp/${path.basename(harnessRoot)}/AGENTS.md before editing.\n`);
|
|
204
199
|
assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} stale pointer`, "instead of");
|
|
205
200
|
}
|
|
206
201
|
if (smokeCase.models.anthropic && !smokeCase.models.openai) {
|
|
207
|
-
const claudePath = path.join(
|
|
208
|
-
|
|
202
|
+
const claudePath = path.join(
|
|
203
|
+
firstConsumerRoot,
|
|
204
|
+
findEntrypoint(
|
|
205
|
+
consumerEntrypoints,
|
|
206
|
+
(entrypoint) => entrypoint.model === "anthropic" && entrypoint.routing === "harness",
|
|
207
|
+
"Claude consumer entrypoint",
|
|
208
|
+
).path,
|
|
209
|
+
);
|
|
210
|
+
const claudeMemoryPath = path.join(
|
|
211
|
+
firstConsumerRoot,
|
|
212
|
+
findEntrypoint(
|
|
213
|
+
consumerEntrypoints,
|
|
214
|
+
(entrypoint) => entrypoint.model === "anthropic" && entrypoint.routing === "claude-memory",
|
|
215
|
+
"Claude memory consumer entrypoint",
|
|
216
|
+
).path,
|
|
217
|
+
);
|
|
209
218
|
await writeFile(claudePath, `This mentions ${path.basename(harnessRoot)} but has no usable path.\n`);
|
|
210
219
|
assertFails(nodeCommand, ["scripts/check-workspace.mjs"], harnessRoot, `${smokeCase.name} substring-only Claude pointer`, "does not contain a resolvable");
|
|
211
220
|
await writeFile(claudePath, `Read /tmp/${path.basename(harnessRoot)}/CLAUDE.md before editing.\n`);
|
|
@@ -228,10 +237,11 @@ for (const smokeCase of cases) {
|
|
|
228
237
|
await validateCase(smokeCase);
|
|
229
238
|
}
|
|
230
239
|
|
|
231
|
-
async function validateNegativeConfigCase({ name, overrides, args = [], expectedMessage }) {
|
|
240
|
+
async function validateNegativeConfigCase({ name, overrides, args = [], expectedMessage, setup }) {
|
|
232
241
|
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}${name}-`));
|
|
233
242
|
const smokeCase = { name, models: { openai: true, anthropic: false }, consumers: [{ name: "product-app", purpose: "Application repository" }] };
|
|
234
243
|
const configPath = await writeConfig(workspaceRoot, smokeCase, overrides);
|
|
244
|
+
if (setup) await setup(workspaceRoot);
|
|
235
245
|
assertFails(
|
|
236
246
|
nodeCommand,
|
|
237
247
|
[path.join(repoRoot, initHarnessScript), "--config", configPath, "--dry-run", ...args],
|
|
@@ -260,6 +270,19 @@ await validateNegativeConfigCase({
|
|
|
260
270
|
overrides: { output: { path: path.join(os.tmpdir(), "absolute-harness-output") } },
|
|
261
271
|
expectedMessage: "absolute output paths require --allow-absolute-output",
|
|
262
272
|
});
|
|
273
|
+
await validateNegativeConfigCase({
|
|
274
|
+
name: "relative-traversal-output",
|
|
275
|
+
overrides: { output: { path: "../outside-harness-output" } },
|
|
276
|
+
expectedMessage: "workspace boundary",
|
|
277
|
+
});
|
|
278
|
+
await validateNegativeConfigCase({
|
|
279
|
+
name: "symlink-output",
|
|
280
|
+
overrides: { output: { path: "./linked-harness-output" } },
|
|
281
|
+
expectedMessage: "symlinked output directories",
|
|
282
|
+
setup: async (workspaceRoot) => {
|
|
283
|
+
await symlink(path.join(workspaceRoot, "product-app"), path.join(workspaceRoot, "linked-harness-output"), "dir");
|
|
284
|
+
},
|
|
285
|
+
});
|
|
263
286
|
await validateNegativeConfigCase({
|
|
264
287
|
name: "template-root-output",
|
|
265
288
|
overrides: { output: { path: repoRoot } },
|
|
@@ -292,6 +315,170 @@ await validateNegativeConfigCase({
|
|
|
292
315
|
overrides: { output: { path: "./generated/.git/harness" } },
|
|
293
316
|
expectedMessage: ".git path segment",
|
|
294
317
|
});
|
|
318
|
+
await validateNegativeConfigCase({
|
|
319
|
+
name: "absolute-consumer",
|
|
320
|
+
overrides: {
|
|
321
|
+
consumers: [{
|
|
322
|
+
name: "outside-app",
|
|
323
|
+
path: path.join(os.tmpdir(), "outside-app"),
|
|
324
|
+
purpose: "Application repository",
|
|
325
|
+
validation: {},
|
|
326
|
+
}],
|
|
327
|
+
},
|
|
328
|
+
expectedMessage: "absolute paths are not allowed",
|
|
329
|
+
});
|
|
330
|
+
await validateNegativeConfigCase({
|
|
331
|
+
name: "traversal-consumer",
|
|
332
|
+
overrides: {
|
|
333
|
+
consumers: [{
|
|
334
|
+
name: "outside-app",
|
|
335
|
+
path: "../outside-app",
|
|
336
|
+
purpose: "Application repository",
|
|
337
|
+
validation: {},
|
|
338
|
+
}],
|
|
339
|
+
},
|
|
340
|
+
expectedMessage: "relative traversal",
|
|
341
|
+
});
|
|
342
|
+
await validateNegativeConfigCase({
|
|
343
|
+
name: "force-traversal-consumer",
|
|
344
|
+
overrides: {
|
|
345
|
+
consumers: [{
|
|
346
|
+
name: "outside-app",
|
|
347
|
+
path: "../outside-app",
|
|
348
|
+
purpose: "Application repository",
|
|
349
|
+
validation: {},
|
|
350
|
+
}],
|
|
351
|
+
},
|
|
352
|
+
args: ["--install-consumer-entrypoints", "--force"],
|
|
353
|
+
expectedMessage: "relative traversal",
|
|
354
|
+
});
|
|
355
|
+
await validateNegativeConfigCase({
|
|
356
|
+
name: "unconfirmed-consumer",
|
|
357
|
+
overrides: {
|
|
358
|
+
consumers: [{
|
|
359
|
+
name: "not-repo",
|
|
360
|
+
path: "./not-repo",
|
|
361
|
+
purpose: "Existing directory without repo signals",
|
|
362
|
+
validation: {},
|
|
363
|
+
}],
|
|
364
|
+
},
|
|
365
|
+
args: ["--install-consumer-entrypoints"],
|
|
366
|
+
expectedMessage: "not a confirmed consumer repository",
|
|
367
|
+
setup: async (workspaceRoot) => {
|
|
368
|
+
await mkdir(path.join(workspaceRoot, "not-repo"), { recursive: true });
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
await validateNegativeConfigCase({
|
|
372
|
+
name: "symlinked-consumer",
|
|
373
|
+
overrides: {
|
|
374
|
+
consumers: [{
|
|
375
|
+
name: "linked-app",
|
|
376
|
+
path: "./linked-app",
|
|
377
|
+
purpose: "Symlinked application repository",
|
|
378
|
+
validation: {},
|
|
379
|
+
}],
|
|
380
|
+
},
|
|
381
|
+
args: ["--install-consumer-entrypoints"],
|
|
382
|
+
expectedMessage: "symlinked consumer paths",
|
|
383
|
+
setup: async (workspaceRoot) => {
|
|
384
|
+
const outsideRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}outside-consumer-`));
|
|
385
|
+
await writeFile(path.join(outsideRoot, "package.json"), `${JSON.stringify({ name: "outside-app" })}\n`);
|
|
386
|
+
await symlink(outsideRoot, path.join(workspaceRoot, "linked-app"), "dir");
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
{
|
|
391
|
+
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}workspace-claude-symlink-`));
|
|
392
|
+
const smokeCase = {
|
|
393
|
+
name: "workspace-claude-symlink",
|
|
394
|
+
models: { openai: false, anthropic: true },
|
|
395
|
+
consumers: [{ name: "product-app", purpose: "Application repository" }],
|
|
396
|
+
};
|
|
397
|
+
const configPath = await writeConfig(workspaceRoot, smokeCase);
|
|
398
|
+
const harnessRoot = path.join(workspaceRoot, "smoke-workspace-claude-symlink-structor");
|
|
399
|
+
const outsideRoot = path.join(workspaceRoot, "outside-claude");
|
|
400
|
+
await mkdir(outsideRoot);
|
|
401
|
+
|
|
402
|
+
run(nodeCommand, [path.join(repoRoot, initHarnessScript), "--config", configPath], repoRoot);
|
|
403
|
+
await symlink(outsideRoot, path.join(workspaceRoot, ".claude"), "dir");
|
|
404
|
+
|
|
405
|
+
assertFails(
|
|
406
|
+
nodeCommand,
|
|
407
|
+
["scripts/bootstrap-workspace.mjs"],
|
|
408
|
+
harnessRoot,
|
|
409
|
+
"workspace .claude symlink",
|
|
410
|
+
"symlinked write targets",
|
|
411
|
+
);
|
|
412
|
+
assertMissing(path.join(outsideRoot, "CLAUDE.md"), "workspace bootstrap should not write through symlinked .claude");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
{
|
|
416
|
+
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}worktree-pointer-symlink-`));
|
|
417
|
+
const smokeCase = {
|
|
418
|
+
name: "worktree-pointer-symlink",
|
|
419
|
+
models: { openai: true, anthropic: false },
|
|
420
|
+
consumers: [{ name: "product-app", purpose: "Application repository" }],
|
|
421
|
+
};
|
|
422
|
+
const configPath = await writeConfig(workspaceRoot, smokeCase);
|
|
423
|
+
const harnessRoot = path.join(workspaceRoot, "smoke-worktree-pointer-symlink-structor");
|
|
424
|
+
const consumerRoot = path.join(workspaceRoot, "product-app");
|
|
425
|
+
const outsideRoot = path.join(workspaceRoot, "outside-pointer");
|
|
426
|
+
const openaiEntrypoint = findEntrypoint(
|
|
427
|
+
consumerEntrypointsForSettings(settingsForSmokeCase(smokeCase)),
|
|
428
|
+
(entrypoint) => entrypoint.model === "openai",
|
|
429
|
+
"OpenAI consumer entrypoint",
|
|
430
|
+
);
|
|
431
|
+
const outsidePointer = path.join(outsideRoot, openaiEntrypoint.path);
|
|
432
|
+
await mkdir(outsideRoot);
|
|
433
|
+
await writeFile(outsidePointer, "Read /tmp/other-structor/AGENTS.md before editing.\n");
|
|
434
|
+
|
|
435
|
+
run(nodeCommand, [path.join(repoRoot, initHarnessScript), "--config", configPath], repoRoot);
|
|
436
|
+
run("git", ["init"], consumerRoot);
|
|
437
|
+
await symlink(outsidePointer, path.join(consumerRoot, openaiEntrypoint.path));
|
|
438
|
+
|
|
439
|
+
assertFails(
|
|
440
|
+
nodeCommand,
|
|
441
|
+
["scripts/bootstrap-codex-worktree.mjs", consumerRoot],
|
|
442
|
+
harnessRoot,
|
|
443
|
+
"worktree pointer symlink",
|
|
444
|
+
"symlinked write targets",
|
|
445
|
+
);
|
|
446
|
+
if ((await readFile(outsidePointer, "utf8")) !== "Read /tmp/other-structor/AGENTS.md before editing.\n") {
|
|
447
|
+
throw new Error("worktree repair should not write through a symlinked pointer file.");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
{
|
|
452
|
+
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}force-consumer-entrypoints-`));
|
|
453
|
+
const smokeCase = {
|
|
454
|
+
name: "force-consumer-entrypoints",
|
|
455
|
+
models: { openai: true, anthropic: false },
|
|
456
|
+
consumers: [{ name: "product-app", purpose: "Application repository" }],
|
|
457
|
+
};
|
|
458
|
+
const configPath = await writeConfig(workspaceRoot, smokeCase);
|
|
459
|
+
const openaiEntrypoint = findEntrypoint(
|
|
460
|
+
consumerEntrypointsForSettings(settingsForSmokeCase(smokeCase)),
|
|
461
|
+
(entrypoint) => entrypoint.model === "openai",
|
|
462
|
+
"OpenAI consumer entrypoint",
|
|
463
|
+
);
|
|
464
|
+
const agentsPath = path.join(workspaceRoot, "product-app", openaiEntrypoint.path);
|
|
465
|
+
await writeFile(agentsPath, "OLD");
|
|
466
|
+
|
|
467
|
+
run(nodeCommand, [path.join(repoRoot, initHarnessScript), "--config", configPath, "--install-consumer-entrypoints"], repoRoot);
|
|
468
|
+
if (await readFile(agentsPath, "utf8") !== "OLD") {
|
|
469
|
+
throw new Error("consumer entrypoint should be skipped without --force.");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
run(
|
|
473
|
+
nodeCommand,
|
|
474
|
+
[path.join(repoRoot, initHarnessScript), "--config", configPath, "--install-consumer-entrypoints", "--force"],
|
|
475
|
+
repoRoot,
|
|
476
|
+
);
|
|
477
|
+
const forcedContent = await readFile(agentsPath, "utf8");
|
|
478
|
+
if (forcedContent === "OLD" || !forcedContent.includes("This consumer repository is governed by")) {
|
|
479
|
+
throw new Error("consumer entrypoint should be overwritten with --force.");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
295
482
|
|
|
296
483
|
{
|
|
297
484
|
const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), `${tempRootPrefix}check-config-`));
|
package/template/AGENTS.md.tpl
CHANGED
|
@@ -15,8 +15,10 @@ This repository is the canonical AI engineering harness for {{PROJECT_NAME}}.
|
|
|
15
15
|
- Treat `ai/*` as canonical harness policy.
|
|
16
16
|
- Keep model-specific files thin.
|
|
17
17
|
- Keep consumer repo implementation details in consumer repos.
|
|
18
|
-
- Do not add runner, polling, PR automation, dashboards,
|
|
19
|
-
this harness.
|
|
18
|
+
- Do not add runner, polling, PR automation, live dashboards, orchestration UI,
|
|
19
|
+
or external writes to this harness.
|
|
20
|
+
- Read-only generated Harness Cockpit views under `ai/views/*` are review
|
|
21
|
+
artifacts, not workflow control surfaces.
|
|
20
22
|
- Validate harness changes with `node scripts/validate-governance.mjs`.
|
|
21
23
|
- Use `node scripts/bootstrap-workspace.mjs --dry-run` before installing or
|
|
22
24
|
refreshing workspace-level or consumer repo entrypoints.
|
package/template/README.md.tpl
CHANGED
|
@@ -55,6 +55,11 @@ Validate the harness:
|
|
|
55
55
|
node scripts/validate-governance.mjs
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
Generated artifact, entrypoint, and check participation lives in
|
|
59
|
+
`scripts/generated-harness-contract.mjs`. The validator uses that contract to
|
|
60
|
+
resolve required files, trusted check dependencies, and enabled client-support
|
|
61
|
+
surfaces.
|
|
62
|
+
|
|
58
63
|
`validate-governance.mjs` also runs client-support checks when the matching
|
|
59
64
|
surfaces are enabled:
|
|
60
65
|
|
|
@@ -13,8 +13,11 @@ instead of chat history or human memory.
|
|
|
13
13
|
- The harness defines what must be true.
|
|
14
14
|
- A runner decides when and how work executes.
|
|
15
15
|
- The harness owns policy, contracts, templates, quality, and validation.
|
|
16
|
-
- Runtime state, polling, PR automation, dashboards, auto-merge,
|
|
17
|
-
loops belong outside the canonical docs layer unless
|
|
16
|
+
- Runtime state, polling, PR automation, live dashboards, auto-merge, repair
|
|
17
|
+
loops, and orchestration UI belong outside the canonical docs layer unless
|
|
18
|
+
explicitly authorized.
|
|
19
|
+
- Read-only generated Harness Cockpit views are allowed when they are derived
|
|
20
|
+
from canonical local files and do not execute validation or workflows.
|
|
18
21
|
|
|
19
22
|
## System Of Record
|
|
20
23
|
|
|
@@ -24,13 +24,16 @@ It does not implement product behavior and it is not a runner.
|
|
|
24
24
|
- long-running polling
|
|
25
25
|
- agent session lifecycle
|
|
26
26
|
- PR lifecycle automation
|
|
27
|
-
- dashboards
|
|
27
|
+
- live dashboards or orchestration UI
|
|
28
28
|
- auto-merge
|
|
29
29
|
- production autonomous execution
|
|
30
30
|
- runtime state stores
|
|
31
31
|
- repair-loop daemons
|
|
32
32
|
- external client automation that is not validated as a local harness guardrail
|
|
33
33
|
|
|
34
|
+
Read-only generated Harness Cockpit views under `ai/views/*` are allowed when
|
|
35
|
+
they summarize canonical local files and do not execute workflows.
|
|
36
|
+
|
|
34
37
|
## Harness vs Runner
|
|
35
38
|
|
|
36
39
|
The harness answers what must be true. A runner answers when and how work is
|
|
@@ -10,6 +10,63 @@
|
|
|
10
10
|
"scripts/hooks/lib/codex-hooks-core.mjs",
|
|
11
11
|
"ai/contracts/codex-hooks.md"
|
|
12
12
|
],
|
|
13
|
-
"forbiddenTokens": [
|
|
13
|
+
"forbiddenTokens": [
|
|
14
|
+
"fetch(",
|
|
15
|
+
"writeFile(",
|
|
16
|
+
"appendFile(",
|
|
17
|
+
"chmod(",
|
|
18
|
+
"chown(",
|
|
19
|
+
"copyFile(",
|
|
20
|
+
"cp(",
|
|
21
|
+
"fchmod(",
|
|
22
|
+
"fchown(",
|
|
23
|
+
"fdatasync(",
|
|
24
|
+
"fsync(",
|
|
25
|
+
"ftruncate(",
|
|
26
|
+
"futimes(",
|
|
27
|
+
"lchmod(",
|
|
28
|
+
"lchown(",
|
|
29
|
+
"link(",
|
|
30
|
+
"lutimes(",
|
|
31
|
+
"mkdir(",
|
|
32
|
+
"mkdtemp(",
|
|
33
|
+
"rename(",
|
|
34
|
+
"rm(",
|
|
35
|
+
"rmdir(",
|
|
36
|
+
"symlink(",
|
|
37
|
+
"truncate(",
|
|
38
|
+
"unlink(",
|
|
39
|
+
"utimes(",
|
|
40
|
+
"writev(",
|
|
41
|
+
"writeFileSync",
|
|
42
|
+
"appendFileSync",
|
|
43
|
+
"chmodSync",
|
|
44
|
+
"chownSync",
|
|
45
|
+
"copyFileSync",
|
|
46
|
+
"cpSync",
|
|
47
|
+
"fchmodSync",
|
|
48
|
+
"fchownSync",
|
|
49
|
+
"fdatasyncSync",
|
|
50
|
+
"fsyncSync",
|
|
51
|
+
"ftruncateSync",
|
|
52
|
+
"futimesSync",
|
|
53
|
+
"lchmodSync",
|
|
54
|
+
"lchownSync",
|
|
55
|
+
"linkSync",
|
|
56
|
+
"lutimesSync",
|
|
57
|
+
"mkdirSync",
|
|
58
|
+
"mkdtempSync",
|
|
59
|
+
"renameSync",
|
|
60
|
+
"rmSync",
|
|
61
|
+
"rmdirSync",
|
|
62
|
+
"symlinkSync",
|
|
63
|
+
"truncateSync",
|
|
64
|
+
"unlinkSync",
|
|
65
|
+
"utimesSync",
|
|
66
|
+
"writeSync",
|
|
67
|
+
"writevSync",
|
|
68
|
+
"createWriteStream",
|
|
69
|
+
"openSync(write flags)"
|
|
70
|
+
],
|
|
14
71
|
"validation": ["node scripts/check-codex-hooks.mjs"]
|
|
15
72
|
}
|