@holdpoint/cli 0.1.0-alpha.1 → 0.1.0-alpha.10

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/dist/index.js CHANGED
@@ -4,23 +4,47 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
7
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync } from "fs";
8
8
  import { join, dirname } from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import chalk from "chalk";
11
11
  import ora from "ora";
12
- import { buildHookJson, buildCheckScript, buildConfigJson } from "@holdpoint/engine-copilot";
12
+ import {
13
+ buildHookJson,
14
+ buildCheckScript,
15
+ buildConfigJson,
16
+ buildEngine
17
+ } from "@holdpoint/engine-copilot";
13
18
  import { buildEngineJson as buildClaudeEngineJson } from "@holdpoint/engine-claude";
14
19
  import { buildEngine as buildCursorEngine } from "@holdpoint/engine-cursor";
20
+ import {
21
+ buildHooksJson as buildCodexHooksJson,
22
+ buildCheckScript as buildCodexCheckScript,
23
+ spliceAgentsMd
24
+ } from "@holdpoint/engine-codex";
15
25
  import { parseHoldpointYaml } from "@holdpoint/yaml-core";
16
26
 
17
27
  // src/detect.ts
18
- import { existsSync } from "fs";
19
- function detectAgent() {
20
- if (existsSync(".github/extensions")) return "copilot";
21
- if (existsSync(".claude")) return "claude";
22
- if (existsSync(".cursorrules")) return "cursor";
23
- return "unknown";
28
+ import { existsSync, readFileSync } from "fs";
29
+ function detectPackageManager() {
30
+ if (existsSync("pnpm-lock.yaml")) return "pnpm";
31
+ if (existsSync("yarn.lock")) return "yarn";
32
+ return "npm";
33
+ }
34
+ function detectInstalledAgents() {
35
+ const agents = [];
36
+ if (existsSync(".github/hooks/holdpoint.json")) agents.push("copilot");
37
+ if (existsSync(".claude/settings.json")) agents.push("claude");
38
+ if (existsSync(".cursorrules")) {
39
+ try {
40
+ if (readFileSync(".cursorrules", "utf8").includes("Holdpoint Rules")) {
41
+ agents.push("cursor");
42
+ }
43
+ } catch {
44
+ }
45
+ }
46
+ if (existsSync(".codex/holdpoint-check.mjs")) agents.push("codex");
47
+ return agents;
24
48
  }
25
49
  function detectStack() {
26
50
  const hasNext = existsSync("next.config.ts") || existsSync("next.config.js") || existsSync("next.config.mjs");
@@ -84,35 +108,43 @@ checks:
84
108
  async function initCommand(options) {
85
109
  const spinner = ora("Initialising Holdpoint\u2026").start();
86
110
  const stack = options.stack ?? detectStack();
87
- const agent = options.agent ?? detectAgent();
88
- spinner.text = `Detected stack: ${chalk.cyan(stack)}, agent: ${chalk.cyan(agent)}`;
111
+ const agentOpt = options.agent;
112
+ const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
113
+ spinner.text = `Stack: ${chalk.cyan(stack)} \u2014 installing for: ${chalk.cyan(agents.join(", "))}`;
89
114
  let yamlContent = MINIMAL_CHECKS_YAML;
90
115
  if (!existsSync2("checks.yaml")) {
91
116
  const templatePath = getTemplatePath(stack);
92
117
  if (templatePath) {
93
- yamlContent = readFileSync(templatePath, "utf8");
118
+ yamlContent = readFileSync2(templatePath, "utf8");
119
+ }
120
+ const pm = detectPackageManager();
121
+ if (pm !== "pnpm") {
122
+ yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
94
123
  }
95
124
  writeFileSync("checks.yaml", yamlContent, "utf8");
96
125
  } else {
97
- yamlContent = readFileSync("checks.yaml", "utf8");
126
+ yamlContent = readFileSync2("checks.yaml", "utf8");
98
127
  }
99
128
  const config = parseHoldpointYaml(yamlContent);
100
129
  const generatedDir = ".github/holdpoint/generated";
101
130
  mkdirSync(generatedDir, { recursive: true });
102
131
  writeFileSync(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
103
- if (agent === "copilot" || agent === "unknown") {
132
+ if (agents.includes("copilot")) {
104
133
  const hooksDir = ".github/hooks";
105
134
  mkdirSync(hooksDir, { recursive: true });
106
135
  writeFileSync(join(hooksDir, "holdpoint.json"), buildHookJson(config), "utf8");
107
136
  writeFileSync(join(hooksDir, "holdpoint-check.mjs"), buildCheckScript(config), "utf8");
137
+ const extDir = ".github/extensions/holdpoint";
138
+ mkdirSync(extDir, { recursive: true });
139
+ writeFileSync(join(extDir, "extension.mjs"), buildEngine(config), "utf8");
108
140
  }
109
- if (agent === "claude") {
141
+ if (agents.includes("claude")) {
110
142
  mkdirSync(".claude", { recursive: true });
111
143
  const settingsPath = ".claude/settings.json";
112
144
  let existing = {};
113
145
  if (existsSync2(settingsPath)) {
114
146
  try {
115
- existing = JSON.parse(readFileSync(settingsPath, "utf8"));
147
+ existing = JSON.parse(readFileSync2(settingsPath, "utf8"));
116
148
  } catch {
117
149
  }
118
150
  }
@@ -123,11 +155,11 @@ async function initCommand(options) {
123
155
  "utf8"
124
156
  );
125
157
  }
126
- if (agent === "cursor") {
158
+ if (agents.includes("cursor")) {
127
159
  const cursorRules = buildCursorEngine(config);
128
160
  const cursorPath = ".cursorrules";
129
161
  if (existsSync2(cursorPath)) {
130
- const existing = readFileSync(cursorPath, "utf8");
162
+ const existing = readFileSync2(cursorPath, "utf8");
131
163
  if (!existing.includes("Holdpoint Rules")) {
132
164
  writeFileSync(cursorPath, existing + "\n" + cursorRules, "utf8");
133
165
  }
@@ -135,6 +167,14 @@ async function initCommand(options) {
135
167
  writeFileSync(cursorPath, cursorRules, "utf8");
136
168
  }
137
169
  }
170
+ if (agents.includes("codex")) {
171
+ mkdirSync(".codex", { recursive: true });
172
+ writeFileSync(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
173
+ writeFileSync(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
174
+ const agentsMdPath = "AGENTS.md";
175
+ const existing = existsSync2(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
176
+ writeFileSync(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
177
+ }
138
178
  if (!existsSync2("MASTER_PROMPT.md")) {
139
179
  const guidePath = getMasterPromptPath();
140
180
  if (guidePath) {
@@ -142,7 +182,7 @@ async function initCommand(options) {
142
182
  } else {
143
183
  writeFileSync(
144
184
  "MASTER_PROMPT.md",
145
- "# Holdpoint\n\nRun `npx holdpoint check` before marking any task complete.\nSee `checks.yaml` for the full list of checks.\n",
185
+ "# Holdpoint\n\nRun `npx @holdpoint/cli@alpha check` before marking any task complete.\nSee `checks.yaml` for the full list of checks.\n",
146
186
  "utf8"
147
187
  );
148
188
  }
@@ -152,272 +192,538 @@ async function initCommand(options) {
152
192
  ${chalk.cyan("Next steps:")}
153
193
  1. Edit ${chalk.yellow("checks.yaml")} to customise your eval checkpoints
154
194
  2. Commit ${chalk.yellow("checks.yaml")} and the generated engine files
155
- 3. Run ${chalk.yellow("npx holdpoint check")} at any time to validate
195
+ 3. Run ${chalk.yellow("npx @holdpoint/cli@alpha check")} at any time to validate
156
196
 
157
- Visual builder: ${chalk.yellow("npx holdpoint builder")} (opens localhost:4321)
158
- Stack: ${chalk.cyan(stack)} Agent: ${chalk.cyan(agent)}
197
+ Visual builder: ${chalk.yellow("npx @holdpoint/cli@alpha builder")} (opens localhost:4321)
198
+ Stack: ${chalk.cyan(stack)} Agents: ${chalk.cyan(agents.join(", "))}
159
199
  `);
160
200
  }
161
201
 
162
202
  // src/commands/check.ts
163
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
203
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
204
+ import { join as join2 } from "path";
164
205
  import chalk2 from "chalk";
165
206
  import ora2 from "ora";
166
207
  import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
167
208
  import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
168
- import { execSync as execSync3 } from "child_process";
169
-
170
- // src/evolve/scanner.ts
171
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
172
- import { join as join2 } from "path";
173
209
  import { execSync } from "child_process";
174
- function tryReadJson(path) {
210
+ var COMMIT_CACHE_PATH = ".holdpoint/checked-commits.json";
211
+ var COMMIT_CACHE_MAX = 100;
212
+ var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
213
+ var CHECK_REPORTS_MAX = 50;
214
+ function getStagedFiles() {
215
+ try {
216
+ const out = execSync("git diff --cached --name-only", {
217
+ encoding: "utf8",
218
+ stdio: ["pipe", "pipe", "ignore"]
219
+ });
220
+ return out.trim().split("\n").filter(Boolean);
221
+ } catch {
222
+ return [];
223
+ }
224
+ }
225
+ function getAllChangedFiles() {
226
+ try {
227
+ const out = execSync("git diff --name-only HEAD", {
228
+ encoding: "utf8",
229
+ stdio: ["pipe", "pipe", "ignore"]
230
+ });
231
+ return out.trim().split("\n").filter(Boolean);
232
+ } catch {
233
+ return [];
234
+ }
235
+ }
236
+ function getLastCommitFiles() {
237
+ try {
238
+ const out = execSync("git diff --name-only HEAD~1 HEAD", {
239
+ encoding: "utf8",
240
+ stdio: ["pipe", "pipe", "ignore"]
241
+ });
242
+ return out.trim().split("\n").filter(Boolean);
243
+ } catch {
244
+ return [];
245
+ }
246
+ }
247
+ function getHeadSha() {
175
248
  try {
176
- return JSON.parse(readFileSync2(path, "utf8"));
249
+ return execSync("git rev-parse HEAD", {
250
+ encoding: "utf8",
251
+ stdio: ["pipe", "pipe", "ignore"]
252
+ }).trim();
177
253
  } catch {
178
254
  return null;
179
255
  }
180
256
  }
181
- function tryReadText(path) {
257
+ function readCommitCache() {
182
258
  try {
183
- return readFileSync2(path, "utf8");
259
+ const raw = readFileSync3(COMMIT_CACHE_PATH, "utf8");
260
+ const parsed = JSON.parse(raw);
261
+ return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
184
262
  } catch {
185
- return "";
263
+ return /* @__PURE__ */ new Set();
186
264
  }
187
265
  }
188
- function scanProject(cwd = process.cwd()) {
189
- const exists = (p) => existsSync3(join2(cwd, p));
190
- const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
191
- const pkg = tryReadJson(join2(cwd, "package.json"));
192
- const scripts = pkg?.scripts ?? {};
193
- const deps = /* @__PURE__ */ new Set([
194
- ...Object.keys(pkg?.dependencies ?? {}),
195
- ...Object.keys(pkg?.devDependencies ?? {})
196
- ]);
197
- const pyprojectText = tryReadText(join2(cwd, "pyproject.toml"));
198
- const requirementsText = tryReadText(join2(cwd, "requirements.txt"));
199
- const pipfileText = tryReadText(join2(cwd, "Pipfile"));
200
- const allPyText = pyprojectText + requirementsText + pipfileText;
201
- const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
202
- const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
203
- const hasAlembic = allPyText.includes("alembic") || deps.has("alembic");
204
- let rootFiles = [];
266
+ function recordCommitCache(sha) {
205
267
  try {
206
- rootFiles = readdirSync(cwd);
268
+ const existing = readCommitCache();
269
+ const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
270
+ mkdirSync2(join2(COMMIT_CACHE_PATH, ".."), { recursive: true });
271
+ writeFileSync2(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
207
272
  } catch {
208
273
  }
209
- const hasDocker = rootFiles.some((f) => f === "Dockerfile" || f.startsWith("Dockerfile.")) || exists("docker-compose.yml") || exists("docker-compose.yaml") || exists("docker-compose.dev.yml");
210
- const hasTerraform = rootFiles.some((f) => f.endsWith(".tf")) || exists("terraform") || exists("infra");
211
- return {
212
- // Languages
213
- hasTypeScript: exists("tsconfig.json") || deps.has("typescript"),
214
- hasPython: Boolean(pyprojectText) || Boolean(requirementsText) || Boolean(pipfileText) || exists("setup.py"),
215
- hasGo: exists("go.mod"),
216
- hasRust: exists("Cargo.toml"),
217
- hasJava: exists("pom.xml") || exists("build.gradle") || exists("build.gradle.kts"),
218
- hasRuby: exists("Gemfile"),
219
- // Frameworks
220
- hasNext: exists("next.config.ts") || exists("next.config.js") || exists("next.config.mjs") || deps.has("next"),
221
- hasReact: deps.has("react"),
222
- // Linting
223
- hasEslint: exists("eslint.config.js") || exists("eslint.config.ts") || exists("eslint.config.mjs") || exists(".eslintrc.js") || exists(".eslintrc.json") || exists(".eslintrc.yml") || exists(".eslintrc.yaml") || deps.has("eslint"),
224
- hasBiome: exists("biome.json") || exists("biome.jsonc") || deps.has("@biomejs/biome"),
225
- hasRuff,
226
- hasPrettier: exists("prettier.config.js") || exists("prettier.config.ts") || exists("prettier.config.mjs") || exists(".prettierrc") || exists(".prettierrc.json") || deps.has("prettier"),
227
- // Testing
228
- hasVitest: deps.has("vitest") || Boolean(scripts["test"]?.includes("vitest")),
229
- hasJest: deps.has("jest") || Boolean(scripts["test"]?.includes("jest")),
230
- hasPytest,
231
- // DB
232
- hasPrisma: exists("prisma/schema.prisma") || deps.has("@prisma/client"),
233
- hasDrizzle: deps.has("drizzle-orm"),
234
- hasMigrations: exists("migrations") || exists("db/migrations") || exists("database/migrations"),
235
- hasAlembic,
236
- // Infra
237
- hasDocker,
238
- hasTerraform,
239
- hasKubernetes: exists("k8s") || exists("kubernetes") || exists("helm"),
240
- // API
241
- hasOpenApi: exists("openapi.yaml") || exists("openapi.yml") || exists("openapi.json") || exists("api/openapi.yaml"),
242
- // CI
243
- hasGithubActions: exists(".github/workflows"),
244
- packageManager,
245
- scripts,
246
- deps
274
+ }
275
+ function recordCheckReport(run) {
276
+ try {
277
+ mkdirSync2(join2(CHECK_REPORTS_PATH, ".."), { recursive: true });
278
+ let existing = { runs: [] };
279
+ if (existsSync3(CHECK_REPORTS_PATH)) {
280
+ try {
281
+ existing = JSON.parse(readFileSync3(CHECK_REPORTS_PATH, "utf8"));
282
+ if (!Array.isArray(existing.runs)) existing.runs = [];
283
+ } catch {
284
+ existing = { runs: [] };
285
+ }
286
+ }
287
+ const updated = {
288
+ runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
289
+ };
290
+ writeFileSync2(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
291
+ } catch {
292
+ }
293
+ }
294
+ async function checkCommand(options) {
295
+ if (!existsSync3("checks.yaml")) {
296
+ console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
297
+ process.exit(1);
298
+ }
299
+ const yamlContent = readFileSync3("checks.yaml", "utf8");
300
+ let config;
301
+ try {
302
+ config = parseHoldpointYaml2(yamlContent);
303
+ } catch (err) {
304
+ console.error(chalk2.red("Invalid checks.yaml:"), err.message);
305
+ process.exit(1);
306
+ }
307
+ const headSha = getHeadSha();
308
+ let changedFiles;
309
+ let usedHeadShaForCache = false;
310
+ if (options.staged) {
311
+ const staged = getStagedFiles();
312
+ if (staged.length > 0) {
313
+ changedFiles = staged;
314
+ } else {
315
+ if (headSha && readCommitCache().has(headSha)) {
316
+ console.log(
317
+ chalk2.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
318
+ );
319
+ process.exit(0);
320
+ }
321
+ const lastCommit = getLastCommitFiles();
322
+ if (lastCommit.length > 0) {
323
+ changedFiles = lastCommit;
324
+ usedHeadShaForCache = true;
325
+ console.log(
326
+ chalk2.yellow("No staged files. Running checks scoped to the most recent commit's files.")
327
+ );
328
+ } else {
329
+ console.log(chalk2.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
330
+ process.exit(0);
331
+ }
332
+ }
333
+ } else {
334
+ changedFiles = getAllChangedFiles();
335
+ if (changedFiles.length === 0) {
336
+ console.log(
337
+ chalk2.yellow("No changed files detected. Running all checks with no file filter.")
338
+ );
339
+ console.log(
340
+ chalk2.dim(
341
+ " Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
342
+ )
343
+ );
344
+ }
345
+ }
346
+ const guides = Object.entries(config.context?.guides ?? {});
347
+ if (guides.length > 0) {
348
+ console.log(chalk2.cyan("\nProject guides:"));
349
+ for (const [key, text] of guides) {
350
+ console.log(chalk2.bold(` ${key}:`), chalk2.dim(String(text).trim()));
351
+ }
352
+ console.log("");
353
+ }
354
+ const taskCount = config.checks.filter((c) => c.cmd !== void 0).length;
355
+ const spinner = ora2(`Running ${taskCount} task(s)\u2026`).start();
356
+ const effectiveFiles = changedFiles.length > 0 ? changedFiles : ["__all__"];
357
+ const results = runDeterministicChecks(config, effectiveFiles);
358
+ const passed = results.filter((r) => r.status === "pass");
359
+ const failed = results.filter((r) => r.status === "fail");
360
+ const skipped = results.filter((r) => r.status === "skip");
361
+ spinner.stop();
362
+ for (const result of results) {
363
+ printResult(result);
364
+ }
365
+ console.log("");
366
+ console.log(
367
+ [
368
+ chalk2.green(`\u2713 ${passed.length} passed`),
369
+ failed.length > 0 ? chalk2.red(`\u2717 ${failed.length} failed`) : "",
370
+ skipped.length > 0 ? chalk2.gray(`\u25CC ${skipped.length} skipped`) : ""
371
+ ].filter(Boolean).join(" ")
372
+ );
373
+ const promptChecks = config.checks.filter(
374
+ (c) => c.prompt !== void 0 && matchesWhen(c.when, changedFiles.length > 0 ? changedFiles : ["__all__"], config.patterns)
375
+ );
376
+ if (promptChecks.length > 0) {
377
+ console.log(`
378
+ ${chalk2.cyan("Agent prompts to act on:")}`);
379
+ for (const c of promptChecks) {
380
+ console.log(` ${chalk2.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
381
+ }
382
+ }
383
+ const reportResults = [
384
+ ...results.map((r) => ({
385
+ id: r.check.id,
386
+ label: r.check.label,
387
+ kind: "cmd",
388
+ status: r.status,
389
+ ...r.output !== void 0 ? { output: r.output } : {},
390
+ ...r.exitCode !== void 0 ? { exitCode: r.exitCode } : {},
391
+ ...r.skipReason !== void 0 ? { skipReason: r.skipReason } : {}
392
+ })),
393
+ ...promptChecks.map((c) => ({
394
+ id: c.id,
395
+ label: c.label,
396
+ kind: "prompt",
397
+ status: "shown"
398
+ }))
399
+ ];
400
+ const run = {
401
+ sha: headSha,
402
+ shortSha: headSha ? headSha.slice(0, 8) : null,
403
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
404
+ files: changedFiles.length > 0 ? changedFiles : [],
405
+ results: reportResults,
406
+ summary: {
407
+ passed: passed.length,
408
+ failed: failed.length,
409
+ skipped: skipped.length,
410
+ shown: promptChecks.length
411
+ }
247
412
  };
413
+ recordCheckReport(run);
414
+ if (failed.length > 0) {
415
+ process.exit(1);
416
+ }
417
+ if (usedHeadShaForCache && headSha) {
418
+ recordCommitCache(headSha);
419
+ }
420
+ }
421
+ function printResult(result) {
422
+ const icon = result.status === "pass" ? chalk2.green("\u2713") : result.status === "fail" ? chalk2.red("\u2717") : result.status === "skip" ? chalk2.gray("\u25CC") : chalk2.yellow("\u2026");
423
+ const label = result.check.label;
424
+ console.log(`${icon} ${label}`);
425
+ if (result.status === "fail" && result.output) {
426
+ const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
427
+ console.log(chalk2.dim(trimmed.replace(/^/gm, " ")));
428
+ }
429
+ if (result.status === "skip" && result.skipReason) {
430
+ console.log(chalk2.dim(` ${result.skipReason}`));
431
+ }
248
432
  }
249
433
 
250
- // src/evolve/templates.ts
251
- function pmScript(profile, script, fallback) {
252
- if (!profile.scripts[script]) return fallback;
253
- if (profile.packageManager === "npm") return `npm run ${script}`;
254
- return `${profile.packageManager} ${script}`;
434
+ // src/commands/validate.ts
435
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
436
+ import chalk3 from "chalk";
437
+ import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
438
+ async function validateCommand() {
439
+ if (!existsSync4("checks.yaml")) {
440
+ console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
441
+ process.exit(1);
442
+ }
443
+ const text = readFileSync4("checks.yaml", "utf8");
444
+ let config;
445
+ try {
446
+ config = parseHoldpointYaml3(text);
447
+ } catch (err) {
448
+ console.error(chalk3.red("Parse error:"), err.message);
449
+ process.exit(1);
450
+ }
451
+ const result = validateConfig(config);
452
+ if (result.valid) {
453
+ console.log(chalk3.green("\u2713 checks.yaml is valid"));
454
+ console.log(
455
+ chalk3.dim(
456
+ ` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
457
+ )
458
+ );
459
+ } else {
460
+ console.error(chalk3.red("\u2717 checks.yaml has errors:"));
461
+ for (const err of result.errors) {
462
+ console.error(` ${chalk3.yellow(err.path)}: ${err.message}`);
463
+ }
464
+ process.exit(1);
465
+ }
255
466
  }
256
- function getTemplates(profile) {
257
- return [
258
- // ── Universal checks (always proposed for any project) ──────────────────
259
- {
260
- id: "git-commit",
261
- label: "Commit all changes before finishing",
262
- cmd: 'git rev-parse --is-inside-work-tree 2>/dev/null || exit 0; [ -z "$(git status --porcelain)" ] && exit 0; git status --short; exit 1',
263
- trigger: () => true
264
- },
265
- {
266
- id: "changelog-update",
267
- label: "Add a CHANGELOG.md entry for this session",
268
- prompt: "Before committing, add an entry to CHANGELOG.md describing what was done. Use Keep a Changelog format \u2014 add under ## [Unreleased] (create the file and that section if absent). Group entries as Added, Changed, Fixed, or Removed. Be concise but specific. The entry text will serve as the commit message.",
269
- trigger: () => true
270
- },
271
- {
272
- id: "readme-sync",
273
- label: "Update README.md if user-facing changes were made",
274
- prompt: "If you added, changed, or removed user-facing functionality \u2014 CLI commands, configuration options, public APIs, or significant new features \u2014 update README.md to reflect those changes.",
275
- trigger: () => true
276
- },
277
- {
278
- id: "no-todos",
279
- label: "No TODO/FIXME left in changed code",
280
- prompt: "Scan the files you changed for any TODO, FIXME, HACK, or XXX comments. Either resolve them before finishing or convert them to GitHub issues. Don't leave incomplete work silently behind.",
281
- trigger: () => true
282
- },
283
- // ── TypeScript / JavaScript ──────────────────────────────────────────────
284
- {
285
- id: "typecheck",
286
- label: "TypeScript type check",
287
- cmd: pmScript(profile, "typecheck", "npx tsc --noEmit"),
288
- trigger: (p) => p.hasTypeScript
289
- },
290
- {
291
- id: "lint",
292
- label: "Lint codebase",
293
- cmd: profile.hasEslint ? pmScript(profile, "lint", "npx eslint .") : profile.hasBiome ? pmScript(profile, "lint", "npx @biomejs/biome check .") : pmScript(profile, "lint", "echo 'No linter detected'"),
294
- trigger: (p) => p.hasEslint || p.hasBiome
295
- },
296
- {
297
- id: "format-check",
298
- label: "Prettier \u2014 format check",
299
- cmd: pmScript(profile, "format:check", "npx prettier --check ."),
300
- trigger: (p) => p.hasPrettier
301
- },
302
- {
303
- id: "test",
304
- label: "Unit tests",
305
- cmd: profile.hasVitest ? pmScript(profile, "test", "npx vitest run") : pmScript(profile, "test", "npx jest --passWithNoTests"),
306
- trigger: (p) => p.hasVitest || p.hasJest
307
- },
308
- {
309
- id: "jsdoc",
310
- label: "JSDoc on changed public functions",
311
- prompt: "Ensure all changed public functions, classes, and module exports have accurate JSDoc comments (description + @param + @returns where applicable).",
312
- trigger: (p) => p.hasTypeScript || p.hasReact
313
- },
314
- {
315
- id: "build",
316
- label: "Production build passes",
317
- cmd: pmScript(profile, "build", "echo 'No build script detected'"),
318
- trigger: (p) => Boolean(p.scripts["build"])
319
- },
320
- // ── Python ───────────────────────────────────────────────────────────────
321
- {
322
- id: "python-lint",
323
- label: "Ruff \u2014 Python linting",
324
- cmd: "ruff check .",
325
- when: "python",
326
- trigger: (p) => p.hasPython && p.hasRuff
327
- },
328
- {
329
- id: "python-test",
330
- label: "Pytest \u2014 Python unit tests",
331
- cmd: "pytest",
332
- when: "python",
333
- trigger: (p) => p.hasPython && p.hasPytest
334
- },
335
- // ── Go ───────────────────────────────────────────────────────────────────
336
- {
337
- id: "go-test",
338
- label: "Go tests",
339
- cmd: "go test ./...",
340
- when: "go",
341
- trigger: (p) => p.hasGo
342
- },
343
- {
344
- id: "go-vet",
345
- label: "Go vet",
346
- cmd: "go vet ./...",
347
- when: "go",
348
- trigger: (p) => p.hasGo
349
- },
350
- // ── Database ─────────────────────────────────────────────────────────────
351
- {
352
- id: "db-migrations",
353
- label: "Database migration for schema changes",
354
- when: "database",
355
- prompt: "If schema or migration files changed, ensure the appropriate migration was generated with your ORM tool (e.g. `prisma migrate dev`, `alembic revision`, `rails db:migrate`) and committed alongside the schema change.",
356
- trigger: (p) => p.hasPrisma || p.hasAlembic || p.hasMigrations || p.hasDrizzle
357
- },
358
- {
359
- id: "prisma-format",
360
- label: "Prisma schema format check",
361
- when: "prisma",
362
- cmd: "npx prisma format --check 2>/dev/null || npx prisma format",
363
- conditionId: "has-prisma",
364
- condition: {
365
- id: "has-prisma",
366
- operator: "file_exists",
367
- path: "prisma/schema.prisma"
368
- },
369
- trigger: (p) => p.hasPrisma
370
- },
371
- // ── OpenAPI ──────────────────────────────────────────────────────────────
372
- {
373
- id: "openapi-sync",
374
- label: "OpenAPI spec updated for API changes",
375
- when: "backend",
376
- conditionId: "has-openapi",
377
- condition: {
378
- id: "has-openapi",
379
- operator: "file_exists",
380
- path: "openapi.yaml"
381
- },
382
- prompt: "If any API routes were added or changed, update openapi.yaml (or openapi.json) to reflect the new endpoints, request/response shapes, and error codes.",
383
- trigger: (p) => p.hasOpenApi
384
- },
385
- // ── Infra ─────────────────────────────────────────────────────────────────
386
- {
387
- id: "docker-build",
388
- label: "Docker build passes",
389
- when: "infra",
390
- cmd: "docker build . --quiet -t app:ci",
391
- conditionId: "has-dockerfile",
392
- condition: {
393
- id: "has-dockerfile",
394
- operator: "file_exists",
395
- path: "Dockerfile"
396
- },
397
- trigger: (p) => p.hasDocker
398
- },
399
- {
400
- id: "terraform-validate",
401
- label: "Terraform validate",
402
- when: "infra",
403
- cmd: "terraform validate",
404
- trigger: (p) => p.hasTerraform
405
- },
406
- // ── Frontend ─────────────────────────────────────────────────────────────
407
- {
408
- id: "i18n",
409
- label: "i18n \u2014 no hardcoded user-facing strings",
410
- when: "frontend",
411
- prompt: "Confirm all user-visible strings are wrapped in the project's i18n function (e.g. `t()`, `useTranslation`, `<Trans>`) and that locale files are updated for any new copy.",
412
- trigger: (p) => p.hasNext && p.hasReact
467
+
468
+ // src/commands/update.ts
469
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
470
+ import chalk4 from "chalk";
471
+ import ora3 from "ora";
472
+ import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
473
+ import {
474
+ buildHookJson as buildHookJson2,
475
+ buildCheckScript as buildCheckScript2,
476
+ buildConfigJson as buildConfigJson2,
477
+ buildEngine as buildEngine2
478
+ } from "@holdpoint/engine-copilot";
479
+ import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
480
+ import { buildEngine as buildCursorEngine2 } from "@holdpoint/engine-cursor";
481
+ import {
482
+ buildHooksJson as buildCodexHooksJson2,
483
+ buildCheckScript as buildCodexCheckScript2,
484
+ spliceAgentsMd as spliceAgentsMd2
485
+ } from "@holdpoint/engine-codex";
486
+ async function updateCommand() {
487
+ if (!existsSync5("checks.yaml")) {
488
+ console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
489
+ process.exit(1);
490
+ }
491
+ const spinner = ora3("Updating Holdpoint engine files\u2026").start();
492
+ const config = parseHoldpointYaml4(readFileSync5("checks.yaml", "utf8"));
493
+ const detected = detectInstalledAgents();
494
+ const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
495
+ const generatedDir = ".github/holdpoint/generated";
496
+ mkdirSync3(generatedDir, { recursive: true });
497
+ writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
498
+ if (agents.includes("copilot")) {
499
+ const hooksDir = ".github/hooks";
500
+ mkdirSync3(hooksDir, { recursive: true });
501
+ writeFileSync3(`${hooksDir}/holdpoint.json`, buildHookJson2(config), "utf8");
502
+ writeFileSync3(`${hooksDir}/holdpoint-check.mjs`, buildCheckScript2(config), "utf8");
503
+ const extDir = ".github/extensions/holdpoint";
504
+ mkdirSync3(extDir, { recursive: true });
505
+ writeFileSync3(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
506
+ }
507
+ if (agents.includes("claude")) {
508
+ mkdirSync3(".claude", { recursive: true });
509
+ const settingsPath = ".claude/settings.json";
510
+ let existing = {};
511
+ if (existsSync5(settingsPath)) {
512
+ try {
513
+ existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
514
+ } catch {
515
+ }
413
516
  }
414
- ];
517
+ const hooks = JSON.parse(buildClaudeEngineJson2(config));
518
+ writeFileSync3(
519
+ settingsPath,
520
+ JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2) + "\n"
521
+ );
522
+ }
523
+ if (agents.includes("cursor")) {
524
+ const cursorRules = buildCursorEngine2(config);
525
+ const cursorPath = ".cursorrules";
526
+ if (existsSync5(cursorPath)) {
527
+ const content = readFileSync5(cursorPath, "utf8");
528
+ const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
529
+ const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
530
+ if (start !== -1 && end !== -1) {
531
+ const afterEnd = content.indexOf("\n", end);
532
+ const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
533
+ writeFileSync3(cursorPath, updated);
534
+ } else {
535
+ writeFileSync3(cursorPath, content + "\n" + cursorRules);
536
+ }
537
+ }
538
+ }
539
+ if (agents.includes("codex")) {
540
+ mkdirSync3(".codex", { recursive: true });
541
+ writeFileSync3(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
542
+ writeFileSync3(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
543
+ const agentsMdPath = "AGENTS.md";
544
+ const existing = existsSync5(agentsMdPath) ? readFileSync5(agentsMdPath, "utf8") : "";
545
+ writeFileSync3(agentsMdPath, spliceAgentsMd2(existing, config), "utf8");
546
+ }
547
+ spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
415
548
  }
416
549
 
417
- // src/evolve/dead-checker.ts
550
+ // src/commands/build.ts
551
+ import { createServer } from "http";
552
+ import { createReadStream, existsSync as existsSync6 } from "fs";
553
+ import { join as join3, extname, dirname as dirname2 } from "path";
554
+ import { fileURLToPath as fileURLToPath2 } from "url";
418
555
  import { execSync as execSync2 } from "child_process";
419
- import { readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
420
- import { join as join3 } from "path";
556
+ import chalk5 from "chalk";
557
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
558
+ var MIME = {
559
+ ".html": "text/html; charset=utf-8",
560
+ ".js": "text/javascript",
561
+ ".mjs": "text/javascript",
562
+ ".css": "text/css",
563
+ ".svg": "image/svg+xml",
564
+ ".png": "image/png",
565
+ ".ico": "image/x-icon",
566
+ ".woff": "font/woff",
567
+ ".woff2": "font/woff2",
568
+ ".ttf": "font/ttf",
569
+ ".json": "application/json"
570
+ };
571
+ function serveFile(res, filePath) {
572
+ const mime = MIME[extname(filePath)] ?? "application/octet-stream";
573
+ res.writeHead(200, { "Content-Type": mime });
574
+ createReadStream(filePath).pipe(res);
575
+ }
576
+ function handleRequest(req, res, uiDir) {
577
+ const url = (req.url ?? "/").split("?")[0] ?? "/";
578
+ if (url === "/__holdpoint/initial-yaml") {
579
+ const checksPath = join3(process.cwd(), "checks.yaml");
580
+ if (existsSync6(checksPath)) {
581
+ res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
582
+ createReadStream(checksPath).pipe(res);
583
+ } else {
584
+ res.writeHead(404, { "Content-Type": "text/plain" });
585
+ res.end("checks.yaml not found in current directory");
586
+ }
587
+ return;
588
+ }
589
+ if (url === "/__holdpoint/initial-reports") {
590
+ const reportsPath = join3(process.cwd(), ".holdpoint", "check-reports.json");
591
+ if (existsSync6(reportsPath)) {
592
+ res.writeHead(200, { "Content-Type": "application/json" });
593
+ createReadStream(reportsPath).pipe(res);
594
+ } else {
595
+ res.writeHead(404, { "Content-Type": "text/plain" });
596
+ res.end("No check reports found");
597
+ }
598
+ return;
599
+ }
600
+ const candidate = join3(uiDir, url === "/" ? "index.html" : url);
601
+ const filePath = existsSync6(candidate) ? candidate : join3(uiDir, "index.html");
602
+ serveFile(res, filePath);
603
+ }
604
+ async function buildCommand() {
605
+ const port = 4321;
606
+ const uiDir = join3(__dirname2, "builder-ui");
607
+ if (!existsSync6(uiDir)) {
608
+ console.error(chalk5.red("\u2717 Builder UI not found.\n"));
609
+ console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
610
+ console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
611
+ process.exit(1);
612
+ }
613
+ const server = createServer((req, res) => handleRequest(req, res, uiDir));
614
+ await new Promise((resolve, reject) => {
615
+ server.listen(port, () => {
616
+ console.log(
617
+ `
618
+ ${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
619
+ );
620
+ console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
621
+ console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
622
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
623
+ try {
624
+ execSync2(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
625
+ } catch {
626
+ }
627
+ });
628
+ server.on("error", reject);
629
+ process.on("SIGINT", () => {
630
+ console.log(chalk5.dim("\n Stopping builder\u2026"));
631
+ server.close(() => resolve());
632
+ });
633
+ });
634
+ }
635
+
636
+ // src/commands/evolve.ts
637
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
638
+ import { execSync as execSync5 } from "child_process";
639
+ import chalk6 from "chalk";
640
+ import ora4 from "ora";
641
+ import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
642
+
643
+ // src/evolve/scanner.ts
644
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync } from "fs";
645
+ import { join as join4 } from "path";
646
+ import { execSync as execSync3 } from "child_process";
647
+ function tryReadJson(path) {
648
+ try {
649
+ return JSON.parse(readFileSync6(path, "utf8"));
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ function tryReadText(path) {
655
+ try {
656
+ return readFileSync6(path, "utf8");
657
+ } catch {
658
+ return "";
659
+ }
660
+ }
661
+ function scanProject(cwd = process.cwd()) {
662
+ const exists = (p) => existsSync7(join4(cwd, p));
663
+ const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
664
+ const pkg = tryReadJson(join4(cwd, "package.json"));
665
+ const scripts = pkg?.scripts ?? {};
666
+ const deps = /* @__PURE__ */ new Set([
667
+ ...Object.keys(pkg?.dependencies ?? {}),
668
+ ...Object.keys(pkg?.devDependencies ?? {})
669
+ ]);
670
+ const pyprojectText = tryReadText(join4(cwd, "pyproject.toml"));
671
+ const requirementsText = tryReadText(join4(cwd, "requirements.txt"));
672
+ const pipfileText = tryReadText(join4(cwd, "Pipfile"));
673
+ const allPyText = pyprojectText + requirementsText + pipfileText;
674
+ const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
675
+ const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
676
+ const hasAlembic = allPyText.includes("alembic") || deps.has("alembic");
677
+ let rootFiles = [];
678
+ try {
679
+ rootFiles = readdirSync(cwd);
680
+ } catch {
681
+ }
682
+ const hasDocker = rootFiles.some((f) => f === "Dockerfile" || f.startsWith("Dockerfile.")) || exists("docker-compose.yml") || exists("docker-compose.yaml") || exists("docker-compose.dev.yml");
683
+ const hasTerraform = rootFiles.some((f) => f.endsWith(".tf")) || exists("terraform") || exists("infra");
684
+ return {
685
+ // Languages
686
+ hasTypeScript: exists("tsconfig.json") || deps.has("typescript"),
687
+ hasPython: Boolean(pyprojectText) || Boolean(requirementsText) || Boolean(pipfileText) || exists("setup.py"),
688
+ hasGo: exists("go.mod"),
689
+ hasRust: exists("Cargo.toml"),
690
+ hasJava: exists("pom.xml") || exists("build.gradle") || exists("build.gradle.kts"),
691
+ hasRuby: exists("Gemfile"),
692
+ // Frameworks
693
+ hasNext: exists("next.config.ts") || exists("next.config.js") || exists("next.config.mjs") || deps.has("next"),
694
+ hasReact: deps.has("react"),
695
+ // Linting
696
+ hasEslint: exists("eslint.config.js") || exists("eslint.config.ts") || exists("eslint.config.mjs") || exists(".eslintrc.js") || exists(".eslintrc.json") || exists(".eslintrc.yml") || exists(".eslintrc.yaml") || deps.has("eslint"),
697
+ hasBiome: exists("biome.json") || exists("biome.jsonc") || deps.has("@biomejs/biome"),
698
+ hasRuff,
699
+ hasPrettier: exists("prettier.config.js") || exists("prettier.config.ts") || exists("prettier.config.mjs") || exists(".prettierrc") || exists(".prettierrc.json") || deps.has("prettier"),
700
+ // Testing
701
+ hasVitest: deps.has("vitest") || Boolean(scripts["test"]?.includes("vitest")),
702
+ hasJest: deps.has("jest") || Boolean(scripts["test"]?.includes("jest")),
703
+ hasPytest,
704
+ // DB
705
+ hasPrisma: exists("prisma/schema.prisma") || deps.has("@prisma/client"),
706
+ hasDrizzle: deps.has("drizzle-orm"),
707
+ hasMigrations: exists("migrations") || exists("db/migrations") || exists("database/migrations"),
708
+ hasAlembic,
709
+ // Infra
710
+ hasDocker,
711
+ hasTerraform,
712
+ hasKubernetes: exists("k8s") || exists("kubernetes") || exists("helm"),
713
+ // API
714
+ hasOpenApi: exists("openapi.yaml") || exists("openapi.yml") || exists("openapi.json") || exists("api/openapi.yaml"),
715
+ // CI
716
+ hasGithubActions: exists(".github/workflows"),
717
+ packageManager,
718
+ scripts,
719
+ deps
720
+ };
721
+ }
722
+
723
+ // src/evolve/dead-checker.ts
724
+ import { execSync as execSync4 } from "child_process";
725
+ import { readdirSync as readdirSync2, existsSync as existsSync8 } from "fs";
726
+ import { join as join5 } from "path";
421
727
  var NAMED_SCOPES = /* @__PURE__ */ new Set([
422
728
  "frontend",
423
729
  "backend",
@@ -462,7 +768,7 @@ function walkDir(dir, root, depth, maxDepth) {
462
768
  const results = [];
463
769
  for (const entry of entries) {
464
770
  if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
465
- const full = join3(dir, entry);
771
+ const full = join5(dir, entry);
466
772
  const rel = full.slice(root.length + 1);
467
773
  results.push(rel);
468
774
  const children = walkDir(full, root, depth + 1, maxDepth);
@@ -472,7 +778,7 @@ function walkDir(dir, root, depth, maxDepth) {
472
778
  }
473
779
  function getRepoFiles(cwd) {
474
780
  try {
475
- const out = execSync2("git ls-files", {
781
+ const out = execSync4("git ls-files", {
476
782
  cwd,
477
783
  encoding: "utf8",
478
784
  stdio: ["pipe", "pipe", "ignore"]
@@ -500,321 +806,196 @@ function detectStaleChecks(config, repoFiles) {
500
806
  const regexStr = patternAlias ? userPatterns[patternAlias] : check.when;
501
807
  let re;
502
808
  try {
503
- re = new RegExp(regexStr);
504
- } catch {
505
- stale.push({ check, reason: `Invalid regex: '${regexStr}'` });
506
- continue;
507
- }
508
- const matches = repoFiles.filter((f) => re.test(f));
509
- if (matches.length === 0) {
510
- const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
511
- const suggestedConditionPath = extractPathFromRegex(regexStr);
512
- const pathGone = !suggestedConditionPath || !existsSync4(join3(process.cwd(), suggestedConditionPath));
513
- if (pathGone) {
514
- stale.push({
515
- check,
516
- reason: `${label} matches 0 files in the repo`,
517
- ...suggestedConditionPath ? { suggestedConditionPath } : {}
518
- });
519
- }
520
- }
521
- }
522
- return stale;
523
- }
524
-
525
- // src/commands/check.ts
526
- function getStagedFiles() {
527
- try {
528
- const out = execSync3("git diff --cached --name-only", {
529
- encoding: "utf8",
530
- stdio: ["pipe", "pipe", "ignore"]
531
- });
532
- return out.trim().split("\n").filter(Boolean);
533
- } catch {
534
- return [];
535
- }
536
- }
537
- function getAllChangedFiles() {
538
- try {
539
- const out = execSync3("git diff --name-only HEAD", {
540
- encoding: "utf8",
541
- stdio: ["pipe", "pipe", "ignore"]
542
- });
543
- return out.trim().split("\n").filter(Boolean);
544
- } catch {
545
- return [];
546
- }
547
- }
548
- async function checkCommand(options) {
549
- if (!existsSync5("checks.yaml")) {
550
- console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
551
- process.exit(1);
552
- }
553
- const yamlContent = readFileSync3("checks.yaml", "utf8");
554
- let config;
555
- try {
556
- config = parseHoldpointYaml2(yamlContent);
557
- } catch (err) {
558
- console.error(chalk2.red("Invalid checks.yaml:"), err.message);
559
- process.exit(1);
560
- }
561
- const changedFiles = options.staged ? getStagedFiles() : getAllChangedFiles();
562
- const guides = Object.entries(config.context?.guides ?? {});
563
- if (guides.length > 0) {
564
- console.log(chalk2.cyan("\nProject guides:"));
565
- for (const [key, text] of guides) {
566
- console.log(chalk2.bold(` ${key}:`), chalk2.dim(String(text).trim()));
567
- }
568
- console.log("");
569
- }
570
- if (changedFiles.length === 0) {
571
- console.log(chalk2.yellow("No changed files detected. Running all checks with no file filter."));
572
- }
573
- const taskCount = config.checks.filter((c) => c.cmd !== void 0).length;
574
- const spinner = ora2(`Running ${taskCount} task(s)\u2026`).start();
575
- const effectiveFiles = changedFiles.length > 0 ? changedFiles : ["__all__"];
576
- const results = runDeterministicChecks(config, effectiveFiles);
577
- const runDrift = matchesWhen("structural", effectiveFiles);
578
- if (runDrift) {
579
- const profile = scanProject();
580
- const existingIds = new Set(config.checks.map((c) => c.id));
581
- const templates = getTemplates(profile);
582
- const proposals = templates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
583
- const repoFiles = getRepoFiles(process.cwd());
584
- const staleChecks = detectStaleChecks(config, repoFiles);
585
- if (proposals.length > 0 || staleChecks.length > 0) {
586
- const lines = [];
587
- if (proposals.length > 0) {
588
- lines.push(`${proposals.length} new check(s) available for your project stack:`);
589
- for (const p of proposals) lines.push(` + ${p.label}`);
590
- }
591
- if (staleChecks.length > 0) {
592
- lines.push(`${staleChecks.length} stale check(s) no longer match your project:`);
593
- for (const s of staleChecks) lines.push(` - ${s.check.label}: ${s.reason}`);
594
- }
595
- lines.push("\nRun: npx holdpoint evolve --apply");
596
- results.push({
597
- check: { id: "__holdpoint_evolve__", label: "Evolve checks with project structure" },
598
- status: "fail",
599
- output: lines.join("\n")
600
- });
601
- }
602
- }
603
- const passed = results.filter((r) => r.status === "pass");
604
- const failed = results.filter((r) => r.status === "fail");
605
- const skipped = results.filter((r) => r.status === "skip");
606
- spinner.stop();
607
- for (const result of results) {
608
- printResult(result);
609
- }
610
- console.log("");
611
- console.log(
612
- [
613
- chalk2.green(`\u2713 ${passed.length} passed`),
614
- failed.length > 0 ? chalk2.red(`\u2717 ${failed.length} failed`) : "",
615
- skipped.length > 0 ? chalk2.gray(`\u25CC ${skipped.length} skipped`) : ""
616
- ].filter(Boolean).join(" ")
617
- );
618
- const promptChecks = config.checks.filter(
619
- (c) => c.prompt !== void 0 && matchesWhen(c.when, changedFiles.length > 0 ? changedFiles : ["__all__"], config.patterns)
620
- );
621
- if (promptChecks.length > 0) {
622
- console.log(`
623
- ${chalk2.cyan("Agent prompts to act on:")}`);
624
- for (const c of promptChecks) {
625
- console.log(` ${chalk2.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
626
- }
627
- }
628
- if (failed.length > 0) {
629
- process.exit(1);
630
- }
631
- }
632
- function printResult(result) {
633
- const icon = result.status === "pass" ? chalk2.green("\u2713") : result.status === "fail" ? chalk2.red("\u2717") : result.status === "skip" ? chalk2.gray("\u25CC") : chalk2.yellow("\u2026");
634
- const label = result.check.label;
635
- console.log(`${icon} ${label}`);
636
- if (result.status === "fail" && result.output) {
637
- const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
638
- console.log(chalk2.dim(trimmed.replace(/^/gm, " ")));
639
- }
640
- if (result.status === "skip" && result.skipReason) {
641
- console.log(chalk2.dim(` ${result.skipReason}`));
642
- }
643
- }
644
-
645
- // src/commands/validate.ts
646
- import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
647
- import chalk3 from "chalk";
648
- import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
649
- async function validateCommand() {
650
- if (!existsSync6("checks.yaml")) {
651
- console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
652
- process.exit(1);
653
- }
654
- const text = readFileSync4("checks.yaml", "utf8");
655
- let config;
656
- try {
657
- config = parseHoldpointYaml3(text);
658
- } catch (err) {
659
- console.error(chalk3.red("Parse error:"), err.message);
660
- process.exit(1);
661
- }
662
- const result = validateConfig(config);
663
- if (result.valid) {
664
- console.log(chalk3.green("\u2713 checks.yaml is valid"));
665
- console.log(
666
- chalk3.dim(
667
- ` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
668
- )
669
- );
670
- } else {
671
- console.error(chalk3.red("\u2717 checks.yaml has errors:"));
672
- for (const err of result.errors) {
673
- console.error(` ${chalk3.yellow(err.path)}: ${err.message}`);
674
- }
675
- process.exit(1);
676
- }
677
- }
678
-
679
- // src/commands/update.ts
680
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
681
- import chalk4 from "chalk";
682
- import ora3 from "ora";
683
- import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
684
- import { buildHookJson as buildHookJson2, buildCheckScript as buildCheckScript2, buildConfigJson as buildConfigJson2 } from "@holdpoint/engine-copilot";
685
- import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
686
- import { buildEngine as buildCursorEngine2 } from "@holdpoint/engine-cursor";
687
- async function updateCommand() {
688
- if (!existsSync7("checks.yaml")) {
689
- console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
690
- process.exit(1);
691
- }
692
- const spinner = ora3("Updating Holdpoint engine files\u2026").start();
693
- const agent = detectAgent();
694
- const config = parseHoldpointYaml4(readFileSync5("checks.yaml", "utf8"));
695
- const generatedDir = ".github/holdpoint/generated";
696
- mkdirSync2(generatedDir, { recursive: true });
697
- writeFileSync2(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
698
- if (agent === "copilot" || agent === "unknown") {
699
- const hooksDir = ".github/hooks";
700
- mkdirSync2(hooksDir, { recursive: true });
701
- writeFileSync2(`${hooksDir}/holdpoint.json`, buildHookJson2(config), "utf8");
702
- writeFileSync2(`${hooksDir}/holdpoint-check.mjs`, buildCheckScript2(config), "utf8");
703
- spinner.text = `Updated ${chalk4.green(".github/hooks/holdpoint.json")} and ${chalk4.green(".github/hooks/holdpoint-check.mjs")}`;
704
- }
705
- if (agent === "claude") {
706
- mkdirSync2(".claude", { recursive: true });
707
- const settingsPath = ".claude/settings.json";
708
- let existing = {};
709
- if (existsSync7(settingsPath)) {
710
- try {
711
- existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
712
- } catch {
713
- }
809
+ re = new RegExp(regexStr);
810
+ } catch {
811
+ stale.push({ check, reason: `Invalid regex: '${regexStr}'` });
812
+ continue;
714
813
  }
715
- const hooks = JSON.parse(buildClaudeEngineJson2(config));
716
- writeFileSync2(settingsPath, JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2));
717
- }
718
- if (agent === "cursor") {
719
- const cursorRules = buildCursorEngine2(config);
720
- const cursorPath = ".cursorrules";
721
- if (existsSync7(cursorPath)) {
722
- const content = readFileSync5(cursorPath, "utf8");
723
- const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
724
- const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
725
- if (start !== -1 && end !== -1) {
726
- const afterEnd = content.indexOf("\n", end);
727
- const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
728
- writeFileSync2(cursorPath, updated);
729
- } else {
730
- writeFileSync2(cursorPath, content + "\n" + cursorRules);
814
+ const matches = repoFiles.filter((f) => re.test(f));
815
+ if (matches.length === 0) {
816
+ const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
817
+ const suggestedConditionPath = extractPathFromRegex(regexStr);
818
+ const pathGone = !suggestedConditionPath || !existsSync8(join5(process.cwd(), suggestedConditionPath));
819
+ if (pathGone) {
820
+ stale.push({
821
+ check,
822
+ reason: `${label} matches 0 files in the repo`,
823
+ ...suggestedConditionPath ? { suggestedConditionPath } : {}
824
+ });
731
825
  }
732
826
  }
733
827
  }
734
- spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
828
+ return stale;
735
829
  }
736
830
 
737
- // src/commands/build.ts
738
- import { createServer } from "http";
739
- import { createReadStream, existsSync as existsSync8 } from "fs";
740
- import { join as join4, extname, dirname as dirname2 } from "path";
741
- import { fileURLToPath as fileURLToPath2 } from "url";
742
- import { execSync as execSync4 } from "child_process";
743
- import chalk5 from "chalk";
744
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
745
- var MIME = {
746
- ".html": "text/html; charset=utf-8",
747
- ".js": "text/javascript",
748
- ".mjs": "text/javascript",
749
- ".css": "text/css",
750
- ".svg": "image/svg+xml",
751
- ".png": "image/png",
752
- ".ico": "image/x-icon",
753
- ".woff": "font/woff",
754
- ".woff2": "font/woff2",
755
- ".ttf": "font/ttf",
756
- ".json": "application/json"
757
- };
758
- function serveFile(res, filePath) {
759
- const mime = MIME[extname(filePath)] ?? "application/octet-stream";
760
- res.writeHead(200, { "Content-Type": mime });
761
- createReadStream(filePath).pipe(res);
831
+ // src/evolve/templates.ts
832
+ function pmScript(profile, script, fallback) {
833
+ if (!profile.scripts[script]) return fallback;
834
+ if (profile.packageManager === "npm") return `npm run ${script}`;
835
+ return `${profile.packageManager} ${script}`;
762
836
  }
763
- function handleRequest(req, res, uiDir) {
764
- const url = (req.url ?? "/").split("?")[0] ?? "/";
765
- if (url === "/__holdpoint/initial-yaml") {
766
- const checksPath = join4(process.cwd(), "checks.yaml");
767
- if (existsSync8(checksPath)) {
768
- res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
769
- createReadStream(checksPath).pipe(res);
770
- } else {
771
- res.writeHead(404, { "Content-Type": "text/plain" });
772
- res.end("checks.yaml not found in current directory");
837
+ function getTemplates(profile) {
838
+ return [
839
+ // ── Universal checks (always proposed for any project) ──────────────────
840
+ {
841
+ id: "git-commit",
842
+ label: "Commit all changes before finishing",
843
+ cmd: 'git rev-parse --is-inside-work-tree 2>/dev/null || exit 0; [ -z "$(git status --porcelain)" ] && exit 0; git status --short; exit 1',
844
+ trigger: () => true
845
+ },
846
+ {
847
+ id: "changelog-update",
848
+ label: "Add a CHANGELOG.md entry for this session",
849
+ prompt: "Before committing, add an entry to CHANGELOG.md describing what was done. Use Keep a Changelog format \u2014 add under ## [Unreleased] (create the file and that section if absent). Group entries as Added, Changed, Fixed, or Removed. Be concise but specific. The entry text will serve as the commit message.",
850
+ trigger: () => true
851
+ },
852
+ {
853
+ id: "readme-sync",
854
+ label: "Update README.md if user-facing changes were made",
855
+ prompt: "If you added, changed, or removed user-facing functionality \u2014 CLI commands, configuration options, public APIs, or significant new features \u2014 update README.md to reflect those changes.",
856
+ trigger: () => true
857
+ },
858
+ {
859
+ id: "no-todos",
860
+ label: "No TODO/FIXME left in changed code",
861
+ prompt: "Scan the files you changed for any TODO, FIXME, HACK, or XXX comments. Either resolve them before finishing or convert them to GitHub issues. Don't leave incomplete work silently behind.",
862
+ trigger: () => true
863
+ },
864
+ // ── TypeScript / JavaScript ──────────────────────────────────────────────
865
+ {
866
+ id: "typecheck",
867
+ label: "TypeScript type check",
868
+ cmd: pmScript(profile, "typecheck", "npx tsc --noEmit"),
869
+ trigger: (p) => p.hasTypeScript
870
+ },
871
+ {
872
+ id: "lint",
873
+ label: "Lint codebase",
874
+ cmd: profile.hasEslint ? pmScript(profile, "lint", "npx eslint .") : profile.hasBiome ? pmScript(profile, "lint", "npx @biomejs/biome check .") : pmScript(profile, "lint", "echo 'No linter detected'"),
875
+ trigger: (p) => p.hasEslint || p.hasBiome
876
+ },
877
+ {
878
+ id: "format-check",
879
+ label: "Prettier \u2014 format check",
880
+ cmd: pmScript(profile, "format:check", "npx prettier --check ."),
881
+ trigger: (p) => p.hasPrettier
882
+ },
883
+ {
884
+ id: "test",
885
+ label: "Unit tests",
886
+ cmd: profile.hasVitest ? pmScript(profile, "test", "npx vitest run") : pmScript(profile, "test", "npx jest --passWithNoTests"),
887
+ trigger: (p) => p.hasVitest || p.hasJest
888
+ },
889
+ {
890
+ id: "jsdoc",
891
+ label: "JSDoc on changed public functions",
892
+ prompt: "Ensure all changed public functions, classes, and module exports have accurate JSDoc comments (description + @param + @returns where applicable).",
893
+ trigger: (p) => p.hasTypeScript || p.hasReact
894
+ },
895
+ {
896
+ id: "build",
897
+ label: "Production build passes",
898
+ cmd: pmScript(profile, "build", "echo 'No build script detected'"),
899
+ trigger: (p) => Boolean(p.scripts["build"])
900
+ },
901
+ // ── Python ───────────────────────────────────────────────────────────────
902
+ {
903
+ id: "python-lint",
904
+ label: "Ruff \u2014 Python linting",
905
+ cmd: "ruff check .",
906
+ when: "python",
907
+ trigger: (p) => p.hasPython && p.hasRuff
908
+ },
909
+ {
910
+ id: "python-test",
911
+ label: "Pytest \u2014 Python unit tests",
912
+ cmd: "pytest",
913
+ when: "python",
914
+ trigger: (p) => p.hasPython && p.hasPytest
915
+ },
916
+ // ── Go ───────────────────────────────────────────────────────────────────
917
+ {
918
+ id: "go-test",
919
+ label: "Go tests",
920
+ cmd: "go test ./...",
921
+ when: "go",
922
+ trigger: (p) => p.hasGo
923
+ },
924
+ {
925
+ id: "go-vet",
926
+ label: "Go vet",
927
+ cmd: "go vet ./...",
928
+ when: "go",
929
+ trigger: (p) => p.hasGo
930
+ },
931
+ // ── Database ─────────────────────────────────────────────────────────────
932
+ {
933
+ id: "db-migrations",
934
+ label: "Database migration for schema changes",
935
+ when: "database",
936
+ prompt: "If schema or migration files changed, ensure the appropriate migration was generated with your ORM tool (e.g. `prisma migrate dev`, `alembic revision`, `rails db:migrate`) and committed alongside the schema change.",
937
+ trigger: (p) => p.hasPrisma || p.hasAlembic || p.hasMigrations || p.hasDrizzle
938
+ },
939
+ {
940
+ id: "prisma-format",
941
+ label: "Prisma schema format check",
942
+ when: "prisma",
943
+ cmd: "npx prisma format --check 2>/dev/null || npx prisma format",
944
+ conditionId: "has-prisma",
945
+ condition: {
946
+ id: "has-prisma",
947
+ operator: "file_exists",
948
+ path: "prisma/schema.prisma"
949
+ },
950
+ trigger: (p) => p.hasPrisma
951
+ },
952
+ // ── OpenAPI ──────────────────────────────────────────────────────────────
953
+ {
954
+ id: "openapi-sync",
955
+ label: "OpenAPI spec updated for API changes",
956
+ when: "backend",
957
+ conditionId: "has-openapi",
958
+ condition: {
959
+ id: "has-openapi",
960
+ operator: "file_exists",
961
+ path: "openapi.yaml"
962
+ },
963
+ prompt: "If any API routes were added or changed, update openapi.yaml (or openapi.json) to reflect the new endpoints, request/response shapes, and error codes.",
964
+ trigger: (p) => p.hasOpenApi
965
+ },
966
+ // ── Infra ─────────────────────────────────────────────────────────────────
967
+ {
968
+ id: "docker-build",
969
+ label: "Docker build passes",
970
+ when: "infra",
971
+ cmd: "docker build . --quiet -t app:ci",
972
+ conditionId: "has-dockerfile",
973
+ condition: {
974
+ id: "has-dockerfile",
975
+ operator: "file_exists",
976
+ path: "Dockerfile"
977
+ },
978
+ trigger: (p) => p.hasDocker
979
+ },
980
+ {
981
+ id: "terraform-validate",
982
+ label: "Terraform validate",
983
+ when: "infra",
984
+ cmd: "terraform validate",
985
+ trigger: (p) => p.hasTerraform
986
+ },
987
+ // ── Frontend ─────────────────────────────────────────────────────────────
988
+ {
989
+ id: "i18n",
990
+ label: "i18n \u2014 no hardcoded user-facing strings",
991
+ when: "frontend",
992
+ prompt: "Confirm all user-visible strings are wrapped in the project's i18n function (e.g. `t()`, `useTranslation`, `<Trans>`) and that locale files are updated for any new copy.",
993
+ trigger: (p) => p.hasNext && p.hasReact
773
994
  }
774
- return;
775
- }
776
- const candidate = join4(uiDir, url === "/" ? "index.html" : url);
777
- const filePath = existsSync8(candidate) ? candidate : join4(uiDir, "index.html");
778
- serveFile(res, filePath);
779
- }
780
- async function buildCommand() {
781
- const port = 4321;
782
- const uiDir = join4(__dirname2, "builder-ui");
783
- if (!existsSync8(uiDir)) {
784
- console.error(chalk5.red("\u2717 Builder UI not found.\n"));
785
- console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
786
- console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
787
- process.exit(1);
788
- }
789
- const server = createServer((req, res) => handleRequest(req, res, uiDir));
790
- await new Promise((resolve, reject) => {
791
- server.listen(port, () => {
792
- console.log(
793
- `
794
- ${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
795
- );
796
- console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
797
- console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
798
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
799
- try {
800
- execSync4(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
801
- } catch {
802
- }
803
- });
804
- server.on("error", reject);
805
- process.on("SIGINT", () => {
806
- console.log(chalk5.dim("\n Stopping builder\u2026"));
807
- server.close(() => resolve());
808
- });
809
- });
995
+ ];
810
996
  }
811
997
 
812
998
  // src/commands/evolve.ts
813
- import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
814
- import { execSync as execSync5 } from "child_process";
815
- import chalk6 from "chalk";
816
- import ora4 from "ora";
817
- import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
818
999
  function extractHeader(yaml) {
819
1000
  const lines = yaml.split("\n");
820
1001
  const commentLines = [];
@@ -843,7 +1024,7 @@ async function evolveCommand(options) {
843
1024
  const cwd = process.cwd();
844
1025
  const profile = scanProject(cwd);
845
1026
  const repoFiles = getRepoFiles(cwd);
846
- const yamlContent = readFileSync6("checks.yaml", "utf8");
1027
+ const yamlContent = readFileSync7("checks.yaml", "utf8");
847
1028
  let config;
848
1029
  try {
849
1030
  config = parseHoldpointYaml5(yamlContent);
@@ -916,7 +1097,7 @@ async function evolveCommand(options) {
916
1097
  console.log(
917
1098
  chalk6.red(`
918
1099
  \u2717 checks.yaml is out of sync with the project profile.`) + `
919
- Run ${chalk6.bold("npx holdpoint evolve --apply")} to apply these changes.`
1100
+ Run ${chalk6.bold("npx @holdpoint/cli@alpha evolve --apply")} to apply these changes.`
920
1101
  );
921
1102
  process.exit(1);
922
1103
  }
@@ -957,10 +1138,10 @@ async function evolveCommand(options) {
957
1138
  };
958
1139
  const header = extractHeader(yamlContent);
959
1140
  const newYaml = withHeader(header, generateYaml(updatedConfig));
960
- writeFileSync3("checks.yaml", newYaml, "utf8");
1141
+ writeFileSync4("checks.yaml", newYaml, "utf8");
961
1142
  applySpinner.text = "Running holdpoint update\u2026";
962
1143
  try {
963
- execSync5("npx holdpoint update", { stdio: "pipe" });
1144
+ execSync5("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
964
1145
  } catch {
965
1146
  applySpinner.warn(
966
1147
  chalk6.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
@@ -984,8 +1165,11 @@ function printAppliedSummary(added, wrapped) {
984
1165
 
985
1166
  // src/index.ts
986
1167
  var program = new Command();
987
- program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version("0.1.0-alpha.1");
988
- program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option("--agent <agent>", "Agent type: copilot | claude | cursor").action(initCommand);
1168
+ program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version("0.1.0-alpha.2");
1169
+ program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option(
1170
+ "--agent <agent>",
1171
+ "Agent to install for: copilot | claude | cursor | codex (default: all four)"
1172
+ ).action(initCommand);
989
1173
  program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
990
1174
  program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
991
1175
  program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);