@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,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const args = process.argv.slice(2);
9
+ const outputArgIndex = args.indexOf("--output");
10
+ const outputRoot = outputArgIndex === -1 ? repoRoot : path.resolve(args[outputArgIndex + 1]);
11
+ const viewsDir = path.join(outputRoot, "ai/views");
12
+
13
+ async function read(relativePath) {
14
+ try {
15
+ return await readFile(path.join(repoRoot, relativePath), "utf8");
16
+ } catch {
17
+ return "";
18
+ }
19
+ }
20
+
21
+ async function files(relativeDir, suffix) {
22
+ try {
23
+ const entries = await readdir(path.join(repoRoot, relativeDir), { withFileTypes: true });
24
+ return entries
25
+ .filter((entry) => entry.isFile() && entry.name.endsWith(suffix))
26
+ .map((entry) => `${relativeDir}/${entry.name}`)
27
+ .sort();
28
+ } catch {
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function escapeHtml(value) {
34
+ return String(value)
35
+ .replaceAll("&", "&")
36
+ .replaceAll("<", "&lt;")
37
+ .replaceAll(">", "&gt;")
38
+ .replaceAll('"', "&quot;");
39
+ }
40
+
41
+ function titleFromMarkdown(content, fallback) {
42
+ return content.match(/^#\s+(.+)$/m)?.[1] ?? fallback;
43
+ }
44
+
45
+ function frontMatterValue(content, key) {
46
+ return content.match(new RegExp(`^${key}:\\s*(.+)$`, "m"))?.[1]?.trim() ?? "";
47
+ }
48
+
49
+ function layout(title, body) {
50
+ return `<!doctype html>
51
+ <html lang="en">
52
+ <head>
53
+ <meta charset="utf-8">
54
+ <meta name="viewport" content="width=device-width, initial-scale=1">
55
+ <title>${escapeHtml(title)} - {{PROJECT_NAME}} Harness Views</title>
56
+ <style>
57
+ body { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; color: #172026; background: #f7f8fa; }
58
+ header, main { max-width: 1080px; margin: 0 auto; padding: 24px; }
59
+ header { border-bottom: 1px solid #d8dee4; background: #fff; }
60
+ h1 { margin: 0 0 8px; font-size: 28px; }
61
+ h2 { margin-top: 28px; font-size: 18px; }
62
+ p { line-height: 1.5; }
63
+ a { color: #0969da; }
64
+ table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #d8dee4; }
65
+ th, td { padding: 10px 12px; border-bottom: 1px solid #d8dee4; text-align: left; vertical-align: top; }
66
+ th { background: #eef2f6; font-size: 13px; }
67
+ code { background: #eef2f6; padding: 2px 4px; border-radius: 4px; }
68
+ .note { color: #57606a; }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <header>
73
+ <h1>${escapeHtml(title)}</h1>
74
+ <p class="note">Generated review artifact. Markdown, JSON, and YAML files remain canonical.</p>
75
+ </header>
76
+ <main>
77
+ ${body}
78
+ </main>
79
+ </body>
80
+ </html>
81
+ `;
82
+ }
83
+
84
+ function table(headers, rows) {
85
+ const head = headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("");
86
+ const body = rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("\n");
87
+ return `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
88
+ }
89
+
90
+ async function indexView() {
91
+ return layout("Harness Review Views", `
92
+ <h2>Views</h2>
93
+ <ul>
94
+ <li><a href="plans.html">Plans</a></li>
95
+ <li><a href="contracts.html">Contracts</a></li>
96
+ <li><a href="readiness.html">Readiness</a></li>
97
+ <li><a href="quality.html">Quality</a></li>
98
+ <li><a href="workspace.html">Workspace</a></li>
99
+ </ul>
100
+ <h2>Canonical Sources</h2>
101
+ <p>Start from <code>ai/HUB.md</code>, <code>ai/context.md</code>, and <code>ai/knowledge-manifest.json</code>.</p>
102
+ `);
103
+ }
104
+
105
+ async function plansView() {
106
+ const planFiles = await files("ai/plans/active", ".md");
107
+ const rows = [];
108
+ for (const file of planFiles) {
109
+ const content = await read(file);
110
+ rows.push([
111
+ `<code>${escapeHtml(file)}</code>`,
112
+ escapeHtml(titleFromMarkdown(content, path.basename(file))),
113
+ escapeHtml(frontMatterValue(content, "status") || "n/a"),
114
+ escapeHtml(frontMatterValue(content, "risk") || "n/a"),
115
+ escapeHtml(frontMatterValue(content, "autonomy") || "n/a"),
116
+ ]);
117
+ }
118
+ return layout("Plans", table(["Source", "Title", "Status", "Risk", "Autonomy"], rows));
119
+ }
120
+
121
+ async function contractsView() {
122
+ const contractFiles = await files("ai/contracts", ".md");
123
+ const rows = [];
124
+ for (const file of contractFiles.filter((item) => item !== "ai/contracts/README.md")) {
125
+ const content = await read(file);
126
+ rows.push([
127
+ `<code>${escapeHtml(file)}</code>`,
128
+ escapeHtml(titleFromMarkdown(content, path.basename(file))),
129
+ `<code>${escapeHtml(file.replace(/\\.md$/, ".contract.json"))}</code>`,
130
+ ]);
131
+ }
132
+ return layout("Contracts", table(["Source", "Title", "Manifest"], rows));
133
+ }
134
+
135
+ async function qualityView() {
136
+ const quality = await read("ai/QUALITY.md");
137
+ const lines = quality.split(/\r?\n/).filter((line) => /^[-*]\s+/.test(line)).slice(0, 20);
138
+ return layout("Quality", `
139
+ <p>Source: <code>ai/QUALITY.md</code></p>
140
+ <ul>${lines.map((line) => `<li>${escapeHtml(line.replace(/^[-*]\s+/, ""))}</li>`).join("")}</ul>
141
+ `);
142
+ }
143
+
144
+ async function readinessView() {
145
+ const readiness = await read("ai/READINESS.md");
146
+ const rows = [];
147
+ const lines = readiness.split(/\r?\n/);
148
+ const start = lines.findIndex((line) => line.startsWith("| Gate | Command | Required evidence |"));
149
+ if (start !== -1) {
150
+ for (const line of lines.slice(start + 2)) {
151
+ if (!line.startsWith("|")) break;
152
+ const cells = line
153
+ .split("|")
154
+ .slice(1, -1)
155
+ .map((cell) => escapeHtml(cell.trim().replaceAll("`", "")));
156
+ if (cells.length === 3) rows.push(cells);
157
+ }
158
+ }
159
+ return layout("Readiness", `
160
+ <p>Source: <code>ai/READINESS.md</code></p>
161
+ ${table(["Gate", "Command", "Required evidence"], rows)}
162
+ `);
163
+ }
164
+
165
+ async function workspaceView() {
166
+ const workspaceFiles = await files("ai/workspace", ".md");
167
+ const rows = [];
168
+ for (const file of workspaceFiles) {
169
+ const content = await read(file);
170
+ rows.push([`<code>${escapeHtml(file)}</code>`, escapeHtml(titleFromMarkdown(content, path.basename(file)))]);
171
+ }
172
+ return layout("Workspace", table(["Source", "Title"], rows));
173
+ }
174
+
175
+ const outputs = {
176
+ "index.html": await indexView(),
177
+ "plans.html": await plansView(),
178
+ "contracts.html": await contractsView(),
179
+ "readiness.html": await readinessView(),
180
+ "quality.html": await qualityView(),
181
+ "workspace.html": await workspaceView(),
182
+ };
183
+
184
+ await mkdir(viewsDir, { recursive: true });
185
+ for (const [name, content] of Object.entries(outputs)) {
186
+ await writeFile(path.join(viewsDir, name), content);
187
+ }
188
+
189
+ console.log(`Generated ${Object.keys(outputs).length} HTML view(s).`);
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { allow, context, evaluate, parseInput } from "./lib/codex-hooks-core.mjs";
5
+
6
+ const event = process.argv[2] ?? "UnknownEvent";
7
+
8
+ function emit(result) {
9
+ const output = JSON.stringify(result);
10
+ console.log(output);
11
+ if (result.action === "deny") process.exit(2);
12
+ }
13
+
14
+ try {
15
+ const rawInput = readFileSync(0, "utf8");
16
+ emit(evaluate(event, parseInput(rawInput)));
17
+ } catch (error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ const result = context(`Codex hook could not parse input safely: ${message}`);
20
+ emit(result);
21
+ }
@@ -0,0 +1,114 @@
1
+ const docsWorkflow = "ai/WORKFLOW.md";
2
+ const docsQuality = "ai/QUALITY.md";
3
+ const docsHub = "ai/HUB.md";
4
+ const docsContext = "ai/context.md";
5
+ const taskBriefTemplate = "ai/templates/task-brief-template.md";
6
+ const eventSessionStart = "SessionStart";
7
+ const eventUserPromptSubmit = "UserPromptSubmit";
8
+ const eventPreToolUse = "PreToolUse";
9
+ const eventPermissionRequest = "PermissionRequest";
10
+ const eventPostToolUse = "PostToolUse";
11
+ const eventStop = "Stop";
12
+ const workflowDocs = [docsWorkflow, docsHub, docsContext];
13
+ const taskDocs = [docsWorkflow, taskBriefTemplate];
14
+ const postToolDocs = [docsWorkflow, docsQuality];
15
+ const stopDocs = [docsWorkflow, docsQuality];
16
+ const promptRequiresContextPattern = /\b(implement|fix|change|refactor|update|edit)\b/i;
17
+ const commandFailurePattern = /fail|timeout|error/i;
18
+ const finalMessagePattern = /commands run|validation|files changed/i;
19
+ const allowedActionAllow = "allow";
20
+
21
+ export const denyRules = [
22
+ {
23
+ id: "destructive-git-reset",
24
+ pattern: /\bgit\s+reset\s+--hard\b/i,
25
+ prevents: "discarding local work without explicit human approval",
26
+ remediation: "stop and ask for approval before destructive git operations",
27
+ policyDocs: ["ai/WORKFLOW.md", "ai/RUNNER-SAFETY.md"],
28
+ falsePositiveNote: "Only applies to destructive reset operations.",
29
+ },
30
+ {
31
+ id: "force-push",
32
+ pattern: /\bgit\s+push\b.*\s--force(?:-with-lease)?\b/i,
33
+ prevents: "rewriting remote history without explicit human approval",
34
+ remediation: "use a normal push or ask for approval with the exact branch and reason",
35
+ policyDocs: ["ai/WORKFLOW.md", "ai/RUNNER-SAFETY.md"],
36
+ falsePositiveNote: "Normal non-force pushes are not denied by this rule.",
37
+ },
38
+ {
39
+ id: "secret-read",
40
+ pattern: /\b(?:cat|sed|grep|rg|less|tail|head)\b.*(?:\.env|secret|token|credential)/i,
41
+ prevents: "unnecessary secret exposure in agent context",
42
+ remediation: "read documented env var names instead of secret values",
43
+ policyDocs: ["ai/RUNNER-SAFETY.md", "ai/contracts/security-boundary.md"],
44
+ falsePositiveNote: "Adjust this generic rule if the project has fixture files with safe secret-like names.",
45
+ },
46
+ ];
47
+
48
+ export function parseInput(rawInput) {
49
+ if (!rawInput.trim()) return {};
50
+ return JSON.parse(rawInput);
51
+ }
52
+
53
+ export function context(message, docs = []) {
54
+ return { action: "context", message, docs };
55
+ }
56
+
57
+ export function allow(message = "Allowed by harness hook policy.") {
58
+ return { action: "allow", message };
59
+ }
60
+
61
+ export function deny(rule) {
62
+ return {
63
+ action: "deny",
64
+ message: `${rule.id}: ${rule.prevents}. ${rule.remediation}.`,
65
+ docs: rule.policyDocs,
66
+ };
67
+ }
68
+
69
+ export function evaluate(event, input) {
70
+ if (event === "SessionStart") {
71
+ return context("Load the repo entrypoint, ai/AGENTS.md, ai/HUB.md, and ai/context.md before feature work.", [
72
+ "AGENTS.md",
73
+ ...workflowDocs,
74
+ ]);
75
+ }
76
+
77
+ if (event === eventUserPromptSubmit) {
78
+ const prompt = String(input.prompt ?? "");
79
+ if (promptRequiresContextPattern.test(prompt)) {
80
+ return context("Before editing, identify expected files, preserved contracts, and validation commands.", [
81
+ ...taskDocs,
82
+ ]);
83
+ }
84
+ return allow();
85
+ }
86
+
87
+ if (event === eventPreToolUse || event === eventPermissionRequest) {
88
+ const command = String(input.command ?? input.toolInput?.cmd ?? "");
89
+ for (const rule of denyRules) {
90
+ if (rule.pattern.test(command)) return deny(rule);
91
+ }
92
+ return allow();
93
+ }
94
+
95
+ if (event === eventPostToolUse) {
96
+ const exitCode = Number(input.exitCode ?? 0);
97
+ const status = String(input.status ?? "");
98
+ if (exitCode !== 0 || commandFailurePattern.test(status)) {
99
+ return context("A command failed or timed out. Report the exact command, failure, and next repair step.", [...postToolDocs]);
100
+ }
101
+ return allow();
102
+ }
103
+
104
+ if (event === eventStop) {
105
+ const changedFiles = Array.isArray(input.changedFiles) ? input.changedFiles : [];
106
+ const finalMessage = String(input.finalMessage ?? "");
107
+ if (changedFiles.length > 0 && !finalMessagePattern.test(finalMessage)) {
108
+ return context("Final response should include files changed and validation evidence.", [...stopDocs]);
109
+ }
110
+ return allow();
111
+ }
112
+
113
+ return context("Unknown hook event. Defaulting to context-only behavior.");
114
+ }