@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.
- package/LICENSE +21 -0
- package/README.md +405 -0
- package/bin/structor.mjs +576 -0
- package/docs/INIT.md +109 -0
- package/docs/adr/0001-default-generated-repo-name.md +9 -0
- package/docs/issues/0001-structor-doctor.md +39 -0
- package/examples/frontend-backend/harness.config.json +35 -0
- package/examples/openai-and-anthropic/harness.config.json +28 -0
- package/examples/single-repo/harness.config.json +26 -0
- package/harness.config.example.json +38 -0
- package/package.json +58 -0
- package/schemas/contract-manifest.schema.json +18 -0
- package/schemas/harness-config.schema.json +85 -0
- package/schemas/task-brief.schema.json +37 -0
- package/scripts/check-config.mjs +76 -0
- package/scripts/check-contract-manifests.mjs +85 -0
- package/scripts/check-model-overlays.mjs +30 -0
- package/scripts/check-placeholders.mjs +48 -0
- package/scripts/check-task-template.mjs +53 -0
- package/scripts/check-template-files.mjs +110 -0
- package/scripts/init-harness.mjs +270 -0
- package/scripts/lib.mjs +190 -0
- package/scripts/smoke-template.mjs +309 -0
- package/scripts/validate-governance.mjs +3 -0
- package/scripts/validate-template.mjs +16 -0
- package/template/.claude/CLAUDE.md.tpl +12 -0
- package/template/.claude/rules/harness-client-surfaces.md.tpl +20 -0
- package/template/.claude/settings.json.tpl +10 -0
- package/template/.codex/hooks.json.tpl +77 -0
- package/template/AGENTS.md.tpl +22 -0
- package/template/CLAUDE.md.tpl +16 -0
- package/template/README.md.tpl +109 -0
- package/template/ai/AGENT-GARBAGE-COLLECTION.md.tpl +18 -0
- package/template/ai/AGENTS.md.tpl +36 -0
- package/template/ai/ARCHITECTURE.md.tpl +35 -0
- package/template/ai/CODEX-HOOKS.md.tpl +23 -0
- package/template/ai/DECISIONS.md.tpl +22 -0
- package/template/ai/DESIGN.md.tpl +22 -0
- package/template/ai/HARNESS-ENGINEERING.md.tpl +107 -0
- package/template/ai/HARNESS.md.tpl +53 -0
- package/template/ai/HUB.md.tpl +53 -0
- package/template/ai/PRODUCT-SUMMARY.md.tpl +28 -0
- package/template/ai/PRODUCT.md.tpl +32 -0
- package/template/ai/QUALITY.md.tpl +37 -0
- package/template/ai/READINESS.md.tpl +39 -0
- package/template/ai/RUNNER-READINESS.md.tpl +14 -0
- package/template/ai/RUNNER-SAFETY.md.tpl +21 -0
- package/template/ai/VERSIONING.md.tpl +16 -0
- package/template/ai/WORKFLOW.md.tpl +42 -0
- package/template/ai/context.md.tpl +17 -0
- package/template/ai/contracts/README.md.tpl +23 -0
- package/template/ai/contracts/api-boundary.contract.json.tpl +11 -0
- package/template/ai/contracts/api-boundary.md.tpl +17 -0
- package/template/ai/contracts/app-legibility.contract.json.tpl +11 -0
- package/template/ai/contracts/app-legibility.md.tpl +24 -0
- package/template/ai/contracts/codex-hooks.contract.json.tpl +15 -0
- package/template/ai/contracts/codex-hooks.md.tpl +18 -0
- package/template/ai/contracts/github-safety.contract.json.tpl +11 -0
- package/template/ai/contracts/github-safety.md.tpl +15 -0
- package/template/ai/contracts/release-flow.contract.json.tpl +12 -0
- package/template/ai/contracts/release-flow.md.tpl +15 -0
- package/template/ai/contracts/repo-boundaries.contract.json.tpl +12 -0
- package/template/ai/contracts/repo-boundaries.md.tpl +18 -0
- package/template/ai/contracts/security-boundary.contract.json.tpl +11 -0
- package/template/ai/contracts/security-boundary.md.tpl +19 -0
- package/template/ai/knowledge-manifest.json.tpl +149 -0
- package/template/ai/model-overlays/anthropic/CLAUDE.md.tpl +14 -0
- package/template/ai/model-overlays/openai/AGENTS.md.tpl +13 -0
- package/template/ai/plans/README.md.tpl +10 -0
- package/template/ai/plans/tech-debt.md.tpl +7 -0
- package/template/ai/skills/README.md.tpl +15 -0
- package/template/ai/skills/review-architecture.md.tpl +41 -0
- package/template/ai/skills/review-contract-drift.md.tpl +41 -0
- package/template/ai/skills/review-governance-drift.md.tpl +42 -0
- package/template/ai/skills/review-security.md.tpl +40 -0
- package/template/ai/specs/README.md.tpl +14 -0
- package/template/ai/templates/README.md.tpl +13 -0
- package/template/ai/templates/fixtures/issues/invalid-placeholder.md.tpl +20 -0
- package/template/ai/templates/fixtures/issues/invalid-protected-surface.md.tpl +21 -0
- package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +105 -0
- package/template/ai/templates/issue-template.md.tpl +107 -0
- package/template/ai/templates/task-brief-template.md.tpl +185 -0
- package/template/ai/workspace/LOCAL-STACK.md.tpl +21 -0
- package/template/ai/workspace/REPOS.md.tpl +19 -0
- package/template/ai/workspace/SESSION-BOOTSTRAP.md.tpl +27 -0
- package/template/ai/workspace/SYSTEM-MAP.md.tpl +19 -0
- package/template/ai/workspace/TEST-STRATEGY.md.tpl +22 -0
- package/template/consumer/.claude/CLAUDE.md.tpl +14 -0
- package/template/consumer/AGENTS.md.tpl +23 -0
- package/template/consumer/CLAUDE.md.tpl +15 -0
- package/template/scripts/bootstrap-codex-worktree.mjs.tpl +52 -0
- package/template/scripts/bootstrap-workspace.mjs.tpl +100 -0
- package/template/scripts/check-claude-compatibility.mjs.tpl +120 -0
- package/template/scripts/check-codex-hooks.mjs.tpl +190 -0
- package/template/scripts/check-contract-manifests.mjs.tpl +81 -0
- package/template/scripts/check-garbage-collection.mjs.tpl +25 -0
- package/template/scripts/check-html-views.mjs.tpl +60 -0
- package/template/scripts/check-issue-template.mjs.tpl +167 -0
- package/template/scripts/check-knowledge-manifest.mjs.tpl +82 -0
- package/template/scripts/check-overlay-drift.mjs.tpl +49 -0
- package/template/scripts/check-plans.mjs.tpl +70 -0
- package/template/scripts/check-readiness.mjs.tpl +130 -0
- package/template/scripts/check-review-skills.mjs.tpl +48 -0
- package/template/scripts/check-task-template.mjs.tpl +63 -0
- package/template/scripts/check-template-governance.mjs.tpl +161 -0
- package/template/scripts/check-workspace.mjs.tpl +212 -0
- package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +122 -0
- package/template/scripts/check-worktrees.mjs.tpl +69 -0
- package/template/scripts/fixtures/worktrees/README.md.tpl +4 -0
- package/template/scripts/generate-html-views.mjs.tpl +189 -0
- package/template/scripts/hooks/codex-hook.mjs.tpl +21 -0
- package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +114 -0
- package/template/scripts/lib/worktree-bootstrap.mjs.tpl +388 -0
- package/template/scripts/validate-governance.mjs.tpl +78 -0
- package/template/workspace/.claude/CLAUDE.md.tpl +9 -0
- package/template/workspace/.claude/rules/harness-client-surfaces.md.tpl +15 -0
- package/template/workspace/.claude/settings.json.tpl +10 -0
- package/template/workspace/AGENTS.md.tpl +17 -0
- 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);
|