@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,39 @@
1
+ # Add a Structor doctor command
2
+
3
+ ## Summary
4
+
5
+ Add a future `structor doctor` command that diagnoses and optionally repairs a
6
+ previously generated Structor workspace after installation drift, stale consumer
7
+ entrypoints, moved folders, or incomplete setup.
8
+
9
+ ## Motivation
10
+
11
+ The setup wizard should make first-time installation easy, but users may later
12
+ move folders, edit pointer files, delete generated files, or partially complete
13
+ manual setup. A doctor flow would make Structor easier to adopt by providing a
14
+ clear recovery path instead of forcing users to understand every generated file.
15
+
16
+ ## Scope
17
+
18
+ - Inspect `harness.config.json`, generated harness files, workspace pointers,
19
+ and configured consumer entrypoints.
20
+ - Report healthy, missing, stale, and unsafe surfaces.
21
+ - Offer preview-first repairs for local files that Structor owns.
22
+ - Reuse existing validation commands where possible.
23
+ - Avoid runner behavior, polling, remote services, or automatic repository
24
+ mutation without confirmation.
25
+
26
+ ## Non-Goals
27
+
28
+ - No external service repair.
29
+ - No GitHub, CI, deployment, database, or production mutations.
30
+ - No automatic rewrite of hand-written consumer instructions without explicit
31
+ confirmation.
32
+ - No replacement for the setup wizard or deterministic initializer.
33
+
34
+ ## Open Questions
35
+
36
+ - Should repair be a separate `structor doctor --repair` mode or an interactive
37
+ prompt after diagnosis?
38
+ - Which files should be considered Structor-owned versus user-owned?
39
+ - Should doctor be available in the wizard MVP or shipped as a follow-up issue?
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "../../schemas/harness-config.schema.json",
3
+ "project": {
4
+ "name": "Frontend Backend Product",
5
+ "slug": "frontend-backend-product",
6
+ "harnessRepoName": "frontend-backend-product-structor"
7
+ },
8
+ "output": {
9
+ "path": "../../generated/frontend-backend-product-structor"
10
+ },
11
+ "models": {
12
+ "openai": true,
13
+ "anthropic": true
14
+ },
15
+ "consumers": [
16
+ {
17
+ "name": "product-frontend",
18
+ "path": "../../../product-frontend",
19
+ "purpose": "Frontend application repository",
20
+ "validation": {
21
+ "lint": "npm run lint",
22
+ "build": "npm run build"
23
+ }
24
+ },
25
+ {
26
+ "name": "product-backend",
27
+ "path": "../../../product-backend",
28
+ "purpose": "Backend API repository",
29
+ "validation": {
30
+ "lint": "npm run lint",
31
+ "test": "npm test"
32
+ }
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../schemas/harness-config.schema.json",
3
+ "project": {
4
+ "name": "Multi Model Product",
5
+ "slug": "multi-model-product",
6
+ "harnessRepoName": "multi-model-product-structor"
7
+ },
8
+ "output": {
9
+ "path": "../../generated/multi-model-product-structor"
10
+ },
11
+ "models": {
12
+ "openai": true,
13
+ "anthropic": true
14
+ },
15
+ "consumers": [
16
+ {
17
+ "name": "multi-model-app",
18
+ "path": "../../../multi-model-app",
19
+ "purpose": "Application repository used by both OpenAI and Anthropic agents",
20
+ "validation": {
21
+ "install": "npm install",
22
+ "lint": "npm run lint",
23
+ "test": "npm test",
24
+ "build": "npm run build"
25
+ }
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "../../schemas/harness-config.schema.json",
3
+ "project": {
4
+ "name": "Single Repo Product",
5
+ "slug": "single-repo-product",
6
+ "harnessRepoName": "single-repo-product-structor"
7
+ },
8
+ "output": {
9
+ "path": "../../generated/single-repo-product-structor"
10
+ },
11
+ "models": {
12
+ "openai": true,
13
+ "anthropic": false
14
+ },
15
+ "consumers": [
16
+ {
17
+ "name": "single-repo-app",
18
+ "path": "../../../single-repo-app",
19
+ "purpose": "Application repository",
20
+ "validation": {
21
+ "lint": "npm run lint",
22
+ "test": "npm test"
23
+ }
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "./schemas/harness-config.schema.json",
3
+ "project": {
4
+ "name": "Example Project",
5
+ "slug": "example-project",
6
+ "harnessRepoName": "example-structor"
7
+ },
8
+ "output": {
9
+ "path": "../example-structor"
10
+ },
11
+ "models": {
12
+ "openai": true,
13
+ "anthropic": true
14
+ },
15
+ "clientSupport": {
16
+ "codex": {
17
+ "hooks": true
18
+ },
19
+ "claude": {
20
+ "rules": true,
21
+ "hooks": false,
22
+ "skills": false
23
+ }
24
+ },
25
+ "consumers": [
26
+ {
27
+ "name": "example-app",
28
+ "path": "../example-app",
29
+ "purpose": "Primary application repository",
30
+ "validation": {
31
+ "install": "npm install",
32
+ "lint": "npm run lint",
33
+ "test": "npm test",
34
+ "build": "npm run build"
35
+ }
36
+ }
37
+ ]
38
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@structor-dev/cli",
3
+ "version": "0.1.0",
4
+ "description": "Harness-engineering toolkit that generates repository-local AI engineering harnesses for consumer repos.",
5
+ "license": "MIT",
6
+ "author": "Nicolay Camacho",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wari1986/structor.git"
10
+ },
11
+ "keywords": [
12
+ "ai",
13
+ "agents",
14
+ "codex",
15
+ "claude-code",
16
+ "harness",
17
+ "governance",
18
+ "scaffolding"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "structor": "bin/structor.mjs"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "docs/",
30
+ "examples/",
31
+ "harness.config.example.json",
32
+ "schemas/",
33
+ "scripts/",
34
+ "template/",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "test": "node --test",
40
+ "prepublishOnly": "npm run validate",
41
+ "check:ci": "node scripts/check-config.mjs && node scripts/check-template-files.mjs && node scripts/check-task-template.mjs && node scripts/check-contract-manifests.mjs && node scripts/check-model-overlays.mjs && node scripts/check-placeholders.mjs",
42
+ "check:config": "node scripts/check-config.mjs",
43
+ "check:placeholders": "node scripts/check-placeholders.mjs",
44
+ "check:templates": "node scripts/check-template-files.mjs",
45
+ "check:task-template": "node scripts/check-task-template.mjs",
46
+ "check:contracts": "node scripts/check-contract-manifests.mjs",
47
+ "check:model-overlays": "node scripts/check-model-overlays.mjs",
48
+ "check:smoke": "node scripts/smoke-template.mjs",
49
+ "validate": "npm run check:ci && npm run test && npm run check:smoke",
50
+ "validate:governance": "node scripts/validate-governance.mjs",
51
+ "init": "node bin/structor.mjs init",
52
+ "generate": "node bin/structor.mjs generate",
53
+ "init:harness": "node scripts/init-harness.mjs"
54
+ },
55
+ "engines": {
56
+ "node": ">=20"
57
+ }
58
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://example.com/schemas/contract-manifest.schema.json",
4
+ "title": "Contract Manifest",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["id", "name", "version", "owners", "affectedRepos", "requiredFiles"],
8
+ "properties": {
9
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
10
+ "name": { "type": "string", "minLength": 1 },
11
+ "version": { "type": "string", "minLength": 1 },
12
+ "owners": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
13
+ "affectedRepos": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
14
+ "requiredFiles": { "type": "array", "items": { "type": "string" } },
15
+ "forbiddenTokens": { "type": "array", "items": { "type": "string" } },
16
+ "validation": { "type": "array", "items": { "type": "string" } }
17
+ }
18
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://example.com/schemas/harness-config.schema.json",
4
+ "title": "Harness Config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["project", "output", "models", "consumers"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "project": {
11
+ "type": "object",
12
+ "additionalProperties": false,
13
+ "required": ["name", "slug", "harnessRepoName"],
14
+ "properties": {
15
+ "name": { "type": "string", "minLength": 1 },
16
+ "slug": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
17
+ "harnessRepoName": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" }
18
+ }
19
+ },
20
+ "output": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": ["path"],
24
+ "properties": {
25
+ "path": { "type": "string", "minLength": 1 }
26
+ }
27
+ },
28
+ "models": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["openai", "anthropic"],
32
+ "properties": {
33
+ "openai": { "type": "boolean" },
34
+ "anthropic": { "type": "boolean" }
35
+ }
36
+ },
37
+ "clientSupport": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "codex": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "hooks": { "type": "boolean" }
46
+ }
47
+ },
48
+ "claude": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "rules": { "type": "boolean" },
53
+ "hooks": { "const": false },
54
+ "skills": { "const": false }
55
+ }
56
+ }
57
+ }
58
+ },
59
+ "consumers": {
60
+ "type": "array",
61
+ "minItems": 1,
62
+ "items": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "required": ["name", "path", "purpose", "validation"],
66
+ "properties": {
67
+ "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
68
+ "path": { "type": "string", "minLength": 1 },
69
+ "purpose": { "type": "string", "minLength": 1 },
70
+ "validation": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "properties": {
74
+ "install": { "type": "string" },
75
+ "lint": { "type": "string" },
76
+ "test": { "type": "string" },
77
+ "build": { "type": "string" },
78
+ "health": { "type": "string" }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import {
6
+ assertSafeOutputRoot,
7
+ collectFiles,
8
+ exists,
9
+ failIfErrors,
10
+ readJson,
11
+ repoRoot,
12
+ validateConfigShape,
13
+ } from "./lib.mjs";
14
+
15
+ const errors = [];
16
+ const args = process.argv.slice(2);
17
+ const configArgIndex = args.indexOf("--config");
18
+ const requireExistingConsumers = args.includes("--require-existing-consumers");
19
+ const allowAbsoluteOutput = args.includes("--allow-absolute-output");
20
+ const checkingExamples = configArgIndex === -1;
21
+ const configFiles = checkingExamples
22
+ ? ["harness.config.example.json", ...(await collectFiles("examples", (file) => file.endsWith("harness.config.json")))]
23
+ : [path.resolve(args[configArgIndex + 1])];
24
+
25
+ for (const configPath of configFiles) {
26
+ const label = checkingExamples ? configPath : path.relative(process.cwd(), configPath);
27
+ const config = checkingExamples
28
+ ? await readJson(configPath)
29
+ : JSON.parse(await readFile(configPath, "utf8"));
30
+ errors.push(...(await validateConfigShape(config, label)));
31
+
32
+ if (checkingExamples && path.isAbsolute(config.output?.path ?? "")) {
33
+ errors.push(`${label}: output.path must be relative for examples.`);
34
+ }
35
+ if (!checkingExamples) {
36
+ const configDir = path.dirname(configPath);
37
+ const outputPath = config.output.path;
38
+ const outputRoot = path.resolve(configDir, outputPath);
39
+ const consumerRepos = Array.isArray(config.consumers)
40
+ ? config.consumers.map((consumer) => path.resolve(configDir, consumer.path))
41
+ : [];
42
+ try {
43
+ assertSafeOutputRoot({
44
+ outputPath,
45
+ outputRoot,
46
+ repoRoot,
47
+ workspaceRoot: configDir,
48
+ consumerRepos,
49
+ allowAbsoluteOutput,
50
+ });
51
+ } catch (error) {
52
+ errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
53
+ }
54
+ }
55
+
56
+ if (Array.isArray(config.consumers)) {
57
+ for (const consumer of config.consumers) {
58
+ if (checkingExamples && path.isAbsolute(consumer.path)) {
59
+ errors.push(`${label}: consumer path for ${consumer.name} must be relative in checked-in examples.`);
60
+ }
61
+ if (requireExistingConsumers) {
62
+ const configDir = checkingExamples ? path.dirname(path.join(repoRoot, configPath)) : path.dirname(configPath);
63
+ const consumerPath = path.resolve(configDir, consumer.path);
64
+ if (!(await exists(consumerPath))) {
65
+ errors.push(`${label}: consumer path for ${consumer.name} does not exist: ${consumerPath}`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ if (repoRoot.endsWith("structor") === false) {
73
+ errors.push("repository folder should be named structor.");
74
+ }
75
+
76
+ failIfErrors("Config check", errors);
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readdir, readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { failIfErrors, repoRoot } from "./lib.mjs";
6
+
7
+ const contractsDirectory = "template/ai/contracts";
8
+ const contractsReadmePath = `${contractsDirectory}/README.md.tpl`;
9
+ const contractTemplateSuffix = ".contract.json.tpl";
10
+ const docTemplateSuffix = ".md.tpl";
11
+ const requiredFields = ["id", "name", "version", "owners", "affectedRepos", "requiredFiles"];
12
+ const semverPattern = /^\d+\.\d+\.\d+$/;
13
+ const placeholderStart = "{".repeat(2);
14
+ const harnessRepoPlaceholder = `${placeholderStart}HARNESS_REPO_NAME}}`;
15
+ const consumerReposPlaceholder = `${placeholderStart}CONSUMER_REPO_NAMES_JSON}}`;
16
+
17
+ function renderTemplate(content) {
18
+ return content
19
+ .replaceAll(harnessRepoPlaceholder, "template-harness")
20
+ .replaceAll(consumerReposPlaceholder, JSON.stringify(["consumer-app"]));
21
+ }
22
+
23
+ function validateManifest(manifest, label, errors) {
24
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
25
+ errors.push(`${label} must be a JSON object.`);
26
+ return;
27
+ }
28
+ for (const field of requiredFields) {
29
+ if (!Object.hasOwn(manifest, field)) {
30
+ errors.push(`${label} is missing '${field}'.`);
31
+ }
32
+ }
33
+ if (typeof manifest.version === "string" && !semverPattern.test(manifest.version)) {
34
+ errors.push(`${label}.version must use semver-like x.y.z.`);
35
+ }
36
+ for (const field of ["owners", "affectedRepos", "requiredFiles"]) {
37
+ if (!Array.isArray(manifest[field]) || manifest[field].length === 0) {
38
+ errors.push(`${label}.${field} must be a non-empty array.`);
39
+ }
40
+ }
41
+ }
42
+
43
+ const errors = [];
44
+ const entries = await readdir(path.join(repoRoot, contractsDirectory), { withFileTypes: true });
45
+ const readme = await readFile(path.join(repoRoot, contractsReadmePath), "utf8");
46
+
47
+ const docFiles = entries
48
+ .filter((entry) => entry.isFile() && entry.name.endsWith(docTemplateSuffix) && entry.name !== "README.md.tpl")
49
+ .map((entry) => entry.name);
50
+ for (const docName of docFiles) {
51
+ const linkedName = docName.replace(docTemplateSuffix, ".md");
52
+ if (!readme.includes(linkedName)) {
53
+ errors.push(`${contractsDirectory}/${docName} is not linked from ${contractsReadmePath}.`);
54
+ }
55
+ }
56
+
57
+ const contractFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(contractTemplateSuffix));
58
+ for (const entry of contractFiles) {
59
+ const relativePath = `${contractsDirectory}/${entry.name}`;
60
+ const content = await readFile(path.join(repoRoot, relativePath), "utf8");
61
+ if (content.includes(placeholderStart)) {
62
+ const rendered = renderTemplate(content);
63
+ try {
64
+ const manifest = JSON.parse(rendered);
65
+ validateManifest(manifest, relativePath, errors);
66
+ } catch (error) {
67
+ errors.push(`${relativePath} must render to valid JSON before generation.`);
68
+ continue;
69
+ }
70
+ } else {
71
+ try {
72
+ const manifest = JSON.parse(content);
73
+ validateManifest(manifest, relativePath, errors);
74
+ } catch {
75
+ errors.push(`${relativePath} must be valid JSON.`);
76
+ }
77
+ }
78
+
79
+ const docPath = `${relativePath.replace(contractTemplateSuffix, docTemplateSuffix)}`;
80
+ if (!(await readFile(path.join(repoRoot, docPath), "utf8").catch(() => null))) {
81
+ errors.push(`${relativePath} must have a sibling ${docPath}.`);
82
+ }
83
+ }
84
+
85
+ failIfErrors("Contract manifest check", errors);
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { failIfErrors, repoRoot } from "./lib.mjs";
6
+
7
+ const overlayFiles = [
8
+ "template/ai/model-overlays/openai/AGENTS.md.tpl",
9
+ "template/ai/model-overlays/anthropic/CLAUDE.md.tpl",
10
+ "template/consumer/AGENTS.md.tpl",
11
+ "template/consumer/CLAUDE.md.tpl",
12
+ "template/consumer/.claude/CLAUDE.md.tpl",
13
+ "template/.claude/CLAUDE.md.tpl",
14
+ ];
15
+ const errors = [];
16
+
17
+ for (const relativePath of overlayFiles) {
18
+ const content = await readFile(path.join(repoRoot, relativePath), "utf8");
19
+ if (!/canonical policy belongs|Canonical policy lives|Read the harness first|Read:/i.test(content)) {
20
+ errors.push(`${relativePath} must route to canonical policy.`);
21
+ }
22
+ if (!/thin|short|pointer/i.test(content)) {
23
+ errors.push(`${relativePath} must state that it stays thin or short.`);
24
+ }
25
+ if (content.length > 1200) {
26
+ errors.push(`${relativePath} is too large for a thin pointer file.`);
27
+ }
28
+ }
29
+
30
+ failIfErrors("Model overlay check", errors);
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { collectFiles, failIfErrors, repoRoot } from "./lib.mjs";
6
+
7
+ const errors = [];
8
+ const activeFiles = await collectFiles(".", (file) => {
9
+ if (file.startsWith(".git/")) return false;
10
+ if (file.startsWith("template/")) return false;
11
+ return [".md", ".json", ".mjs"].some((suffix) => file.endsWith(suffix));
12
+ });
13
+
14
+ // Structor ships with no hardcoded project terms. Consumers can opt into this
15
+ // leak check with a comma-separated HARNESS_FORBIDDEN_PROJECT_TERMS value.
16
+ const forbiddenProjectTermsEnvVar = "HARNESS_FORBIDDEN_PROJECT_TERMS";
17
+ const termListSeparator = ",";
18
+
19
+ const configuredForbiddenProjectTerms = (process.env[forbiddenProjectTermsEnvVar] ?? "")
20
+ .split(termListSeparator)
21
+ .map((term) => term.trim())
22
+ .filter(Boolean);
23
+
24
+ const forbiddenProjectTerms = [...new Set(configuredForbiddenProjectTerms)]
25
+ .map((term) => new RegExp(`\\b${escapeRegExp(term)}\\b`, "i"));
26
+
27
+ function escapeRegExp(value) {
28
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29
+ }
30
+
31
+ for (const relativePath of activeFiles) {
32
+ const content = await readFile(path.join(repoRoot, relativePath), "utf8");
33
+ if (/\{\{[A-Z0-9_]+\}\}/.test(content)) {
34
+ errors.push(`${relativePath} contains an unresolved template placeholder.`);
35
+ }
36
+ if (relativePath !== "scripts/check-placeholders.mjs" && /TODO|TBD|fixme/i.test(content)) {
37
+ errors.push(`${relativePath} contains TODO/TBD/fixme placeholder text.`);
38
+ }
39
+ if (relativePath === "scripts/check-placeholders.mjs") continue;
40
+ for (const pattern of forbiddenProjectTerms) {
41
+ if (pattern.test(content)) {
42
+ errors.push(`${relativePath} contains a configured forbidden project term.`);
43
+ break;
44
+ }
45
+ }
46
+ }
47
+
48
+ failIfErrors("Placeholder check", errors);
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { failIfErrors, repoRoot } from "./lib.mjs";
6
+
7
+ const relativePath = "template/ai/templates/task-brief-template.md.tpl";
8
+ const content = await readFile(path.join(repoRoot, relativePath), "utf8");
9
+ const requiredSections = [
10
+ "## Summary",
11
+ "## Context",
12
+ "## Goals",
13
+ "## Non-Goals",
14
+ "## Scope",
15
+ "## Path Contract",
16
+ "## Requirements",
17
+ "## Bootstrap Requirements",
18
+ "## Proposed Approach",
19
+ "## Agent Execution Protocol",
20
+ "## Success Criteria",
21
+ "## Validation",
22
+ "## Validation Evidence Required",
23
+ "## Risk and Autonomy",
24
+ "## Review Routing",
25
+ "## Dependencies",
26
+ "## Rollback / Recovery",
27
+ "## Open Questions",
28
+ "## Notes for the Agent",
29
+ ];
30
+ const requiredFrontmatter = [
31
+ "id:",
32
+ "status:",
33
+ "risk:",
34
+ "autonomy:",
35
+ "model_policy:",
36
+ "model:",
37
+ "repos:",
38
+ "allowed_paths:",
39
+ "forbidden_paths:",
40
+ "requires_human_approval:",
41
+ ];
42
+
43
+ const errors = [];
44
+ for (const token of [...requiredSections, ...requiredFrontmatter]) {
45
+ if (!content.includes(token)) errors.push(`${relativePath} is missing ${token}`);
46
+ }
47
+
48
+ const modelLine = content.split("\n").find((line) => line.startsWith("model:"));
49
+ if (modelLine && /\b(?:gpt-|claude|opus|sonnet|haiku)/i.test(modelLine)) {
50
+ errors.push(`${relativePath} must use a runtime-neutral model selector, not a concrete provider model.`);
51
+ }
52
+
53
+ failIfErrors("Task template check", errors);