@holdpoint/cli 0.1.0-alpha.16 → 0.1.0-alpha.18

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,20 +4,23 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
- import { execSync } from "child_process";
8
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
9
- import { join as join2, dirname as dirname2 } from "path";
7
+ import { execSync as execSync2 } from "child_process";
8
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
9
+ import { join as join2, dirname as dirname3 } from "path";
10
10
  import { fileURLToPath as fileURLToPath2 } from "url";
11
- import chalk from "chalk";
11
+ import chalk2 from "chalk";
12
12
  import ora from "ora";
13
13
  import { buildConfigJson, buildEngine } from "@holdpoint/engine-copilot";
14
14
  import { buildEngineJson as buildClaudeEngineJson } from "@holdpoint/engine-claude";
15
- import { buildEngine as buildCursorEngine } from "@holdpoint/engine-cursor";
15
+ import {
16
+ buildCheckScript as buildCursorCheckScript,
17
+ buildEngine as buildCursorEngine,
18
+ buildHooksJson as buildCursorHooksJson
19
+ } from "@holdpoint/engine-cursor";
16
20
  import {
17
21
  buildConfigToml as buildCodexConfigToml,
18
22
  buildHooksJson as buildCodexHooksJson,
19
- buildCheckScript as buildCodexCheckScript,
20
- spliceAgentsMd
23
+ buildCheckScript as buildCodexCheckScript
21
24
  } from "@holdpoint/engine-codex";
22
25
  import { parseHoldpointYaml } from "@holdpoint/yaml-core";
23
26
 
