@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
@@ -4,6 +4,7 @@ import { access, readFile } from "node:fs/promises";
4
4
  import { constants as fsConstants } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { requiredGeneratedHarnessFilesForGovernance } from "./generated-harness-contract.mjs";
7
8
 
8
9
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
10
  const models = {
@@ -16,120 +17,7 @@ const clientSupport = {
16
17
  claudeHooks: {{CLIENT_CLAUDE_HOOKS_ENABLED}},
17
18
  claudeSkills: {{CLIENT_CLAUDE_SKILLS_ENABLED}},
18
19
  };
19
- const openaiRequiredFiles = ["AGENTS.md", "ai/model-overlays/openai/AGENTS.md", "workspace/AGENTS.md"];
20
- const claudeRequiredFiles = [
21
- "CLAUDE.md",
22
- ".claude/CLAUDE.md",
23
- ".claude/settings.json",
24
- "ai/model-overlays/anthropic/CLAUDE.md",
25
- "workspace/CLAUDE.md",
26
- "workspace/.claude/CLAUDE.md",
27
- "workspace/.claude/settings.json",
28
- "scripts/check-claude-compatibility.mjs",
29
- ];
30
- const codexRequiredFiles = [
31
- ".codex/hooks.json",
32
- "ai/contracts/codex-hooks.contract.json",
33
- "scripts/check-codex-hooks.mjs",
34
- "scripts/hooks/codex-hook.mjs",
35
- "scripts/hooks/lib/codex-hooks-core.mjs",
36
- ];
37
- const claudeRulesRequiredFiles = [".claude/rules/harness-client-surfaces.md", "workspace/.claude/rules/harness-client-surfaces.md"];
38
- const driftFile = "scripts/check-overlay-drift.mjs";
39
- const requiredFiles = [
40
- "README.md",
41
- "ai/AGENTS.md",
42
- "ai/HUB.md",
43
- "ai/context.md",
44
- "ai/HARNESS.md",
45
- "ai/HARNESS-ENGINEERING.md",
46
- "ai/READINESS.md",
47
- "ai/QUALITY.md",
48
- "ai/DECISIONS.md",
49
- "ai/PRODUCT-SUMMARY.md",
50
- "ai/PRODUCT.md",
51
- "ai/ARCHITECTURE.md",
52
- "ai/DESIGN.md",
53
- "ai/WORKFLOW.md",
54
- "ai/VERSIONING.md",
55
- "ai/CODEX-HOOKS.md",
56
- "ai/RUNNER-SAFETY.md",
57
- "ai/RUNNER-READINESS.md",
58
- "ai/AGENT-GARBAGE-COLLECTION.md",
59
- "ai/knowledge-manifest.json",
60
- "ai/workspace/REPOS.md",
61
- "ai/workspace/SYSTEM-MAP.md",
62
- "ai/workspace/SESSION-BOOTSTRAP.md",
63
- "ai/workspace/LOCAL-STACK.md",
64
- "ai/workspace/TEST-STRATEGY.md",
65
- "ai/contracts/README.md",
66
- "ai/contracts/repo-boundaries.md",
67
- "ai/contracts/repo-boundaries.contract.json",
68
- "ai/contracts/app-legibility.md",
69
- "ai/contracts/app-legibility.contract.json",
70
- "ai/contracts/api-boundary.md",
71
- "ai/contracts/api-boundary.contract.json",
72
- "ai/contracts/security-boundary.md",
73
- "ai/contracts/security-boundary.contract.json",
74
- "ai/contracts/codex-hooks.md",
75
- "ai/contracts/release-flow.md",
76
- "ai/contracts/release-flow.contract.json",
77
- "ai/contracts/github-safety.md",
78
- "ai/contracts/github-safety.contract.json",
79
- "ai/templates/README.md",
80
- "ai/templates/task-brief-template.md",
81
- "ai/templates/issue-template.md",
82
- "ai/templates/fixtures/issues/valid-ready.md",
83
- "ai/templates/fixtures/issues/invalid-placeholder.md",
84
- "ai/templates/fixtures/issues/invalid-protected-surface.md",
85
- "ai/skills/README.md",
86
- "ai/skills/review-architecture.md",
87
- "ai/skills/review-security.md",
88
- "ai/skills/review-contract-drift.md",
89
- "ai/skills/review-governance-drift.md",
90
- "ai/plans/README.md",
91
- "ai/plans/tech-debt.md",
92
- "ai/specs/README.md",
93
- "scripts/bootstrap-workspace.mjs",
94
- "scripts/check-workspace.mjs",
95
- "scripts/validate-governance.mjs",
96
- "scripts/check-template-governance.mjs",
97
- "scripts/check-readiness.mjs",
98
- "scripts/check-task-template.mjs",
99
- "scripts/check-issue-template.mjs",
100
- "scripts/check-knowledge-manifest.mjs",
101
- "scripts/check-plans.mjs",
102
- "scripts/check-review-skills.mjs",
103
- "scripts/check-garbage-collection.mjs",
104
- "scripts/check-contract-manifests.mjs",
105
- "scripts/generate-html-views.mjs",
106
- "scripts/check-html-views.mjs",
107
- "scripts/bootstrap-codex-worktree.mjs",
108
- "scripts/check-worktrees.mjs",
109
- "scripts/check-worktree-bootstrap-fixtures.mjs",
110
- "scripts/lib/worktree-bootstrap.mjs",
111
- "scripts/fixtures/worktrees/README.md",
112
- ];
113
-
114
- if (models.openai) {
115
- requiredFiles.push(...openaiRequiredFiles);
116
- }
117
-
118
- if (models.anthropic) {
119
- requiredFiles.push(...claudeRequiredFiles);
120
- }
121
-
122
- if (clientSupport.codexHooks) {
123
- requiredFiles.push(...codexRequiredFiles);
124
- }
125
-
126
- if (clientSupport.claudeRules) {
127
- requiredFiles.push(...claudeRulesRequiredFiles);
128
- }
129
-
130
- if (models.openai || models.anthropic) {
131
- requiredFiles.push(driftFile);
132
- }
20
+ const requiredFiles = requiredGeneratedHarnessFilesForGovernance({ models, clientSupport });
133
21
 
134
22
  async function exists(relativePath) {
135
23
  try {
@@ -5,6 +5,12 @@ import { constants as fsConstants } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { assertReferencesHarnessRoot } from "./lib/worktree-bootstrap.mjs";
8
+ import {
9
+ consumerEntrypointsForSettings,
10
+ requiredHarnessRepoFilesForWorkspaceCheck,
11
+ requiredWorkspaceFilesForWorkspaceCheck,
12
+ workspaceEntrypointsForSettings,
13
+ } from "./generated-harness-contract.mjs";
8
14
 
9
15
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
16
  const workspaceRoot = path.resolve(repoRoot, "..");
@@ -18,93 +24,15 @@ const clientSupport = {
18
24
  codexHooks: {{CLIENT_CODEX_HOOKS_ENABLED}},
19
25
  claudeRules: {{CLIENT_CLAUDE_RULES_ENABLED}},
20
26
  };
27
+ const settings = { models, clientSupport };
21
28
  const harnessRepoNameError = "repo folder name: expected";
22
29
  const missingEntryPrefix = "missing ";
23
- const repoBaseFiles = [
24
- "README.md",
25
- "ai/AGENTS.md",
26
- "ai/HUB.md",
27
- "ai/context.md",
28
- "ai/HARNESS.md",
29
- "ai/HARNESS-ENGINEERING.md",
30
- "ai/READINESS.md",
31
- "ai/QUALITY.md",
32
- "ai/DECISIONS.md",
33
- "ai/PRODUCT-SUMMARY.md",
34
- "ai/PRODUCT.md",
35
- "ai/ARCHITECTURE.md",
36
- "ai/DESIGN.md",
37
- "ai/WORKFLOW.md",
38
- "ai/VERSIONING.md",
39
- "ai/CODEX-HOOKS.md",
40
- "ai/RUNNER-SAFETY.md",
41
- "ai/RUNNER-READINESS.md",
42
- "ai/AGENT-GARBAGE-COLLECTION.md",
43
- "ai/knowledge-manifest.json",
44
- "ai/workspace/REPOS.md",
45
- "ai/workspace/SYSTEM-MAP.md",
46
- "ai/workspace/SESSION-BOOTSTRAP.md",
47
- "ai/workspace/LOCAL-STACK.md",
48
- "ai/workspace/TEST-STRATEGY.md",
49
- "ai/contracts/README.md",
50
- "ai/templates/README.md",
51
- "ai/skills/README.md",
52
- "ai/specs/README.md",
53
- "scripts/bootstrap-workspace.mjs",
54
- "scripts/check-workspace.mjs",
55
- "scripts/validate-governance.mjs",
56
- "scripts/check-readiness.mjs",
57
- "scripts/check-task-template.mjs",
58
- "scripts/check-issue-template.mjs",
59
- "scripts/check-knowledge-manifest.mjs",
60
- "scripts/check-plans.mjs",
61
- "scripts/check-review-skills.mjs",
62
- "scripts/check-garbage-collection.mjs",
63
- "scripts/check-contract-manifests.mjs",
64
- "scripts/generate-html-views.mjs",
65
- "scripts/check-html-views.mjs",
66
- "scripts/bootstrap-codex-worktree.mjs",
67
- "scripts/check-worktrees.mjs",
68
- "scripts/check-worktree-bootstrap-fixtures.mjs",
69
- ];
70
- const openaiRepoFiles = ["AGENTS.md", "ai/model-overlays/openai/AGENTS.md", "scripts/check-overlay-drift.mjs"];
71
- const anthopicRepoFiles = [
72
- "CLAUDE.md",
73
- ".claude/CLAUDE.md",
74
- ".claude/settings.json",
75
- "ai/model-overlays/anthropic/CLAUDE.md",
76
- "scripts/check-claude-compatibility.mjs",
77
- "scripts/check-overlay-drift.mjs",
78
- ];
79
- const codexRepoFiles = [".codex/hooks.json", "scripts/check-codex-hooks.mjs", "scripts/hooks/codex-hook.mjs"];
80
- const claudeRulesRepoFiles = [".claude/rules/harness-client-surfaces.md"];
81
- const workspaceOpenaiFiles = ["AGENTS.md"];
82
- const workspaceAnthropicFiles = ["CLAUDE.md", ".claude/CLAUDE.md", ".claude/settings.json"];
83
- const workspaceClaudeRulesFiles = [".claude/rules/harness-client-surfaces.md"];
84
- const CLAUDE_MD = "CLAUDE.md";
85
-
86
- const repoRequiredFiles = [...repoBaseFiles];
87
-
88
- if (models.openai) {
89
- repoRequiredFiles.push(...openaiRepoFiles);
90
- }
91
-
92
- if (models.anthropic) {
93
- repoRequiredFiles.push(...anthopicRepoFiles);
94
- }
95
-
96
- if (clientSupport.codexHooks) {
97
- repoRequiredFiles.push(...codexRepoFiles);
98
- }
99
-
100
- if (clientSupport.claudeRules) {
101
- repoRequiredFiles.push(...claudeRulesRepoFiles);
102
- }
103
-
104
- const workspaceRequiredFiles = [];
105
- if (models.openai) workspaceRequiredFiles.push(...workspaceOpenaiFiles);
106
- if (models.anthropic) workspaceRequiredFiles.push(...workspaceAnthropicFiles);
107
- if (clientSupport.claudeRules) workspaceRequiredFiles.push(...workspaceClaudeRulesFiles);
30
+ const repoRequiredFiles = requiredHarnessRepoFilesForWorkspaceCheck(settings);
31
+ const workspaceRequiredFiles = requiredWorkspaceFilesForWorkspaceCheck(settings);
32
+ const workspaceRoutingEntrypoints = workspaceEntrypointsForSettings(settings).filter(
33
+ (entrypoint) => entrypoint.routing !== "presence",
34
+ );
35
+ const consumerRoutingEntrypoints = consumerEntrypointsForSettings(settings);
108
36
 
109
37
  async function exists(filePath) {
110
38
  try {
@@ -160,6 +88,13 @@ async function collectClaudeMemoryRoutingIssue({ basePath, relativePath }) {
160
88
  });
161
89
  }
162
90
 
91
+ async function collectEntrypointRoutingIssue({ basePath, entrypoint, expectedHarnessRoot }) {
92
+ if (entrypoint.routing === "claude-memory") {
93
+ return collectClaudeMemoryRoutingIssue({ basePath, relativePath: entrypoint.path });
94
+ }
95
+ return collectHarnessRoutingIssue({ basePath, relativePath: entrypoint.path, expectedHarnessRoot });
96
+ }
97
+
163
98
  async function main() {
164
99
  const missing = [];
165
100
  if (path.basename(repoRoot) !== harnessRepoName) {
@@ -169,31 +104,20 @@ async function main() {
169
104
  missing.push(...(await collectMissing(repoRoot, repoRequiredFiles, "repo")));
170
105
  missing.push(...(await collectMissing(workspaceRoot, workspaceRequiredFiles, "workspace")));
171
106
 
107
+ for (const entrypoint of workspaceRoutingEntrypoints) {
108
+ const issue = await collectEntrypointRoutingIssue({ basePath: workspaceRoot, entrypoint, expectedHarnessRoot: repoRoot });
109
+ if (issue) missing.push(`workspace:${issue}`);
110
+ }
111
+
172
112
  for (const consumer of consumers) {
173
113
  const consumerRoot = path.resolve(workspaceRoot, consumer.workspacePath);
174
114
  if (!(await exists(consumerRoot))) {
175
115
  missing.push(`consumer:${consumer.name}:missing repo at ${consumerRoot}`);
176
116
  continue;
177
117
  }
178
- if (models.openai) {
179
- const issue = await collectHarnessRoutingIssue({ basePath: workspaceRoot, relativePath: "AGENTS.md", expectedHarnessRoot: repoRoot });
180
- if (issue) missing.push(`workspace:${issue}`);
181
- }
182
- if (models.anthropic) {
183
- const issue = await collectHarnessRoutingIssue({ basePath: workspaceRoot, relativePath: CLAUDE_MD, expectedHarnessRoot: repoRoot });
184
- if (issue) missing.push(`workspace:${issue}`);
185
- const memoryIssue = await collectClaudeMemoryRoutingIssue({ basePath: workspaceRoot, relativePath: ".claude/CLAUDE.md" });
186
- if (memoryIssue) missing.push(`workspace:${memoryIssue}`);
187
- }
188
- if (models.openai) {
189
- const issue = await collectHarnessRoutingIssue({ basePath: consumerRoot, relativePath: "AGENTS.md", expectedHarnessRoot: repoRoot });
190
- if (issue) missing.push(`consumer:${consumer.name}:${issue}`);
191
- }
192
- if (models.anthropic) {
193
- const issue = await collectHarnessRoutingIssue({ basePath: consumerRoot, relativePath: CLAUDE_MD, expectedHarnessRoot: repoRoot });
118
+ for (const entrypoint of consumerRoutingEntrypoints) {
119
+ const issue = await collectEntrypointRoutingIssue({ basePath: consumerRoot, entrypoint, expectedHarnessRoot: repoRoot });
194
120
  if (issue) missing.push(`consumer:${consumer.name}:${issue}`);
195
- const memoryIssue = await collectClaudeMemoryRoutingIssue({ basePath: consumerRoot, relativePath: ".claude/CLAUDE.md" });
196
- if (memoryIssue) missing.push(`consumer:${consumer.name}:${memoryIssue}`);
197
121
  }
198
122
  }
199
123
 
@@ -8,8 +8,10 @@ import {
8
8
  classifyWorktreeBootstrap,
9
9
  models,
10
10
  parseWorktreeListPorcelain,
11
+ repairCommand,
11
12
  requiredPointerFiles,
12
13
  renderPointerFile,
14
+ shellQuote,
13
15
  } from "./lib/worktree-bootstrap.mjs";
14
16
 
15
17
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -32,6 +34,8 @@ const tempConsumerRoot = "/tmp/{{PRIMARY_CONSUMER_NAME}}";
32
34
  const testCaseTemplateConsumer = "{{PRIMARY_CONSUMER_NAME}}";
33
35
  const invalidHarnessRoot = "Read ../{{HARNESS_REPO_NAME}}/AGENTS.md before editing.";
34
36
  const wrongHarnessRootPointer = `Read /other/{{HARNESS_REPO_NAME}}/AGENTS.md before editing.`;
37
+ const unsafeHarnessRoot = "/workspace/{{HARNESS_REPO_NAME}} with spaces/'quotes';$(touch /tmp/unsafe)";
38
+ const unsafeCheckoutPath = "/workspace/{{PRIMARY_CONSUMER_NAME}} with spaces/'quotes';$(touch /tmp/checkout)";
35
39
  const harnessFiles = new Set([
36
40
  ...(models.openai ? ["AGENTS.md"] : []),
37
41
  ...(models.anthropic ? ["CLAUDE.md"] : []),
@@ -115,8 +119,16 @@ assert.equal(mixedResult.state, stateMissing, "mixed_references");
115
119
 
116
120
  const pointerPattern = new RegExp(`{{HARNESS_REPO_NAME}}/${models.openai ? "AGENTS" : "CLAUDE"}\\.md`);
117
121
  const worktreePorcelainOutput = "worktree /repo/main\nHEAD abc\nbranch refs/heads/main\n\nworktree /repo/detached\nHEAD def\ndetached\n";
122
+ const quotedPointer = renderPointerFile({
123
+ relativePath: requiredPointerFiles[0],
124
+ harnessRoot: unsafeHarnessRoot,
125
+ repoName: testCaseTemplateConsumer,
126
+ });
127
+ const quotedBootstrapScript = path.join(path.resolve(unsafeHarnessRoot), "scripts/bootstrap-codex-worktree.mjs");
118
128
 
119
129
  assert.match(renderPointerFile({ relativePath: requiredPointerFiles[0], harnessRoot, repoName: testCaseTemplateConsumer }), pointerPattern);
130
+ assert.match(quotedPointer, new RegExp(`node ${shellQuote(quotedBootstrapScript).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} ${shellQuote("<checkout-path>").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
131
+ assert.equal(repairCommand({ harnessRoot: unsafeHarnessRoot, targetPath: unsafeCheckoutPath }), `node ${shellQuote(path.join(unsafeHarnessRoot, "scripts/bootstrap-codex-worktree.mjs"))} ${shellQuote(unsafeCheckoutPath)}`);
120
132
  assert.equal(parseWorktreeListPorcelain(worktreePorcelainOutput).length, 2);
121
133
 
122
134
  console.log("Worktree bootstrap fixture check passed.");