@structor-dev/cli 0.1.0 → 0.2.1

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 (65) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +131 -21
  3. package/ROADMAP.md +38 -0
  4. package/SECURITY.md +33 -0
  5. package/bin/structor.mjs +561 -29
  6. package/contrib/self-harness/files/README.md +32 -0
  7. package/contrib/self-harness/files/ai/AGENTS.md +35 -0
  8. package/contrib/self-harness/files/ai/ARCHITECTURE.md +38 -0
  9. package/contrib/self-harness/files/ai/HUB.md +59 -0
  10. package/contrib/self-harness/files/ai/PRODUCT.md +36 -0
  11. package/contrib/self-harness/files/ai/QUALITY.md +31 -0
  12. package/contrib/self-harness/files/ai/context.md +38 -0
  13. package/contrib/self-harness/files/scripts/check-workspace.mjs +72 -0
  14. package/contrib/self-harness/harness.config.json +37 -0
  15. package/docs/CONTRIBUTOR-SETUP.md +45 -0
  16. package/docs/INIT.md +55 -2
  17. package/docs/public-launch.md +150 -0
  18. package/examples/anthropic-only/harness.config.json +26 -0
  19. package/examples/frontend-backend/harness.config.json +8 -8
  20. package/examples/generated-harness-tree.md +432 -0
  21. package/examples/openai-and-anthropic/harness.config.json +7 -7
  22. package/examples/single-repo/harness.config.json +7 -7
  23. package/harness.config.example.json +1 -1
  24. package/package.json +12 -4
  25. package/schemas/contract-manifest.schema.json +0 -1
  26. package/schemas/harness-config.schema.json +5 -2
  27. package/scripts/check-config.mjs +20 -31
  28. package/scripts/check-examples.mjs +146 -0
  29. package/scripts/check-placeholders.mjs +2 -0
  30. package/scripts/check-public-hygiene.mjs +249 -0
  31. package/scripts/check-schemas.mjs +42 -0
  32. package/scripts/check-template-files.mjs +15 -98
  33. package/scripts/generated-harness-contract.mjs +416 -0
  34. package/scripts/init-harness.mjs +227 -139
  35. package/scripts/lib.mjs +462 -12
  36. package/scripts/rendered-config.mjs +109 -0
  37. package/scripts/setup-contributor.mjs +125 -0
  38. package/scripts/smoke-template.mjs +260 -73
  39. package/template/AGENTS.md.tpl +4 -2
  40. package/template/README.md.tpl +5 -0
  41. package/template/ai/CODEX-HOOKS.md.tpl +1 -1
  42. package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
  43. package/template/ai/HARNESS.md.tpl +4 -1
  44. package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
  45. package/template/ai/contracts/codex-hooks.md.tpl +6 -0
  46. package/template/ai/contracts/release-flow.md.tpl +1 -1
  47. package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
  48. package/template/ai/templates/issue-template.md.tpl +3 -1
  49. package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
  50. package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
  51. package/template/consumer/AGENTS.md.tpl +4 -4
  52. package/template/consumer/CLAUDE.md.tpl +4 -4
  53. package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
  54. package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
  55. package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
  56. package/template/scripts/check-template-governance.mjs.tpl +2 -114
  57. package/template/scripts/check-workspace.mjs.tpl +27 -103
  58. package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
  59. package/template/scripts/generate-html-views.mjs.tpl +357 -56
  60. package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
  61. package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
  62. package/template/scripts/lib/path-safety.mjs.tpl +87 -0
  63. package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
  64. package/template/scripts/validate-governance.mjs.tpl +52 -36
  65. 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(firstConsumerRoot, openaiRootEntrypoint);
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(firstConsumerRoot, claudeRootEntrypoint);
208
- const claudeMemoryPath = path.join(firstConsumerRoot, claudeMemoryEntrypoint);
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-`));
@@ -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, or external writes to
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.
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  # Codex Hooks
2
2
 
3
- Codex hooks provide lightweight local guardrails for `{{PROJECT_NAME}}`.
3
+ Codex hooks provide lightweight local guardrails for {{PROJECT_NAME_CODE}}.
4
4
 
5
5
  ## Scope
6
6
 
@@ -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, and repair
17
- loops belong outside the canonical docs layer unless explicitly authorized.
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": ["fetch(", "writeFile(", "appendFile("],
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
  }