@structor-dev/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -0
  3. package/bin/structor.mjs +576 -0
  4. package/docs/INIT.md +109 -0
  5. package/docs/adr/0001-default-generated-repo-name.md +9 -0
  6. package/docs/issues/0001-structor-doctor.md +39 -0
  7. package/examples/frontend-backend/harness.config.json +35 -0
  8. package/examples/openai-and-anthropic/harness.config.json +28 -0
  9. package/examples/single-repo/harness.config.json +26 -0
  10. package/harness.config.example.json +38 -0
  11. package/package.json +58 -0
  12. package/schemas/contract-manifest.schema.json +18 -0
  13. package/schemas/harness-config.schema.json +85 -0
  14. package/schemas/task-brief.schema.json +37 -0
  15. package/scripts/check-config.mjs +76 -0
  16. package/scripts/check-contract-manifests.mjs +85 -0
  17. package/scripts/check-model-overlays.mjs +30 -0
  18. package/scripts/check-placeholders.mjs +48 -0
  19. package/scripts/check-task-template.mjs +53 -0
  20. package/scripts/check-template-files.mjs +110 -0
  21. package/scripts/init-harness.mjs +270 -0
  22. package/scripts/lib.mjs +190 -0
  23. package/scripts/smoke-template.mjs +309 -0
  24. package/scripts/validate-governance.mjs +3 -0
  25. package/scripts/validate-template.mjs +16 -0
  26. package/template/.claude/CLAUDE.md.tpl +12 -0
  27. package/template/.claude/rules/harness-client-surfaces.md.tpl +20 -0
  28. package/template/.claude/settings.json.tpl +10 -0
  29. package/template/.codex/hooks.json.tpl +77 -0
  30. package/template/AGENTS.md.tpl +22 -0
  31. package/template/CLAUDE.md.tpl +16 -0
  32. package/template/README.md.tpl +109 -0
  33. package/template/ai/AGENT-GARBAGE-COLLECTION.md.tpl +18 -0
  34. package/template/ai/AGENTS.md.tpl +36 -0
  35. package/template/ai/ARCHITECTURE.md.tpl +35 -0
  36. package/template/ai/CODEX-HOOKS.md.tpl +23 -0
  37. package/template/ai/DECISIONS.md.tpl +22 -0
  38. package/template/ai/DESIGN.md.tpl +22 -0
  39. package/template/ai/HARNESS-ENGINEERING.md.tpl +107 -0
  40. package/template/ai/HARNESS.md.tpl +53 -0
  41. package/template/ai/HUB.md.tpl +53 -0
  42. package/template/ai/PRODUCT-SUMMARY.md.tpl +28 -0
  43. package/template/ai/PRODUCT.md.tpl +32 -0
  44. package/template/ai/QUALITY.md.tpl +37 -0
  45. package/template/ai/READINESS.md.tpl +39 -0
  46. package/template/ai/RUNNER-READINESS.md.tpl +14 -0
  47. package/template/ai/RUNNER-SAFETY.md.tpl +21 -0
  48. package/template/ai/VERSIONING.md.tpl +16 -0
  49. package/template/ai/WORKFLOW.md.tpl +42 -0
  50. package/template/ai/context.md.tpl +17 -0
  51. package/template/ai/contracts/README.md.tpl +23 -0
  52. package/template/ai/contracts/api-boundary.contract.json.tpl +11 -0
  53. package/template/ai/contracts/api-boundary.md.tpl +17 -0
  54. package/template/ai/contracts/app-legibility.contract.json.tpl +11 -0
  55. package/template/ai/contracts/app-legibility.md.tpl +24 -0
  56. package/template/ai/contracts/codex-hooks.contract.json.tpl +15 -0
  57. package/template/ai/contracts/codex-hooks.md.tpl +18 -0
  58. package/template/ai/contracts/github-safety.contract.json.tpl +11 -0
  59. package/template/ai/contracts/github-safety.md.tpl +15 -0
  60. package/template/ai/contracts/release-flow.contract.json.tpl +12 -0
  61. package/template/ai/contracts/release-flow.md.tpl +15 -0
  62. package/template/ai/contracts/repo-boundaries.contract.json.tpl +12 -0
  63. package/template/ai/contracts/repo-boundaries.md.tpl +18 -0
  64. package/template/ai/contracts/security-boundary.contract.json.tpl +11 -0
  65. package/template/ai/contracts/security-boundary.md.tpl +19 -0
  66. package/template/ai/knowledge-manifest.json.tpl +149 -0
  67. package/template/ai/model-overlays/anthropic/CLAUDE.md.tpl +14 -0
  68. package/template/ai/model-overlays/openai/AGENTS.md.tpl +13 -0
  69. package/template/ai/plans/README.md.tpl +10 -0
  70. package/template/ai/plans/tech-debt.md.tpl +7 -0
  71. package/template/ai/skills/README.md.tpl +15 -0
  72. package/template/ai/skills/review-architecture.md.tpl +41 -0
  73. package/template/ai/skills/review-contract-drift.md.tpl +41 -0
  74. package/template/ai/skills/review-governance-drift.md.tpl +42 -0
  75. package/template/ai/skills/review-security.md.tpl +40 -0
  76. package/template/ai/specs/README.md.tpl +14 -0
  77. package/template/ai/templates/README.md.tpl +13 -0
  78. package/template/ai/templates/fixtures/issues/invalid-placeholder.md.tpl +20 -0
  79. package/template/ai/templates/fixtures/issues/invalid-protected-surface.md.tpl +21 -0
  80. package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +105 -0
  81. package/template/ai/templates/issue-template.md.tpl +107 -0
  82. package/template/ai/templates/task-brief-template.md.tpl +185 -0
  83. package/template/ai/workspace/LOCAL-STACK.md.tpl +21 -0
  84. package/template/ai/workspace/REPOS.md.tpl +19 -0
  85. package/template/ai/workspace/SESSION-BOOTSTRAP.md.tpl +27 -0
  86. package/template/ai/workspace/SYSTEM-MAP.md.tpl +19 -0
  87. package/template/ai/workspace/TEST-STRATEGY.md.tpl +22 -0
  88. package/template/consumer/.claude/CLAUDE.md.tpl +14 -0
  89. package/template/consumer/AGENTS.md.tpl +23 -0
  90. package/template/consumer/CLAUDE.md.tpl +15 -0
  91. package/template/scripts/bootstrap-codex-worktree.mjs.tpl +52 -0
  92. package/template/scripts/bootstrap-workspace.mjs.tpl +100 -0
  93. package/template/scripts/check-claude-compatibility.mjs.tpl +120 -0
  94. package/template/scripts/check-codex-hooks.mjs.tpl +190 -0
  95. package/template/scripts/check-contract-manifests.mjs.tpl +81 -0
  96. package/template/scripts/check-garbage-collection.mjs.tpl +25 -0
  97. package/template/scripts/check-html-views.mjs.tpl +60 -0
  98. package/template/scripts/check-issue-template.mjs.tpl +167 -0
  99. package/template/scripts/check-knowledge-manifest.mjs.tpl +82 -0
  100. package/template/scripts/check-overlay-drift.mjs.tpl +49 -0
  101. package/template/scripts/check-plans.mjs.tpl +70 -0
  102. package/template/scripts/check-readiness.mjs.tpl +130 -0
  103. package/template/scripts/check-review-skills.mjs.tpl +48 -0
  104. package/template/scripts/check-task-template.mjs.tpl +63 -0
  105. package/template/scripts/check-template-governance.mjs.tpl +161 -0
  106. package/template/scripts/check-workspace.mjs.tpl +212 -0
  107. package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +122 -0
  108. package/template/scripts/check-worktrees.mjs.tpl +69 -0
  109. package/template/scripts/fixtures/worktrees/README.md.tpl +4 -0
  110. package/template/scripts/generate-html-views.mjs.tpl +189 -0
  111. package/template/scripts/hooks/codex-hook.mjs.tpl +21 -0
  112. package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +114 -0
  113. package/template/scripts/lib/worktree-bootstrap.mjs.tpl +388 -0
  114. package/template/scripts/validate-governance.mjs.tpl +78 -0
  115. package/template/workspace/.claude/CLAUDE.md.tpl +9 -0
  116. package/template/workspace/.claude/rules/harness-client-surfaces.md.tpl +15 -0
  117. package/template/workspace/.claude/settings.json.tpl +10 -0
  118. package/template/workspace/AGENTS.md.tpl +17 -0
  119. package/template/workspace/CLAUDE.md.tpl +18 -0
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const models = {
10
+ openai: {{MODEL_OPENAI_ENABLED}},
11
+ anthropic: {{MODEL_ANTHROPIC_ENABLED}},
12
+ };
13
+ const clientSupport = {
14
+ codexHooks: {{CLIENT_CODEX_HOOKS_ENABLED}},
15
+ claudeRules: {{CLIENT_CLAUDE_RULES_ENABLED}},
16
+ claudeHooks: {{CLIENT_CLAUDE_HOOKS_ENABLED}},
17
+ claudeSkills: {{CLIENT_CLAUDE_SKILLS_ENABLED}},
18
+ };
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
+ }
133
+
134
+ async function exists(relativePath) {
135
+ try {
136
+ await access(path.join(repoRoot, relativePath), fsConstants.F_OK);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ const errors = [];
144
+ for (const relativePath of requiredFiles) {
145
+ if (!(await exists(relativePath))) errors.push(`missing ${relativePath}`);
146
+ }
147
+
148
+ for (const relativePath of requiredFiles.filter((item) => item.endsWith(".md"))) {
149
+ const content = await readFile(path.join(repoRoot, relativePath), "utf8");
150
+ if (/\{\{[A-Z0-9_]+\}\}/.test(content)) {
151
+ errors.push(`${relativePath} contains an unresolved template placeholder.`);
152
+ }
153
+ }
154
+
155
+ if (errors.length > 0) {
156
+ console.error("Generated harness governance check failed.");
157
+ for (const error of errors) console.error(`- ${error}`);
158
+ process.exit(1);
159
+ }
160
+
161
+ console.log("Generated harness governance check passed.");
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { assertReferencesHarnessRoot } from "./lib/worktree-bootstrap.mjs";
8
+
9
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const workspaceRoot = path.resolve(repoRoot, "..");
11
+ const harnessRepoName = "{{HARNESS_REPO_NAME}}";
12
+ const consumers = {{CONSUMER_CONFIG_JSON}};
13
+ const models = {
14
+ openai: {{MODEL_OPENAI_ENABLED}},
15
+ anthropic: {{MODEL_ANTHROPIC_ENABLED}},
16
+ };
17
+ const clientSupport = {
18
+ codexHooks: {{CLIENT_CODEX_HOOKS_ENABLED}},
19
+ claudeRules: {{CLIENT_CLAUDE_RULES_ENABLED}},
20
+ };
21
+ const harnessRepoNameError = "repo folder name: expected";
22
+ 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);
108
+
109
+ async function exists(filePath) {
110
+ try {
111
+ await access(filePath, fsConstants.F_OK);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ async function collectMissing(basePath, relativePaths, prefix) {
119
+ const missing = [];
120
+ for (const relativePath of relativePaths) {
121
+ if (!(await exists(path.join(basePath, relativePath)))) {
122
+ missing.push(`${prefix}:${relativePath}`);
123
+ }
124
+ }
125
+ return missing;
126
+ }
127
+
128
+ async function readIfExists(filePath) {
129
+ if (!(await exists(filePath))) return null;
130
+ return readFile(filePath, "utf8");
131
+ }
132
+
133
+ async function collectHarnessRoutingIssue({ basePath, relativePath, expectedHarnessRoot }) {
134
+ const pointerPath = path.join(basePath, relativePath);
135
+ const pointerContent = await readIfExists(pointerPath);
136
+ if (pointerContent === null) return `${relativePath} missing.`;
137
+ return assertReferencesHarnessRoot({
138
+ pointerPath: relativePath,
139
+ pointerContent,
140
+ consumerRoot: basePath,
141
+ expectedHarnessRoot,
142
+ models,
143
+ });
144
+ }
145
+
146
+ async function collectClaudeMemoryRoutingIssue({ basePath, relativePath }) {
147
+ const pointerPath = path.join(basePath, relativePath);
148
+ const pointerContent = await readIfExists(pointerPath);
149
+ if (pointerContent === null) return `${relativePath} missing.`;
150
+ if (!pointerContent.includes("../CLAUDE.md")) {
151
+ return `${relativePath} must route through ../CLAUDE.md.`;
152
+ }
153
+ return assertReferencesHarnessRoot({
154
+ pointerPath: relativePath,
155
+ pointerContent,
156
+ consumerRoot: basePath,
157
+ expectedHarnessRoot: repoRoot,
158
+ models,
159
+ requireHarnessReference: false,
160
+ });
161
+ }
162
+
163
+ async function main() {
164
+ const missing = [];
165
+ if (path.basename(repoRoot) !== harnessRepoName) {
166
+ missing.push(`${harnessRepoNameError} ${harnessRepoName}, found ${path.basename(repoRoot)}`);
167
+ }
168
+
169
+ missing.push(...(await collectMissing(repoRoot, repoRequiredFiles, "repo")));
170
+ missing.push(...(await collectMissing(workspaceRoot, workspaceRequiredFiles, "workspace")));
171
+
172
+ for (const consumer of consumers) {
173
+ const consumerRoot = path.resolve(workspaceRoot, consumer.workspacePath);
174
+ if (!(await exists(consumerRoot))) {
175
+ missing.push(`consumer:${consumer.name}:missing repo at ${consumerRoot}`);
176
+ continue;
177
+ }
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 });
194
+ 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
+ }
198
+ }
199
+
200
+ if (missing.length > 0) {
201
+ console.error("Workspace bootstrap check failed.");
202
+ for (const item of missing) console.error(`- ${item}`);
203
+ process.exit(1);
204
+ }
205
+
206
+ console.log("Workspace layout check passed.");
207
+ }
208
+
209
+ main().catch((error) => {
210
+ console.error(error instanceof Error ? error.message : String(error));
211
+ process.exit(1);
212
+ });
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ buildRepairPlan,
8
+ classifyWorktreeBootstrap,
9
+ models,
10
+ parseWorktreeListPorcelain,
11
+ requiredPointerFiles,
12
+ renderPointerFile,
13
+ } from "./lib/worktree-bootstrap.mjs";
14
+
15
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
16
+ const harnessRoot = "/workspace/{{HARNESS_REPO_NAME}}";
17
+ const consumerRoot = "/workspace/{{PRIMARY_CONSUMER_NAME}}";
18
+ const workspaceAgentsPath = "AGENTS.md";
19
+ const workspaceClaudePath = "CLAUDE.md";
20
+ const workspaceCodexPath = ".codex/hooks.json";
21
+ const stateValid = "valid";
22
+ const stateMissing = "missing";
23
+ const stateStaleRelative = "stale_relative";
24
+ const stateWrongHarnessRoot = "wrong_harness_root";
25
+ const stateUnsupportedRepo = "unsupported_repo";
26
+ const stateMissingPath = "missing_path";
27
+ const stateDetached = "detached";
28
+ const statePrunable = "prunable";
29
+ const fixturePathPrefix = "scripts/fixtures/worktrees";
30
+ const fixturesMissingPathSuffix = "missing-path";
31
+ const tempConsumerRoot = "/tmp/{{PRIMARY_CONSUMER_NAME}}";
32
+ const testCaseTemplateConsumer = "{{PRIMARY_CONSUMER_NAME}}";
33
+ const invalidHarnessRoot = "Read ../{{HARNESS_REPO_NAME}}/AGENTS.md before editing.";
34
+ const wrongHarnessRootPointer = `Read /other/{{HARNESS_REPO_NAME}}/AGENTS.md before editing.`;
35
+ const harnessFiles = new Set([
36
+ ...(models.openai ? ["AGENTS.md"] : []),
37
+ ...(models.anthropic ? ["CLAUDE.md"] : []),
38
+ "ai/AGENTS.md",
39
+ "ai/HUB.md",
40
+ "ai/context.md",
41
+ ]);
42
+
43
+ function files(entries) {
44
+ return [...requiredPointerFiles, workspaceCodexPath].map((relativePath) => ({
45
+ relativePath,
46
+ exists: Object.hasOwn(entries, relativePath),
47
+ content: entries[relativePath] ?? "",
48
+ }));
49
+ }
50
+
51
+ async function fileIsFile(filePath) {
52
+ const relativePath = path.relative(harnessRoot, filePath).replaceAll(path.sep, "/");
53
+ return harnessFiles.has(relativePath);
54
+ }
55
+
56
+ const validAgentPointer = `Read ${harnessRoot}/AGENTS.md before editing.`;
57
+ const validClaudePointer = `Read ${harnessRoot}/CLAUDE.md before editing.`;
58
+ const mixedAgentPointer = models.openai
59
+ ? `Read ${harnessRoot}/AGENTS.md and ${harnessRoot}/ai/MISSING.md before editing.`
60
+ : `Read ${harnessRoot}/CLAUDE.md and ${harnessRoot}/ai/MISSING.md before editing.`;
61
+ const staleAgentPointer = invalidHarnessRoot;
62
+ const validPointers = {
63
+ ...(models.openai ? { [workspaceAgentsPath]: validAgentPointer } : {}),
64
+ ...(models.anthropic ? { [workspaceClaudePath]: validClaudePointer } : {}),
65
+ };
66
+ const mixedPointers = models.openai
67
+ ? {
68
+ [workspaceAgentsPath]: mixedAgentPointer,
69
+ ...(models.anthropic ? { [workspaceClaudePath]: validClaudePointer } : {}),
70
+ }
71
+ : {
72
+ [workspaceClaudePath]: mixedAgentPointer,
73
+ };
74
+ const stalePointers = Object.fromEntries(requiredPointerFiles.map((relativePath) => [relativePath, staleAgentPointer]));
75
+ const wrongRootPointers = Object.fromEntries(requiredPointerFiles.map((relativePath) => [relativePath, wrongHarnessRootPointer]));
76
+
77
+ const cases = [
78
+ { name: "valid", targetPath: consumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files(validPointers), expectedState: stateValid, expectedValid: true, expectedRepairable: false },
79
+ { name: "missing", targetPath: consumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files({}), expectedState: stateMissing, expectedValid: false, expectedRepairable: true },
80
+ { name: "stale_relative", targetPath: tempConsumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files(stalePointers), expectedState: stateStaleRelative, expectedValid: false, expectedRepairable: true },
81
+ { name: "wrong_harness_root", targetPath: consumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files(wrongRootPointers), expectedState: stateWrongHarnessRoot, expectedValid: false, expectedRepairable: true },
82
+ { name: "unsupported_repo", targetPath: "/workspace/random-app", repoName: null, targetExists: true, files: files({ [workspaceAgentsPath]: validAgentPointer }), expectedState: stateUnsupportedRepo, expectedValid: false, expectedRepairable: false },
83
+ { name: "missing_path", targetPath: path.join(repoRoot, `${fixturePathPrefix}/${fixturesMissingPathSuffix}`), repoName: testCaseTemplateConsumer, targetExists: false, files: files({}), expectedState: stateMissingPath, expectedValid: false, expectedRepairable: false },
84
+ { name: "detached", targetPath: tempConsumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files(validPointers), worktreeRecord: { detached: true }, expectedState: stateDetached, expectedValid: true, expectedRepairable: false },
85
+ { name: "prunable", targetPath: tempConsumerRoot, repoName: testCaseTemplateConsumer, targetExists: true, files: files({ [workspaceAgentsPath]: validAgentPointer }), worktreeRecord: { prunable: true }, expectedState: statePrunable, expectedValid: false, expectedRepairable: false },
86
+ ];
87
+
88
+ for (const testCase of cases) {
89
+ const result = await classifyWorktreeBootstrap({
90
+ targetPath: testCase.targetPath,
91
+ targetExists: testCase.targetExists,
92
+ harnessRoot,
93
+ repoName: testCase.repoName,
94
+ files: testCase.files,
95
+ worktreeRecord: testCase.worktreeRecord ?? {},
96
+ fileIsFile,
97
+ });
98
+ assert.equal(result.state, testCase.expectedState, testCase.name);
99
+ assert.equal(result.valid, testCase.expectedValid, testCase.name);
100
+ assert.equal(result.repairable, testCase.expectedRepairable, testCase.name);
101
+ if (result.repairable) assert.equal(buildRepairPlan({ inspection: result, harnessRoot }).writes.length > 0, true, testCase.name);
102
+ }
103
+
104
+ const mixedResult = await classifyWorktreeBootstrap({
105
+ targetPath: consumerRoot,
106
+ targetExists: true,
107
+ harnessRoot,
108
+ repoName: testCaseTemplateConsumer,
109
+ files: files(mixedPointers),
110
+ fileIsFile,
111
+ });
112
+ assert.equal(mixedResult.valid, false, "mixed_references");
113
+ assert.equal(mixedResult.repairable, true, "mixed_references");
114
+ assert.equal(mixedResult.state, stateMissing, "mixed_references");
115
+
116
+ const pointerPattern = new RegExp(`{{HARNESS_REPO_NAME}}/${models.openai ? "AGENTS" : "CLAUDE"}\\.md`);
117
+ const worktreePorcelainOutput = "worktree /repo/main\nHEAD abc\nbranch refs/heads/main\n\nworktree /repo/detached\nHEAD def\ndetached\n";
118
+
119
+ assert.match(renderPointerFile({ relativePath: requiredPointerFiles[0], harnessRoot, repoName: testCaseTemplateConsumer }), pointerPattern);
120
+ assert.equal(parseWorktreeListPorcelain(worktreePorcelainOutput).length, 2);
121
+
122
+ console.log("Worktree bootstrap fixture check passed.");
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ canonicalRepos,
7
+ exists,
8
+ formatInspection,
9
+ inspectCheckout,
10
+ listGitWorktrees,
11
+ } from "./lib/worktree-bootstrap.mjs";
12
+
13
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ const workspaceRoot = path.resolve(repoRoot, "..");
15
+ const includeCanonicalArg = "--include-canonical";
16
+ const includeNoWorktreeListArg = "--no-worktree-list";
17
+ const strictArg = "--strict";
18
+ const canonicalInspectionSource = "canonical";
19
+ const worktreeInspectionSource = "worktree";
20
+ const noTargetsMessage = "No worktree bootstrap targets selected. Use --include-canonical or allow git worktree discovery.";
21
+
22
+ const args = new Set(process.argv.slice(2));
23
+ const includeCanonical = args.has(includeCanonicalArg);
24
+ const includeWorktreeList = !args.has(includeNoWorktreeListArg);
25
+ const strict = args.has(strictArg);
26
+
27
+ const inspections = [];
28
+ if (includeCanonical) {
29
+ for (const repoName of canonicalRepos) {
30
+ const inspection = await inspectCheckout({ targetPath: path.join(workspaceRoot, repoName), harnessRoot: repoRoot });
31
+ inspection.source = canonicalInspectionSource;
32
+ inspections.push(inspection);
33
+ }
34
+ }
35
+
36
+ if (includeWorktreeList) {
37
+ for (const repoName of canonicalRepos) {
38
+ const canonicalPath = path.join(workspaceRoot, repoName);
39
+ if (!(await exists(canonicalPath))) continue;
40
+ let records = [];
41
+ try {
42
+ records = await listGitWorktrees(canonicalPath);
43
+ } catch {
44
+ continue;
45
+ }
46
+ for (const record of records) {
47
+ if (includeCanonical && path.resolve(record.path) === path.resolve(canonicalPath)) continue;
48
+ const inspection = await inspectCheckout({ targetPath: record.path, harnessRoot: repoRoot, worktreeRecord: record });
49
+ inspection.source = worktreeInspectionSource;
50
+ inspections.push(inspection);
51
+ }
52
+ }
53
+ }
54
+
55
+ if (inspections.length === 0) {
56
+ console.log(noTargetsMessage);
57
+ process.exit(0);
58
+ }
59
+
60
+ console.log("Worktree bootstrap check:");
61
+ for (const inspection of inspections) {
62
+ console.log(`- ${formatInspection(inspection, { harnessRoot: repoRoot }).replaceAll("\n", "\n ")}`);
63
+ }
64
+
65
+ const blocking = strict ? inspections.filter((inspection) => !inspection.valid) : inspections.filter((inspection) => inspection.source === canonicalInspectionSource && !inspection.valid);
66
+ if (blocking.length > 0) {
67
+ console.error(`Worktree bootstrap check found ${blocking.length} blocking issue(s).`);
68
+ process.exit(1);
69
+ }
@@ -0,0 +1,4 @@
1
+ # Worktree Fixtures
2
+
3
+ This directory documents fixture intent for `check-worktree-bootstrap-fixtures.mjs`.
4
+ The checker uses in-memory fixtures so it can run without creating worktrees.