@structor-dev/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) 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 +553 -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-public-hygiene.mjs +249 -0
  30. package/scripts/check-schemas.mjs +42 -0
  31. package/scripts/check-template-files.mjs +15 -98
  32. package/scripts/generated-harness-contract.mjs +416 -0
  33. package/scripts/init-harness.mjs +227 -139
  34. package/scripts/lib.mjs +462 -12
  35. package/scripts/rendered-config.mjs +109 -0
  36. package/scripts/setup-contributor.mjs +125 -0
  37. package/scripts/smoke-template.mjs +260 -73
  38. package/template/AGENTS.md.tpl +4 -2
  39. package/template/README.md.tpl +5 -0
  40. package/template/ai/CODEX-HOOKS.md.tpl +1 -1
  41. package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
  42. package/template/ai/HARNESS.md.tpl +4 -1
  43. package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
  44. package/template/ai/contracts/codex-hooks.md.tpl +6 -0
  45. package/template/ai/contracts/release-flow.md.tpl +1 -1
  46. package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
  47. package/template/ai/templates/issue-template.md.tpl +3 -1
  48. package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
  49. package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
  50. package/template/consumer/AGENTS.md.tpl +4 -4
  51. package/template/consumer/CLAUDE.md.tpl +4 -4
  52. package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
  53. package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
  54. package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
  55. package/template/scripts/check-template-governance.mjs.tpl +2 -114
  56. package/template/scripts/check-workspace.mjs.tpl +27 -103
  57. package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
  58. package/template/scripts/generate-html-views.mjs.tpl +357 -56
  59. package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
  60. package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
  61. package/template/scripts/lib/path-safety.mjs.tpl +87 -0
  62. package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
  63. package/template/scripts/validate-governance.mjs.tpl +52 -36
  64. package/schemas/task-brief.schema.json +0 -37
@@ -1,11 +1,14 @@
1
- import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { constants as fsConstants } from "node:fs";
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
2
  import { execFile } from "node:child_process";
4
3
  import path from "node:path";
5
4
  import { promisify } from "node:util";
5
+ import { assertSafeWriteTarget, exists } from "./path-safety.mjs";
6
+
7
+ export { exists };
6
8
 
7
9
  const execFileAsync = promisify(execFile);
8
10
 
11
+ const projectName = {{PROJECT_NAME_JSON}};
9
12
  export const canonicalRepos = ["{{HARNESS_REPO_NAME}}", ...{{CONSUMER_REPO_NAMES_JSON}}];
