@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.
- package/CHANGELOG.md +56 -0
- package/README.md +131 -21
- package/ROADMAP.md +38 -0
- package/SECURITY.md +33 -0
- package/bin/structor.mjs +553 -29
- package/contrib/self-harness/files/README.md +32 -0
- package/contrib/self-harness/files/ai/AGENTS.md +35 -0
- package/contrib/self-harness/files/ai/ARCHITECTURE.md +38 -0
- package/contrib/self-harness/files/ai/HUB.md +59 -0
- package/contrib/self-harness/files/ai/PRODUCT.md +36 -0
- package/contrib/self-harness/files/ai/QUALITY.md +31 -0
- package/contrib/self-harness/files/ai/context.md +38 -0
- package/contrib/self-harness/files/scripts/check-workspace.mjs +72 -0
- package/contrib/self-harness/harness.config.json +37 -0
- package/docs/CONTRIBUTOR-SETUP.md +45 -0
- package/docs/INIT.md +55 -2
- package/docs/public-launch.md +150 -0
- package/examples/anthropic-only/harness.config.json +26 -0
- package/examples/frontend-backend/harness.config.json +8 -8
- package/examples/generated-harness-tree.md +432 -0
- package/examples/openai-and-anthropic/harness.config.json +7 -7
- package/examples/single-repo/harness.config.json +7 -7
- package/harness.config.example.json +1 -1
- package/package.json +12 -4
- package/schemas/contract-manifest.schema.json +0 -1
- package/schemas/harness-config.schema.json +5 -2
- package/scripts/check-config.mjs +20 -31
- package/scripts/check-examples.mjs +146 -0
- package/scripts/check-public-hygiene.mjs +249 -0
- package/scripts/check-schemas.mjs +42 -0
- package/scripts/check-template-files.mjs +15 -98
- package/scripts/generated-harness-contract.mjs +416 -0
- package/scripts/init-harness.mjs +227 -139
- package/scripts/lib.mjs +462 -12
- package/scripts/rendered-config.mjs +109 -0
- package/scripts/setup-contributor.mjs +125 -0
- package/scripts/smoke-template.mjs +260 -73
- package/template/AGENTS.md.tpl +4 -2
- package/template/README.md.tpl +5 -0
- package/template/ai/CODEX-HOOKS.md.tpl +1 -1
- package/template/ai/HARNESS-ENGINEERING.md.tpl +5 -2
- package/template/ai/HARNESS.md.tpl +4 -1
- package/template/ai/contracts/codex-hooks.contract.json.tpl +58 -1
- package/template/ai/contracts/codex-hooks.md.tpl +6 -0
- package/template/ai/contracts/release-flow.md.tpl +1 -1
- package/template/ai/templates/fixtures/issues/valid-ready.md.tpl +3 -1
- package/template/ai/templates/issue-template.md.tpl +3 -1
- package/template/ai/workspace/LOCAL-STACK.md.tpl +1 -1
- package/template/ai/workspace/SYSTEM-MAP.md.tpl +2 -2
- package/template/consumer/AGENTS.md.tpl +4 -4
- package/template/consumer/CLAUDE.md.tpl +4 -4
- package/template/scripts/bootstrap-workspace.mjs.tpl +11 -25
- package/template/scripts/check-claude-compatibility.mjs.tpl +62 -9
- package/template/scripts/check-codex-hooks.mjs.tpl +262 -20
- package/template/scripts/check-template-governance.mjs.tpl +2 -114
- package/template/scripts/check-workspace.mjs.tpl +27 -103
- package/template/scripts/check-worktree-bootstrap-fixtures.mjs.tpl +12 -0
- package/template/scripts/generate-html-views.mjs.tpl +357 -56
- package/template/scripts/generated-harness-contract.mjs.tpl +1 -0
- package/template/scripts/hooks/lib/codex-hooks-core.mjs.tpl +14 -3
- package/template/scripts/lib/path-safety.mjs.tpl +87 -0
- package/template/scripts/lib/worktree-bootstrap.mjs.tpl +16 -13
- package/template/scripts/validate-governance.mjs.tpl +52 -36
- package/schemas/task-brief.schema.json +0 -37
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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 {
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
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
|
|
54
|
-
await
|
|
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
|
|
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
|
-
|
|
67
|
-
await runCheck(
|
|
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
|
-
}
|