@@ -32,9 +35,17 @@ function detectInstalledAgents() {
32
35
  const agents = [];
33
36
  if (existsSync(".github/extensions/holdpoint/extension.mjs")) agents.push("copilot");
34
37
  if (existsSync(".claude/settings.json")) agents.push("claude");
38
+ if (existsSync(".cursor/hooks.json")) {
39
+ try {
40
+ if (readFileSync(".cursor/hooks.json", "utf8").includes("HOLDPOINT_MANAGED=cursor")) {
41
+ agents.push("cursor");
42
+ }
43
+ } catch {
44
+ }
45
+ }
35
46
  if (existsSync(".cursorrules")) {
36
47
  try {
37
- if (readFileSync(".cursorrules", "utf8").includes("Holdpoint Rules")) {
48
+ if (!agents.includes("cursor") && readFileSync(".cursorrules", "utf8").includes("Holdpoint Rules")) {
38
49
  agents.push("cursor");
39
50
  }
40
51
  } catch {
@@ -43,20 +54,6 @@ function detectInstalledAgents() {
43
54
  if (existsSync(".codex/holdpoint-check.mjs")) agents.push("codex");
44
55
  return agents;
45
56
  }
46
- function detectStack() {
47
- const hasNext = existsSync("next.config.ts") || existsSync("next.config.js") || existsSync("next.config.mjs");
48
- const hasTsConfig = existsSync("tsconfig.json");
49
- const hasPyproject = existsSync("pyproject.toml") || existsSync("requirements.txt") || existsSync("setup.py");
50
- const hasPrisma = existsSync("prisma/schema.prisma");
51
- const hasApi = existsSync("server") || existsSync("api") || existsSync("backend");
52
- const hasGoMod = existsSync("go.mod");
53
- if (hasNext && (hasPrisma || hasApi)) return "fullstack";
54
- if (hasNext) return "nextjs";
55
- if (hasTsConfig) return "typescript";
56
- if (hasPyproject) return "python";
57
- if (hasGoMod) return "go";
58
- return "unknown";
59
- }
60
57
 
61
58
  // src/templates.ts
62
59
  import { copyFileSync, existsSync as existsSync2, writeFileSync } from "fs";
@@ -90,20 +87,239 @@ function ensureBundledFile(outputPath, templateFilename, fallbackContent) {
90
87
  return true;
91
88
  }
92
89
 
90
+ // src/lib/preflight.ts
91
+ import { execSync } from "child_process";
92
+ import chalk from "chalk";
93
+ function silentExec(cmd) {
94
+ try {
95
+ const stdout = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString();
96
+ return { ok: true, stdout };
97
+ } catch {
98
+ return { ok: false, stdout: "" };
99
+ }
100
+ }
101
+ function checkCopilot() {
102
+ const gh = silentExec("gh --version");
103
+ if (!gh.ok) {
104
+ return {
105
+ agent: "copilot",
106
+ status: "action_required",
107
+ message: "GitHub CLI not found on PATH",
108
+ command: "brew install gh # or: https://cli.github.com/"
109
+ };
110
+ }
111
+ const copilot = silentExec("gh copilot --version");
112
+ if (!copilot.ok) {
113
+ return {
114
+ agent: "copilot",
115
+ status: "action_required",
116
+ message: "Copilot CLI extension not installed",
117
+ command: "gh extension install github/gh-copilot"
118
+ };
119
+ }
120
+ return {
121
+ agent: "copilot",
122
+ status: "action_required",
123
+ message: "Copilot CLI detected \u2014 experimental mode required for EXTENSIONS",
124
+ command: "Inside Copilot CLI, run: /experimental on"
125
+ };
126
+ }
127
+ function checkClaude() {
128
+ const claude = silentExec("claude --version");
129
+ if (!claude.ok) {
130
+ return {
131
+ agent: "claude",
132
+ status: "unknown",
133
+ message: "Claude Code binary not on PATH (hooks still written for when it is)"
134
+ };
135
+ }
136
+ return {
137
+ agent: "claude",
138
+ status: "ok",
139
+ message: "Claude Code detected \u2014 hooks installed at .claude/settings.json"
140
+ };
141
+ }
142
+ function checkCursor() {
143
+ return {
144
+ agent: "cursor",
145
+ status: "ok",
146
+ message: "Cursor \u2014 .cursor/hooks.json gate + .cursor/rules breadcrumb installed",
147
+ docs: "https://holdpoint.dev/docs#cursor"
148
+ };
149
+ }
150
+ function checkCodex() {
151
+ const codex = silentExec("codex --version");
152
+ if (!codex.ok) {
153
+ return {
154
+ agent: "codex",
155
+ status: "action_required",
156
+ message: "Codex CLI not found on PATH",
157
+ command: "Install Codex: https://github.com/openai/codex"
158
+ };
159
+ }
160
+ return {
161
+ agent: "codex",
162
+ status: "action_required",
163
+ message: "Codex detected \u2014 project-level hooks require trust approval",
164
+ command: "In the Codex TUI: codex trust (or /hooks to review)"
165
+ };
166
+ }
167
+ var CHECKS = {
168
+ copilot: checkCopilot,
169
+ claude: checkClaude,
170
+ cursor: checkCursor,
171
+ codex: checkCodex
172
+ };
173
+ function runPreflight(agents) {
174
+ return agents.flatMap((agent) => {
175
+ const check = CHECKS[agent];
176
+ return check ? [check()] : [];
177
+ });
178
+ }
179
+ function printPreflight(results) {
180
+ if (results.length === 0) return;
181
+ const ok = results.filter((r) => r.status === "ok");
182
+ const unknown = results.filter((r) => r.status === "unknown");
183
+ const action = results.filter((r) => r.status === "action_required");
184
+ console.log("");
185
+ console.log(chalk.bold("Agent preflight:"));
186
+ for (const r of ok) {
187
+ console.log(` ${chalk.green("\u2713")} ${r.agent.padEnd(7)} ${chalk.dim(r.message)}`);
188
+ }
189
+ for (const r of unknown) {
190
+ console.log(` ${chalk.dim("?")} ${r.agent.padEnd(7)} ${chalk.dim(r.message)}`);
191
+ }
192
+ for (const r of action) {
193
+ console.log(` ${chalk.yellow("\u2192")} ${chalk.bold(r.agent.padEnd(7))} ${r.message}`);
194
+ if (r.command) console.log(` ${chalk.cyan(r.command)}`);
195
+ }
196
+ }
197
+
198
+ // src/claude-settings.ts
199
+ var HOLDPOINT_CLAUDE_HOOK_MARKER = "HOLDPOINT_MANAGED=claude";
200
+ function isObject(value) {
201
+ return value != null && typeof value === "object" && !Array.isArray(value);
202
+ }
203
+ function asHookArray(value) {
204
+ return Array.isArray(value) ? value : [];
205
+ }
206
+ function isManagedHookCommand(value) {
207
+ return isObject(value) && typeof value.command === "string" && value.command.includes(HOLDPOINT_CLAUDE_HOOK_MARKER);
208
+ }
209
+ function isLegacyManagedHookCommand(value) {
210
+ if (!isObject(value) || typeof value.command !== "string") return false;
211
+ return value.command === "node_modules/.bin/holdpoint event --engine claude --from-hook || true" || value.command === "node_modules/.bin/holdpoint check --staged";
212
+ }
213
+ function isManagedHookEntry(value) {
214
+ if (!isObject(value)) return false;
215
+ const hooks = asHookArray(value.hooks);
216
+ return hooks.length > 0 && (hooks.every(isManagedHookCommand) || hooks.every(isLegacyManagedHookCommand));
217
+ }
218
+ function mergeClaudeSettings(existing, generated) {
219
+ const existingHooks = isObject(existing.hooks) ? existing.hooks : {};
220
+ const generatedHooks = isObject(generated.hooks) ? generated.hooks : {};
221
+ const mergedHooks = {};
222
+ for (const eventName of /* @__PURE__ */ new Set([
223
+ ...Object.keys(existingHooks),
224
+ ...Object.keys(generatedHooks)
225
+ ])) {
226
+ const preserved = asHookArray(existingHooks[eventName]).filter(
227
+ (entry) => !isManagedHookEntry(entry)
228
+ );
229
+ const next = asHookArray(generatedHooks[eventName]);
230
+ if (preserved.length > 0 || next.length > 0) {
231
+ mergedHooks[eventName] = [...preserved, ...next];
232
+ }
233
+ }
234
+ return { ...existing, ...generated, hooks: mergedHooks };
235
+ }
236
+
237
+ // src/cursor-hooks.ts
238
+ var HOLDPOINT_CURSOR_HOOK_MARKER = "HOLDPOINT_MANAGED=cursor";
239
+ function isObject2(value) {
240
+ return value != null && typeof value === "object" && !Array.isArray(value);
241
+ }
242
+ function asHookArray2(value) {
243
+ return Array.isArray(value) ? value : [];
244
+ }
245
+ function isManagedCursorHook(value) {
246
+ return isObject2(value) && typeof value.command === "string" && (value.command.includes(HOLDPOINT_CURSOR_HOOK_MARKER) || value.command.includes(".cursor/holdpoint-hook.mjs"));
247
+ }
248
+ function mergeCursorHooks(existing, generated) {
249
+ const existingHooks = isObject2(existing.hooks) ? existing.hooks : {};
250
+ const generatedHooks = isObject2(generated.hooks) ? generated.hooks : {};
251
+ const mergedHooks = {};
252
+ for (const eventName of /* @__PURE__ */ new Set([
253
+ ...Object.keys(existingHooks),
254
+ ...Object.keys(generatedHooks)
255
+ ])) {
256
+ const preserved = asHookArray2(existingHooks[eventName]).filter(
257
+ (entry) => !isManagedCursorHook(entry)
258
+ );
259
+ const next = asHookArray2(generatedHooks[eventName]);
260
+ if (preserved.length > 0 || next.length > 0) {
261
+ mergedHooks[eventName] = [...preserved, ...next];
262
+ }
263
+ }
264
+ return { ...existing, ...generated, hooks: mergedHooks };
265
+ }
266
+
267
+ // src/lib/instructions-breadcrumb.ts
268
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
269
+ import { dirname as dirname2 } from "path";
270
+ var START_MARKER = "<!-- HOLDPOINT_MANAGED \u2014 content between these markers is auto-generated by holdpoint init / holdpoint update -->";
271
+ var END_MARKER = "<!-- /HOLDPOINT_MANAGED -->";
272
+ var BREADCRUMB_BODY = `## Holdpoint workflow
273
+
274
+ This repo uses [Holdpoint](https://holdpoint.dev) to gate task completion on deterministic checks.
275
+
276
+ Before marking any task done:
277
+
278
+ 1. Run \`holdpoint check\` (or it will run automatically via Stop / TaskCompleted hooks).
279
+ 2. Fix any failures. Re-run until exit 0.
280
+ 3. Never bypass with \`--no-verify\` or by skipping the agent's stop hook.
281
+
282
+ Full workflow reference: [\`MASTER_PROMPT.md\`](./MASTER_PROMPT.md) (always injected at session start).
283
+ Deep reference: [\`HOLDPOINT_REFERENCE.md\`](./HOLDPOINT_REFERENCE.md) (read on demand).
284
+ Active checks: [\`checks.yaml\`](./checks.yaml).`;
285
+ function spliceBreadcrumb(filePath, body = BREADCRUMB_BODY, createIfMissing = true) {
286
+ const block = `${START_MARKER}
287
+
288
+ ${body}
289
+
290
+ ${END_MARKER}`;
291
+ if (!existsSync3(filePath)) {
292
+ if (!createIfMissing) return;
293
+ mkdirSync(dirname2(filePath), { recursive: true });
294
+ writeFileSync2(filePath, block + "\n", "utf8");
295
+ return;
296
+ }
297
+ const existing = readFileSync2(filePath, "utf8");
298
+ const startIdx = existing.indexOf(START_MARKER);
299
+ const endIdx = existing.indexOf(END_MARKER);
300
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
301
+ const before = existing.slice(0, startIdx);
302
+ const after = existing.slice(endIdx + END_MARKER.length);
303
+ writeFileSync2(filePath, before + block + after, "utf8");
304
+ return;
305
+ }
306
+ const sep2 = existing.endsWith("\n") ? "\n" : "\n\n";
307
+ writeFileSync2(filePath, existing + sep2 + block + "\n", "utf8");
308
+ }
309
+
93
310
  // src/commands/init.ts
94
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
95
- function getTemplatePath(stack) {
96
- const name = stack === "unknown" ? "_base" : stack;
311
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
312
+ function getDefaultTemplatePath() {
97
313
  const candidates = [
98
- join2(__dirname2, "templates", `${name}.yaml`),
314
+ join2(__dirname2, "templates", "default.yaml"),
99
315
  // dist/templates/ (published package)
100
- join2(__dirname2, "../../../templates", `${name}.yaml`),
316
+ join2(__dirname2, "../../../templates", "default.yaml"),
101
317
  // monorepo dev fallback
102
- join2(process.cwd(), "templates", `${name}.yaml`)
318
+ join2(process.cwd(), "templates", "default.yaml")
103
319
  // cwd fallback
104
320
  ];
105
321
  for (const p of candidates) {
106
- if (existsSync3(p)) return p;
322
+ if (existsSync4(p)) return p;
107
323
  }
108
324
  return "";
109
325
  }
@@ -125,11 +341,16 @@ var MINIMAL_MASTER_PROMPT = `# Holdpoint
125
341
  Run \`holdpoint check\` before marking any task complete.
126
342
  See \`checks.yaml\` for the full list of checks.
127
343
  `;
344
+ var MINIMAL_HOLDPOINT_REFERENCE = `# Holdpoint reference
345
+
346
+ Read \`MASTER_PROMPT.md\` first for the mandatory workflow, then use this file for deeper project-specific Holdpoint notes.
347
+ `;
128
348
  var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
129
349
 
130
- Holdpoint installed repo-local adapters for one or more AI coding agents. Before relying on them locally, review these setup notes:
350
+ Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
131
351
 
132
352
  - **GitHub Copilot CLI** \u2014 Holdpoint's \`.github/extensions/holdpoint/extension.mjs\` uses the Copilot CLI **EXTENSIONS** feature. Today that feature is gated behind experimental mode. In Copilot CLI, run \`/experimental on\` so **EXTENSIONS** appears in the enabled feature set before using Holdpoint locally.
353
+ - **Cursor** \u2014 project-level hooks run in trusted workspaces. After opening the repo in Cursor, confirm the workspace is trusted and review Settings \u2192 Hooks if hooks do not fire.
133
354
  - **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
134
355
  - **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
135
356
 
@@ -137,72 +358,95 @@ Docs: https://holdpoint.dev/docs
137
358
  `;
138
359
  async function initCommand(options) {
139
360
  const spinner = ora("Initialising Holdpoint\u2026").start();
140
- const stack = options.stack ?? detectStack();
141
361
  const agentOpt = options.agent;
142
362
  const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
143
- spinner.text = `Stack: ${chalk.cyan(stack)} \u2014 installing for: ${chalk.cyan(agents.join(", "))}`;
363
+ spinner.text = `Installing for: ${chalk2.cyan(agents.join(", "))}`;
144
364
  const pm = detectPackageManager();
145
365
  let yamlContent = MINIMAL_CHECKS_YAML;
146
- if (!existsSync3("checks.yaml")) {
147
- const templatePath = getTemplatePath(stack);
366
+ if (!existsSync4("checks.yaml")) {
367
+ const templatePath = getDefaultTemplatePath();
148
368
  if (templatePath) {
149
- yamlContent = readFileSync2(templatePath, "utf8");
369
+ yamlContent = readFileSync3(templatePath, "utf8");
150
370
  }
151
371
  if (pm !== "pnpm") {
152
372
  yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
153
373
  }
154
- writeFileSync2("checks.yaml", yamlContent, "utf8");
374
+ writeFileSync3("checks.yaml", yamlContent, "utf8");
155
375
  } else {
156
- yamlContent = readFileSync2("checks.yaml", "utf8");
376
+ yamlContent = readFileSync3("checks.yaml", "utf8");
157
377
  }
158
378
  const config = parseHoldpointYaml(yamlContent);
159
379
  const generatedDir = ".github/holdpoint/generated";
160
- mkdirSync(generatedDir, { recursive: true });
161
- writeFileSync2(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
380
+ mkdirSync2(generatedDir, { recursive: true });
381
+ writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
162
382
  if (agents.includes("copilot")) {
163
383
  const extDir = ".github/extensions/holdpoint";
164
- mkdirSync(extDir, { recursive: true });
165
- writeFileSync2(join2(extDir, "extension.mjs"), buildEngine(config), "utf8");
384
+ mkdirSync2(extDir, { recursive: true });
385
+ writeFileSync3(join2(extDir, "extension.mjs"), buildEngine(config), "utf8");
386
+ spliceBreadcrumb(".github/copilot-instructions.md");
166
387
  }
167
388
  if (agents.includes("claude")) {
168
- mkdirSync(".claude", { recursive: true });
389
+ mkdirSync2(".claude", { recursive: true });
169
390
  const settingsPath = ".claude/settings.json";
170
391
  let existing = {};
171
- if (existsSync3(settingsPath)) {
392
+ if (existsSync4(settingsPath)) {
172
393
  try {
173
- existing = JSON.parse(readFileSync2(settingsPath, "utf8"));
394
+ existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
174
395
  } catch {
175
396
  }
176
397
  }
177
398
  const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
178
- writeFileSync2(
399
+ writeFileSync3(
179
400
  settingsPath,
180
- JSON.stringify({ ...existing, hooks: holdpointHooks.hooks }, null, 2),
401
+ JSON.stringify(mergeClaudeSettings(existing, holdpointHooks), null, 2),
181
402
  "utf8"
182
403
  );
404
+ spliceBreadcrumb("CLAUDE.md");
183
405
  }
184
406
  if (agents.includes("cursor")) {
407
+ mkdirSync2(".cursor", { recursive: true });
408
+ const cursorHooksPath = ".cursor/hooks.json";
409
+ let existingHooks = {};
410
+ if (existsSync4(cursorHooksPath)) {
411
+ try {
412
+ existingHooks = JSON.parse(readFileSync3(cursorHooksPath, "utf8"));
413
+ } catch {
414
+ }
415
+ }
416
+ const cursorHooks = JSON.parse(buildCursorHooksJson(config));
417
+ writeFileSync3(
418
+ cursorHooksPath,
419
+ JSON.stringify(mergeCursorHooks(existingHooks, cursorHooks), null, 2) + "\n",
420
+ "utf8"
421
+ );
422
+ writeFileSync3(".cursor/holdpoint-hook.mjs", buildCursorCheckScript(), "utf8");
185
423
  const cursorRules = buildCursorEngine(config);
186
424
  const cursorPath = ".cursorrules";
187
- if (existsSync3(cursorPath)) {
188
- const existing = readFileSync2(cursorPath, "utf8");
425
+ if (existsSync4(cursorPath)) {
426
+ const existing = readFileSync3(cursorPath, "utf8");
189
427
  if (!existing.includes("Holdpoint Rules")) {
190
- writeFileSync2(cursorPath, existing + "\n" + cursorRules, "utf8");
428
+ writeFileSync3(cursorPath, `${existing.trimEnd()}
429
+
430
+ ${cursorRules}`, "utf8");
191
431
  }
192
432
  } else {
193
- writeFileSync2(cursorPath, cursorRules, "utf8");
433
+ writeFileSync3(cursorPath, cursorRules, "utf8");
194
434
  }
435
+ spliceBreadcrumb(".cursor/rules/holdpoint.md");
195
436
  }
196
437
  if (agents.includes("codex")) {
197
- mkdirSync(".codex", { recursive: true });
198
- writeFileSync2(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
199
- writeFileSync2(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
200
- writeFileSync2(".codex/config.toml", buildCodexConfigToml(), "utf8");
201
- const agentsMdPath = "AGENTS.md";
202
- const existing = existsSync3(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
203
- writeFileSync2(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
438
+ mkdirSync2(".codex", { recursive: true });
439
+ writeFileSync3(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
440
+ writeFileSync3(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
441
+ writeFileSync3(".codex/config.toml", buildCodexConfigToml(), "utf8");
442
+ spliceBreadcrumb("AGENTS.md");
204
443
  }
205
444
  ensureBundledFile("MASTER_PROMPT.md", "MASTER_PROMPT.md", MINIMAL_MASTER_PROMPT);
445
+ ensureBundledFile(
446
+ "HOLDPOINT_REFERENCE.md",
447
+ "HOLDPOINT_REFERENCE.md",
448
+ MINIMAL_HOLDPOINT_REFERENCE
449
+ );
206
450
  ensureBundledFile(
207
451
  "HOLDPOINT_PREREQUISITES.md",
208
452
  "HOLDPOINT_PREREQUISITES.md",
@@ -210,44 +454,42 @@ async function initCommand(options) {
210
454
  );
211
455
  spinner.text = "Installing holdpoint as a devDependency\u2026";
212
456
  const installCmds = {
213
- pnpm: "pnpm add -D holdpoint@alpha",
214
- yarn: "yarn add --dev holdpoint@alpha",
215
- npm: "npm install --save-dev holdpoint@alpha"
457
+ pnpm: "pnpm add -D holdpoint",
458
+ yarn: "yarn add --dev holdpoint",
459
+ npm: "npm install --save-dev holdpoint"
216
460
  };
217
461
  const installCmd = installCmds[pm];
218
462
  try {
219
- execSync(installCmd, { stdio: "pipe" });
220
- spinner.succeed(chalk.bold.green("Holdpoint initialised!"));
463
+ execSync2(installCmd, { stdio: "pipe" });
464
+ spinner.succeed(chalk2.bold.green("Holdpoint initialised!"));
221
465
  } catch {
222
466
  spinner.warn(
223
- chalk.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
224
- Run manually: ${chalk.yellow(installCmd)}`
467
+ chalk2.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
468
+ Run manually: ${chalk2.yellow(installCmd)}`
225
469
  );
226
470
  }
471
+ const preflight = runPreflight(agents);
472
+ printPreflight(preflight);
227
473
  console.log(`
228
- ${chalk.cyan("Next steps:")}
229
- 1. Edit ${chalk.yellow("checks.yaml")} to customise your eval checkpoints
230
- 2. Review ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")} for agent setup notes
231
- 3. Commit ${chalk.yellow("checks.yaml")}, ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")}, and the generated engine files
232
- 4. Run ${chalk.yellow("holdpoint check")} at any time to validate
474
+ ${chalk2.cyan("Next steps:")}
475
+ 1. Edit ${chalk2.yellow("checks.yaml")} to customise your eval checkpoints
476
+ 2. Address any ${chalk2.yellow("\u2192")} items above (full notes in ${chalk2.yellow("HOLDPOINT_PREREQUISITES.md")})
477
+ 3. Commit ${chalk2.yellow("checks.yaml")}, ${chalk2.yellow("HOLDPOINT_PREREQUISITES.md")}, and the generated engine files
478
+ 4. Run ${chalk2.yellow("holdpoint check")} at any time to validate
233
479
 
234
- ${chalk.bgYellow.black(" Copilot local use ")} Run ${chalk.yellow("/experimental on")} in GitHub Copilot CLI so the
235
- ${chalk.yellow("EXTENSIONS")} feature is enabled before using Holdpoint locally.
236
- See ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")} for the full handoff notes.
237
-
238
- Visual builder: ${chalk.yellow("holdpoint builder")} (opens localhost:4321)
239
- Stack: ${chalk.cyan(stack)} Agents: ${chalk.cyan(agents.join(", "))}
480
+ Visual builder: ${chalk2.yellow("holdpoint builder")} (opens the daemon at /builder)
481
+ Agents: ${chalk2.cyan(agents.join(", "))}
240
482
  `);
241
483
  }
242
484
 
243
485
  // src/commands/check.ts
244
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
486
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
245
487
  import { join as join3 } from "path";
246
- import chalk2 from "chalk";
488
+ import chalk3 from "chalk";
247
489
  import ora2 from "ora";
248
490
  import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
249
491
  import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
250
- import { execSync as execSync2 } from "child_process";
492
+ import { execSync as execSync3 } from "child_process";
251
493
  import { randomUUID } from "crypto";
252
494
  import { identifyProject } from "@holdpoint/live-daemon";
253
495
  import { BridgeClient } from "@holdpoint/sdk";
@@ -257,7 +499,7 @@ var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
257
499
  var CHECK_REPORTS_MAX = 50;
258
500
  function getStagedFiles() {
259
501
  try {
260
- const out = execSync2("git diff --cached --name-only", {
502
+ const out = execSync3("git diff --cached --name-only", {
261
503
  encoding: "utf8",
262
504
  stdio: ["pipe", "pipe", "ignore"]
263
505
  });
@@ -268,7 +510,7 @@ function getStagedFiles() {
268
510
  }
269
511
  function getAllChangedFiles() {
270
512
  try {
271
- const out = execSync2("git diff --name-only HEAD", {
513
+ const out = execSync3("git diff --name-only HEAD", {
272
514
  encoding: "utf8",
273
515
  stdio: ["pipe", "pipe", "ignore"]
274
516
  });
@@ -279,7 +521,7 @@ function getAllChangedFiles() {
279
521
  }
280
522
  function getLastCommitFiles() {
281
523
  try {
282
- const out = execSync2("git diff --name-only HEAD~1 HEAD", {
524
+ const out = execSync3("git diff --name-only HEAD~1 HEAD", {
283
525
  encoding: "utf8",
284
526
  stdio: ["pipe", "pipe", "ignore"]
285
527
  });
@@ -290,7 +532,7 @@ function getLastCommitFiles() {
290
532
  }
291
533
  function getHeadSha() {
292
534
  try {
293
- return execSync2("git rev-parse HEAD", {
535
+ return execSync3("git rev-parse HEAD", {
294
536
  encoding: "utf8",
295
537
  stdio: ["pipe", "pipe", "ignore"]
296
538
  }).trim();
@@ -300,7 +542,7 @@ function getHeadSha() {
300
542
  }
301
543
  function readCommitCache() {
302
544
  try {
303
- const raw = readFileSync3(COMMIT_CACHE_PATH, "utf8");
545
+ const raw = readFileSync4(COMMIT_CACHE_PATH, "utf8");
304
546
  const parsed = JSON.parse(raw);
305
547
  return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
306
548
  } catch {
@@ -311,18 +553,18 @@ function recordCommitCache(sha) {
311
553
  try {
312
554
  const existing = readCommitCache();
313
555
  const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
314
- mkdirSync2(join3(COMMIT_CACHE_PATH, ".."), { recursive: true });
315
- writeFileSync3(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
556
+ mkdirSync3(join3(COMMIT_CACHE_PATH, ".."), { recursive: true });
557
+ writeFileSync4(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
316
558
  } catch {
317
559
  }
318
560
  }
319
561
  function recordCheckReport(run) {
320
562
  try {
321
- mkdirSync2(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
563
+ mkdirSync3(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
322
564
  let existing = { runs: [] };
323
- if (existsSync4(CHECK_REPORTS_PATH)) {
565
+ if (existsSync5(CHECK_REPORTS_PATH)) {
324
566
  try {
325
- existing = JSON.parse(readFileSync3(CHECK_REPORTS_PATH, "utf8"));
567
+ existing = JSON.parse(readFileSync4(CHECK_REPORTS_PATH, "utf8"));
326
568
  if (!Array.isArray(existing.runs)) existing.runs = [];
327
569
  } catch {
328
570
  existing = { runs: [] };
@@ -331,21 +573,21 @@ function recordCheckReport(run) {
331
573
  const updated = {
332
574
  runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
333
575
  };
334
- writeFileSync3(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
576
+ writeFileSync4(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
335
577
  } catch {
336
578
  }
337
579
  }
338
580
  async function checkCommand(options) {
339
- if (!existsSync4("checks.yaml")) {
340
- console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
581
+ if (!existsSync5("checks.yaml")) {
582
+ console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
341
583
  process.exit(1);
342
584
  }
343
- const yamlContent = readFileSync3("checks.yaml", "utf8");
585
+ const yamlContent = readFileSync4("checks.yaml", "utf8");
344
586
  let config;
345
587
  try {
346
588
  config = parseHoldpointYaml2(yamlContent);
347
589
  } catch (err) {
348
- console.error(chalk2.red("Invalid checks.yaml:"), err.message);
590
+ console.error(chalk3.red("Invalid checks.yaml:"), err.message);
349
591
  process.exit(1);
350
592
  }
351
593
  const headSha = getHeadSha();
@@ -358,7 +600,7 @@ async function checkCommand(options) {
358
600
  } else {
359
601
  if (headSha && readCommitCache().has(headSha)) {
360
602
  console.log(
361
- chalk2.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
603
+ chalk3.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
362
604
  );
363
605
  process.exit(0);
364
606
  }
@@ -367,10 +609,10 @@ async function checkCommand(options) {
367
609
  changedFiles = lastCommit;
368
610
  usedHeadShaForCache = true;
369
611
  console.log(
370
- chalk2.yellow("No staged files. Running checks scoped to the most recent commit's files.")
612
+ chalk3.yellow("No staged files. Running checks scoped to the most recent commit's files.")
371
613
  );
372
614
  } else {
373
- console.log(chalk2.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
615
+ console.log(chalk3.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
374
616
  process.exit(0);
375
617
  }
376
618
  }
@@ -378,10 +620,10 @@ async function checkCommand(options) {
378
620
  changedFiles = getAllChangedFiles();
379
621
  if (changedFiles.length === 0) {
380
622
  console.log(
381
- chalk2.yellow("No changed files detected. Running all checks with no file filter.")
623
+ chalk3.yellow("No changed files detected. Running all checks with no file filter.")
382
624
  );
383
625
  console.log(
384
- chalk2.dim(
626
+ chalk3.dim(
385
627
  " Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
386
628
  )
387
629
  );
@@ -389,9 +631,9 @@ async function checkCommand(options) {
389
631
  }
390
632
  const guides = Object.entries(config.context?.guides ?? {});
391
633
  if (guides.length > 0) {
392
- console.log(chalk2.cyan("\nProject guides:"));
634
+ console.log(chalk3.cyan("\nProject guides:"));
393
635
  for (const [key, text] of guides) {
394
- console.log(chalk2.bold(` ${key}:`), chalk2.dim(String(text).trim()));
636
+ console.log(chalk3.bold(` ${key}:`), chalk3.dim(String(text).trim()));
395
637
  }
396
638
  console.log("");
397
639
  }
@@ -409,9 +651,9 @@ async function checkCommand(options) {
409
651
  console.log("");
410
652
  console.log(
411
653
  [
412
- chalk2.green(`\u2713 ${passed.length} passed`),
413
- failed.length > 0 ? chalk2.red(`\u2717 ${failed.length} failed`) : "",
414
- skipped.length > 0 ? chalk2.gray(`\u25CC ${skipped.length} skipped`) : ""
654
+ chalk3.green(`\u2713 ${passed.length} passed`),
655
+ failed.length > 0 ? chalk3.red(`\u2717 ${failed.length} failed`) : "",
656
+ skipped.length > 0 ? chalk3.gray(`\u25CC ${skipped.length} skipped`) : ""
415
657
  ].filter(Boolean).join(" ")
416
658
  );
417
659
  const promptChecks = config.checks.filter(
@@ -419,9 +661,9 @@ async function checkCommand(options) {
419
661
  );
420
662
  if (promptChecks.length > 0) {
421
663
  console.log(`
422
- ${chalk2.cyan("Agent prompts to act on:")}`);
664
+ ${chalk3.cyan("Agent prompts to act on:")}`);
423
665
  for (const c of promptChecks) {
424
- console.log(` ${chalk2.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
666
+ console.log(` ${chalk3.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
425
667
  }
426
668
  }
427
669
  const reportResults = [
@@ -487,151 +729,195 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
487
729
  }
488
730
  }
489
731
  function printResult(result) {
490
- const icon = result.status === "pass" ? chalk2.green("\u2713") : result.status === "fail" ? chalk2.red("\u2717") : result.status === "skip" ? chalk2.gray("\u25CC") : chalk2.yellow("\u2026");
732
+ const icon = result.status === "pass" ? chalk3.green("\u2713") : result.status === "fail" ? chalk3.red("\u2717") : result.status === "skip" ? chalk3.gray("\u25CC") : chalk3.yellow("\u2026");
491
733
  const label = result.check.label;
492
734
  console.log(`${icon} ${label}`);
493
735
  if (result.status === "fail" && result.output) {
494
736
  const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
495
- console.log(chalk2.dim(trimmed.replace(/^/gm, " ")));
737
+ console.log(chalk3.dim(trimmed.replace(/^/gm, " ")));
496
738
  }
497
739
  if (result.status === "skip" && result.skipReason) {
498
- console.log(chalk2.dim(` ${result.skipReason}`));
740
+ console.log(chalk3.dim(` ${result.skipReason}`));
499
741
  }
500
742
  }
501
743
 
502
744
  // src/commands/validate.ts
503
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
504
- import chalk3 from "chalk";
745
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
746
+ import chalk4 from "chalk";
505
747
  import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
506
748
  async function validateCommand() {
507
- if (!existsSync5("checks.yaml")) {
508
- console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
749
+ if (!existsSync6("checks.yaml")) {
750
+ console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
509
751
  process.exit(1);
510
752
  }
511
- const text = readFileSync4("checks.yaml", "utf8");
753
+ const text = readFileSync5("checks.yaml", "utf8");
512
754
  let config;
513
755
  try {
514
756
  config = parseHoldpointYaml3(text);
515
757
  } catch (err) {
516
- console.error(chalk3.red("Parse error:"), err.message);
758
+ console.error(chalk4.red("Parse error:"), err.message);
517
759
  process.exit(1);
518
760
  }
519
761
  const result = validateConfig(config);
520
762
  if (result.valid) {
521
- console.log(chalk3.green("\u2713 checks.yaml is valid"));
763
+ console.log(chalk4.green("\u2713 checks.yaml is valid"));
522
764
  console.log(
523
- chalk3.dim(
765
+ chalk4.dim(
524
766
  ` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
525
767
  )
526
768
  );
527
769
  } else {
528
- console.error(chalk3.red("\u2717 checks.yaml has errors:"));
770
+ console.error(chalk4.red("\u2717 checks.yaml has errors:"));
529
771
  for (const err of result.errors) {
530
- console.error(` ${chalk3.yellow(err.path)}: ${err.message}`);
772
+ console.error(` ${chalk4.yellow(err.path)}: ${err.message}`);
531
773
  }
532
774
  process.exit(1);
533
775
  }
534
776
  }
535
777
 
536
778
  // src/commands/update.ts
537
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
538
- import chalk4 from "chalk";
779
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
780
+ import chalk5 from "chalk";
539
781
  import ora3 from "ora";
540
782
  import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
541
783
  import { buildConfigJson as buildConfigJson2, buildEngine as buildEngine2 } from "@holdpoint/engine-copilot";
542
784
  import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
543
- import { buildEngine as buildCursorEngine2 } from "@holdpoint/engine-cursor";
785
+ import {
786
+ buildCheckScript as buildCursorCheckScript2,
787
+ buildEngine as buildCursorEngine2,
788
+ buildHooksJson as buildCursorHooksJson2
789
+ } from "@holdpoint/engine-cursor";
544
790
  import {
545
791
  buildConfigToml as buildCodexConfigToml2,
546
792
  buildHooksJson as buildCodexHooksJson2,
547
- buildCheckScript as buildCodexCheckScript2,
548
- spliceAgentsMd as spliceAgentsMd2
793
+ buildCheckScript as buildCodexCheckScript2
549
794
  } from "@holdpoint/engine-codex";
550
795
  var MINIMAL_PREREQUISITES2 = `# Holdpoint prerequisites
551
796
 
552
- Holdpoint installed repo-local adapters for one or more AI coding agents. Before relying on them locally, review these setup notes:
797
+ Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
553
798
 
554
799
  - **GitHub Copilot CLI** \u2014 Holdpoint's \`.github/extensions/holdpoint/extension.mjs\` uses the Copilot CLI **EXTENSIONS** feature. Today that feature is gated behind experimental mode. In Copilot CLI, run \`/experimental on\` so **EXTENSIONS** appears in the enabled feature set before using Holdpoint locally.
800
+ - **Cursor** \u2014 project-level hooks run in trusted workspaces. After opening the repo in Cursor, confirm the workspace is trusted and review Settings \u2192 Hooks if hooks do not fire.
555
801
  - **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
556
802
  - **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
557
803
 
558
804
  Docs: https://holdpoint.dev/docs
559
805
  `;
806
+ var MINIMAL_HOLDPOINT_REFERENCE2 = `# Holdpoint reference
807
+
808
+ Read \`MASTER_PROMPT.md\` first for the mandatory workflow, then use this file for deeper project-specific Holdpoint notes.
809
+ `;
560
810
  async function updateCommand() {
561
- if (!existsSync6("checks.yaml")) {
562
- console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
811
+ if (!existsSync7("checks.yaml")) {
812
+ console.error(chalk5.red("No checks.yaml found. Run `holdpoint init` first."));
563
813
  process.exit(1);
564
814
  }
565
815
  const spinner = ora3("Updating Holdpoint engine files\u2026").start();
566
- const config = parseHoldpointYaml4(readFileSync5("checks.yaml", "utf8"));
816
+ const config = parseHoldpointYaml4(readFileSync6("checks.yaml", "utf8"));
567
817
  const detected = detectInstalledAgents();
568
818
  const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
569
819
  const generatedDir = ".github/holdpoint/generated";
570
- mkdirSync3(generatedDir, { recursive: true });
571
- writeFileSync4(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
820
+ mkdirSync4(generatedDir, { recursive: true });
821
+ writeFileSync5(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
572
822
  if (agents.includes("copilot")) {
573
823
  const extDir = ".github/extensions/holdpoint";
574
- mkdirSync3(extDir, { recursive: true });
575
- writeFileSync4(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
824
+ mkdirSync4(extDir, { recursive: true });
825
+ writeFileSync5(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
826
+ spliceBreadcrumb(".github/copilot-instructions.md");
576
827
  }
577
828
  if (agents.includes("claude")) {
578
- mkdirSync3(".claude", { recursive: true });
829
+ mkdirSync4(".claude", { recursive: true });
579
830
  const settingsPath = ".claude/settings.json";
580
831
  let existing = {};
581
- if (existsSync6(settingsPath)) {
832
+ if (existsSync7(settingsPath)) {
582
833
  try {
583
- existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
834
+ existing = JSON.parse(readFileSync6(settingsPath, "utf8"));
584
835
  } catch {
585
836
  }
586
837
  }
587
838
  const hooks = JSON.parse(buildClaudeEngineJson2(config));
588
- writeFileSync4(
839
+ writeFileSync5(
589
840
  settingsPath,
590
- JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2) + "\n"
841
+ JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
591
842
  );
843
+ spliceBreadcrumb("CLAUDE.md");
592
844
  }
593
845
  if (agents.includes("cursor")) {
846
+ mkdirSync4(".cursor", { recursive: true });
847
+ const cursorHooksPath = ".cursor/hooks.json";
848
+ let existingHooks = {};
849
+ if (existsSync7(cursorHooksPath)) {
850
+ try {
851
+ existingHooks = JSON.parse(readFileSync6(cursorHooksPath, "utf8"));
852
+ } catch {
853
+ }
854
+ }
855
+ const cursorHooks = JSON.parse(buildCursorHooksJson2(config));
856
+ writeFileSync5(
857
+ cursorHooksPath,
858
+ JSON.stringify(mergeCursorHooks(existingHooks, cursorHooks), null, 2) + "\n",
859
+ "utf8"
860
+ );
861
+ writeFileSync5(".cursor/holdpoint-hook.mjs", buildCursorCheckScript2(), "utf8");
594
862
  const cursorRules = buildCursorEngine2(config);
595
863
  const cursorPath = ".cursorrules";
596
- if (existsSync6(cursorPath)) {
597
- const content = readFileSync5(cursorPath, "utf8");
864
+ if (existsSync7(cursorPath)) {
865
+ const content = readFileSync6(cursorPath, "utf8");
598
866
  const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
599
867
  const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
600
868
  if (start !== -1 && end !== -1) {
601
869
  const afterEnd = content.indexOf("\n", end);
602
- const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
603
- writeFileSync4(cursorPath, updated);
870
+ const prefix = content.slice(0, start).trimEnd();
871
+ const suffix = content.slice(afterEnd === -1 ? end : afterEnd + 1).trimStart();
872
+ const updated = (prefix ? `${prefix}
873
+
874
+ ` : "") + cursorRules + (suffix ? `
875
+ ${suffix}` : "");
876
+ writeFileSync5(cursorPath, updated);
604
877
  } else {
605
- writeFileSync4(cursorPath, content + "\n" + cursorRules);
878
+ writeFileSync5(cursorPath, `${content.trimEnd()}
879
+
880
+ ${cursorRules}`);
606
881
  }
882
+ } else {
883
+ writeFileSync5(cursorPath, cursorRules);
607
884
  }
885
+ spliceBreadcrumb(".cursor/rules/holdpoint.md");
608
886
  }
609
887
  if (agents.includes("codex")) {
610
- mkdirSync3(".codex", { recursive: true });
611
- writeFileSync4(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
612
- writeFileSync4(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
888
+ mkdirSync4(".codex", { recursive: true });
889
+ writeFileSync5(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
890
+ writeFileSync5(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
613
891
  const configTomlPath = ".codex/config.toml";
614
- if (!existsSync6(configTomlPath)) {
615
- writeFileSync4(configTomlPath, buildCodexConfigToml2(), "utf8");
892
+ if (!existsSync7(configTomlPath)) {
893
+ writeFileSync5(configTomlPath, buildCodexConfigToml2(), "utf8");
616
894
  } else {
617
- const existing2 = readFileSync5(configTomlPath, "utf8");
618
- if (!existing2.includes("[features]")) {
619
- writeFileSync4(configTomlPath, existing2.trimEnd() + "\n\n" + buildCodexConfigToml2(), "utf8");
895
+ const existing = readFileSync6(configTomlPath, "utf8");
896
+ if (!existing.includes("[features]")) {
897
+ writeFileSync5(configTomlPath, existing.trimEnd() + "\n\n" + buildCodexConfigToml2(), "utf8");
620
898
  }
621
899
  }
622
- const agentsMdPath = "AGENTS.md";
623
- const existing = existsSync6(agentsMdPath) ? readFileSync5(agentsMdPath, "utf8") : "";
624
- writeFileSync4(agentsMdPath, spliceAgentsMd2(existing, config), "utf8");
900
+ spliceBreadcrumb("AGENTS.md");
625
901
  }
902
+ const wroteReference = ensureBundledFile(
903
+ "HOLDPOINT_REFERENCE.md",
904
+ "HOLDPOINT_REFERENCE.md",
905
+ MINIMAL_HOLDPOINT_REFERENCE2
906
+ );
626
907
  const wrotePrerequisites = ensureBundledFile(
627
908
  "HOLDPOINT_PREREQUISITES.md",
628
909
  "HOLDPOINT_PREREQUISITES.md",
629
910
  MINIMAL_PREREQUISITES2
630
911
  );
631
- spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
912
+ spinner.succeed(chalk5.green("Engine files updated from current checks.yaml"));
913
+ if (wroteReference) {
914
+ console.log(
915
+ chalk5.cyan("Created HOLDPOINT_REFERENCE.md with the full Holdpoint workflow reference.")
916
+ );
917
+ }
632
918
  if (wrotePrerequisites) {
633
919
  console.log(
634
- chalk4.cyan(
920
+ chalk5.cyan(
635
921
  "Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
636
922
  )
637
923
  );
@@ -639,118 +925,131 @@ async function updateCommand() {
639
925
  }
640
926
 
641
927
  // src/commands/build.ts
642
- import { createServer } from "http";
643
- import { createReadStream, existsSync as existsSync7 } from "fs";
644
- import { join as join4, extname, dirname as dirname3 } from "path";
645
- import { fileURLToPath as fileURLToPath3 } from "url";
646
- import { execSync as execSync3 } from "child_process";
647
- import chalk5 from "chalk";
648
- var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
649
- var MIME = {
650
- ".html": "text/html; charset=utf-8",
651
- ".js": "text/javascript",
652
- ".mjs": "text/javascript",
653
- ".css": "text/css",
654
- ".svg": "image/svg+xml",
655
- ".png": "image/png",
656
- ".ico": "image/x-icon",
657
- ".woff": "font/woff",
658
- ".woff2": "font/woff2",
659
- ".ttf": "font/ttf",
660
- ".json": "application/json"
661
- };
662
- function serveFile(res, filePath) {
663
- const mime = MIME[extname(filePath)] ?? "application/octet-stream";
664
- res.writeHead(200, { "Content-Type": mime });
665
- createReadStream(filePath).pipe(res);
666
- }
667
- function handleRequest(req, res, uiDir) {
668
- const url = (req.url ?? "/").split("?")[0] ?? "/";
669
- if (url === "/__holdpoint/initial-yaml") {
670
- const checksPath = join4(process.cwd(), "checks.yaml");
671
- if (existsSync7(checksPath)) {
672
- res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
673
- createReadStream(checksPath).pipe(res);
674
- } else {
675
- res.writeHead(404, { "Content-Type": "text/plain" });
676
- res.end("checks.yaml not found in current directory");
928
+ import chalk6 from "chalk";
929
+
930
+ // src/lib/ensure-daemon.ts
931
+ import { spawn } from "child_process";
932
+ import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
933
+ function sleep(ms) {
934
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
935
+ }
936
+ async function ensureDaemon(timeoutMs = 5e3) {
937
+ const existing = await readHealthyDaemonLock();
938
+ if (existing) {
939
+ return { info: existing, started: false };
940
+ }
941
+ const cliEntry = process.argv[1];
942
+ if (!cliEntry) {
943
+ throw new Error("Cannot determine the current holdpoint CLI entrypoint");
944
+ }
945
+ const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
946
+ stdio: "ignore",
947
+ env: process.env,
948
+ cwd: process.cwd()
949
+ });
950
+ child.unref();
951
+ const deadline = Date.now() + timeoutMs;
952
+ while (Date.now() < deadline) {
953
+ const lock = await readHealthyDaemonLock();
954
+ if (lock) {
955
+ return { info: lock, started: true };
677
956
  }
678
- return;
957
+ await sleep(100);
679
958
  }
680
- if (url === "/__holdpoint/initial-reports") {
681
- const reportsPath = join4(process.cwd(), ".holdpoint", "check-reports.json");
682
- if (existsSync7(reportsPath)) {
683
- res.writeHead(200, { "Content-Type": "application/json" });
684
- createReadStream(reportsPath).pipe(res);
685
- } else {
686
- res.writeHead(404, { "Content-Type": "text/plain" });
687
- res.end("No check reports found");
959
+ throw new Error("Daemon unavailable + cannot spawn");
960
+ }
961
+
962
+ // src/lib/open-browser.ts
963
+ import { execSync as execSync4 } from "child_process";
964
+ function openBrowser(url) {
965
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
966
+ try {
967
+ execSync4(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
968
+ } catch {
969
+ }
970
+ }
971
+
972
+ // src/lib/project.ts
973
+ import { existsSync as existsSync8 } from "fs";
974
+ import { dirname as dirname4, join as join4 } from "path";
975
+ import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
976
+ function findChecksYaml(startDir) {
977
+ let current = startDir;
978
+ for (; ; ) {
979
+ const candidate = join4(current, "checks.yaml");
980
+ if (existsSync8(candidate)) {
981
+ return candidate;
982
+ }
983
+ const parent = dirname4(current);
984
+ if (parent === current) {
985
+ return null;
688
986
  }
987
+ current = parent;
988
+ }
989
+ }
990
+ function tryResolveCurrentProject() {
991
+ const checksYaml = findChecksYaml(process.cwd());
992
+ if (checksYaml) {
993
+ return identifyProject2(dirname4(checksYaml));
994
+ }
995
+ try {
996
+ return identifyProject2(process.cwd());
997
+ } catch {
998
+ return null;
999
+ }
1000
+ }
1001
+ function appendProjectAuthParams(url, project) {
1002
+ if (!project) {
689
1003
  return;
690
1004
  }
691
- const candidate = join4(uiDir, url === "/" ? "index.html" : url);
692
- const filePath = existsSync7(candidate) ? candidate : join4(uiDir, "index.html");
693
- serveFile(res, filePath);
1005
+ url.searchParams.set("project", project.hash);
1006
+ url.searchParams.set("name", project.name);
1007
+ url.searchParams.set("root", project.root);
694
1008
  }
1009
+
1010
+ // src/commands/build.ts
695
1011
  async function buildCommand() {
696
- const port = 4321;
697
- const uiDir = join4(__dirname3, "builder-ui");
698
- if (!existsSync7(uiDir)) {
699
- console.error(chalk5.red("\u2717 Builder UI not found.\n"));
700
- console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
701
- console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
702
- process.exit(1);
703
- }
704
- const server = createServer((req, res) => handleRequest(req, res, uiDir));
705
- await new Promise((resolve2, reject) => {
706
- server.listen(port, () => {
707
- console.log(
708
- `
709
- ${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
710
- );
711
- console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
712
- console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
713
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
714
- try {
715
- execSync3(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
716
- } catch {
717
- }
718
- });
719
- server.on("error", reject);
720
- process.on("SIGINT", () => {
721
- console.log(chalk5.dim("\n Stopping builder\u2026"));
722
- server.close(() => resolve2());
723
- });
724
- });
1012
+ const { info, started } = await ensureDaemon();
1013
+ const url = new URL("/__holdpoint/live-auth", `http://127.0.0.1:${info.port}`);
1014
+ url.searchParams.set("token", info.token);
1015
+ url.searchParams.set("path", "/builder/");
1016
+ appendProjectAuthParams(url, tryResolveCurrentProject());
1017
+ openBrowser(url.toString());
1018
+ console.log(
1019
+ chalk6.green(
1020
+ started ? "\u2713 Started Holdpoint Live and opened the builder" : "\u2713 Opened Holdpoint builder"
1021
+ )
1022
+ );
1023
+ console.log(` url: ${chalk6.cyan(url.toString())}`);
725
1024
  }
726
1025
 
727
1026
  // src/commands/evolve.ts
728
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
729
- import { execSync as execSync6 } from "child_process";
730
- import chalk6 from "chalk";
1027
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
1028
+ import { execSync as execSync7 } from "child_process";
1029
+ import chalk7 from "chalk";
731
1030
  import ora4 from "ora";
732
1031
  import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
733
1032
 
734
1033
  // src/evolve/scanner.ts
735
- import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync } from "fs";
1034
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync } from "fs";
736
1035
  import { join as join5 } from "path";
737
- import { execSync as execSync4 } from "child_process";
1036
+ import { execSync as execSync5 } from "child_process";
738
1037
  function tryReadJson(path) {
739
1038
  try {
740
- return JSON.parse(readFileSync6(path, "utf8"));
1039
+ return JSON.parse(readFileSync7(path, "utf8"));
741
1040
  } catch {
742
1041
  return null;
743
1042
  }
744
1043
  }
745
1044
  function tryReadText(path) {
746
1045
  try {
747
- return readFileSync6(path, "utf8");
1046
+ return readFileSync7(path, "utf8");
748
1047
  } catch {
749
1048
  return "";
750
1049
  }
751
1050
  }
752
1051
  function scanProject(cwd = process.cwd()) {
753
- const exists = (p) => existsSync8(join5(cwd, p));
1052
+ const exists = (p) => existsSync9(join5(cwd, p));
754
1053
  const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
755
1054
  const pkg = tryReadJson(join5(cwd, "package.json"));
756
1055
  const scripts = pkg?.scripts ?? {};
@@ -812,8 +1111,8 @@ function scanProject(cwd = process.cwd()) {
812
1111
  }
813
1112
 
814
1113
  // src/evolve/dead-checker.ts
815
- import { execSync as execSync5 } from "child_process";
816
- import { readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
1114
+ import { execSync as execSync6 } from "child_process";
1115
+ import { readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
817
1116
  import { join as join6 } from "path";
818
1117
  var NAMED_SCOPES = /* @__PURE__ */ new Set([
819
1118
  "frontend",
@@ -869,7 +1168,7 @@ function walkDir(dir, root, depth, maxDepth) {
869
1168
  }
870
1169
  function getRepoFiles(cwd) {
871
1170
  try {
872
- const out = execSync5("git ls-files", {
1171
+ const out = execSync6("git ls-files", {
873
1172
  cwd,
874
1173
  encoding: "utf8",
875
1174
  stdio: ["pipe", "pipe", "ignore"]
@@ -906,7 +1205,7 @@ function detectStaleChecks(config, repoFiles) {
906
1205
  if (matches.length === 0) {
907
1206
  const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
908
1207
  const suggestedConditionPath = extractPathFromRegex(regexStr);
909
- const pathGone = !suggestedConditionPath || !existsSync9(join6(process.cwd(), suggestedConditionPath));
1208
+ const pathGone = !suggestedConditionPath || !existsSync10(join6(process.cwd(), suggestedConditionPath));
910
1209
  if (pathGone) {
911
1210
  stale.push({
912
1211
  check,
@@ -925,6 +1224,9 @@ function pmScript(profile, script, fallback) {
925
1224
  if (profile.packageManager === "npm") return `npm run ${script}`;
926
1225
  return `${profile.packageManager} ${script}`;
927
1226
  }
1227
+ var blockedMarkerTerms = ["TODO", "FIXME", "HACK", "XXX"];
1228
+ var blockedMarkerLabel = `No ${blockedMarkerTerms[0]}/${blockedMarkerTerms[1]} left in changed code`;
1229
+ var blockedMarkerPrompt = `Scan the files you changed for any ${blockedMarkerTerms.join(", ")} comments. Either resolve them before finishing or convert them to GitHub issues. Don't leave incomplete work silently behind.`;
928
1230
  function getTemplates(profile) {
929
1231
  return [
930
1232
  // ── Universal checks (always proposed for any project) ──────────────────
@@ -948,8 +1250,8 @@ function getTemplates(profile) {
948
1250
  },
949
1251
  {
950
1252
  id: "no-todos",
951
- label: "No TODO/FIXME left in changed code",
952
- 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.",
1253
+ label: blockedMarkerLabel,
1254
+ prompt: blockedMarkerPrompt,
953
1255
  trigger: () => true
954
1256
  },
955
1257
  // ── TypeScript / JavaScript ──────────────────────────────────────────────
@@ -1107,20 +1409,20 @@ function withHeader(header, newYaml) {
1107
1409
  return header + "\n\n" + newYaml;
1108
1410
  }
1109
1411
  async function evolveCommand(options) {
1110
- if (!existsSync10("checks.yaml")) {
1111
- console.error(chalk6.red("No checks.yaml found. Run `holdpoint init` first."));
1412
+ if (!existsSync11("checks.yaml")) {
1413
+ console.error(chalk7.red("No checks.yaml found. Run `holdpoint init` first."));
1112
1414
  process.exit(1);
1113
1415
  }
1114
1416
  const spinner = ora4("Scanning project profile\u2026").start();
1115
1417
  const cwd = process.cwd();
1116
1418
  const profile = scanProject(cwd);
1117
1419
  const repoFiles = getRepoFiles(cwd);
1118
- const yamlContent = readFileSync7("checks.yaml", "utf8");
1420
+ const yamlContent = readFileSync8("checks.yaml", "utf8");
1119
1421
  let config;
1120
1422
  try {
1121
1423
  config = parseHoldpointYaml5(yamlContent);
1122
1424
  } catch (err) {
1123
- spinner.fail(chalk6.red("Invalid checks.yaml:") + " " + err.message);
1425
+ spinner.fail(chalk7.red("Invalid checks.yaml:") + " " + err.message);
1124
1426
  process.exit(1);
1125
1427
  }
1126
1428
  spinner.stop();
@@ -1129,7 +1431,7 @@ async function evolveCommand(options) {
1129
1431
  const allTemplates = getTemplates(profile);
1130
1432
  const proposals = allTemplates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
1131
1433
  const staleChecks = detectStaleChecks(config, repoFiles);
1132
- console.log(chalk6.bold("\n\u{1F4CB} Project profile:"));
1434
+ console.log(chalk7.bold("\n\u{1F4CB} Project profile:"));
1133
1435
  const traits = [
1134
1436
  ["TypeScript", profile.hasTypeScript, "tsconfig.json"],
1135
1437
  ["ESLint", profile.hasEslint, "eslint.config.*"],
@@ -1155,40 +1457,40 @@ async function evolveCommand(options) {
1155
1457
  ];
1156
1458
  const detected = traits.filter(([, yes]) => yes);
1157
1459
  if (detected.length === 0) {
1158
- console.log(chalk6.dim(" (empty project \u2014 only universal checks apply)"));
1460
+ console.log(chalk7.dim(" (empty project \u2014 only universal checks apply)"));
1159
1461
  } else {
1160
1462
  for (const [name, , hint] of detected) {
1161
- console.log(` ${chalk6.green("\u2713")} ${name.padEnd(18)} ${chalk6.dim(hint)}`);
1463
+ console.log(` ${chalk7.green("\u2713")} ${name.padEnd(18)} ${chalk7.dim(hint)}`);
1162
1464
  }
1163
1465
  }
1164
1466
  if (staleChecks.length > 0) {
1165
- console.log(chalk6.bold(`
1467
+ console.log(chalk7.bold(`
1166
1468
  \u26A0\uFE0F Stale checks (${staleChecks.length}):`));
1167
1469
  for (const { check, reason, suggestedConditionPath } of staleChecks) {
1168
- const fix = suggestedConditionPath ? chalk6.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk6.dim(" \u2192 no path inferred; review manually");
1169
- console.log(` ${chalk6.yellow("\u25CC")} ${chalk6.bold(check.id)} ${chalk6.dim(reason)}${fix}`);
1470
+ const fix = suggestedConditionPath ? chalk7.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk7.dim(" \u2192 no path inferred; review manually");
1471
+ console.log(` ${chalk7.yellow("\u25CC")} ${chalk7.bold(check.id)} ${chalk7.dim(reason)}${fix}`);
1170
1472
  }
1171
1473
  }
1172
1474
  if (proposals.length === 0 && staleChecks.length === 0) {
1173
- console.log(chalk6.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
1475
+ console.log(chalk7.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
1174
1476
  return;
1175
1477
  }
1176
1478
  if (proposals.length > 0) {
1177
- console.log(chalk6.bold(`
1479
+ console.log(chalk7.bold(`
1178
1480
  \u{1F4A1} Proposed additions (${proposals.length}):`));
1179
1481
  for (const t of proposals) {
1180
- const scope = t.when ? chalk6.cyan(` when: ${t.when}`) : "";
1181
- const type = t.cmd ? chalk6.dim("cmd") : chalk6.dim("prompt");
1182
- const preview = t.cmd ? chalk6.dim(` ${t.cmd.slice(0, 80)}${t.cmd.length > 80 ? "\u2026" : ""}`) : chalk6.dim(` ${(t.prompt ?? "").slice(0, 80)}${(t.prompt?.length ?? 0) > 80 ? "\u2026" : ""}`);
1183
- console.log(` ${chalk6.green("+")} ${chalk6.bold(t.id.padEnd(24))} [${type}]${scope}`);
1482
+ const scope = t.when ? chalk7.cyan(` when: ${t.when}`) : "";
1483
+ const type = t.cmd ? chalk7.dim("cmd") : chalk7.dim("prompt");
1484
+ const preview = t.cmd ? chalk7.dim(` ${t.cmd.slice(0, 80)}${t.cmd.length > 80 ? "\u2026" : ""}`) : chalk7.dim(` ${(t.prompt ?? "").slice(0, 80)}${(t.prompt?.length ?? 0) > 80 ? "\u2026" : ""}`);
1485
+ console.log(` ${chalk7.green("+")} ${chalk7.bold(t.id.padEnd(24))} [${type}]${scope}`);
1184
1486
  console.log(` ${preview}`);
1185
1487
  }
1186
1488
  }
1187
1489
  if (!options.apply) {
1188
1490
  console.log(
1189
- chalk6.red(`
1491
+ chalk7.red(`
1190
1492
  \u2717 checks.yaml is out of sync with the project profile.`) + `
1191
- Run ${chalk6.bold("npx @holdpoint/cli@alpha evolve --apply")} to apply these changes.`
1493
+ Run ${chalk7.bold("npx @holdpoint/cli suggest --apply")} to apply these changes.`
1192
1494
  );
1193
1495
  process.exit(1);
1194
1496
  }
@@ -1229,128 +1531,55 @@ async function evolveCommand(options) {
1229
1531
  };
1230
1532
  const header = extractHeader(yamlContent);
1231
1533
  const newYaml = withHeader(header, generateYaml(updatedConfig));
1232
- writeFileSync5("checks.yaml", newYaml, "utf8");
1534
+ writeFileSync6("checks.yaml", newYaml, "utf8");
1233
1535
  applySpinner.text = "Running holdpoint update\u2026";
1234
1536
  try {
1235
- execSync6("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
1537
+ execSync7("npx @holdpoint/cli update", { stdio: "pipe" });
1236
1538
  } catch {
1237
1539
  applySpinner.warn(
1238
- chalk6.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
1540
+ chalk7.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
1239
1541
  );
1240
1542
  printAppliedSummary(proposals.length, staleChecks.length);
1241
1543
  return;
1242
1544
  }
1243
- applySpinner.succeed(chalk6.green("checks.yaml updated and engine files regenerated."));
1545
+ applySpinner.succeed(chalk7.green("checks.yaml updated and engine files regenerated."));
1244
1546
  printAppliedSummary(proposals.length, staleChecks.length);
1245
1547
  }
1246
1548
  function printAppliedSummary(added, wrapped) {
1247
1549
  const parts = [];
1248
- if (added > 0) parts.push(chalk6.green(`${added} check${added === 1 ? "" : "s"} added`));
1550
+ if (added > 0) parts.push(chalk7.green(`${added} check${added === 1 ? "" : "s"} added`));
1249
1551
  if (wrapped > 0)
1250
- parts.push(chalk6.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
1552
+ parts.push(chalk7.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
1251
1553
  if (parts.length > 0) console.log(" " + parts.join(" \xB7 "));
1252
1554
  console.log(
1253
- chalk6.dim("\n Review checks.yaml, then commit: ") + chalk6.yellow("git add checks.yaml && git commit -m 'chore: evolve holdpoint checks'")
1555
+ chalk7.dim("\n Review checks.yaml, then commit: ") + chalk7.yellow("git add checks.yaml && git commit -m 'chore: update holdpoint checks'")
1254
1556
  );
1255
1557
  }
1256
1558
 
1257
1559
  // src/commands/live.ts
1258
- import { existsSync as existsSync11 } from "fs";
1259
- import { dirname as dirname4, join as join7 } from "path";
1260
- import chalk7 from "chalk";
1261
- import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
1262
-
1263
- // src/lib/ensure-daemon.ts
1264
- import { spawn } from "child_process";
1265
- import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
1266
- function sleep(ms) {
1267
- return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
1268
- }
1269
- async function ensureDaemon(timeoutMs = 5e3) {
1270
- const existing = await readHealthyDaemonLock();
1271
- if (existing) {
1272
- return { info: existing, started: false };
1273
- }
1274
- const cliEntry = process.argv[1];
1275
- if (!cliEntry) {
1276
- throw new Error("Cannot determine the current holdpoint CLI entrypoint");
1277
- }
1278
- const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
1279
- stdio: "ignore",
1280
- env: process.env,
1281
- cwd: process.cwd()
1282
- });
1283
- child.unref();
1284
- const deadline = Date.now() + timeoutMs;
1285
- while (Date.now() < deadline) {
1286
- const lock = await readHealthyDaemonLock();
1287
- if (lock) {
1288
- return { info: lock, started: true };
1289
- }
1290
- await sleep(100);
1291
- }
1292
- throw new Error("Daemon unavailable + cannot spawn");
1293
- }
1294
-
1295
- // src/lib/open-browser.ts
1296
- import { execSync as execSync7 } from "child_process";
1297
- function openBrowser(url) {
1298
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1299
- try {
1300
- execSync7(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
1301
- } catch {
1302
- }
1303
- }
1304
-
1305
- // src/commands/live.ts
1306
- function findChecksYaml(startDir) {
1307
- let current = startDir;
1308
- for (; ; ) {
1309
- const candidate = join7(current, "checks.yaml");
1310
- if (existsSync11(candidate)) {
1311
- return candidate;
1312
- }
1313
- const parent = dirname4(current);
1314
- if (parent === current) {
1315
- return null;
1316
- }
1317
- current = parent;
1318
- }
1319
- }
1320
- function tryResolveCurrentProject() {
1321
- const checksYaml = findChecksYaml(process.cwd());
1322
- if (checksYaml) {
1323
- return identifyProject2(dirname4(checksYaml));
1324
- }
1325
- try {
1326
- return identifyProject2(process.cwd());
1327
- } catch {
1328
- return null;
1329
- }
1330
- }
1560
+ import chalk8 from "chalk";
1331
1561
  async function liveCommand(options = {}) {
1332
1562
  const { info, started } = await ensureDaemon();
1333
1563
  const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
1334
1564
  baseUrl.searchParams.set("token", info.token);
1565
+ baseUrl.searchParams.set("path", "/live/");
1335
1566
  const currentProject = options.project ? null : tryResolveCurrentProject();
1336
1567
  if (options.project) {
1337
1568
  baseUrl.searchParams.set("project", options.project);
1338
1569
  } else if (currentProject) {
1339
- baseUrl.searchParams.set("project", currentProject.hash);
1340
- baseUrl.searchParams.set("name", currentProject.name);
1341
- baseUrl.searchParams.set("root", currentProject.root);
1570
+ appendProjectAuthParams(baseUrl, currentProject);
1342
1571
  }
1343
1572
  openBrowser(baseUrl.toString());
1344
1573
  console.log(
1345
- chalk7.green(
1574
+ chalk8.green(
1346
1575
  started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
1347
1576
  )
1348
1577
  );
1349
- console.log(` url: ${chalk7.cyan(baseUrl.toString())}`);
1578
+ console.log(` url: ${chalk8.cyan(baseUrl.toString())}`);
1350
1579
  }
1351
1580
 
1352
1581
  // src/commands/daemon.ts
1353
- import chalk8 from "chalk";
1582
+ import chalk9 from "chalk";
1354
1583
  import {
1355
1584
  DaemonAlreadyRunningError,
1356
1585
  isProcessAlive,
@@ -1391,38 +1620,38 @@ async function daemonStartCommand() {
1391
1620
  const { info, started } = await ensureDaemon();
1392
1621
  const sessionCount = await fetchSessionCount(info);
1393
1622
  const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
1394
- console.log(chalk8.green(`\u2713 ${headline}`));
1395
- console.log(` pid: ${chalk8.cyan(String(info.pid))}`);
1396
- console.log(` port: ${chalk8.cyan(String(info.port))}`);
1397
- console.log(` uptime: ${chalk8.cyan(formatUptime(info))}`);
1623
+ console.log(chalk9.green(`\u2713 ${headline}`));
1624
+ console.log(` pid: ${chalk9.cyan(String(info.pid))}`);
1625
+ console.log(` port: ${chalk9.cyan(String(info.port))}`);
1626
+ console.log(` uptime: ${chalk9.cyan(formatUptime(info))}`);
1398
1627
  if (sessionCount !== null) {
1399
- console.log(` sessions: ${chalk8.cyan(String(sessionCount))}`);
1628
+ console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
1400
1629
  }
1401
1630
  }
1402
1631
  async function daemonStatusCommand() {
1403
1632
  const lock = await readHealthyDaemonLock2();
1404
1633
  if (!lock) {
1405
- console.log(chalk8.yellow("Holdpoint Live daemon is not running."));
1634
+ console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
1406
1635
  return;
1407
1636
  }
1408
1637
  const sessionCount = await fetchSessionCount(lock);
1409
- console.log(chalk8.green("\u2713 Holdpoint Live daemon is running"));
1410
- console.log(` pid: ${chalk8.cyan(String(lock.pid))}`);
1411
- console.log(` port: ${chalk8.cyan(String(lock.port))}`);
1412
- console.log(` uptime: ${chalk8.cyan(formatUptime(lock))}`);
1638
+ console.log(chalk9.green("\u2713 Holdpoint Live daemon is running"));
1639
+ console.log(` pid: ${chalk9.cyan(String(lock.pid))}`);
1640
+ console.log(` port: ${chalk9.cyan(String(lock.port))}`);
1641
+ console.log(` uptime: ${chalk9.cyan(formatUptime(lock))}`);
1413
1642
  if (sessionCount !== null) {
1414
- console.log(` sessions: ${chalk8.cyan(String(sessionCount))}`);
1643
+ console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
1415
1644
  }
1416
1645
  }
1417
1646
  async function daemonStopCommand() {
1418
1647
  const lock = readDaemonLock();
1419
1648
  if (!lock) {
1420
- console.log(chalk8.yellow("Holdpoint Live daemon is not running."));
1649
+ console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
1421
1650
  return;
1422
1651
  }
1423
1652
  if (!isProcessAlive(lock.pid)) {
1424
1653
  removeDaemonLock(void 0, lock.token);
1425
- console.log(chalk8.yellow("Removed stale Holdpoint Live lockfile."));
1654
+ console.log(chalk9.yellow("Removed stale Holdpoint Live lockfile."));
1426
1655
  return;
1427
1656
  }
1428
1657
  process.kill(lock.pid, "SIGTERM");
@@ -1430,7 +1659,7 @@ async function daemonStopCommand() {
1430
1659
  while (Date.now() < deadline) {
1431
1660
  if (!isProcessAlive(lock.pid)) {
1432
1661
  removeDaemonLock(void 0, lock.token);
1433
- console.log(chalk8.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
1662
+ console.log(chalk9.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
1434
1663
  return;
1435
1664
  }
1436
1665
  await sleep2(100);
@@ -1438,7 +1667,7 @@ async function daemonStopCommand() {
1438
1667
  process.kill(lock.pid, "SIGKILL");
1439
1668
  await sleep2(100);
1440
1669
  removeDaemonLock(void 0, lock.token);
1441
- console.log(chalk8.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
1670
+ console.log(chalk9.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
1442
1671
  }
1443
1672
  async function daemonServeCommand(options) {
1444
1673
  try {
@@ -1456,20 +1685,24 @@ async function daemonServeCommand(options) {
1456
1685
  }
1457
1686
 
1458
1687
  // src/commands/engines.ts
1459
- import chalk9 from "chalk";
1688
+ import chalk10 from "chalk";
1460
1689
 
1461
1690
  // src/engines.ts
1462
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
1463
- import { dirname as dirname5, join as join8, resolve, sep } from "path";
1691
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1692
+ import { dirname as dirname5, join as join7, resolve, sep } from "path";
1464
1693
  import { createRequire } from "module";
1465
- import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
1694
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
1466
1695
  import { parseEventV1 } from "@holdpoint/live-protocol";
1467
1696
  var require2 = createRequire(import.meta.url);
1468
- var CLI_SRC_DIR = dirname5(fileURLToPath4(import.meta.url));
1697
+ var CLI_SRC_DIR = dirname5(fileURLToPath3(import.meta.url));
1469
1698
  var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
1470
- var BUILTIN_LIVE_ENGINE_PACKAGES = ["@holdpoint/engine-claude"];
1699
+ var BUILTIN_LIVE_ENGINE_PACKAGES = [
1700
+ "@holdpoint/engine-claude",
1701
+ "@holdpoint/engine-codex",
1702
+ "@holdpoint/engine-cursor"
1703
+ ];
1471
1704
  var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
1472
- function isObject(value) {
1705
+ function isObject3(value) {
1473
1706
  return value != null && typeof value === "object" && !Array.isArray(value);
1474
1707
  }
1475
1708
  function isExternalLiveEnginePackageName(packageName) {
@@ -1480,8 +1713,8 @@ function readJsonFile(path) {
1480
1713
  return null;
1481
1714
  }
1482
1715
  try {
1483
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
1484
- return isObject(parsed) ? parsed : null;
1716
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
1717
+ return isObject3(parsed) ? parsed : null;
1485
1718
  } catch {
1486
1719
  return null;
1487
1720
  }
@@ -1489,7 +1722,7 @@ function readJsonFile(path) {
1489
1722
  function findNearestPackageRoot(startDir) {
1490
1723
  let current = resolve(startDir);
1491
1724
  while (true) {
1492
- if (existsSync12(join8(current, "package.json"))) {
1725
+ if (existsSync12(join7(current, "package.json"))) {
1493
1726
  return current;
1494
1727
  }
1495
1728
  const parent = dirname5(current);
@@ -1502,7 +1735,7 @@ function findNearestPackageRoot(startDir) {
1502
1735
  function findPackageRootFromFile(path) {
1503
1736
  let current = dirname5(path);
1504
1737
  while (true) {
1505
- if (existsSync12(join8(current, "package.json"))) {
1738
+ if (existsSync12(join7(current, "package.json"))) {
1506
1739
  return current;
1507
1740
  }
1508
1741
  const parent = dirname5(current);
@@ -1513,14 +1746,14 @@ function findPackageRootFromFile(path) {
1513
1746
  }
1514
1747
  }
1515
1748
  function getDependencyEnginePackageNames(projectRoot) {
1516
- const packageJson = readJsonFile(join8(projectRoot, "package.json"));
1749
+ const packageJson = readJsonFile(join7(projectRoot, "package.json"));
1517
1750
  if (!packageJson) {
1518
1751
  return [];
1519
1752
  }
1520
1753
  const packageNames = /* @__PURE__ */ new Set();
1521
1754
  for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
1522
1755
  const deps = packageJson[field];
1523
- if (!isObject(deps)) {
1756
+ if (!isObject3(deps)) {
1524
1757
  continue;
1525
1758
  }
1526
1759
  for (const packageName of Object.keys(deps)) {
@@ -1554,7 +1787,7 @@ function resolvePackageRoot(packageName, projectRoot) {
1554
1787
  const scopedName = packageName.split("/")[1];
1555
1788
  if (scopedName) {
1556
1789
  const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
1557
- if (existsSync12(join8(packageDir, "package.json"))) {
1790
+ if (existsSync12(join7(packageDir, "package.json"))) {
1558
1791
  return packageDir;
1559
1792
  }
1560
1793
  }
@@ -1566,7 +1799,7 @@ function formatImportError(error) {
1566
1799
  return error instanceof Error && error.message ? error.message : String(error);
1567
1800
  }
1568
1801
  function parseManifest(value) {
1569
- if (!isObject(value)) {
1802
+ if (!isObject3(value)) {
1570
1803
  return null;
1571
1804
  }
1572
1805
  if (value.manifestVersion !== 1) {
@@ -1585,7 +1818,7 @@ function parseManifest(value) {
1585
1818
  };
1586
1819
  }
1587
1820
  function parseLiveCapabilities(value) {
1588
- if (!isObject(value)) {
1821
+ if (!isObject3(value)) {
1589
1822
  return null;
1590
1823
  }
1591
1824
  const capabilities = {};
@@ -1608,7 +1841,7 @@ function parseLiveCapabilities(value) {
1608
1841
  return capabilities;
1609
1842
  }
1610
1843
  function parseLiveAdapter(value, manifest) {
1611
- if (!isObject(value)) {
1844
+ if (!isObject3(value)) {
1612
1845
  return null;
1613
1846
  }
1614
1847
  if (typeof value.id !== "string" || typeof value.displayName !== "string") {
@@ -1674,7 +1907,7 @@ async function resolveCandidate(packageName, source, projectRoot) {
1674
1907
  reason: "package could not be resolved from this project"
1675
1908
  };
1676
1909
  }
1677
- const packageJson = readJsonFile(join8(packageRoot, "package.json"));
1910
+ const packageJson = readJsonFile(join7(packageRoot, "package.json"));
1678
1911
  if (!packageJson) {
1679
1912
  return {
1680
1913
  packageName,
@@ -1692,7 +1925,7 @@ async function resolveCandidate(packageName, source, projectRoot) {
1692
1925
  reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
1693
1926
  };
1694
1927
  }
1695
- const metadata = isObject(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
1928
+ const metadata = isObject3(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
1696
1929
  if (!metadata?.manifest) {
1697
1930
  return {
1698
1931
  packageName,
@@ -1825,28 +2058,28 @@ async function enginesCommand(options = {}) {
1825
2058
  return;
1826
2059
  }
1827
2060
  if (engines.length === 0) {
1828
- console.log("No Holdpoint live adapters were discovered.");
2061
+ console.log("No Holdpoint Live engines were discovered.");
1829
2062
  return;
1830
2063
  }
1831
2064
  for (const engine of engines) {
1832
2065
  if (engine.status === "loaded" && engine.manifest) {
1833
2066
  console.log(
1834
- `${chalk9.green("loaded")} ${chalk9.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk9.yellow(engine.packageName)} [${engine.source}]`
2067
+ `${chalk10.green("loaded")} ${chalk10.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk10.yellow(engine.packageName)} [${engine.source}]`
1835
2068
  );
1836
2069
  continue;
1837
2070
  }
1838
2071
  console.log(
1839
- `${chalk9.yellow("ignored")} ${chalk9.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
2072
+ `${chalk10.yellow("ignored")} ${chalk10.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
1840
2073
  );
1841
2074
  }
1842
2075
  }
1843
2076
 
1844
2077
  // src/commands/event.ts
1845
- import { readFileSync as readFileSync9 } from "fs";
2078
+ import { readFileSync as readFileSync10 } from "fs";
1846
2079
  import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
1847
2080
  import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
1848
2081
  function readStdin() {
1849
- return readFileSync9(0, "utf8");
2082
+ return readFileSync10(0, "utf8");
1850
2083
  }
1851
2084
  async function eventCommand(options) {
1852
2085
  const stdin = readStdin().trim();
@@ -1895,27 +2128,284 @@ async function eventCommand(options) {
1895
2128
  }
1896
2129
  }
1897
2130
 
2131
+ // src/commands/changeset.ts
2132
+ import { execSync as execSync8 } from "child_process";
2133
+ import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync } from "fs";
2134
+ import { join as join8, relative } from "path";
2135
+ import chalk11 from "chalk";
2136
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2137
+ ".git",
2138
+ ".next",
2139
+ ".turbo",
2140
+ "coverage",
2141
+ "dist",
2142
+ "node_modules",
2143
+ "test-results"
2144
+ ]);
2145
+ function runGit(command) {
2146
+ try {
2147
+ const out = execSync8(command, {
2148
+ encoding: "utf8",
2149
+ stdio: ["pipe", "pipe", "ignore"]
2150
+ });
2151
+ return out.trim().split("\n").filter(Boolean);
2152
+ } catch {
2153
+ return [];
2154
+ }
2155
+ }
2156
+ function readJson(path) {
2157
+ try {
2158
+ return JSON.parse(readFileSync11(path, "utf8"));
2159
+ } catch {
2160
+ return null;
2161
+ }
2162
+ }
2163
+ function normalizePath(path) {
2164
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
2165
+ }
2166
+ function getDefaultBranchRef() {
2167
+ const [symbolic] = runGit("git symbolic-ref --quiet --short refs/remotes/origin/HEAD");
2168
+ if (symbolic) return symbolic;
2169
+ const candidates = ["origin/main", "origin/master"];
2170
+ for (const candidate of candidates) {
2171
+ if (runGit(`git rev-parse --verify ${candidate}`).length > 0) {
2172
+ return candidate;
2173
+ }
2174
+ }
2175
+ return null;
2176
+ }
2177
+ function getBranchChangedFiles() {
2178
+ const defaultBranch = getDefaultBranchRef();
2179
+ if (!defaultBranch) return [];
2180
+ const [base] = runGit(`git merge-base HEAD ${defaultBranch}`);
2181
+ if (!base) return [];
2182
+ return runGit(`git diff --name-only ${base}...HEAD`);
2183
+ }
2184
+ function uniqueFiles(files) {
2185
+ return [...new Set(files.map(normalizePath))];
2186
+ }
2187
+ function getChangedFiles(options) {
2188
+ const staged = runGit("git diff --cached --name-only");
2189
+ if (options.staged && staged.length > 0) return staged;
2190
+ const untracked = runGit("git ls-files --others --exclude-standard");
2191
+ if (!options.staged) {
2192
+ const unstaged = runGit("git diff --name-only HEAD");
2193
+ const workingTree = uniqueFiles([...unstaged, ...untracked]);
2194
+ if (workingTree.length > 0) return workingTree;
2195
+ }
2196
+ const branch = getBranchChangedFiles();
2197
+ if (branch.length > 0 || untracked.length > 0) return uniqueFiles([...branch, ...untracked]);
2198
+ return runGit("git diff --name-only HEAD~1 HEAD");
2199
+ }
2200
+ function parsePnpmWorkspacePatterns() {
2201
+ if (!existsSync13("pnpm-workspace.yaml")) return [];
2202
+ const lines = readFileSync11("pnpm-workspace.yaml", "utf8").split(/\r?\n/);
2203
+ return lines.map((line) => line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/)?.[1]).filter((line) => Boolean(line)).filter((line) => !line.startsWith("!"));
2204
+ }
2205
+ function expandOneLevelWorkspacePattern(pattern) {
2206
+ const normalized = normalizePath(pattern).replace(/\/package\.json$/, "");
2207
+ if (!normalized.includes("*")) {
2208
+ return existsSync13(join8(normalized, "package.json")) ? [normalized] : [];
2209
+ }
2210
+ const starIndex = normalized.indexOf("*");
2211
+ const parent = normalized.slice(0, starIndex).replace(/\/$/, "");
2212
+ const suffix = normalized.slice(starIndex + 1).replace(/^\//, "");
2213
+ if (!parent || suffix.includes("*") || !existsSync13(parent)) {
2214
+ return [];
2215
+ }
2216
+ return readdirSync3(parent).map((entry) => join8(parent, entry, suffix)).map(normalizePath).filter((candidate) => existsSync13(join8(candidate, "package.json")));
2217
+ }
2218
+ function walkPackageRoots(start, roots) {
2219
+ let entries;
2220
+ try {
2221
+ entries = readdirSync3(start);
2222
+ } catch {
2223
+ return;
2224
+ }
2225
+ if (start !== "." && existsSync13(join8(start, "package.json"))) {
2226
+ roots.push(normalizePath(start));
2227
+ return;
2228
+ }
2229
+ for (const entry of entries) {
2230
+ if (IGNORED_DIRS.has(entry)) continue;
2231
+ const candidate = join8(start, entry);
2232
+ let stats;
2233
+ try {
2234
+ stats = statSync(candidate);
2235
+ } catch {
2236
+ continue;
2237
+ }
2238
+ if (stats.isDirectory()) {
2239
+ walkPackageRoots(candidate, roots);
2240
+ }
2241
+ }
2242
+ }
2243
+ function readPackageRoot(path) {
2244
+ const pkg = readJson(join8(path, "package.json"));
2245
+ if (!pkg) return null;
2246
+ return {
2247
+ path: normalizePath(path === "." ? "" : path),
2248
+ name: typeof pkg.name === "string" ? pkg.name : path || "root",
2249
+ private: pkg.private === true
2250
+ };
2251
+ }
2252
+ function discoverPackageRoots(includePatterns = []) {
2253
+ const explicitRoots = includePatterns.flatMap(expandOneLevelWorkspacePattern);
2254
+ if (explicitRoots.length > 0) {
2255
+ return uniquePackageRoots(
2256
+ explicitRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg))
2257
+ );
2258
+ }
2259
+ const rootPackage = readJson("package.json");
2260
+ const workspacePatterns = [
2261
+ ...parsePnpmWorkspacePatterns(),
2262
+ ...extractPackageJsonWorkspacePatterns(rootPackage)
2263
+ ];
2264
+ const workspaceRoots = workspacePatterns.flatMap(expandOneLevelWorkspacePattern);
2265
+ if (workspaceRoots.length > 0) {
2266
+ return uniquePackageRoots(
2267
+ workspaceRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
2268
+ );
2269
+ }
2270
+ const discovered = [];
2271
+ walkPackageRoots(".", discovered);
2272
+ const roots = discovered.length > 0 ? discovered : existsSync13("package.json") ? ["."] : [];
2273
+ return uniquePackageRoots(
2274
+ roots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
2275
+ );
2276
+ }
2277
+ function extractPackageJsonWorkspacePatterns(pkg) {
2278
+ const workspaces = pkg?.workspaces;
2279
+ if (Array.isArray(workspaces)) {
2280
+ return workspaces.filter((entry) => typeof entry === "string");
2281
+ }
2282
+ if (workspaces && typeof workspaces === "object" && "packages" in workspaces && Array.isArray(workspaces.packages)) {
2283
+ return workspaces.packages.filter(
2284
+ (entry) => typeof entry === "string"
2285
+ );
2286
+ }
2287
+ return [];
2288
+ }
2289
+ function uniquePackageRoots(packages) {
2290
+ const byPath = /* @__PURE__ */ new Map();
2291
+ for (const pkg of packages) {
2292
+ byPath.set(pkg.path, pkg);
2293
+ }
2294
+ return [...byPath.values()].sort((left, right) => right.path.length - left.path.length);
2295
+ }
2296
+ function isChangesetFile(file) {
2297
+ return /^\.changeset\/(?!README\.md$)[^/]+\.md$/.test(file);
2298
+ }
2299
+ function isReleaseAffectingPackageFile(relativePath) {
2300
+ if (/(^|\/)(__tests__|test|tests|spec|e2e)\//.test(relativePath) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(relativePath)) {
2301
+ return false;
2302
+ }
2303
+ if (relativePath === "README.md" || relativePath === "CHANGELOG.md" || relativePath.startsWith("docs/") || relativePath.startsWith("dist/") || relativePath.startsWith("coverage/")) {
2304
+ return false;
2305
+ }
2306
+ return /^(package\.json|src\/|lib\/|bin\/|templates\/|scripts\/|[^/]+\.config\.)/.test(
2307
+ relativePath
2308
+ );
2309
+ }
2310
+ function findPackageForFile(file, packageRoots) {
2311
+ const normalized = normalizePath(file);
2312
+ return packageRoots.find((pkg) => {
2313
+ if (pkg.path === "") return true;
2314
+ return normalized === pkg.path || normalized.startsWith(`${pkg.path}/`);
2315
+ }) ?? null;
2316
+ }
2317
+ function analyzeChangesetRequirement(input) {
2318
+ const changedFiles = input.changedFiles.map(normalizePath);
2319
+ const hasChangeset = changedFiles.some(isChangesetFile);
2320
+ const requiredFiles = changedFiles.flatMap((file) => {
2321
+ if (file.startsWith(".changeset/")) return [];
2322
+ const pkg = findPackageForFile(file, input.packageRoots);
2323
+ if (!pkg) return [];
2324
+ const relativePath = pkg.path === "" ? file : normalizePath(relative(pkg.path, file));
2325
+ if (!isReleaseAffectingPackageFile(relativePath)) return [];
2326
+ return [{ file, packageName: pkg.name }];
2327
+ });
2328
+ return { requiredFiles, hasChangeset };
2329
+ }
2330
+ async function requireChangesetCommand(options) {
2331
+ const changedFiles = getChangedFiles(options);
2332
+ if (changedFiles.length === 0) {
2333
+ console.log(chalk11.green("\u2713 No changed files detected \u2014 no changeset required."));
2334
+ return;
2335
+ }
2336
+ const packageRoots = discoverPackageRoots(options.include ?? []);
2337
+ if (packageRoots.length === 0) {
2338
+ console.log(chalk11.green("\u2713 No package roots detected \u2014 no changeset required."));
2339
+ return;
2340
+ }
2341
+ const hasChangesetSetup = existsSync13(".changeset");
2342
+ const { requiredFiles, hasChangeset } = analyzeChangesetRequirement({
2343
+ changedFiles,
2344
+ packageRoots
2345
+ });
2346
+ if (requiredFiles.length === 0) {
2347
+ console.log(chalk11.green("\u2713 No release-affecting package files changed."));
2348
+ return;
2349
+ }
2350
+ if (hasChangeset) {
2351
+ console.log(chalk11.green("\u2713 Package changes include a changeset."));
2352
+ return;
2353
+ }
2354
+ console.error(chalk11.red("\u2717 Package changes need a changeset."));
2355
+ console.error("");
2356
+ console.error(chalk11.bold("Changed package files:"));
2357
+ for (const item of requiredFiles.slice(0, 12)) {
2358
+ console.error(` - ${item.file} (${item.packageName})`);
2359
+ }
2360
+ if (requiredFiles.length > 12) {
2361
+ console.error(` - \u2026and ${requiredFiles.length - 12} more`);
2362
+ }
2363
+ console.error("");
2364
+ if (!hasChangesetSetup) {
2365
+ console.error(
2366
+ "No .changeset directory was found. Create one and add a changeset before finishing:"
2367
+ );
2368
+ console.error(chalk11.yellow(" mkdir -p .changeset"));
2369
+ } else {
2370
+ console.error("Add a changeset before finishing:");
2371
+ }
2372
+ console.error(chalk11.yellow(" pnpm changeset"));
2373
+ console.error(chalk11.dim(" or add a .changeset/<name>.md file manually"));
2374
+ process.exit(1);
2375
+ }
2376
+
1898
2377
  // src/index.ts
1899
2378
  var program = new Command();
1900
- program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION).action(() => {
1901
- void liveCommand();
2379
+ program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION);
2380
+ program.action(() => {
2381
+ program.outputHelp();
1902
2382
  });
1903
- program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option(
2383
+ program.command("init").description("Initialise Holdpoint in the current project").option(
1904
2384
  "--agent <agent>",
1905
2385
  "Agent to install for: copilot | claude | cursor | codex (default: all four)"
1906
2386
  ).action(initCommand);
1907
2387
  program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
1908
2388
  program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
1909
2389
  program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
1910
- program.command("builder").description("Open the visual builder UI on localhost:4321").action(buildCommand);
2390
+ program.command("builder").description("Open the visual builder UI in the Holdpoint daemon").action(buildCommand);
1911
2391
  program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
1912
2392
  var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
1913
2393
  daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
1914
2394
  daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
1915
2395
  daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
1916
2396
  program.command("event").description("Internal: read event JSON from stdin and publish it to Holdpoint Live").option("--engine <engine>", "Engine name when converting native hook payloads").option("--from-hook", "Interpret stdin as an engine-native hook payload").action(eventCommand);
1917
- program.command("engines").description("List discovered Holdpoint Live adapter packages").option("--json", "Print machine-readable discovery output").action(enginesCommand);
2397
+ program.command("engines").description("List discovered Holdpoint Live engine packages").option("--json", "Print machine-readable discovery output").action(enginesCommand);
2398
+ program.command("require-changeset").description("Ensure release-affecting package changes include a changeset").option("--staged", "Prefer git-staged files when deciding what changed").option(
2399
+ "--include <pattern...>",
2400
+ "Package directory glob(s) to enforce, e.g. packages/* apps/builder"
2401
+ ).action(requireChangesetCommand);
1918
2402
  program.command("daemon-serve").description("Internal: run the Holdpoint Live daemon in the foreground").option("--port <port>", "Fixed port for the daemon process").action(daemonServeCommand);
1919
- program.command("evolve").description("Scan project and propose (or apply) new checks to keep checks.yaml in sync").option("--apply", "Write proposed changes to checks.yaml and regenerate engine files").action(evolveCommand);
2403
+ program.command("suggest").description("Scan project and propose (or apply) new checks to keep checks.yaml in sync").option("--apply", "Write proposed changes to checks.yaml and regenerate engine files").action(evolveCommand);
2404
+ program.command("evolve", { hidden: true }).description("Deprecated alias for `holdpoint suggest`").option("--apply", "Write proposed changes to checks.yaml and regenerate engine files").action(async (options) => {
2405
+ process.stderr.write(
2406
+ "warning: `holdpoint evolve` is deprecated; use `holdpoint suggest` instead.\n"
2407
+ );
2408
+ await evolveCommand(options);
2409
+ });
1920
2410
  program.parse();
1921
2411
  //# sourceMappingURL=index.js.map