10
13
  export const models = {
11
14
  openai: {{MODEL_OPENAI_ENABLED}},
@@ -19,15 +22,6 @@ export const optionalPointerFiles = [];
19
22
 
20
23
  const repairableStates = new Set(["missing", "stale_relative", "wrong_harness_root"]);
21
24
 
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
25
  export function repoNameFromRemote(remoteUrl) {
32
26
  if (!remoteUrl) return null;
33
27
  const withoutGitSuffix = remoteUrl.trim().replace(/\.git$/, "");
@@ -290,7 +284,7 @@ export async function inspectCheckout({ targetPath, harnessRoot, worktreeRecord
290
284
 
291
285
  export function renderPointerFile({ relativePath, harnessRoot, repoName }) {
292
286
  const normalizedHarnessRoot = path.resolve(harnessRoot);
293
- const bootstrapCommand = `node ${path.join(normalizedHarnessRoot, "scripts/bootstrap-codex-worktree.mjs")} <checkout-path>`;
287
+ const bootstrapCommand = repairCommand({ harnessRoot: normalizedHarnessRoot, targetPath: "<checkout-path>" });
294
288
  const title = relativePath === "CLAUDE.md" ? "Project Agent Guide" : "Agent Bootstrap";
295
289
  const guidance = [
296
290
  ...(relativePath === "AGENTS.md" && models.openai ? [path.join(normalizedHarnessRoot, "AGENTS.md")] : []),
@@ -301,7 +295,7 @@ export function renderPointerFile({ relativePath, harnessRoot, repoName }) {
301
295
  ];
302
296
  return `# ${title}
303
297
 
304
- This checkout is part of the {{PROJECT_NAME}} workspace.
298
+ This checkout is part of the ${projectName} workspace.
305
299
  The canonical AI guidance lives in the harness repo.
306
300
 
307
301
  Canonical repo: \`${repoName}\`
@@ -325,6 +319,7 @@ export function buildRepairPlan({ inspection, harnessRoot }) {
325
319
  const affectedRequiredPaths = new Set(inspection.issues.filter((issue) => issue.required).map((issue) => issue.relativePath));
326
320
  const writes = [...affectedRequiredPaths].map((relativePath) => ({
327
321
  relativePath,
322
+ rootPath: inspection.targetPath,
328
323
  targetPath: path.join(inspection.targetPath, relativePath),
329
324
  content: renderPointerFile({ relativePath, harnessRoot, repoName: inspection.repoName }),
330
325
  }));
@@ -333,6 +328,14 @@ export function buildRepairPlan({ inspection, harnessRoot }) {
333
328
 
334
329
  export async function writeRepairPlan(repairPlan) {
335
330
  for (const write of repairPlan.writes) {
331
+ if (!write.rootPath) {
332
+ throw new Error(`Worktree pointer ${write.relativePath} is unsafe: missing write root.`);
333
+ }
334
+ await assertSafeWriteTarget({
335
+ targetPath: write.targetPath,
336
+ rootPath: write.rootPath,
337
+ label: `Worktree pointer ${write.relativePath}`,
338
+ });
336
339
  await mkdir(path.dirname(write.targetPath), { recursive: true });
337
340
  await writeFile(write.targetPath, write.content);
338
341
  }
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { access, constants as fsConstants } from "node:fs/promises";
3
+ import { access, constants as fsConstants, readFile } from "node:fs/promises";
4
4
  import { execFileSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
5
6
  import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
8
 
8
9
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
10
  const models = {
@@ -13,26 +14,12 @@ const models = {
13
14
  const clientSupport = {
14
15
  codexHooks: {{CLIENT_CODEX_HOOKS_ENABLED}},
15
16
  };
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";
17
+ const generatedContractScript = "scripts/generated-harness-contract.mjs";
18
+ const generatedScriptHashes = {{GENERATED_SCRIPT_HASHES_JSON}};
19
+
20
+ function sha256(content) {
21
+ return createHash("sha256").update(content).digest("hex");
22
+ }
36
23
 
37
24
  async function exists(relativePath) {
38
25
  try {
@@ -43,36 +30,65 @@ async function exists(relativePath) {
43
30
  }
44
31
  }
45
32
 
33
+ async function assertTrustedCheck(relativePath) {
34
+ const expectedHash = generatedScriptHashes[relativePath];
35
+ if (!expectedHash) {
36
+ throw new Error(
37
+ `Refusing to execute ${relativePath}: no trusted generated hash is recorded. ` +
38
+ "Inspect the file and regenerate with --force after review if it should be replaced.",
39
+ );
40
+ }
41
+
42
+ let content;
43
+ try {
44
+ content = await readFile(path.join(repoRoot, relativePath));
45
+ } catch (error) {
46
+ if (error?.code === "ENOENT") {
47
+ throw new Error(
48
+ `Refusing to execute ${relativePath}: the expected generated script is missing. ` +
49
+ "Regenerate the harness after reviewing the output directory.",
50
+ );
51
+ }
52
+ throw error;
53
+ }
54
+
55
+ const actualHash = sha256(content);
56
+ if (actualHash !== expectedHash) {
57
+ throw new Error(
58
+ `Refusing to execute ${relativePath}: content does not match the current generated template. ` +
59
+ "Inspect the preserved file and regenerate with --force after review if it should be replaced.",
60
+ );
61
+ }
62
+ }
63
+
46
64
  async function runCheck(relativePath) {
65
+ await assertTrustedCheck(relativePath);
66
+ for (const dependency of validationPlan.checkDependencies[relativePath] ?? []) {
67
+ await assertTrustedCheck(dependency);
68
+ }
69
+
47
70
  execFileSync(process.execPath, [path.join(repoRoot, relativePath)], {
48
71
  cwd: repoRoot,
49
72
  stdio: "inherit",
50
73
  });
51
74
  }
52
75
 
53
- await runCheck("scripts/check-template-governance.mjs");
54
- await runCheck("scripts/check-task-template.mjs");
76
+ await assertTrustedCheck(generatedContractScript);
77
+ const { validationPlanForSettings } = await import(pathToFileURL(path.join(repoRoot, generatedContractScript)).href);
78
+ const validationPlan = validationPlanForSettings({ models, clientSupport });
55
79
 
56
- for (const check of mandatoryChecks) {
80
+ for (const check of validationPlan.requiredChecks) {
57
81
  await runCheck(check);
58
82
  }
59
83
 
60
- for (const optionalCheck of optionalChecks) {
84
+ for (const optionalCheck of validationPlan.optionalChecks) {
61
85
  if (await exists(optionalCheck)) {
62
86
  await runCheck(optionalCheck);
63
87
  }
64
88
  }
65
89
 
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);
90
+ for (const check of validationPlan.conditionalChecks) {
91
+ await runCheck(check);
76
92
  }
77
93
 
78
94
  console.log("Governance validation passed.");
@@ -1,37 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://example.com/schemas/task-brief.schema.json",
4
- "title": "Task Brief",
5
- "type": "object",
6
- "additionalProperties": false,
7
- "required": [
8
- "id",
9
- "status",
10
- "risk",
11
- "autonomy",
12
- "model_policy",
13
- "model",
14
- "repos",
15
- "allowed_paths",
16
- "forbidden_paths",
17
- "requires_human_approval"
18
- ],
19
- "properties": {
20
- "id": { "type": "string", "minLength": 1 },
21
- "status": {
22
- "type": "string",
23
- "enum": ["backlog", "ready", "running", "needs_fix", "report_ready", "pr_ready", "blocked", "done"]
24
- },
25
- "risk": { "type": "string", "enum": ["low", "medium", "high"] },
26
- "autonomy": { "type": "string", "enum": ["report_only", "pr_ready", "auto_merge"] },
27
- "model_policy": {
28
- "type": "string",
29
- "enum": ["cheap", "standard", "reasoning", "frontier", "review_only"]
30
- },
31
- "model": { "type": "string", "minLength": 1 },
32
- "repos": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
33
- "allowed_paths": { "type": "array", "items": { "type": "string" } },
34
- "forbidden_paths": { "type": "array", "items": { "type": "string" } },
35
- "requires_human_approval": { "type": "boolean" }
36
- }
37
- }