@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,388 @@
1
+ import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { execFile } from "node:child_process";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export const canonicalRepos = ["{{HARNESS_REPO_NAME}}", ...{{CONSUMER_REPO_NAMES_JSON}}];
10
+ export const models = {
11
+ openai: {{MODEL_OPENAI_ENABLED}},
12
+ anthropic: {{MODEL_ANTHROPIC_ENABLED}},
13
+ };
14
+ export const requiredPointerFiles = [
15
+ ...(models.openai ? ["AGENTS.md"] : []),
16
+ ...(models.anthropic ? ["CLAUDE.md"] : []),
17
+ ];
18
+ export const optionalPointerFiles = [];
19
+
20
+ const repairableStates = new Set(["missing", "stale_relative", "wrong_harness_root"]);
21
+
22
+ export async function exists(filePath) {
23
+ try {
24
+ await access(filePath, fsConstants.F_OK);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export function repoNameFromRemote(remoteUrl) {
32
+ if (!remoteUrl) return null;
33
+ const withoutGitSuffix = remoteUrl.trim().replace(/\.git$/, "");
34
+ return withoutGitSuffix.match(/[:/]([^/:]+)$/)?.[1] ?? null;
35
+ }
36
+
37
+ export function canonicalRepoNameFromInput({ targetPath, gitRoot, remoteUrl }) {
38
+ const candidates = [repoNameFromRemote(remoteUrl), gitRoot ? path.basename(gitRoot) : null, targetPath ? path.basename(targetPath) : null].filter(Boolean);
39
+ return candidates.find((candidate) => canonicalRepos.includes(candidate)) ?? null;
40
+ }
41
+
42
+ export async function gitMetadataForPath(targetPath) {
43
+ try {
44
+ const { stdout: gitRootOutput } = await execFileAsync("git", ["-C", targetPath, "rev-parse", "--show-toplevel"]);
45
+ const gitRoot = gitRootOutput.trim();
46
+ let remoteUrl = "";
47
+ try {
48
+ const { stdout } = await execFileAsync("git", ["-C", gitRoot, "remote", "get-url", "origin"]);
49
+ remoteUrl = stdout.trim();
50
+ } catch {
51
+ remoteUrl = "";
52
+ }
53
+ return { gitRoot, remoteUrl };
54
+ } catch {
55
+ return { gitRoot: null, remoteUrl: "" };
56
+ }
57
+ }
58
+
59
+ function cleanReference(rawReference) {
60
+ return rawReference.trim().replace(/^[`'"]+/, "").replace(/[`'",;:.)\]}]+$/, "");
61
+ }
62
+
63
+ export function extractHarnessReferences(content) {
64
+ const escapedName = "{{HARNESS_REPO_NAME}}".replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
65
+ const pattern = new RegExp("(?:\\.\\.?/|/)[^`'\"\\s)<\\]}]*(?:" + escapedName + ")[^`'\"\\s)<\\]}]*", "g");
66
+ return [...new Set((content.match(pattern) ?? []).map(cleanReference).filter(Boolean))];
67
+ }
68
+
69
+ export function resolveHarnessReferenceTarget({ reference: rawReference, consumerRoot }) {
70
+ const reference = cleanReference(rawReference);
71
+ const absoluteReference = path.isAbsolute(reference) ? path.resolve(reference) : path.resolve(consumerRoot, reference);
72
+ const parts = absoluteReference.split(path.sep);
73
+ if (parts.lastIndexOf("{{HARNESS_REPO_NAME}}") === -1) return null;
74
+ return absoluteReference;
75
+ }
76
+
77
+ export function resolveHarnessReferenceRoot({ reference: rawReference, consumerRoot }) {
78
+ const target = resolveHarnessReferenceTarget({ reference: rawReference, consumerRoot });
79
+ if (!target) return null;
80
+ const parts = target.split(path.sep);
81
+ const index = parts.lastIndexOf("{{HARNESS_REPO_NAME}}");
82
+ return parts.slice(0, index + 1).join(path.sep) || path.sep;
83
+ }
84
+
85
+ async function isFileTarget(target) {
86
+ try {
87
+ return (await stat(target)).isFile();
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ export async function validateHarnessReferences({
94
+ pointerPath,
95
+ pointerContent,
96
+ consumerRoot,
97
+ expectedHarnessRoot,
98
+ fileIsFile = isFileTarget,
99
+ models: enabledModels = models,
100
+ requireHarnessReference = true,
101
+ }) {
102
+ const references = extractHarnessReferences(pointerContent);
103
+ if (references.length === 0) {
104
+ if (!requireHarnessReference) return null;
105
+ return [{ kind: "missing", message: `${pointerPath} does not contain a resolvable {{HARNESS_REPO_NAME}} path.` }];
106
+ }
107
+
108
+ const issues = [];
109
+ let matchedExpectedRoot = false;
110
+ for (const reference of references) {
111
+ const target = resolveHarnessReferenceTarget({ reference, consumerRoot });
112
+ if (!target) {
113
+ issues.push({ kind: "missing", reference, message: `${pointerPath} does not contain a resolvable {{HARNESS_REPO_NAME}} path.` });
114
+ continue;
115
+ }
116
+
117
+ const referenceRoot = resolveHarnessReferenceRoot({ reference, consumerRoot });
118
+ if (referenceRoot === null || path.resolve(referenceRoot) !== path.resolve(expectedHarnessRoot)) {
119
+ issues.push({
120
+ kind: "wrong_harness_root",
121
+ reference,
122
+ message: `${pointerPath} points at ${referenceRoot ?? reference} instead of ${expectedHarnessRoot}.`,
123
+ });
124
+ continue;
125
+ }
126
+
127
+ matchedExpectedRoot = true;
128
+ const relativeTarget = path.relative(expectedHarnessRoot, target).replaceAll(path.sep, "/");
129
+ if (!enabledModels.openai && relativeTarget === "AGENTS.md") {
130
+ issues.push({
131
+ kind: "disabled_entrypoint",
132
+ reference,
133
+ message: `${pointerPath} must not reference ${relativeTarget} when OpenAI support is disabled.`,
134
+ });
135
+ continue;
136
+ }
137
+ if (!enabledModels.anthropic && relativeTarget === "CLAUDE.md") {
138
+ issues.push({
139
+ kind: "disabled_entrypoint",
140
+ reference,
141
+ message: `${pointerPath} must not reference ${relativeTarget} when Anthropic support is disabled.`,
142
+ });
143
+ continue;
144
+ }
145
+ if (relativeTarget === "" || !(await fileIsFile(target))) {
146
+ const targetLabel = relativeTarget === "" ? "." : relativeTarget;
147
+ issues.push({
148
+ kind: "missing_target",
149
+ reference,
150
+ message: `${pointerPath} references missing generated-harness file ${targetLabel}.`,
151
+ });
152
+ }
153
+ }
154
+
155
+ if (!matchedExpectedRoot && issues.length === 0) {
156
+ issues.push({
157
+ kind: "wrong_harness_root",
158
+ message: `${pointerPath} points at ${references.join(", ")} instead of ${expectedHarnessRoot}.`,
159
+ });
160
+ }
161
+
162
+ return issues.length > 0 ? issues : null;
163
+ }
164
+
165
+ export async function assertReferencesHarnessRoot({
166
+ pointerPath,
167
+ pointerContent,
168
+ consumerRoot,
169
+ expectedHarnessRoot,
170
+ fileIsFile,
171
+ models: enabledModels,
172
+ requireHarnessReference,
173
+ }) {
174
+ const issues = await validateHarnessReferences({
175
+ pointerPath,
176
+ pointerContent,
177
+ consumerRoot,
178
+ expectedHarnessRoot,
179
+ fileIsFile,
180
+ models: enabledModels,
181
+ requireHarnessReference,
182
+ });
183
+ if (!issues) return null;
184
+ return issues[0].message;
185
+ }
186
+
187
+ async function classifyPointerContent({ relativePath, content, targetPath, harnessRoot, fileIsFile }) {
188
+ const issues = await validateHarnessReferences({
189
+ pointerPath: relativePath,
190
+ pointerContent: content,
191
+ consumerRoot: targetPath,
192
+ expectedHarnessRoot: harnessRoot,
193
+ fileIsFile,
194
+ });
195
+ if (!issues) return null;
196
+ const references = extractHarnessReferences(content);
197
+ const referenceKinds = new Set(issues.map((issue) => issue.kind));
198
+ if (referenceKinds.has("disabled_entrypoint")) {
199
+ return { kind: "wrong_harness_root", relativePath, references, message: issues[0].message };
200
+ }
201
+ if (referenceKinds.has("missing_target")) {
202
+ return { kind: "missing", relativePath, references, message: issues[0].message };
203
+ }
204
+ if (referenceKinds.has("missing")) {
205
+ return { kind: "missing", relativePath, references, message: issues[0].message };
206
+ }
207
+ if (referenceKinds.has("wrong_harness_root")) {
208
+ if (references.some((reference) => !path.isAbsolute(reference))) {
209
+ return { kind: "stale_relative", relativePath, references, message: issues[0].message };
210
+ }
211
+ return { kind: "wrong_harness_root", relativePath, references, message: issues[0].message };
212
+ }
213
+ return { kind: "wrong_harness_root", relativePath, references, message: issues[0].message };
214
+ }
215
+
216
+ function stateFromIssues(issues) {
217
+ if (issues.some((issue) => issue.kind === "missing")) return "missing";
218
+ if (issues.some((issue) => issue.kind === "stale_relative")) return "stale_relative";
219
+ return "wrong_harness_root";
220
+ }
221
+
222
+ export async function classifyWorktreeBootstrap({
223
+ targetPath,
224
+ targetExists = true,
225
+ harnessRoot,
226
+ repoName,
227
+ files,
228
+ worktreeRecord = {},
229
+ fileIsFile = isFileTarget,
230
+ }) {
231
+ const resolvedTargetPath = targetPath ? path.resolve(targetPath) : "";
232
+ if (worktreeRecord.prunable) {
233
+ return { state: "prunable", valid: false, repairable: false, repoName: repoName ?? "unknown", targetPath: resolvedTargetPath, issues: [{ kind: "prunable", message: worktreeRecord.prunableReason ?? "Git reports this worktree as prunable." }] };
234
+ }
235
+ if (!targetExists) {
236
+ return { state: "missing_path", valid: false, repairable: false, repoName: repoName ?? "unknown", targetPath: resolvedTargetPath, issues: [{ kind: "missing_path", message: "Target path does not exist." }] };
237
+ }
238
+ if (!repoName || !canonicalRepos.includes(repoName)) {
239
+ return { state: "unsupported_repo", valid: false, repairable: false, repoName: repoName ?? "unknown", targetPath: resolvedTargetPath, issues: [{ kind: "unsupported_repo", message: "Target is not a canonical repo for this harness." }] };
240
+ }
241
+
242
+ const byPath = new Map(files.map((file) => [file.relativePath, file]));
243
+ const issues = [];
244
+ for (const relativePath of requiredPointerFiles) {
245
+ const file = byPath.get(relativePath);
246
+ if (!file?.exists) {
247
+ issues.push({ kind: "missing", relativePath, required: true, message: `${relativePath} is missing.` });
248
+ continue;
249
+ }
250
+ if (repoName === "{{HARNESS_REPO_NAME}}") continue;
251
+ const issue = await classifyPointerContent({ relativePath, content: file.content, targetPath: resolvedTargetPath, harnessRoot, fileIsFile });
252
+ if (issue) issues.push({ ...issue, required: true });
253
+ }
254
+ for (const relativePath of optionalPointerFiles) {
255
+ const file = byPath.get(relativePath);
256
+ if (!file?.exists || repoName === "{{HARNESS_REPO_NAME}}") continue;
257
+ const issue = await classifyPointerContent({ relativePath, content: file.content, targetPath: resolvedTargetPath, harnessRoot, fileIsFile });
258
+ if (issue) issues.push({ ...issue, required: false });
259
+ }
260
+ if (issues.length === 0) {
261
+ return { state: worktreeRecord.detached ? "detached" : "valid", valid: true, repairable: false, repoName, targetPath: resolvedTargetPath, issues: [] };
262
+ }
263
+ const state = stateFromIssues(issues);
264
+ const requiredIssues = issues.filter((issue) => issue.required);
265
+ const optionalIssues = issues.filter((issue) => !issue.required);
266
+ return { state, valid: false, repairable: requiredIssues.length > 0 && optionalIssues.length === 0 && repairableStates.has(state), repoName, targetPath: resolvedTargetPath, issues };
267
+ }
268
+
269
+ export async function readPointerFiles(targetPath) {
270
+ const files = [];
271
+ for (const relativePath of [...requiredPointerFiles, ...optionalPointerFiles]) {
272
+ const filePath = path.join(targetPath, relativePath);
273
+ if (!(await exists(filePath))) {
274
+ files.push({ relativePath, exists: false, content: "" });
275
+ } else {
276
+ files.push({ relativePath, exists: true, content: await readFile(filePath, "utf8") });
277
+ }
278
+ }
279
+ return files;
280
+ }
281
+
282
+ export async function inspectCheckout({ targetPath, harnessRoot, worktreeRecord = {} }) {
283
+ const resolvedTargetPath = path.resolve(targetPath);
284
+ const targetExists = await exists(resolvedTargetPath);
285
+ const metadata = targetExists && !worktreeRecord.prunable ? await gitMetadataForPath(resolvedTargetPath) : { gitRoot: null, remoteUrl: "" };
286
+ const repoName = canonicalRepoNameFromInput({ targetPath: resolvedTargetPath, gitRoot: metadata.gitRoot, remoteUrl: metadata.remoteUrl });
287
+ const files = targetExists && !worktreeRecord.prunable ? await readPointerFiles(resolvedTargetPath) : [];
288
+ return classifyWorktreeBootstrap({ targetPath: resolvedTargetPath, targetExists, harnessRoot, repoName, files, worktreeRecord });
289
+ }
290
+
291
+ export function renderPointerFile({ relativePath, harnessRoot, repoName }) {
292
+ const normalizedHarnessRoot = path.resolve(harnessRoot);
293
+ const bootstrapCommand = `node ${path.join(normalizedHarnessRoot, "scripts/bootstrap-codex-worktree.mjs")} <checkout-path>`;
294
+ const title = relativePath === "CLAUDE.md" ? "Project Agent Guide" : "Agent Bootstrap";
295
+ const guidance = [
296
+ ...(relativePath === "AGENTS.md" && models.openai ? [path.join(normalizedHarnessRoot, "AGENTS.md")] : []),
297
+ ...(models.anthropic ? [path.join(normalizedHarnessRoot, "CLAUDE.md")] : []),
298
+ path.join(normalizedHarnessRoot, "ai/AGENTS.md"),
299
+ path.join(normalizedHarnessRoot, "ai/HUB.md"),
300
+ path.join(normalizedHarnessRoot, "ai/context.md"),
301
+ ];
302
+ return `# ${title}
303
+
304
+ This checkout is part of the {{PROJECT_NAME}} workspace.
305
+ The canonical AI guidance lives in the harness repo.
306
+
307
+ Canonical repo: \`${repoName}\`
308
+
309
+ Read before changing files:
310
+
311
+ ${guidance.map((entry, index) => `${index + 1}. \`${entry}\``).join("\n")}
312
+
313
+ Regenerate this pointer with:
314
+
315
+ \`\`\`bash
316
+ ${bootstrapCommand}
317
+ \`\`\`
318
+ `;
319
+ }
320
+
321
+ export function buildRepairPlan({ inspection, harnessRoot }) {
322
+ if (!inspection.repairable) {
323
+ return { repairable: false, writes: [], reason: inspection.issues.map((issue) => issue.message).join(" ") };
324
+ }
325
+ const affectedRequiredPaths = new Set(inspection.issues.filter((issue) => issue.required).map((issue) => issue.relativePath));
326
+ const writes = [...affectedRequiredPaths].map((relativePath) => ({
327
+ relativePath,
328
+ targetPath: path.join(inspection.targetPath, relativePath),
329
+ content: renderPointerFile({ relativePath, harnessRoot, repoName: inspection.repoName }),
330
+ }));
331
+ return { repairable: true, writes, reason: "" };
332
+ }
333
+
334
+ export async function writeRepairPlan(repairPlan) {
335
+ for (const write of repairPlan.writes) {
336
+ await mkdir(path.dirname(write.targetPath), { recursive: true });
337
+ await writeFile(write.targetPath, write.content);
338
+ }
339
+ }
340
+
341
+ export function shellQuote(value) {
342
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
343
+ }
344
+
345
+ export function repairCommand({ harnessRoot, targetPath }) {
346
+ return `node ${shellQuote(path.join(harnessRoot, "scripts/bootstrap-codex-worktree.mjs"))} ${shellQuote(targetPath)}`;
347
+ }
348
+
349
+ export function parseWorktreeListPorcelain(output) {
350
+ const records = [];
351
+ let current = null;
352
+ function pushCurrent() {
353
+ if (current) records.push(current);
354
+ current = null;
355
+ }
356
+ for (const line of output.split(/\r?\n/)) {
357
+ if (line.trim() === "") {
358
+ pushCurrent();
359
+ } else if (line.startsWith("worktree ")) {
360
+ pushCurrent();
361
+ current = { path: line.slice("worktree ".length), detached: false, prunable: false };
362
+ } else if (current && line.startsWith("HEAD ")) {
363
+ current.head = line.slice("HEAD ".length);
364
+ } else if (current && line.startsWith("branch ")) {
365
+ current.branch = line.slice("branch ".length);
366
+ } else if (current && line === "detached") {
367
+ current.detached = true;
368
+ } else if (current && line.startsWith("prunable")) {
369
+ current.prunable = true;
370
+ current.prunableReason = line.slice("prunable".length).trim();
371
+ }
372
+ }
373
+ pushCurrent();
374
+ return records;
375
+ }
376
+
377
+ export async function listGitWorktrees(repoPath) {
378
+ const { stdout } = await execFileAsync("git", ["-C", repoPath, "worktree", "list", "--porcelain"]);
379
+ return parseWorktreeListPorcelain(stdout);
380
+ }
381
+
382
+ export function formatInspection(inspection, { harnessRoot } = {}) {
383
+ const suffix = inspection.valid ? "" : ` repair=${inspection.repairable ? "available" : "manual"}`;
384
+ const lines = [`${inspection.state} ${inspection.repoName} ${inspection.targetPath}${suffix}`];
385
+ for (const issue of inspection.issues) lines.push(` - ${issue.message}`);
386
+ if (!inspection.valid && inspection.repairable && harnessRoot) lines.push(` - repair: ${repairCommand({ harnessRoot, targetPath: inspection.targetPath })}`);
387
+ return lines.join("\n");
388
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { access, constants as fsConstants } from "node:fs/promises";
4
+ import { execFileSync } from "node:child_process";
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
+ };
16
+ const mandatoryChecks = [
17
+ "scripts/check-readiness.mjs",
18
+ "scripts/check-issue-template.mjs",
19
+ "scripts/check-knowledge-manifest.mjs",
20
+ "scripts/check-plans.mjs",
21
+ "scripts/check-review-skills.mjs",
22
+ "scripts/check-garbage-collection.mjs",
23
+ "scripts/check-contract-manifests.mjs",
24
+ "scripts/check-html-views.mjs",
25
+ "scripts/check-worktree-bootstrap-fixtures.mjs",
26
+ ];
27
+ const optionalChecks = [
28
+ "scripts/check-repo-name-consistency.mjs",
29
+ "scripts/check-linear-contract.mjs",
30
+ "scripts/check-contract-conformance.mjs",
31
+ "scripts/check-domain-contract-matrix.mjs",
32
+ ];
33
+ const checkCodexHooksScript = "scripts/check-codex-hooks.mjs";
34
+ const checkClaudeCompatibilityScript = "scripts/check-claude-compatibility.mjs";
35
+ const checkOverlayDriftScript = "scripts/check-overlay-drift.mjs";
36
+
37
+ async function exists(relativePath) {
38
+ try {
39
+ await access(path.join(repoRoot, relativePath), fsConstants.F_OK);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ async function runCheck(relativePath) {
47
+ execFileSync(process.execPath, [path.join(repoRoot, relativePath)], {
48
+ cwd: repoRoot,
49
+ stdio: "inherit",
50
+ });
51
+ }
52
+
53
+ await runCheck("scripts/check-template-governance.mjs");
54
+ await runCheck("scripts/check-task-template.mjs");
55
+
56
+ for (const check of mandatoryChecks) {
57
+ await runCheck(check);
58
+ }
59
+
60
+ for (const optionalCheck of optionalChecks) {
61
+ if (await exists(optionalCheck)) {
62
+ await runCheck(optionalCheck);
63
+ }
64
+ }
65
+
66
+ if (clientSupport.codexHooks) {
67
+ await runCheck(checkCodexHooksScript);
68
+ }
69
+
70
+ if (models.anthropic) {
71
+ await runCheck(checkClaudeCompatibilityScript);
72
+ }
73
+
74
+ if (models.openai || models.anthropic) {
75
+ await runCheck(checkOverlayDriftScript);
76
+ }
77
+
78
+ console.log("Governance validation passed.");
@@ -0,0 +1,9 @@
1
+ # {{PROJECT_NAME}} Workspace Claude Memory
2
+
3
+ Use this with the workspace root `../CLAUDE.md`.
4
+
5
+ @../CLAUDE.md
6
+
7
+ The generated harness is the policy source of truth. Consumer repos and
8
+ worktrees should route through the workspace-level guide before local details.
9
+
@@ -0,0 +1,15 @@
1
+ ---
2
+ paths:
3
+ - "AGENTS.md"
4
+ - "CLAUDE.md"
5
+ - ".claude/**"
6
+ - "{{HARNESS_REPO_NAME}}/**"
7
+ ---
8
+
9
+ # Workspace Client Surface Rules
10
+
11
+ - Treat `{{HARNESS_REPO_NAME}}/ai/*` as the canonical harness guidance.
12
+ - Keep workspace-level `AGENTS.md`, `CLAUDE.md`, and `.claude/**` as thin
13
+ pointers to the generated harness.
14
+ - Do not copy full harness policy into the workspace root.
15
+ - Review existing workspace files before using bootstrap `--force`.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "deny": [
4
+ "Read(./.agent.env)",
5
+ "Read(./.env)",
6
+ "Read(./.env.*)",
7
+ "Read(./secrets/**)"
8
+ ]
9
+ }
10
+ }
@@ -0,0 +1,17 @@
1
+ # {{PROJECT_NAME}} Workspace Agent Guide
2
+
3
+ This workspace is governed by the {{PROJECT_NAME}} engineering harness.
4
+
5
+ Read the harness first:
6
+
7
+ 1. `./{{HARNESS_REPO_NAME}}/AGENTS.md`
8
+ 2. `./{{HARNESS_REPO_NAME}}/ai/AGENTS.md`
9
+ 3. `./{{HARNESS_REPO_NAME}}/ai/HUB.md`
10
+ 4. `./{{HARNESS_REPO_NAME}}/ai/context.md`
11
+
12
+ Consumer repositories:
13
+
14
+ {{CONSUMER_REPOS_LIST}}
15
+
16
+ Keep this file short. Canonical policy belongs in the harness repo.
17
+
@@ -0,0 +1,18 @@
1
+ # {{PROJECT_NAME}} Workspace Claude Guide
2
+
3
+ This workspace is governed by the {{PROJECT_NAME}} engineering harness.
4
+
5
+ @./{{HARNESS_REPO_NAME}}/ai/context.md
6
+
7
+ Read the harness first:
8
+
9
+ 1. `./{{HARNESS_REPO_NAME}}/CLAUDE.md`
10
+ 2. `./{{HARNESS_REPO_NAME}}/ai/AGENTS.md`
11
+ 3. `./{{HARNESS_REPO_NAME}}/ai/HUB.md`
12
+ 4. `./{{HARNESS_REPO_NAME}}/ai/context.md`
13
+
14
+ Consumer repositories:
15
+
16
+ {{CONSUMER_REPOS_LIST}}
17
+
18
+ Keep this file short. Canonical policy belongs in the harness repo.